I'm trying to figure out how to deal nicely with dyn Trait
in no_std
Rust. I'm using RTIC, for context. I could explicitly list every concrete type I use in my code, I guess, but that's awful to write, read, and maintain. I'd much prefer to have an e.g. heapless::Vec<Box<dyn Thing>>
stored somewhere, to which I can add the (third party) classes whose type parameters depend on which pins they're tied to.
Unfortunately, Box
is an std
thing. This makes sense, since by default it needs to allocate memory on the heap to move its contents off the stack, and we don't (necessarily) have a heap. However, in much the same way as we have heapless::Vec<TYPE, COUNT>
that just preallocates some memory and moves the object there, I'd expect there to be a heapless::Box<TYPE, BYTES>
that just moved the object into its internal buffer instead of the heap. No such luck.
Well, I figured, it seems like a fairly straightforward thing to implement myself, right? Just a memory copy basically. Nope! 8 hours later and still no success. I did eventually manage to get something to at least compile, but I think there's a missing step that ends up with it reading memory wrong, and there's no telling whether the rest of it is actually safe. Here's a tidied version of my test code (also uploaded to GitHub ).
Note that my test code isn't technically no_std
, but my use case code is.
#![feature(unsize)] // *Ideally* I wouldn't need these
#![feature(coerce_unsized)]
use core::{
marker::{PhantomData, Unsize},
ops::CoerceUnsized,
ptr::NonNull,
};
use heapless::Vec;
pub trait Valued {
fn get_value(&self) -> u32;
}
struct FooS {
foo: u32,
}
impl Valued for FooS {
fn get_value(&self) -> u32 {
return self.foo;
}
}
pub struct Bin<T: ?Sized> {
data: [u8; 128], // Eventually this would be configurable
pointer: NonNull<T>,
_marker: PhantomData<T>,
}
impl<T: ?Sized> Bin<T> {
pub fn get(&self) -> &T {
unsafe {
return self.pointer.as_ref();
}
}
pub fn get_mut(&mut self) -> &mut T {
unsafe {
return self.pointer.as_mut();
}
}
}
/*
// This MIGHT be key to fixing the problem, except it seems the type parameter syntax can't express "where the input type implements the output type"
impl<T> Bin<T> {
pub fn toUnsized<U: ?Sized>(self) -> U
where
T: U, // "expected trait, found type parameter `U`"
{
//...
}
}
// I thought maybe if I used the specific trait I could at least solve half the problem and test if the idea works.
// Did not work; still read memory wrong.
impl<T: 'static> Bin<T> {
pub fn toUnsized(mut self) -> Bin<dyn Valued>
where
T: Valued,
{
let mut a = self.get_mut();
let mut b = a as &mut dyn Valued;
let c: &mut dyn Valued = unsafe { core::ptr::read(&raw mut b) };
let n: NonNull<dyn Valued> = NonNull::new(c as *mut _).unwrap();
let r: Bin<dyn Valued> = Bin {
data: self.data,
pointer: n,
_marker: PhantomData {},
};
return r;
}
}
*/
// I suspect this is not working right.
impl<T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<Bin<U>> for Bin<T> {}
pub fn get_bin<T>(mut t: T) -> Bin<T> {
let n: NonNull<T> = NonNull::new(&mut t as *mut _).unwrap();
let mut bin: Bin<T> = Bin {
data: [0; 128],
pointer: n,
_marker: PhantomData {},
};
let a = (&raw mut bin.data);
let b = a as *mut T;
unsafe {
core::ptr::write(b, t); //LEAK I should drop the contents when the wrapper is dropped
}
bin.pointer = NonNull::new(b).unwrap();
return bin;
}
pub fn main() {
let foo = FooS { foo: 16 };
let mut list: Vec<Bin<dyn Valued>, 16> = Vec::new();
let bf = get_bin::<FooS>(foo);
println!("FooS value: {}", bf.get().get_value()); // "FooS value: 16"
let bf2: Bin<dyn Valued> = bf as Bin<dyn Valued>;
println!("Valued value: {}", bf2.get().get_value()); // "Valued value: 1432019472"
// let bf2: Bin<dyn Valued> = bf.toUnsized();
list.push(bf2)
.unwrap_or_else(|_| panic!("Something went horribly wrong!"));
println!("list[0] value: {}", list[0].get().get_value()); // "list[0] value: 4294954704"
// Notably, list[0] is a different wrong value. Maybe there's a pointer not updated in the move.
}
Immediately after moving the FooS
into the Bin<FooS>
, it gives the correct behavior (though I acknowledge this could still be UB), but after casting to Bin<dyn Valued>
this stops being the case.
My theory is that there's a pointer conversion that needs to take place that isn't; in the debugger I see new mention of vtables that weren't there before the cast and I suspect perhaps it's pointing to the wrong thing. I tried adding a layer to explicitly convert the pointers but it didn't work.
What else am I inevitably doing wrong? How would you do something like this, move a struct onto somewhere in the stack rather than the heap, cast to a trait like Box
can do?
The problem is that your type contains a pointer to itself and you use it after the Bin
gets moved. Structs with pointers to itself so called self referential structs are highly problematic because by default every type in Rust is movable, but such a move invalidates these pointers.
A solution is to never use the stored pointer value, you can calculate the correct value whenever you need it from the data
field:
#![feature(set_ptr_value, unsize, coerce_unsized)]
#[repr(align(8))]
struct AlignedArray([u8; 128]);
pub struct Bin<T: ?Sized> {
data: AlignedArray,
// ptr is only used to store the metadata, `CoerceUnsized` requires us to
// store it here, but where it points to is irrelevant and should not be used
ptr: *const T,
_marker: PhantomData<T>,
}
impl<T> Bin<T> {
fn new(t: T) -> Self {
const {
assert!(std::mem::align_of::<T>() <= 8);
assert!(std::mem::size_of::<T>() <= 128);
}
let mut bin: Bin<T> = Bin {
data: AlignedArray([0; 128]),
ptr: &raw const t,
_marker: PhantomData,
};
let a = (&raw mut bin.data.0).cast::<T>();
unsafe {
a.write(t);
}
return bin;
}
}
impl<T: ?Sized> Bin<T> {
pub fn get(&self) -> &T {
unsafe { &*(&raw const self.data.0).with_metadata_of(self.ptr) }
}
pub fn get_mut(&mut self) -> &mut T {
unsafe {
&mut *(&raw mut self.data.0).with_metadata_of(self.ptr)
}
}
}
impl<T: ?Sized + Unsize<U>, U: ?Sized> CoerceUnsized<Bin<U>> for Bin<T> {}
impl<T: ?Sized> Drop for Bin<T> {
fn drop(&mut self) {
unsafe { std::ptr::drop_in_place(self.get_mut()) };
}
}
Working with the metadata is still experimental and requires an additional feature set_ptr_value
or ptr_metadata
if we use Metadata
instead of a pointer, but that doesn't work well with CoerceUnsized
.