*const as *mut compiles but *const as *const does not

Is this expected?

use std::mem::ManuallyDrop;

fn main() {}

trait SomeTrait {}

// Compiles
fn func_1(x: *const ManuallyDrop<dyn SomeTrait>) -> *mut dyn SomeTrait { x as _ }

// Does not compile
fn func_2(x: *const ManuallyDrop<dyn SomeTrait>) -> *const dyn SomeTrait { x as _ }

Wow, do you happen to know how that first coercion happens? I have no clue how it manages to get the inner dyn Trait out of the ManuallyDrop via an as cast.

Ahhhhh duhhhhh I’m an idiot, it’s just a normal pointer cast

I see a suspicious #[rustc_pub_transparent] on ManuallyDrop

Edit: :ferris_clueless: struct Foo<T: ?Sized>(T) has the same effect

…and that reminded me to look beyond the fact that ManuallyDrop<T>: Deref<Target = T>

I’m too used to focusing on cursed coercions to remember the obvious ones :expressionless:

It doesn't really get anything out of anything. The pointer stays the same (as all pointer casts) and the metadata stays the same (the metadata for ManuallyDrop<dyn Trait> is still DynMetadata<dyn Trait>). It ends up being pretty much a transmute of the pointer (although mem::transmute is not guaranteed to work AFAIK).

In nightly land it would be the equivalent of the following:

fn func_1(x: *const ManuallyDrop<dyn SomeTrait>) -> *mut dyn SomeTrait {
    let ptr = x as *const () as *mut ();
    let metadata = core::ptr::metadata(x);
    core::ptr::from_raw_parts_mut(ptr, metadata)
}

Anyway the second fails for a straightforward reason. It could be either an unsizing coercion or a pointer cast.

Or maybe it’s more accurate to say that the pointer cast is to some *const T such that T unsizes to dyn SomeTrait. And the compiler doesn’t know which to pick. (Or, from the error message, maybe it does try and it’s choosing wrong?)

There’s probably other similar possible sequences of coercions that I haven’t thought of yet.

I suggest you report a bug. Though if you ask me, there probably is a bug, but it's that the *const as *mut is accepted and not that *const as *const is not, as casting between different trait-object-unsized types is generally disallowed.

The reason for the bug is that the casts are handled differently in the compiler; we've had such bugs in the past.

I agree with @chrefr it should not be allowed. I wonder if it would be a breaking change to fix though.

#[allow(unused)]
fn main() {
    trait SomeTrait { fn pr(&self); }
    struct Foo<T: ?Sized>(u8, T);
    impl SomeTrait for u64 {
        fn pr(&self) { println!("{self}"); }
    }
    let a: Foo<u64> = Foo(0, 1);
    let b = &a as &Foo<dyn SomeTrait>;
    let c = b as *const Foo<dyn SomeTrait>;
    println!("{:p}", c); // try comment out
    let d = c as *mut dyn SomeTrait;
    println!("{:p}", d); // try comment out
    unsafe { (*d).pr(); }
}

My take is basically unsizing coercion is taking a strong priority for *mut to *mut or *const, and for *const to *const.

impl<T, U> CoerceUnsized<*const U> for *const T
where
    T: Unsize<U> + ?Sized,
    U: ?Sized,

impl<T, U> CoerceUnsized<*const U> for *mut T
where
    T: Unsize<U> + ?Sized,
    U: ?Sized,

impl<T, U> CoerceUnsized<*mut U> for *mut T
where
    T: Unsize<U> + ?Sized,
    U: ?Sized,

But there is no *const to *mut coercion (via unsizing or directly); that has to be a ptr-to-ptr cast.

I vaguely thought I'd seen this before but didn't find an issue. I agree it's worth it to file one for visibility if nothing else. (Edit: It's here.)


Here's a workaround.

fn helper_c<T: ?Sized>(m: *const ManuallyDrop<T>) -> *const T {
    m as _
}
fn helper_m<T: ?Sized>(m: *mut ManuallyDrop<T>) -> *mut T {
    m as _
}

pub fn func_mm(x: *mut ManuallyDrop<dyn SomeTrait>) -> *mut dyn SomeTrait {
    helper_m(x)
}
pub fn func_mc(x: *mut ManuallyDrop<dyn SomeTrait>) -> *const dyn SomeTrait {
    helper_c(x)
}
pub fn func_cm(x: *const ManuallyDrop<dyn SomeTrait>) -> *mut dyn SomeTrait {
    x as _
}
pub fn func_cc(x: *const ManuallyDrop<dyn SomeTrait>) -> *const dyn SomeTrait {
    helper_c(x)
}

Found it.

Discussed and FCP'd here,[1] though given the workarounds, I don't think any newly possible coercions were allowed by the PR per se (just an improvement on how you can do the coercions). So maybe the real FCP on metadata compatible casts is elsewhere.

you can cast between two wide raw pointers so long as the metadata is the same vtable in both", roughly speaking?

Yes. I consider this to be fixing an implementation bug, since we were previously preferring to attempt an "unsize" coercion even if it didn't work. This PR just fixes the pointer unsizing check to not commit to something that'll certainly cause an error, which will allow it to fall into the check for metadata compatibility. We already, for example, allow casting *const dyn Tr to *const W<dyn Tr>.


  1. that PR was reverted, but for performance reasons ↩︎

Okay, thanks for finding it. Not filling an issue then since this one exists.

I don't see why that should be disallowed, if the metadata is the same between the two pointees then casting is just a noop.