Why Layout Matters
Memory layout in Rust is not just an optimization detail. It directly affects:
- FFI correctness.
- serialization compatibility.
- cache efficiency.
- unsafe code soundness.
If you cross language boundaries or write low-level code, repr choices are critical.
repr(Rust): Default Representation
Without any attribute, Rust is free to choose layout optimizations.
Implications:
- field order is not part of stable ABI contract.
- layout may differ across compiler versions/targets.
- never assume C-compatible memory layout.
Use default representation for pure Rust internal types.
Padding and Alignment Basics
CPU alignment constraints often add padding bytes.
struct Example {
a: u8,
b: u64,
c: u32,
}
The logical field sizes are 1 + 8 + 4, but actual size is larger because b needs alignment.
Inspect layout safely:
use std::mem::{align_of, size_of};
println!("size={} align={}", size_of::<Example>(), align_of::<Example>());
repr(C): Stable C ABI Layout
Use #[repr(C)] when interoperating with C.
#[repr(C)]
pub struct CRecord {
pub id: u32,
pub flag: u8,
pub score: f64,
}
repr(C) gives C-compatible field ordering and alignment rules for target platform.
Still validate with both sides’ headers/tests in CI.
repr(transparent) for Newtype Wrappers
repr(transparent) is ideal when wrapping one non-zero-sized field and preserving ABI.
#[repr(transparent)]
pub struct UserId(pub u64);
Useful for type safety without changing calling convention at FFI boundaries.
repr(packed): Use Sparingly
repr(packed) removes/changes padding, but may create unaligned references.
#[repr(packed)]
struct PacketHeader {
version: u8,
length: u16,
}
Danger: directly referencing packed fields can be undefined behavior on some architectures.
Safer approach:
- copy bytes out using read helpers.
- avoid creating aligned references to misaligned fields.
repr(align(N)) for Custom Alignment
Use repr(align(N)) when cache-line or SIMD alignment is needed.
#[repr(align(64))]
struct CacheLine {
data: [u8; 64],
}
Typical use cases:
- reduce false sharing.
- optimize high-throughput lock-free structures.
Enum Layout and Niche Optimization
Rust may optimize enum layout using niche values (for example Option<&T> often same size as &T).
This is powerful but should not be treated as stable ABI unless representation is explicitly specified and documented.
FFI Checklist
When exposing Rust types to C/C++:
- add
repr(C)orrepr(transparent). - avoid Rust-specific types in signatures (
String,Vec<T>, references). - use raw pointers and explicit lengths.
- define ownership rules for allocation/deallocation.
- add round-trip integration tests.
Serialization vs In-Memory Layout
Never serialize by blindly dumping struct memory bytes unless layout and endianness are explicitly defined.
Prefer explicit encoders/decoders for stable wire format.
Performance Trade-Offs
- Reordering fields can reduce padding.
- For hot structs, layout changes can improve cache locality.
- Over-optimizing layout without profiling can hurt readability and maintainability.
Measure before and after.
Common Mistakes
- Assuming default Rust layout is C-compatible.
- Using
repr(packed)to save bytes without handling unaligned access safely. - Forgetting target-specific ABI differences.
- Exposing unstable internal structs as public FFI contract.
Conclusion
repr attributes are low-level contracts. Use them deliberately:
repr(Rust)for internal flexibility.repr(C)for FFI.repr(transparent)for ABI-safe wrappers.repr(packed)andrepr(align)only with clear justification.
The safest approach is explicit contracts + tests across targets.
Resources
- Rust Reference: Type Layout
- Rust Nomicon: Data Representation
- Rust FFI Omnibus
- std::mem Documentation
Comments