Transmuting types with repr(transparent) parameters

Does repr(transparent) allow transmuting between two types if their parameters are repr(transparent)? Basically is the following safe?

struct A<T>(T);

#[repr(transparent)]
struct B(*const ());
#[repr(transparent)]
struct C(*const ());

transmute::<A<B>, A<*const ()>>;
transmute::<A<C>, A<*const ()>>;
transmute::<A<B>, A<C>>;

The reason for the question is that I have a recursive data structure that is currently Arc based. However the need for Arc is only necessary for a few long lived values, for the shorter lived ones I would like to use arena allocation while still letting the arena allocated nodes refer to the Arc allocated nodes without any copying. Since repr(transparent) is gives the same layout and call convention I think this should be safe but I wasn't able to find out for sure that it applies for types with such types in type parameters.

use std::{fmt, marker::PhantomData, mem, ops::Deref, ptr::NonNull, sync::Arc};

// An "Arc" that points directly to the value instead of to the counters, followed by the value
#[repr(transparent)]
struct MyPtr<T>(NonNull<T>);

impl<T> MyPtr<T> {
    fn new(value: T) -> Self {
        unsafe {
            MyPtr(NonNull::new_unchecked(
                Arc::into_raw(Arc::new(value)) as *mut T
            ))
        }
    }
}

impl<T> Drop for MyPtr<T> {
    fn drop(&mut self) {
        unsafe {
            Arc::from_raw(self.0.as_ptr());
        }
    }
}

impl<T> Deref for MyPtr<T> {
    type Target = T;

    fn deref(&self) -> &T {
        unsafe { self.0.as_ref() }
    }
}

impl<T> Clone for MyPtr<T> {
    fn clone(&self) -> Self {
        unsafe {
            let a = Arc::from_raw(self.0.as_ptr());
            mem::forget((a.clone(), a));
            Self(self.0)
        }
    }
}

#[derive(Clone)]
#[repr(transparent)]
struct OwnedType(MyPtr<Type<OwnedType>>);

impl fmt::Debug for OwnedType {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:#?}", *self.0)
    }
}

impl OwnedType {
    fn new(value: Type<OwnedType>) -> Self {
        OwnedType(MyPtr::new(value))
    }
}

#[derive(Clone)]
#[repr(transparent)]
struct RefType<'a>(NonNull<Type<RefType<'a>>>, PhantomData<&'a ()>);

impl<'a> Deref for RefType<'a> {
    type Target = Type<RefType<'a>>;

    fn deref(&self) -> &Self::Target {
        unsafe { self.0.as_ref() }
    }
}

impl<'a> fmt::Debug for RefType<'a> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:#?}", **self)
    }
}

#[derive(Debug)]
enum Type<T> {
    Int,
    Function(T, T),
}

impl Type<OwnedType> {
    // Here is the transmute, the idea is that long lived `Arc` based values can be referred
    // to as plain references. Thereby allowing cheaply created arena allocated references to
    // transparently refer to long lived `Arc` references
    fn as_ref<'a>(&'a self) -> &'a Type<RefType<'a>> {
        unsafe { mem::transmute(self) }
    }
}

fn main() {
    let int = OwnedType::new(Type::Int);

    let f = Type::Function(
        OwnedType::new(Type::Function(int.clone(), int.clone())),
        int.clone(),
    );
    println!("{:#?}", f);
    println!("{:#?}", f.as_ref());

    let int2: Type<RefType> = Type::Int;
    let f2 = Type::Function(&int2, f.as_ref());
    println!("{:#?}", f2);
}

(Playground)

Output:

Function(
    Function(
        Int,
        Int
    ),
    Int
)
Function(
    Function(
        Int,
        Int
    ),
    Int
)
Function(
    Int,
    Function(
        Function(
            Int,
            Int
        ),
        Int
    )
)

Errors:

   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.57s
     Running `target/debug/playground`

On byte level, yes. You'll still have to pay attention to semantics of the types, so you don't transmute Zero to NonZero, cast away lifetimes, etc.

