Safe transmute for transparent struct

For transparent struct, the transmute operation is commonly used, and safe enough.

But transmute is marked as unsafe, so unsafe block must be used. It would be much more pleasant if it could support the following syntax (or similar).

#[repr(transparent)]
pub struct I32(i32);

fn foo() {
    // use std::mem::transmute;

    let i = I32(123);
    let _ = i as i32; // same as: let _: i32 = unsafe { transmute(i) };
    
    let i = &mut I32(123);
    let _ = i as &mut i32; // same as: let _: &mut i32 = unsafe { transmute(i) };
    
    use std::pin::Pin;
    let i = &mut I32(123);
    let i = Pin::new(i);
    let _ = i as Pin<&mut i32>;// same as: let _: Pin<&mut i32> = unsafe { transmute(i) };
}
1 Like

I am strongly against expanding as casts to something new like this. As to the transmute, it is important to keep in mind that newtypes can impose safety invariants on the inner type. For this reason a safe transmute that is based solely on its repr would not be sound.

16 Likes

For example, NonNull and the NonZero* integers are all transparent, and we can't let it be safe to make them zero.

6 Likes

My focus is not on as, but on having a safe transmute for transparent struct.

Besides #[repr(transparent)] you would need some other annotation to assert that this transmute is always safe, something like #[all_values_are_valid] or something. This annotation would interact with unsafe code but I think it wouldn't be unsafe per se

1 Like

Even being transparent is sometimes an implementation detail and not a layout guarantee to consumers. See also the discussion in

2 Likes

Is there a reason not to make the wrapped field pub? That seems closest to “safe transmute for transparent struct” if you don’t need generics (which as wouldn’t do either). It clearly demonstrates that there’s no safety invariant and that it’s okay to access the field directly. It doesn’t quite cover your Pin use case, but the others would be fine.

6 Likes

Maybe it is a good idea to add the Transparent trait and add safe transparent_transmute() function

Maybe it is a good idea to add the Transparent trait and add safe transparent_transmute() function

I agree if it can be derived automatically.

1 Like

Ideally this should be handled by project-safe-transmute, though I haven't see progress on it recently.

3 Likes

In general, allowing to cast &mut Wrapper<T> to &mut T is unsound, since Wrapper<T> can impose extra invariants. For example,

#[repr(transparent)]
struct NotNull(u32);

let mut not_null = NotNull(1);
{
    let inner: &mut u32 = &mut not_null as &mut u32;
    *inner = 0;
}
assert_eq!(not_null.0, 0); // BOOM

For similar reasons, we shouldn't generally allow casting Pin<&mut Wrapper<T>> to Pin<&mut T>. Casting a &Wrapper<T> to &T is probably sound, but only if T doesn't have interior mutability, otherwise we have the same issue.

That said, currently it is impossible to safely implement a conversion between &mut T and &mut Wrapper<T>, even if Wrapper<T> is a trivial wrapper without any extra semantics, e.g. if it's used just to sidestep the orphan rules. For this reason some sort of similar feature would imho be desirable. It just should be exposed as a way to safely implement the relevant conversions for the wrapper's author, rather than a free-for-all conversion on #[repr(transparent)] types.

4 Likes

These sorts of conversions shouldn't exactly be based on repr(transparent), that provides more guarantees than required (namely that converting fn(T) -> fn(U) and vice-versa is valid if U is a transparent wrapper of T). Transmuting the value simply requires that they have compatible layouts.

Do these any of these structs have compatible layouts given that randomizing member order is allowed?

struct A { x: f64, y: f64 }
struct B { y: f64, x: f64 }
struct C { a: f64, b: f64 }

Same question if they all have #[repr(C)] on them. Basically, do names matter? Do the declaration order of the names matter?

In a very loose sense, they are compatible, but the semantics may still be very different (imagine if A is Euclidian and B is Polar).

Given that, what kind of syntax would exist to say that A and B are compatible? If they're in different crates, which is allowed to make the declaration (presumably the one that declares one and can "see" all others)? Is this property transitive where if A and B are declared compatible and A and C are compatible that B and C are now compatible?

This is not guaranteed. -Zrandomize-layout will reorder fields and insert random padding.

Yes, they are guaranteed to be layout compatible.

No, they don't matter for #[repr(C)].

1 Like

This doesn't matter for validity, just for safety and avoiding logical bugs (and has semver implications). Which is part of why project-safe-transmute is so complicated, it's not just about whether the transmute is UB or not.

In fact, repr(transparent) both gives more guarantees and less guarantees than needed, as evidenced by NonNull, it guarantees that for the subset of shared valid values the ABI is the same (which requires that for those values the layout is also the same), but says nothing about the cases where rustc_scalar_valid_value is used to restrict validity of the wrapper.

6 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.