Memory Layout and Representation

#[repr(C)]

#[repr(C)] instructs the compiler to lay out a struct, enum, or union according to the target platform's C ABI (Application Binary Interface). It disables field reordering and makes the memory layout deterministic and interoperable with C, C++, and foreign function interfaces (FFI).

#[repr(C)] on Structs

For a struct #[repr(C)] struct S { f1: T1, f2: T2, ..., fn: Tn }, the layout follows three deterministic steps:

  1. Alignment: The struct alignment is the maximum alignment of all its fields ($\max_i \text{align}(T_i)$).
  2. Sequential offsets: f1 starts at offset 0. Each subsequent field fi starts at the smallest offset greater than or equal to the end of f(i-1) that is a multiple of $\text{align}(T_i)$.
  3. Total size: The total size is rounded up to the smallest multiple of the struct's alignment.
use std::mem::offset_of;

#[repr(C)]
struct CPoint3D {
    tag: u8,
    x: f64,
    y: f64,
    z: f64,
}

fn main() {
    println!("size:  {} bytes", size_of::<CPoint3D>());
    println!("align: {} bytes", align_of::<CPoint3D>());
    println!(
        "offsets: tag=@{}, x=@{}, y=@{}, z=@{}",
        offset_of!(CPoint3D, tag),
        offset_of!(CPoint3D, x),
        offset_of!(CPoint3D, y),
        offset_of!(CPoint3D, z),
    );
}
size:  32 bytes
align: 8 bytes
offsets: tag=@0, x=@8, y=@16, z=@24

Zero-Sized Fields in #[repr(C)] Structs

In standard C, an empty struct (struct Empty {};) is either illegal (ISO C) or has size 1 (C++ and GNU C extension), whereas in Rust a #[repr(C)] struct with no fields—or zero-sized fields like PhantomData<T>—still occupies 0 bytes. When matching a C++ empty struct across FFI, include an explicit _pad: u8 field.

#[repr(C)] on Fieldless Enums

On a fieldless enum, #[repr(C)] sets the discriminant representation to the target platform's default C enum integer size (typically c_int, which is 4 bytes on 32-bit and 64-bit targets).

#[repr(C)]
enum CCodec {
    Raw = 0,
    Lz4 = 1,
    Zstd = 2,
}

fn main() {
    println!("size of CCodec:  {} bytes", size_of::<CCodec>());
    println!("align of CCodec: {} bytes", align_of::<CCodec>());
}
size of CCodec:  4 bytes
align of CCodec: 4 bytes

#[repr(C)] on Data-Carrying Enums

When #[repr(C)] is applied to an enum with fields, the compiler represents it as a #[repr(C)] struct containing two fields:

  1. A C-enum discriminant tag.
  2. A #[repr(C)] union containing a #[repr(C)] struct for each variant's payload.
#[repr(C)]
enum CValue {
    Int(i32),
    Pair(f64, f64),
}

Equivalent C Layout (24 bytes, align 8):
struct CValue {
    int32_t tag;         // offset 0..4 (+ 4 bytes padding at 4..8)
    union {
        struct { int32_t _0; } Int;
        struct { double _0; double _1; } Pair;
    } payload;           // offset 8..24
};
#[repr(C)]
enum CValue {
    Int(i32),
    Pair(f64, f64),
}

fn main() {
    println!("size of CValue:  {} bytes", size_of::<CValue>());
    println!("align of CValue: {} bytes", align_of::<CValue>());
}
size of CValue:  24 bytes
align of CValue: 8 bytes

Combining #[repr(C)] with align(N) or packed

#[repr(C)] can be combined with align(N) (#[repr(C, align(16))]) to enforce SIMD or hardware descriptor alignment, or with packed (#[repr(C, packed)]) to match __attribute__((packed)) C structs. It cannot be combined with both packed and align(N) on the same type.