Is a slice of transmutable elements transmutable into a slice of transmuted elements?

Let's say I have these definitions:

#[repr(transparent)]
struct Foo(String);

fn main() {
    let foos: &[Foo] = &[
        Foo("Foo 0".to_string()),
        Foo("Foo 1".to_string()),
        Foo("Foo 2".to_string()),
    ];
    
    let strings = unsafe { 
        std::mem::transmute::<_, &[String]>(foos)
    };
    println!("{}", strings.iter());
}

}

This compiles and gives the expected result. The question is:
Is the slice transmute from foos to strings itself sound?

In other words, if I know I can soundly perform std::mem::transmute<T, U>(some_t_value), does that imply that std::mem::transmute<&[T], &[U]>(slice_of_t_values) is also sound?

Rust does not guarantee that the transmute is sound, but if you use a raw pointer cast instead, then it is sound.

5 Likes

You’re transmuting between different types of slice reference, which is not allowed as far as I know, since the layout of fat pointers is not stable. I don’t see any arrays here (as part of the transmute) so the title is somewhat inaccurate. You should use a pointer cast instead.

#[repr(transparent)]
struct Foo(String);

fn main() {
    let foos: &[Foo] = &[
        Foo("Foo 0".to_string()),
        Foo("Foo 1".to_string()),
        Foo("Foo 2".to_string()),
    ];

    let strings = unsafe { &*(foos as *const [Foo] as *const [String]) };
    
    println!("{:?}", strings);
}

If you were asking about actual arrays instead, then e.g. transmuting &[Foo; 42] to &[String; 42] should be sound.

7 Likes

I would expect so, because [T] has a guaranteed memory layout: One contiguous block of memory, aligned for type T with a stride of size_of::<T>(). Because these values are the same for T and U, any pointer offset calculation for one tupe will also be valid for the other. Also, according to the Pointee docs¹, the metadata field of slices are guaranteed to be the number of items as a usize, which has the same interpretation for both types.

I’m not super familiar with the compiler internals, though, so I’ll defer to @alice and @steffahn who say that it isn’t.


¹ As this trait is nightly-only, what its documentation says might change in future compiler versions.

Fixed, thanks.
I actually wanted to know about both arrays and slices, but I kind of assumed that similar restrictions would hold for both, which appears to not be the case.

Good to know though, for slices there's pointer casts and for arrays a transmute will actually work.

The slice is not the problem, it’s the fat pointer (i.e. the reference) to the slice.

For example it’s not guaranteed that the address will come before the metadata part. Your argument however does correctly describe why that pointer cast is fine, and dereferencing / re-borrowing the pointer afterwards is sound, too.

If the metadata types wouldn’t match, the pointer cast would fail (with a compilation error). If the layouts of the slices themself would differ, dereferencing/using the cast pointer could be UB.


As a point of comparison, for &mut […] you’d need to go through *mut […] pointers and cast those; for Box<[…]> or Arc<[…]>/Rc<[…]>, you’d need to work with their from_raw/into_raw methods and cast the intermediate *const […] pointer.


Also of course, the guarantees could be strengthened to allow direct transmutes in the future.

6 Likes

The transmute doesn’t actually require this, only that all fat pointers with usize metadata within the binary are laid out the same. No such guarantee exists, so this is de jure UB, but it also feels nonsensical for the compiler to ever make different choices for these two cases in a single run— Depending on how many crates accidentally rely on this behavior, this may be de facto sound, as the language maintainers avoid ecosystem-breaking changes.