RFC 1758: repr-transparent

lang (data-types | repr | machine)

Summary

Extend the existing #[repr] attribute on newtypes with a transparent option specifying that the type representation is the representation of its only field. This matters in FFI context where struct Foo(T) might not behave the same as T.

Motivation

On some ABIs, structures with one field aren't handled the same way as values of the same type as the single field. For example on ARM64, functions returning a structure with a single f64 field return nothing and take a pointer to be filled with the return value, whereas functions returning a f64 return the floating-point number directly.

This means that if someone wants to wrap a f64 value in a struct tuple wrapper and use that wrapper as the return type of a FFI function that actually returns a bare f64, the calls to this function will be compiled incorrectly by Rust and the execution of the program will segfault.

This also means that UnsafeCell<T> cannot be soundly used in place of a bare T in FFI context, which might be necessary to signal to the Rust side of things that this T value may unexpectedly be mutated.

// The value is returned directly in a floating-point register on ARM64.
double do_something_and_return_a_double(void);
mod bogus {
    #[repr(C)]
    struct FancyWrapper(f64);

    extern {
        // Incorrect: the wrapped value on ARM64 is indirectly returned and the
        // function takes a pointer to where the return value must be stored.
        fn do_something_and_return_a_double() -> FancyWrapper;
    }
}

mod correct {
    #[repr(transparent)]
    struct FancyWrapper(f64);

    extern {
        // Correct: FancyWrapper is handled exactly the same as f64 on all
        // platforms.
        fn do_something_and_return_a_double() -> FancyWrapper;
    }
}

Given this attribute delegates all representation concerns, no other repr attribute should be present on the type. This means the following definitions are illegal:

#[repr(transparent, align = "128")]
struct BogusAlign(f64);

#[repr(transparent, packed)]
struct BogusPacked(f64);

Detailed design

The #[repr] attribute on newtypes will be extended to include a form such as:

#[repr(transparent)]
struct TransparentNewtype(f64);

This structure will still have the same representation as a raw f64 value.

Syntactically, the repr meta list will be extended to accept a meta item with the name "transparent". This attribute can be placed on newtypes, i.e. structures (and structure tuples) with a single field, and on structures that are logically equivalent to a newtype, i.e. structures with multiple fields where only a single one of them has a non-zero size.

Some examples of #[repr(transparent)] are:

// Transparent struct tuple.
#[repr(transparent)]
struct TransparentStructTuple(i32);

// Transparent structure.
#[repr(transparent)]
struct TransparentStructure { only_field: f64 }

// Transparent struct wrapper with a marker.
#[repr(transparent)]
struct TransparentWrapper<T> {
    only_non_zero_sized_field: f64,
    marker: PhantomData<T>,
}

This new representation is mostly useful when the structure it is put on must be used in FFI context as a wrapper to the underlying type without actually being affected by any ABI semantics.

It is also useful for AtomicUsize-like types, which RFC 1649 states should have the same representation as their underlying types.

This new representation cannot be used with any other representation attribute:

#[repr(transparent, align = "128")]
struct BogusAlign(f64); // Error, must be aligned like the underlying type.

#[repr(C, transparent)]
struct BogusRepr(f64); // Error, repr cannot be C and transparent.

As a matter of optimisation, eligible #[repr(Rust)] structs behave as if they were #[repr(transparent)] but as an implementation detail that can't be relied upon by users.

struct ImplicitlyTransparentWrapper(f64);

#[repr(C)]
struct BogusRepr {
    // While ImplicitlyTransparentWrapper implicitly has the same representation
    // as f64, this will fail to compile because ImplicitlyTransparentWrapper
    // has no explicit transparent or C representation.
    wrapper: ImplicitlyTransparentWrapper,
}

The representation of a transparent wrapper is the representation of its only non-zero-sized field, transitively:

#[repr(transparent)]
struct Transparent<T>(T);

#[repr(transparent)]
struct F64(f64);

#[repr(C)]
struct C(usize);

type TransparentF64 = Transparent<F64>; // Behaves as f64.

type TransparentString = Transparent<String>; // Representation is Rust.

type TransparentC = Transparent<C>; // Representation is C.

type TransparentTransparentC = Transparent<Transparent<C>>; // Transitively C.

Coercions and casting between the transparent wrapper and its non-zero-sized types are forbidden.

Drawbacks

None.

Alternatives

The only alternative to such a construct for FFI purposes is to use the exact same types as specified in the C header (or wherever the FFI types come from) and to make additional wrappers for them in Rust. This does not help if a field using interior mutability (i.e. uses UnsafeCell<T>) has to be passed to the FFI side, so this alternative does not actually cover all the uses cases allowed by #[repr(transparent)].

Unresolved questions