History of the question:
Prior to RFC 1758, Rust lacked a mechanism for zero-cost newtypes in FFI. Developers relied on #[repr(C)], which imposes deterministic layout but may introduce unnecessary padding, or #[repr(Rust)], which permits aggressive compiler optimizations like field reordering and niche exploitation. This created a fundamental dilemma: enforcing type safety through wrapper structs versus guaranteeing ABI stability for foreign function calls. #[repr(transparent)] was introduced specifically to resolve this tension by promising that a struct containing exactly one non-zero-sized field possesses an identical memory layout, alignment, and calling convention to that underlying field.
The problem:
When a #[repr(Rust)] newtype is passed by reference or value to a foreign function expecting the raw inner type (e.g., a u32 handle), the compiler remains free to reorder the wrapper's fields or apply niche optimizations. Because #[repr(Rust)] offers no stability guarantees, the wrapper might acquire a different size, bit-pattern validity, or padding than the inner type. This causes the foreign C code to potentially read misaligned memory, interpret invalid bit patterns as valid pointers, or access garbage data, resulting in immediate undefined behavior and catastrophic memory corruption at the boundary.
The solution:
#[repr(transparent)] instructs the compiler to enforce that the wrapper and its single non-zero field share identical size, alignment, and ABI, effectively making the wrapper a compile-time-only abstraction. The compiler statically verifies that exactly one field has non-zero size (permitting additional PhantomData or unit type fields). This allows the wrapper to be safely transmuted to the inner type or passed directly across FFI boundaries without conversion overhead, as demonstrated below:
#[repr(transparent)] pub struct SocketFd(i32); extern "C" { fn close_socket(fd: i32); } pub fn close(sock: SocketFd) { // Safe: SocketFd has identical ABI to i32 unsafe { close_socket(sock.0); } }
A developer integrates a Rust application with a Linux kernel netlink socket API, which communicates via raw integer file descriptors. To prevent accidental mixing of socket types, they define struct NetlinkSocket(i32) as a newtype. Initially marked with #[repr(Rust)], they pass references to NetlinkSocket to an extern "C" callback expecting a pointer to i32. During local development, this appears to function correctly, but in release builds utilizing LTO (Link-Time Optimization), the compiler applies aggressive niche optimization to NetlinkSocket, fundamentally altering its memory representation. The C kernel module subsequently receives a corrupted pointer value, triggering a critical kernel panic.
Three distinct solutions were evaluated. First, #[repr(C)] was considered to enforce a stable, deterministic layout. While this ensured memory safety, it disabled beneficial niche optimizations and potentially introduced padding bytes, unnecessarily bloating the struct size and complicating the API surface for purely Rust-internal usage.
Second, manually dereferencing the inner field (socket.0) at every FFI call site was attempted. This approach avoided layout assumptions but proved highly error-prone and verbose, effectively breaking the abstraction barrier and allowing raw, untyped integers to propagate unchecked throughout the codebase.
Third, #[repr(transparent)] was applied to NetlinkSocket. This guarantee ensured ABI equivalence with i32 while preserving the type distinction within Rust, allowing the struct to be passed seamlessly to C without manual unwrapping or conversion logic.
The engineering team ultimately adopted #[repr(transparent)], which completely eliminated the kernel panics while maintaining a zero-cost abstraction. The wrapper now serves as a rigorous compile-time guard within Rust while remaining entirely invisible and compatible with the C ABI.
Why does #[repr(transparent)] explicitly forbid the single non-zero field from being a zero-sized type, and how does this restriction prevent undefined behavior in FFI when passing by value?
#[repr(transparent)] guarantees that the wrapper is ABI-identical to its inner type. A Zero-Sized Type (ZST) possesses size zero and alignment 1. If the wrapper were permitted to wrap exclusively a ZST, the resulting struct would itself be zero-sized; however, C lacks zero-sized types and its calling conventions typically expect at least one byte of data for "pass by value" semantics. Passing a ZST by value across FFI constitutes undefined behavior because C cannot represent or properly handle zero-sized values. This restriction ensures that the wrapper always maintains the same non-zero size and alignment as its underlying field, preserving a well-defined ABI compatible with C's expectations.
Can #[repr(transparent)] be applied to enums, and what constraints govern the discriminant's visibility across FFI boundaries?
Yes, #[repr(transparent)] can be applied to enums containing exactly one variant, which itself must contain exactly one non-zero-sized field. The enum must also specify an explicit primitive representation (e.g., #[repr(u8)]) to define the discriminant type. However, #[repr(transparent)] guarantees that the final layout is identical to the non-zero field, effectively eliding the discriminant from the ABI. Consequently, passing such an enum to C as the underlying field type is safe, but attempting to access or interpret a discriminant value from C results in undefined behavior. Candidates frequently misunderstand that the discriminant is physically absent from the layout, not merely hidden or inaccessible.
How does the presence of PhantomData<T> as an additional field in a #[repr(transparent)] struct influence variance and drop-checking without affecting the ABI?
PhantomData<T> is explicitly permitted as a secondary field within #[repr(transparent)] structs because it is zero-sized with alignment 1. While it does not alter the size, alignment, or ABI of the wrapper (since #[repr(transparent)] considers only the single non-zero field for layout), it crucially informs the compiler of the structural relationship to the type parameter T. This affects variance: for example, a struct Wrapper<T>(*const T, PhantomData<fn(T)>) will be contravariant over T due to the PhantomData marker. Additionally, it enables the Drop Check (dropck) analysis to recognize that the struct may conceptually own data of type T, preventing unsoundness when T carries non-'static lifetimes. Candidates often mistakenly believe PhantomData affects memory layout or ignore its essential role in maintaining lifetime and ownership invariants for generic FFI wrappers.