this post was submitted on 26 Nov 2024
11 points (100.0% liked)

Rust Programming

8234 readers
71 users here now

founded 5 years ago
MODERATORS
11
submitted 3 weeks ago* (last edited 3 weeks ago) by [email protected] to c/[email protected]
 

Hello,

I was playing around with rust and wondered if I could use const generics for toggling debug code on and off to avoid any runtime cost while still being able to toggle the DEBUG flag during runtime. I came up with a nifty solution that requires a single dynamic dispatch which many programs have anyways. It works by rewriting the vtable. It's a zero cost bool!

Is this technique worth it?

Probably not.

It's funny though.

Repo: https://github.com/raldone01/runtime_const_generics_rs/tree/v1.0.0

Full source code below:

use std::mem::transmute;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;

use replace_with::replace_with_or_abort;

trait GameObject {
  fn run(&mut self);
  fn set_debug(&mut self, flag: bool) -> &mut dyn GameObject;
}

trait GameObjectBoxExt {
  fn set_debug(self: Box<Self>, flag: bool) -> Box<dyn GameObject>;
}

impl GameObjectBoxExt for dyn GameObject {
  fn set_debug(self: Box<Self>, flag: bool) -> Box<dyn GameObject> {
    unsafe {
      let selv = Box::into_raw(self);
      let selv = (&mut *selv).set_debug(flag);
      return Box::from_raw(selv);
    }
  }
}

static ID_CNT: AtomicU32 = AtomicU32::new(0);

struct Node3D<const DEBUG: bool = false> {
  id: u32,
  cnt: u32,
}

impl Node3D {
  const TYPE_NAME: &str = "Node3D";
  fn new() -> Self {
    let id = ID_CNT.fetch_add(1, Ordering::Relaxed);
    let selv = Self { id, cnt: 0 };
    return selv;
  }
}

impl<const DEBUG: bool> GameObject for Node3D<DEBUG> {
  fn run(&mut self) {
    println!("Hello {} from {}@{}!", self.cnt, Node3D::TYPE_NAME, self.id);
    if DEBUG {
      println!("Debug {} from {}@{}!", self.cnt, Node3D::TYPE_NAME, self.id);
    }
    self.cnt += 1;
  }

  fn set_debug(&mut self, flag: bool) -> &mut dyn GameObject {
    unsafe {
      match flag {
        true => transmute::<_, &mut Node3D<true>>(self) as &mut dyn GameObject,
        false => transmute::<_, &mut Node3D<false>>(self) as &mut dyn GameObject,
      }
    }
  }
}

struct Node2D<const DEBUG: bool = false> {
  id: u32,
  cnt: u32,
}

impl Node2D {
  const TYPE_NAME: &str = "Node2D";
  fn new() -> Self {
    let id = ID_CNT.fetch_add(1, Ordering::Relaxed);
    let selv = Self { id, cnt: 0 };
    return selv;
  }
}

impl<const DEBUG: bool> GameObject for Node2D<DEBUG> {
  fn run(&mut self) {
    println!("Hello {} from {}@{}!", self.cnt, Node2D::TYPE_NAME, self.id);
    if DEBUG {
      println!("Debug {} from {}@{}!", self.cnt, Node2D::TYPE_NAME, self.id);
    }
    self.cnt += 1;
  }

  fn set_debug(&mut self, flag: bool) -> &mut dyn GameObject {
    unsafe {
      match flag {
        true => transmute::<_, &mut Node2D<true>>(self) as &mut dyn GameObject,
        false => transmute::<_, &mut Node2D<false>>(self) as &mut dyn GameObject,
      }
    }
  }
}

fn main() {
  let mut objects = Vec::new();
  for _ in 0..10 {
    objects.push(Box::new(Node3D::new()) as Box<dyn GameObject>);
    objects.push(Box::new(Node2D::new()) as Box<dyn GameObject>);
  }

  for o in 0..3 {
    for (i, object) in objects.iter_mut().enumerate() {
      let debug = (o + i) % 2 == 0;
      replace_with_or_abort(object, |object| object.set_debug(debug));
      object.run();
    }
  }
}

Note:

If anyone gets the following to work without unsafe, maybe by using the replace_with crate I would be very happy:

impl GameObjectBoxExt for dyn GameObject {
  fn set_debug(self: Box<Self>, flag: bool) -> Box<dyn GameObject> {
    unsafe {
      let selv = Box::into_raw(self);
      let selv = (&mut *selv).set_debug(flag);
      return Box::from_raw(selv);
    }
  }

I am curious to hear your thoughts.

you are viewing a single comment's thread
view the rest of the comments
[–] BB_C 2 points 3 weeks ago* (last edited 3 weeks ago) (1 children)

I'm stating the obvious here, but if you have ownership at the time of conversion, then you can just change the type, you don't have to use dyn (a la impl Foo<false> { fn into_debug(self) -> Foo<true> {} }). That may require some code restructuring of course.

[–] [email protected] 1 points 3 weeks ago* (last edited 3 weeks ago) (1 children)

Well I did not think of that. So in the into_debug I would move all struct members from Foo<false> into a "new" Foo<true> and return that?

[–] BB_C 2 points 3 weeks ago

I would move all struct members from Foo into a “new” Foo and return that?

Yes... if you don't define Drop for Foo.

If you do, then you will have to either use mem::take()/mem::replace() with each field, or if you don't mind unsafe {}, you can just do a trivial transmute. Justunsafe { transmute(self) } should work.