Rc<Obj>, Rc<dyn T1>, Rc<dyn T2>, Rc<dyn T3>, clobbering of handles, sharing of counts

Consider the following:

pub struct Foo{}
pub trait T1{}
pub trait T2{}
pub trait T3{}
impl T1 for Foo{}
impl T2 for Foo{}
impl T3 for Foo{}

fn main() {
  let x = Rc::new(Foo{});
  let t1: Rc<dyn T1> = x.clone();
  let t2: Rc<dyn T2> = x.clone();
  let t3: Rc<dyn T3> = x.clone();
}

Now, the count on object x is 4 right? Furthermore, the t1, t2, t3 have vtable handles -- where are these vtable handles stored?

If the handles are stored inside the object Foo, they are going to clobber each other. If they are not stored inside the object Foo, where are they stored?

Yes, the count is 4.

Vtable handles are stored next to the actual data pointer - the size of t1 is 16 bytes, while the size of x is 8.

1 Like

They [vtables] are stored in the Rc handles, alongside with the pointer. The Foo itself (and refcounts) is intact. This concept is called "fat pointer". Check out this cheat sheet. It doesn't explain Rc<dyn Trait> directly, but it explains Rc<T> and Box<dyn Trait>.

1 Like

@krdln : The diagrams are really helpful.

Does this mean that Box<T> and Box<dyn Trait> have different size? I used to think that Box<T> was always the same size regardless of T because T was stored on the heap, so Box<T> was just one pointer.

However, it now appears that Box<T> is always 1 ptr while Box<dyn T> is 2 ptrs? It's almost as if "struct Box" behaves drastically differently depending on whether it's given a Type or a Trait.

Well it is just a pointer. The thing is that a pointer to a trait object is a fat pointer in Rust. You'll find the same happening for &dyn Trait.

Yep, it's the same for Box as it is for Rc.

I'm still struggling understanding this 'fat pointer.'

Is this also why we can have Rc<dyn A> but not Rc<dyn A + B>, because in a "fat pointer", we only have 1 vtable slot and thus can't say "this object satisfies trait A & trait B" ?

Yes

Not necessarily - you could have a single vtable for all of the methods of A and B, like if you made a trait C: A + B and used dyn C.

@zeroexcuses since sometimes actual code can help make things clearer (although sometimes it doesn't), here is a PoC of manually implementing dyn Trait functionality:

Click to expand
#[cfg(FALSE)] /// This "becomes"
pub
trait T1 {
    fn method (&self, x: usize) -> bool;
    fn name (&self) -> String;
}

pub use compiler_generated::Trait as T1; 
mod compiler_generated {
    use ::std::{mem, ptr};

    pub type Something = u8; // actual type is "erased"

    pub
    trait Trait : CompilerExt {
        fn method (&self, x: usize) -> bool;
        fn name (&self) -> String;
        
        
        fn coerce_to_box_dyn (self: Box<Self>) -> BoxDynTrait
        where
            Self : Sized,
        {
            BoxDynTrait {
                this: Box::into_raw(self).cast(),
                vtable: &Self::VTABLE,
            }
        }
    }

    pub
    trait CompilerExt {
        const VTABLE: VTable;
    }
    pub
    struct VTable {
        size: usize,
        align: usize,
        drop_in_place: unsafe fn (*mut Something),

        method: unsafe fn (*const Something, x: usize) -> bool,
        name: unsafe fn (*const Something) -> String,
    }
    impl<T : Trait> CompilerExt for T {
        const VTABLE: VTable = VTable {
            size: mem::size_of::<T>(),
            align: mem::align_of::<T>(),
            drop_in_place: unsafe { mem::transmute(
                ptr::drop_in_place::<T> as *const ()
            )},

            method: unsafe { mem::transmute(
                <T as Trait>::method as *const (),
            )},
            name: unsafe { mem::transmute(
                <T as Trait>::name as *const (),
            )},
        };
    }

    pub
    struct BoxDynTrait {
        this: *mut Something,
        vtable: &'static VTable,
    }
    impl Trait for BoxDynTrait {
        #[inline(always)]
        fn method (&self, x: usize) -> bool
        {
            unsafe {
                (self.vtable.method)(self.this, x)
            }
        }
        #[inline(always)]
        fn name (&self) -> String
        {
            unsafe {
                (self.vtable.name)(self.this)
            }
        }
    }
    impl Drop for BoxDynTrait {
        fn drop (self: &mut Self)
        {
            // drop(Box::<??>::from_raw(self.this))
            unsafe {
                (self.vtable.drop_in_place)(self.this);
                if self.vtable.size != 0 {
                    extern crate alloc as alloc_crate;
                    use alloc_crate::alloc;

                    alloc::dealloc(
                        self.this,
                        alloc::Layout::from_size_align_unchecked(
                            self.vtable.size,
                            self.vtable.align,
                        ),    
                    )
                }
            }
        }
    }
}