Why is using Box::into_raw to downcast from `dyn Any` to `T` not considered UB?

Hello. I've come across two examples that are confusing me. First is used by the anymap crate, another is in the std lib itself.

Consider the following from the popular anymap crate (Self is dyn Any):

unsafe fn downcast_unchecked<T: 'static>(self: Box<Self>) -> Box<T> {
    Box::from_raw(Box::into_raw(self) as *mut T)
}

or even:

unsafe fn downcast_ref_unchecked<T: 'static>(&self) -> &T {
    &*(self as *const Self as *const T)
}

This is used to downcast types from dyn Any to some T.

The similar example using std, copied from the docs:

use std::any::Any;

fn print_if_string(value: Box<dyn Any>) {
    if let Ok(string) = value.downcast::<String>() {
        println!("String ({}): {}", string.len(), string);
    }
}

let my_string = "Hello World".to_string();
print_if_string(Box::new(my_string));
print_if_string(Box::new(0i8));

Both use Box::into_raw on a Box<dyn Any> and then converts the raw ptr to a Box<T> using Box::from_raw (and the 2nd example from anymap does something similar through unsafe casting).

Now, the reason I'm confused about this is because I know that a ptr to a dyn Trait is a fat ptr, of completely different size to a ptr to a concrete T (double the size). And I also know for a fact that the fat ptr layout is unspecified. Who's to say the fat ptr won't change so that the first aligned ptr will actually point to the vtable? Isn't this also leaking memory because we're completely forgetting the vtable ptr in the fat ptr?

For me, this sounds the exact same as the following:

std::mem::transmute::<Box<dyn Any>, Box<T>>(b)

which actually doesn't compile because of different sizes (16 vs 8). But also similar to this which compiles:

std::mem::transmute::<&Box<dyn Any>, &Box<T>>(b)

^ But this to me looks like UB, depending on the fact that the first aligned ptr of a fat ptr is the one that pts to T (but unlike the consuming downcast, doesn't suffer from a mem leak).

So my question is, how is this not depending on UB, especialy for anymap. Because for the std lib case one can claim that it's exactly because it's the std lib it has the right to depend on unspecified spec (fat ptr layout). But even then, it doesn't explain the mem leak I think I'm seeing (from just reasoning about the code).

Am I missing something?

2 Likes

That's what the as does. The as casts are not transmute – they usually do something other than reinterpretation of bytes. If as were the same as transmute, then it would make no sense to have both in the language.

For example, some_int as f64 doesn't just copy the bits, it manipulates the representation so you get the same numeric value that you started with. Likewise, fat_ptr as *mut Concrete concrete throws away the vtable pointer and returns a thin pointer to the data only.

On some occasions, you'll see people asking questions in which they transmute pointers, and you'll also occasionally see me answer that they shouldn't do that, for precisely this reason (and a whole other series of reasons, too).

No. Vtables are statically allocated, as there is exactly one vtable per type. There's no need to allocate a separate vtable for every instance.

5 Likes

Explicit casting using as, like

potentially_wide_ref as *const _ as *const SomethingThin

will give you the data pointer, and never accidentally use the wide-pointer metadata. It's a language level operation. Remember, you can also cast

&something_thin as &dyn Whatever

and the language will conjure up the metadata required to form the wide reference, even though it's a different layout than &something_thin.


Incidentally, dyn Trait is also a "concrete T", even though it's not Sized and requires a wide pointer. (So are str, [U], and other unsized types.) Although dynamically sized and used to enable dynamic dispatch, dyn Trait is a static type.


One word of warning on top of @H2CO3's vtable explanation: you can't count on the vtables not being duplicated (in static memory), either. At least not currently.

5 Likes

That's what I was missing :slight_smile:
Never even doubted the as operator. Makes perfect sense now.

Even if, I was talking about the ptr (just the usize, not the vtable itself), but I understand what you mean.

Thanks!

Should have suspected as had I remembered this particular example. Thanks for expanding on it.

1 Like

I'm not sure I get that. The pointer itself isn't heap-allocated either, it doesn't leak memory.

1 Like

You're very right. In my confusion, since I thought the as was similar to the following transmute (this doesn't compile but assuming it does):

transmute<Box<dyn Any>, Box<Foo>>(b)

If this did compile, I'm not even sure if the stack would have been able to deal with this properly without crashing? But either way, it's not leaking memory since it's on the stack, you're right. Thanks.

1 Like