Is this guaranteed by the compiler, or is just that it happens to work like this at the moment.

Transparent layout is guaranteed by the compiler. That's the entire point of that feature.

1 Like

But struct A isn't marked transparent, is it still guaranteed to have repr(transparent) layout?

edit: I misunderstood the example. It is an interesting property (if true) that if B == C with regards to layout, then A<B> == A<C> is also guaranteed.

2 Likes

Yep, still other semantics to take into account, but for the explicit case of Arc-like to &-like that is fine (transmuting the other way around clearly isn't though).

I don't think that's actually guaranteed. You'd need A to be transparent too.

:dizzy_face: Really !?

:thinking: I can't see how that could be the case:

The only ways A can depend on its parameter <T> are:

  • PhantomData<T>, in which case all A<T> have the same layout;

  • *const T / *mut T / &'a T / &'a mut T (where T : 'a), in which case if B and C have the same "sizedness" the layout is the same,

  • plain T; in which case

    if T = B and #[repr(transparent)] struct B /* = */ (C), a value of type B has the exact same size / alignment / layout representation as a value of type C (i.e. B and C are indistinguishable!), and thus two structs containing either one are indistinguishable too;

  • some struct A'<T>, in which case we can recurse the reasoning into A'


EDIT (from almost two years later): indeed, having A be a #[repr(transparent)] wrapper around B (or the other way around) does not suffice to allow transmuting any T<A> to/from a T<B> (for instance, it is incorrect to do transmute::<Vec<A>, Vec<B>). For some special choices of T<_>, however, it is valid, such as arrays or function pointers provided the A/B type parameter is directly present in the function signature (as a function parameter or in return position).

That's not correct, and the proposed rule does not hold in all cases. Here is a counterexample where B and C have identical representation but A<B> and A<C> do not, despite A being transparent.

trait AssocType {
    type Item;
}

struct B;
struct C;

#[repr(transparent)]
struct A<T: AssocType>(T::Item);

impl AssocType for B {
    type Item = ();
}

impl AssocType for C {
    type Item = String;
}

fn main() {
    println!("size_of A<B> = {}", std::mem::size_of::<A<B>>());
    println!("size_of A<C> = {}", std::mem::size_of::<A<C>>());
}

Playground

1 Like

You have an implicit assumption that layout is deterministic for repr(Rust), and there's no guarantee of that. To the contrary, there are people actively advocating for randomized layout to be legal as a way to help catch mistaken assumptions.

3 Likes

I see. I had indeed such an assumption, since randomizing layouts seems really counterintuitive to me. Can you link me to discussions on this topic ? Thanks.

https://github.com/rust-lang/unsafe-code-guidelines/issues/35

(And possible other places in that repo.)

1 Like

For what it's worth, the goal is not to randomize layouts -- it's to keep layout unspecified unless you specifically require it, to allow future optimizations. For example, packing fields into the least bytes regardless of the order you wrote them in. (Rustc did not do this in the early days, but does now.)

Randomizing layouts is a cute trick to catch places where programs unintentionally rely on layout.

2 Likes

Since it is possible to transmute between &mut [T] and &[Cell<T>] but arguably that is a special case since [T] is a "pointer" rather than a struct/enum.

#[repr(C)]
struct A<T>(T);

#[repr(transparent)]
struct B(*const ());
#[repr(transparent)]
struct C(*const ());

transmute::<A<B>, A<*const ()>>;
transmute::<A<C>, A<*const ()>>;
transmute::<A<B>, A<C>>;

I guess if repr(C) were specified on A then this would actually be safe since the layout is guaranteed to be the same then. Since I need an enum I don't believe that would work (though repr(C) doesn't error/warn on enums it seems).

EDIT: Actually, it seems that repr(C) is intended to work with non-C-like enums so I annotating with that should be enough to make this sound even in the enum case https://github.com/rust-lang/rfcs/pull/2195 .

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