How does this code work?


pub trait MyFoo {
    fn as_any(&self) -> &dyn std::any::Any;
}

impl MyFoo for f32 {
    fn as_any(&self) -> &dyn Any {
        self}}

impl MyFoo for u32 {
    fn as_any(&self) -> &dyn Any {
        self}}

#[test]
fn it_works() {
    let a = 1_u32;
    let b = 1_f32;

    let a_ptr: &dyn MyFoo = &a;
    let b_ptr: &dyn MyFoo = &b;

    println!("{:?}", a_ptr.as_any().downcast_ref::<u32>().is_some());
    println!("{:?}", a_ptr.as_any().downcast_ref::<f32>().is_some());

    println!("{:?}", b_ptr.as_any().downcast_ref::<u32>().is_some());
    println!("{:?}", b_ptr.as_any().downcast_ref::<f32>().is_some());}

this outputs t, f, f, t, as expected.

Question: how does this work. There seems to be no 'space' in the f32/u32 to store a tag. Is the type stored in the &dyn MyFoo itself ?

EDIT: To the best of my current mental model, for the downcast to behave correctly, as it does above, we need to store some 'tag' somewhere. I don't see how there is space to store this tag in the u32 / f32 itself. Therefore, where is it stored? Is it stored in the &dyn MyFoo ?

Yes, this is one of the reason why a dyn Trait is unsized and so &dyn Trait is a fat pointer. One of the sub-pointers points to the "data" (the value of the original underlying type) itself, while the other one points to the vtable, which holds type information.

2 Likes

The TypeId isn't in the vtable as far as I know. But the Any trait has the type_id method, so the base type's TypeId is still available via method call.

Example.

2 Likes

Okay, let's see if we can build a less wrong mental model for this.

The dyn trait itself is going to look something like:

struct fake_dyn_Trait {
  data: void*;
  vtable: vtable*;
}

For

x: &dyn MyFoo

the downcast<u32> ends up being something like:

if (x.vtable.as_any().type_id() == Type_id::of::<u32>()) {
  return Some(x.data as *u32);
} else {
  return None;
}

Is this an approximate/plausible gist of what is going on ?

Indeed, there is no type information in the vtable itself, but it still at least uniquely identifies the type (my wording was intentionally vague because I wasn't sure exactly what usage of the vtable allows one to get to a concrete type). So based on that Any impl, it looks like that the type information is encoded in the body of the Any::type_id() method itself.

It's almost exactly like that – you can check the actual source code.

2 Likes

Yes, pretty much. Here's downcast_ref:

    pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
        if self.is::<T>() {
            // SAFETY: just checked whether we are pointing to the correct type, and we can rely on
            // that check for memory safety because we have implemented Any for all types; no other
            // impls can exist as they would conflict with our impl.
            unsafe { Some(&*(self as *const dyn Any as *const T)) }
        } else {
            None
        }
    }

And here's is:

    pub fn is<T: Any>(&self) -> bool {
        // Get `TypeId` of the type this function is instantiated with.
        let t = TypeId::of::<T>();

        // Get `TypeId` of the type in the trait object (`self`).
        let concrete = self.type_id();

        // Compare both `TypeId`s on equality.
        t == concrete
    }
1 Like

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.