"Flat" trait objects?

I work with a big array of &dyn Trait. The trait has a relatively small number of methods. The problem is that on each method call we first go to the v-table and only then jump to the method's code. Ideally I would like to remove the indirection and store function pointers together with data pointer.

In other words, today &dyn Trait is represented as (data_ptr, vtable_ptr), but I would like to work with (data_ptr, method1_ptr, method2_ptr, ...).

Is it possible to do it in Rust? Even with unsafe code and manual construction of the type-erased tuple, I am not sure it can be done in a sound manner.

1 Like

You could take a look at how the stdlib models the std::task::Waker struct and how it implements the conversion from Arc<W: Wake> to it.

1 Like

Never mind, that only works for one concrete data type T, and not for dynamic types :sweat_smile:

RawWakerVTable works with explicit function pointers which accept *const () in place of self.

My concern is that the following code is unsound due to the lack of stable ABI in Rust:

use core::{marker::PhantomData, mem};

trait Foo {
    fn foo1(&self, arg: u32);
    fn foo2(&self) -> u32;
}

struct FlatDynFoo<'a> {
    data_ptr: *const (),
    foo1_ptr: fn(*const (), u32),
    foo2_ptr: fn(*const ()) -> u32,
    _pd: PhantomData<&'a ()>,
}

impl<'a> FlatDynFoo<'a> {
    fn flatten<T: Foo>(val: &'a T) -> Self {
        unsafe {
            FlatDynFoo {
                data_ptr: val as *const T as *const (),
                foo1_ptr: mem::transmute(T::foo1 as fn(&T, u32)),
                foo2_ptr: mem::transmute(T::foo2 as fn(&T) -> u32),
                _pd: PhantomData,
            }
        }
    }
    
    fn foo1(&self, arg: u32) {
        (self.foo1_ptr)(self.data_ptr, arg)
    }

    fn foo2(&self) -> u32 {
        (self.foo2_ptr)(self.data_ptr)
    }
}

The ABI being unstable means code compiled with different versions of the compiler might be incompatible. You can still use function pointers.

If that technique was unsound all code using futures would be unsound

(Note: I haven't reviewed your code carefully enough to necessarily say it's sound as is though)

So I can assume that ABIs for fn(&T, u32) and fn(*const (), u32) match? Because I thought that in theory compiler has right to transform fn(&T, u32) to fn(u32, &T) (ABI-wise) or use stack for passing arguments instead of registers, while fn(*const (), u32) will be processed differently.

I cited the conversion from Arc<W: Wake> because it has to deal with a similar problem. It does so by not blindly transmuting the function pointer but instead it creates a function which takes a raw pointer and calls the concrete T's method. With your example this would become:

use core::{marker::PhantomData, mem};

trait Foo {
    fn foo1(&self, arg: u32);
    fn foo2(&self) -> u32;
}

struct FlatDynFoo<'a> {
    data_ptr: *const (),
    foo1_ptr: fn(*const (), u32),
    foo2_ptr: fn(*const ()) -> u32,
    _pd: PhantomData<&'a ()>,
}

impl<'a> FlatDynFoo<'a> {
    fn flatten<T: Foo>(val: &'a T) -> Self {
        unsafe {
            FlatDynFoo {
                data_ptr: val as *const T as *const (),
                foo1_ptr: |ptr, arg| T::foo1(&*ptr.cast(), arg),
                foo2_ptr: |ptr| T::foo2(&*ptr.cast()),
                _pd: PhantomData,
            }
        }
    }
    
    fn foo1(&self, arg: u32) {
        (self.foo1_ptr)(self.data_ptr, arg)
    }

    fn foo2(&self) -> u32 {
        (self.foo2_ptr)(self.data_ptr)
    }
}
2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.