Why does downcasting using the Any trait not work when the type is transparent?

I was messing around with the Any trait and seeing what was possible. I had a theory that if I made a wrapper around a type and made the wrapper #[repr(transparent)] so they (from what I understand) have the same representation in memory, I would be able to create an instance of the wrapped type, and downcast to the wrapper type and it would be fine. Below is the code:

use std::any::Any;
use std::mem;

// A wrapper around a Vec<u8> that is transparent.
#[repr(transparent)]
#[derive(Debug)]
struct MyString {
    string: Vec<u8>
}

fn main() {
    // Sanity check to make sure the types do have the same repr in memory. (I know this doesn't prove they do but will prove it if they don't)
    println!("String size: {}, alignment: {}", mem::size_of::<MyString>(), mem::align_of::<MyString>());
    println!("Vec size: {}, alignment: {}", mem::size_of::<Vec<u8>>(), mem::align_of::<Vec<u8>>());
    // Create a Vec<u8>
    let vec: Vec<u8> = "JumboTrout".as_bytes().to_owned();
    // Convert to an Any trait object.
    let vec: Box<dyn Any> = Box::new(vec);
    // Pass vec to function that will downcast.
    print_any(vec);
}

fn print_any(any: Box<dyn Any>) {
    // Downcast the Vec<u8> to MyString
    let string = any.downcast::<MyString>().unwrap(); // panic!
    println!("{string}");
}

#[repr(transparent)]
#[derive(Debug)]
struct MyString {
    string: Vec<u8>
}

impl std::fmt::Display for MyString {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}", String::from_utf8(self.string.clone()).unwrap())
    }
}

This code panics when I try to unwrap after the attempt to downcast. This result was surprising because (I thought) the two types were identical in memory. If they are identical, shouldn't they effectively be the same type at runtime? It appears my understanding of the Rust memory model still needs work. What is going on here?

FWIW: I tried #[repr(C)] when this didn't work to see if that would change anything, and it crashed as well.

PS: I am just messing around. I like messing around and seeing what kind of "illegal" stuff I can do using memory manipulation. I am intentionally doing something very non-idiomatic.

No, they're still distinct types, and thus can have different methods, different implementations of traits, different vtables, etc. repr is about specifying the layout only.

Thanks for responding! How does the runtime "know" they aren't the same type if they have the same layout in memory? It seems to me that at runtime there would be no way to know.

Im sorry, I reread your reply a couple times and now I think I understand what you are saying. The vtables are screwing us up. Do you think there is a way to do some wizardry to make the vtables consistent. Like implementing identical methods and traits?

The closest thing Rust has to RTTI is TypeId, and that's what the Any trait uses. Different types get different IDs.

No. And the different types having different TypeIds is by design.

Consider something like NonZeroU8. It intentionally has the same layout as a u8, but has extra invariants -- it can never be 0. Unsafe code can rely on this. If you could downcast a type-erased u8 to a NonZeroU8, that would open a soundness hole.

1 Like

Ah... That explains everything. Thank you for the help!

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.