Memory Layout and Representation

#[repr(transparent)]

#[repr(transparent)] is a representation attribute for single-field structs and single-variant enums that guarantees the wrapper type has the exact same memory layout and function-call ABI as its single non-zero-sized inner field.

Structural Requirements

A type annotated with #[repr(transparent)] must satisfy two rules:

  1. It must contain at most one field with non-zero size.
  2. Any additional fields must be zero-sized types (ZSTs) with alignment 1 (such as PhantomData<T>, PhantomPinned, or ()).

If a second non-zero-sized field or a ZST with alignment greater than 1 is present, compilation fails with error E0690 or E0691.

// ❌ does not compile
#[repr(transparent)]
struct InvalidWrapper {
    id: u64,
    extra: u8,
}

fn main() {}
error[E0690]: transparent struct needs at most one non-zero-sized field, but has 2
 --> src/main.rs:3:1
  |
3 | struct InvalidWrapper {
  | ^^^^^^^^^^^^^^^^^^^^^ needs at most one non-zero-sized field, but has 2
4 |     id: u64,
  |     ------- this field is non-zero-sized
5 |     extra: u8,
  |     --------- this field is non-zero-sized

Layout and ABI Identity

Without #[repr(transparent)], a single-field #[repr(Rust)] struct (struct Wrapper(f64);) happens to have the same size and alignment as f64, but the compiler does not guarantee that it is passed in the same CPU register across extern "C" FFI boundaries. With #[repr(transparent)], the wrapper is ABI-identical to its inner type:

  • If the inner type f64 is passed in floating-point register xmm0 / d0, #[repr(transparent)] struct Meters(f64) is also passed in xmm0 / d0.
  • References &Inner and &Wrapper (or slices &[Inner] and &[Wrapper]) can be safely converted via pointer casts because their memory layouts are guaranteed identical.
  • Any niches present in the inner type (such as NonNull<T> or NonZeroU64) are preserved in the outer #[repr(transparent)] type.
use std::marker::PhantomData;
use std::num::NonZeroU64;

struct UserTag;

#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct EntityId<Tag> {
    raw: NonZeroU64,
    _tag: PhantomData<Tag>,
}

impl<Tag> EntityId<Tag> {
    fn new(value: u64) -> Option<Self> {
        NonZeroU64::new(value).map(|raw| Self {
            raw,
            _tag: PhantomData,
        })
    }
}

fn main() {
    println!("size of EntityId<UserTag>:          {} bytes", size_of::<EntityId<UserTag>>());
    println!("size of Option<EntityId<UserTag>>:  {} bytes", size_of::<Option<EntityId<UserTag>>>());

    let id = EntityId::<UserTag>::new(99).unwrap();
    println!("id = {:?}", id.raw);
}
size of EntityId<UserTag>:          8 bytes
size of Option<EntityId<UserTag>>:  8 bytes
id = 99

Safe Reference Casting Pattern

Because #[repr(transparent)] guarantees identical layout between Wrapper and Inner, a shared reference &Inner can be converted to &Wrapper using a pointer cast:

#[repr(transparent)]
#[derive(Debug)]
struct AsciiStr(str);

impl AsciiStr {
    fn from_str(s: &str) -> Option<&Self> {
        if s.is_ascii() {
            // SAFETY: `AsciiStr` is `#[repr(transparent)]` over `str`,
            // and `s.is_ascii()` was verified above.
            Some(unsafe { &*(s as *const str as *const AsciiStr) })
        } else {
            None
        }
    }

    fn as_str(&self) -> &str {
        &self.0
    }
}

fn main() {
    let valid = AsciiStr::from_str("HELLO_123").unwrap();
    let invalid = AsciiStr::from_str("Héllo");

    println!("valid:   {}", valid.as_str());
    println!("invalid: {:?}", invalid);
}
valid:   HELLO_123
invalid: None