#[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:
- Alignment: The struct alignment is the maximum alignment of all its fields ($\max_i \text{align}(T_i)$).
- Sequential offsets:
f1starts at offset0. Each subsequent fieldfistarts at the smallest offset greater than or equal to the end off(i-1)that is a multiple of $\text{align}(T_i)$. - 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:
- A C-enum discriminant tag.
- 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.