Type Layout Control (repr C, packed, align) & Zero-Sized Types

Master low-level struct layout control, C ABI compatibility, packed memory alignment hazards, and Zero-Sized Type (ZST) optimizations.

04. Unsafe Rust, Layout Control & Systems Internals7 min read

By default, Rust does not guarantee a stable memory layout for structs or enums (repr(Rust)). The compiler reserves the right to reorder fields between compiler releases to minimize padding and maximize hardware cache line utilization.

For systems programming, hardware register mapping, and Foreign Function Interfaces (FFI), Rust provides layout control attributes.

repr(packed) AlignmentHardware Layout
Align = 1

Packed structs strip all alignment padding bytes, forcing aggregate alignment down to 1 byte.

1. Struct Representation Modifiers#

Architecture Diagram
Rendering diagram...

Mathematical Layout Formulas#

AttributeSize FormulaAlignment FormulaPractical Application
repr(C)∑S(Fi)+∑Pi\sum S(F_i) + \sum P_i (C rules)max⁡(A(F1),…,A(Fk))\max(A(F_1), \dots, A(F_k))FFI with C libraries and OS system calls.
repr(packed)∑i=1kS(Fi)\sum_{i=1}^{k} S(F_i) (Zero padding)11 byteParsing network packet headers and raw disk blocks.
repr(align(N))Multiple of NN bytesmax⁡(N, Adefault)\max(N, \, A_{\text{default}})Aligning buffers to CPU cache lines (6464B) or page boundaries (40964096B).
Zero-Sized Type00 bytes11 byteCompile-time state machines, marker traits, PhantomData.

2. The Dangers of repr(packed) and Unaligned References#

While repr(packed) eliminates padding waste, it introduces a severe hardware hazard: misaligned data fields.

In safe Rust, every reference &T must point to an address that is a multiple of A(T)A(T). In a packed struct, a u32 field (which requires 4-byte alignment) might be placed at an odd byte address like 0x1001:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#[repr(packed)] struct NetworkHeader { version: u8, // 1 byte (Offset 0) packet_id: u32 // 4 bytes (Offset 1: UNALIGNED!) } fn main() { let header = NetworkHeader { version: 1, packet_id: 100 }; // ILLEGAL: Taking a reference to an unaligned field triggers a compile error! // let id_ref: &u32 = &header.packet_id; // SAFE ACCESS: Read the unaligned value by COPYING it out using ptr::read_unaligned: let packet_id = unsafe { std::ptr::addr_of!(header.packet_id).read_unaligned() }; println!("Packet ID: {}", packet_id); }
Unaligned References are UB
Creating an unaligned reference &u32 is immediate Undefined Behavior, even if you never read from it! Modern CPUs may trigger hardware bus faults (SIGBUS) when executing instructions on misaligned pointers. Always use ptr::read_unaligned for packed fields.

3. Zero-Sized Types (ZST) & The Type-State Pattern#

Zero-Sized Types (such as () or empty structs struct Unlocked;) consume exactly 0 bytes of physical memory:

Size(Z)=0,Align(Z)=1\text{Size}(Z) = 0, \qquad \text{Align}(Z) = 1

Because ZSTs exist purely in the compiler's type system, you can use them as zero-cost state machine markers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
use std::marker::PhantomData; // Zero-sized state markers struct Locked; struct Active; struct Channel<State> { port: u16, _marker: PhantomData<State>, // Zero bytes overhead! } impl Channel<Locked> { pub fn new(port: u16) -> Self { Channel { port, _marker: PhantomData } } // State transition consumes 'self' and returns a new state pub fn unlock(self) -> Channel<Active> { Channel { port: self.port, _marker: PhantomData } } } impl Channel<Active> { pub fn send(&self, data: &[u8]) { println!("Transmitting on port {}", self.port); } } fn main() { let chan = Channel::new(8080); // chan.send(b"ping"); // COMPILE ERROR: method 'send' does not exist on Channel<Locked>! let active_chan = chan.unlock(); active_chan.send(b"ping"); // Valid! }

4. Concept Verification#

Test your understanding of type layout control and zero-sized types:

Concept check

Why does creating a reference &header.packet_id to a field in a repr(packed) struct produce a compiler error in modern Rust?

Concept check

How much heap memory is allocated by calling Vec::<()>::with_capacity(1_000_000)?