Skip to content

Data Representation ​

Updated Mar 2026

How Rust lays out data in memory. Understanding repr, alignment, padding, field reordering, enum layout, and null pointer optimization is essential for FFI, performance tuning, and unsafe code.

Memory Layout Fundamentals ​

Every type in Rust has two properties that govern its memory layout:

PropertyDefinition
SizeTotal bytes needed to store the value
AlignmentAddress must be a multiple of this value

Rules:

  • Alignment is always at least 1 and always a power of 2
  • A type's size must always be a multiple of its alignment
  • Composite types inherit the largest alignment of their fields

repr(Rust) -- Default Layout ​

The default representation gives the compiler freedom to optimize field ordering and padding.

Padding and Reordering Example ​

rust
struct A {
    a: u8,   // 1 byte
    b: u32,  // 4 bytes, needs 4-byte alignment
    c: u16,  // 2 bytes, needs 2-byte alignment
}

Naive layout (C-style, field order preserved):

| a (1) | pad (3) | b (4) | c (2) | pad (2) |  = 12 bytes

Rust-optimized layout (fields reordered by alignment):

| b (4) | c (2) | a (1) | pad (1) |  = 8 bytes

Key Guarantees ​

  • Two instances of the same struct type are always laid out identically
  • Two different struct types with identical field definitions may have different layouts
  • Rust reserves the right to reorder fields to minimize padding
  • You cannot rely on field order in repr(Rust) for FFI or serialization

Composite Data Types ​

TypeCategoryDescription
structNamed product typeNamed fields
TupleAnonymous product typePositional fields
ArrayHomogeneous product typeSame-type elements, contiguous
enumNamed sum typeTagged union with variants
unionUntagged unionOverlapping fields, unsafe access

repr(C) -- C-Compatible Layout ​

Use #[repr(C)] when you need predictable, C-compatible memory layout:

rust
#[repr(C)]
struct Point {
    x: f64,  // 8 bytes at offset 0
    y: f64,  // 8 bytes at offset 8
}

#[repr(C)]
struct MixedFields {
    a: u8,   // offset 0
    // 3 bytes padding
    b: u32,  // offset 4
    c: u16,  // offset 8
    // 2 bytes padding
}  // total: 12 bytes

repr(C) Rules ​

  1. Fields are laid out in declaration order (no reordering)
  2. Each field is placed at the next properly-aligned offset
  3. The struct's alignment is the maximum alignment of its fields
  4. The struct's size is rounded up to a multiple of its alignment

When to Use repr(C) ​

ScenarioUse repr(C)?
FFI with C/C++ librariesYes -- required
Writing to binary file formatsYes -- predictable layout
Interfacing with hardware registersYes -- exact offsets matter
Pure Rust internal structsNo -- let the compiler optimize
Network protocol structsUsually yes, with repr(C, packed)

repr(C, packed) -- No Padding ​

Removes all padding between fields:

rust
#[repr(C, packed)]
struct Packed {
    a: u8,   // offset 0
    b: u32,  // offset 1 (unaligned!)
    c: u16,  // offset 5
}  // total: 7 bytes

Warning: Packed structs may have unaligned fields. Taking a reference to an unaligned field is undefined behavior. Use std::ptr::read_unaligned or addr_of! instead:

rust
use std::ptr;

let p = Packed { a: 1, b: 2, c: 3 };
// WRONG: &p.b may be unaligned
// let b_ref = &p.b;

// CORRECT: read unaligned field
let b_val = unsafe { ptr::addr_of!(p.b).read_unaligned() };

repr(transparent) -- Single-Field Wrappers ​

Guarantees the wrapper has the exact same layout as its single non-zero-sized field:

rust
#[repr(transparent)]
struct Wrapper(u32);

// Wrapper has identical layout to u32
// Safe to transmute between them

Used for newtype patterns, especially in FFI where you want type safety without layout overhead.

repr(u8), repr(u16), etc. -- Enum Discriminant Types ​

Controls the size and type of the enum discriminant (tag):

rust
#[repr(u8)]
enum Color {
    Red = 0,
    Green = 1,
    Blue = 2,
}

// Color is exactly 1 byte
assert_eq!(std::mem::size_of::<Color>(), 1);

Enum Layout ​

Discriminated Unions ​

Enums with data are stored as a discriminant (tag) followed by the payload area sized for the largest variant:

rust
enum WebEvent {
    PageLoad,                    // no data
    KeyPress(char),              // 4 bytes
    Click { x: i64, y: i64 },   // 16 bytes
}

// Layout: discriminant + padding + max(0, 4, 16) bytes

Null Pointer Optimization (NPO) ​

This is one of Rust's most elegant optimizations. When an enum has:

  • One variant with no data (like None)
  • One variant containing a non-nullable pointer type

Rust eliminates the discriminant entirely by using the null value to represent the empty variant.

rust
use std::mem::size_of;

// Option<&T> is the same size as &T (one pointer)
assert_eq!(size_of::<Option<&u32>>(), size_of::<&u32>());

// Option<Box<T>> is the same size as Box<T>
assert_eq!(size_of::<Option<Box<u32>>>(), size_of::<Box<u32>>());

Types eligible for NPO:

TypeWhy it works
&TReferences are never null
&mut TMutable references are never null
Box<T>Heap pointer is never null
Vec<T>Internal pointer is never null
StringInternal pointer is never null
fn()Function pointers are never null
NonNull<T>Explicitly non-null pointer
NonZero<u32>Value is never zero

This means Option<Box<T>> is a zero-cost abstraction -- it has the same memory footprint as a raw pointer.

Zero-Sized Types (ZSTs) ​

Types with size 0 that take no space in memory:

rust
struct Marker;  // ZST: size = 0, align = 1

// Arrays of ZSTs take no space
let arr: [Marker; 1000] = [Marker; 1000];
assert_eq!(std::mem::size_of_val(&arr), 0);

// Common ZSTs in std:
// () -- unit type
// PhantomData<T> -- zero-cost type marker
// struct NoData; -- marker structs

ZSTs are useful for:

  • Type-level markers (e.g., PhantomData<T> for variance)
  • State machine patterns using typestates
  • Map keys where only set membership matters (HashSet<T> is HashMap<T, ()>)

Checking Layout at Runtime ​

rust
use std::mem;

println!("Size of i32: {} bytes", mem::size_of::<i32>());
println!("Alignment of i32: {} bytes", mem::align_of::<i32>());
println!("Size of Option<&u32>: {} bytes", mem::size_of::<Option<&u32>>());
println!("Size of Option<Box<u32>>: {} bytes", mem::size_of::<Option<Box<u32>>>());

// Verify NPO in action
assert_eq!(mem::size_of::<Option<&u32>>(), mem::size_of::<&u32>());

Quick Reference ​

reprField OrderPaddingUse Case
repr(Rust)Compiler choosesOptimizedDefault for pure Rust
repr(C)Declaration orderC-compatibleFFI, binary formats
repr(C, packed)Declaration orderNoneWire protocols, hardware
repr(transparent)Same as innerSame as innerNewtype wrappers
repr(u8/u16/...)N/A (enums)Explicit tag sizeC-compatible enums

Built with VitePress | Rust SOP Documentation