What is the result of converting a pointer to a sized type to a pointer to an unsized type?

Consider this example:

fn main() {
    use core::fmt::Debug;
    let i = 1;
    let ptr = &i as * const i32;
    let ptr = ptr as *const dyn Debug;  // #1
    unsafe {
        println!("{:?}", &*ptr); 
    }
}

What is the result of #1? The Rust reference is unclear at this point. Is the result similar to what with_metadata_of defines?

This operation is semantically the same as creating a new pointer with the data pointer value of self but the metadata of meta

That is, the resulting fat pointer will have its data address &i as * const i32 and metadata of dyn Debug?

1 Like

I believe this is the relevant paragraph from the reference:

However, this does not specify what the resulting pointer composes.

What else would it have? A dyn pointer always is composed of the address of the data and the address of the vtable. So in this case it would be a pointer to the variable i and the vtable of the i32 implementation of Debug.

The comparison with with_metadata_of is a bit misleading i think, as it doesn't take the metadata of another pointer. The metadata didn't exist before, the compiler just creates a pointer to the vtable. This is what unsized coercion means. The compiler creates some metadata that is correct for this usecase. Be it a size or a vtable pointer.

1 Like

A trait object pointer consists of a pointer to the instance and a pointer to a vtable of the type:

I recall a topic here where it was stated that vtables for a type T are not unique but might get duplicated per codegen unit, so two instances of T might have different vtable pointers, if I recall correctly. I'll try to see if I can find the topic again.

Edit: Found the topic again:

It has to be a wide pointer with a valid vtable for the trait as per the now-stabilized safety requirement of supertrait upcasting.

(It's always been the case no matter the lack of official docs, but this is a way to more formally conclude it must be so.)

Last paragraph before the examples section here.

2 Likes

the casting of raw pointers with as is (mostly) the same as casting safe, borrowed pointers, i.e., unsize-coercing &i32 to &dyn Debug: the compiler will create the metadata for the fat pointer based on the type information of the source pointer. this has some implications, e.g.:

  • for thin-to-fat pointer casts, this is same as coercion of "normal" references, the source pointee type (i32 in this example) must implement Unsize<DST>, (DST is dyn Debug in this example).

  • for fat-to-fat pointer casts, there's no equivalent in reference coercions, only for raw pointers. the the source and target types must have the same metadata, e.g. a struct use another DST as the last field.

    an example

    here's an example to manually allocate memory for DST:

    struct Foo {
        // other fields omitted
        payload: [u8],
    }
    fn allocate_foo(payload_len: usize) -> *mut Foo {
        let header: Layout = todo!();
        let payload = Layout::array::<u8>(payload_len);
        let foo_layout = header.extend(payload);
        // allocate memory, this is only the address, i.e. thin, no metadata
        let addr = alloc_zeroed(foo_layout);
        // manually assemble a fat pointer of byte slice, `payload_len` is the metadata
        // slices are special case of DST, which can be done in stable
        // other DSTs needs nightly to use the pointer metadata API
        let ptr: *mut [u8] = std::ptr::slice_from_raw_parts(addr, payload_len);
        // then cast it to `*mut Foo`, this is a fat-to-fat cast
        ptr as *mut Foo
    }
    
  • if neither of the above condition are met, the cast would be rejected by the type checker.

    examples
    /// thin-to-fat error: cannot cast thin pointer to fat pointer
    fn thin_to_fat_bad1(ptr: *mut ()) -> *mut str {
        ptr as _
    }
    
    /// thin-to-fat error: `()` doesn't implement `Display`
    fn thin_to_fat_bad2(ptr: *mut ()) -> *mut dyn Display {
        ptr as _
    }
    
    /// fat-to-fat error: metadata mismatch
    fn fat_to_fat_bad1(ptr: *mut dyn Debug) -> *mut str {
        ptr as _
    }
    /// fat-to-fat error: metadata mismatch
    fn fat_to_fat_bad2(ptr: *mut dyn Debug) -> *mut dyn Display {
        ptr as _
    }
    

NOTE:

the cast is accepted by the compiler doesn't mean the resulted pointer is valid. dereferencing a raw pointer needs unsafe anyway.

in contrast, the with_metadata_of() API allows any form of pointer casts, whether it makes sense or not. e.g.

#![feature(set_ptr_value)]

fn thin_to_fat_nonsense1(ptr: *mut ()) -> *mut str {
	ptr.with_metadata_of("wat?")
}
fn thin_to_fat_nonsense2(ptr: *mut ()) -> *mut dyn Display {
	ptr.with_metadata_of(&mut 42i32)
}
fn fat_to_fat_nonsense1(ptr: *mut dyn Debug) -> *mut [u32] {
	ptr.with_metadata_of(&mut [])
}
fn fat_to_fat_nonsense2(ptr: *mut dyn Debug) -> *mut dyn Display {
	ptr.with_metadata_of(&mut true)
}

with_metadata_of takes the metadata of another pointer that you pass to the function. That is, you already need to have a pointer with the value of the metadata you need.

On the other hand dyn Debug is a type, not a pointer, and it does not define a value for its metadata (what would be the point of the metadata if it was always the same after all). Instead, it defined the type of its metadata, which is std::ptr::DynMetadata<dyn Debug>.

The actual value used for the metadata is mostly an implementation detail, but is dependent on the type of the pointer that you're converting. In this case the pointer points to a i32, so the value of std::ptr::DynMetadata<dyn Debug> for the i32 type will be used.

If you want to see an analogous of this operation in the stdlib there is std::ptr::from_raw_parts, but that does not include the process of creating the required metadata value (you'll need to provide it as an input).