🎓 A1Learner
🔍

Rust Type Layout

Type layout is the decision about where every field sits inside a type. The layout covers size, alignment, and field offsets.

Measure any type

size_of and align_of answer at compile time. Alignment is the rule that a value must be stored at an address that is a multiple of its alignment.

use std::mem::{size_of, align_of};

println!("size of u32 = {}", size_of::<u32>());
println!("size of char = {}", size_of::<char>());
println!("align of u32 = {}", align_of::<u32>());

Padding fills the gaps

With #[repr(C)], fields stay in the order you wrote them. A u32 must start at a multiple of 4. The u8 sits at byte 0. So three empty padding bytes are inserted before big.

#[repr(C)]
struct Reading {
    small: u8,
    big: u32,
    tiny: u8,
}

See the offsets

offset_of! tells you where a field starts. tiny ends at byte 9. The size rounds up to a multiple of the alignment, 4. So the struct measures 12 bytes, not 9.

println!("big starts at byte {}", offset_of!(Reading, big));
println!("whole struct = {} bytes", size_of::<Reading>());

The default layout may reorder fields

Without repr(C), Rust may reorder fields to save space, and never promises an order. Measured with the current compiler, these same three fields take 8 bytes, not 12.

struct SameFields {
    small: u8,
    big: u32,
    tiny: u8,
}

repr(transparent): a wrapper with no cost

A one-field wrapper with #[repr(transparent)] has exactly the layout of the field inside it. This is the guarantee behind the newtype pattern.

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

println!("wrapper = {} bytes", size_of::<Wrapper>());

Raise or lower the alignment

#[repr(align(16))] stretches Aligned to a size and alignment of 16. #[repr(packed)] removes all padding instead: Packed measures 5 bytes with alignment 1. Its fields may sit at odd addresses, so copy a field out into a variable before using it.

#[repr(align(16))]
struct Aligned { value: u8 }

#[repr(packed)]
struct Packed { small: u8, big: u32 }

Give an enum a number size

A field-less enum can name the integer type its tag uses. Direction fits in a single byte.

#[repr(u8)]
enum Direction { North, East, South, West }

Some types take no space at all

An empty struct and the unit value () are zero-sized types. They exist for the type system, not for memory.

struct Marker;

println!("marker = {} bytes", size_of::<Marker>());
println!("unit = {} bytes", size_of::<()>());

Example

#![allow(dead_code)]
use std::mem::{align_of, offset_of, size_of};

#[repr(C)]
struct Reading { small: u8, big: u32, tiny: u8 }

struct SameFields { small: u8, big: u32, tiny: u8 }

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

#[repr(packed)]
struct Packed { small: u8, big: u32 }

#[repr(u8)]
enum Direction { North, East, South, West }

struct Marker;

fn main() {
    println!("Reading: size {}, align {}", size_of::<Reading>(), align_of::<Reading>());
    println!("big is at offset {}", offset_of!(Reading, big));
    println!("Same fields, default layout: size {}", size_of::<SameFields>());
    println!("Wrapper: size {}", size_of::<Wrapper>());
    let packed = Packed { small: 1, big: 700 };
    let big_copy = packed.big;
    println!("Packed: size {}, big = {}", size_of::<Packed>(), big_copy);
    println!("Direction: size {}", size_of::<Direction>());
    println!("Marker: size {}", size_of::<Marker>());
}

Key points

  • A layout has three parts: the size in bytes, the alignment, and the offset where each field starts
  • A type's size is always a multiple of its alignment.
  • Rounding up to that multiple is where tail padding comes from.
  • Use #[repr(C)] when a layout must match C: for FFI, or for reading and writing a fixed byte format.
  • Fields stay in declaration order, with C's padding rules
  • Some types are zero-sized types (ZSTs).
  • Examples are (), an empty struct, and [T; 0].
  • They carry information for the compiler.
  • They take no memory at run time.
  • Never guess a struct's size by adding up its fields.
  • A u32 takes 4 bytes on its own.

In this example, one small family of structs is measured six ways. Reading uses repr(C). So its fields stay in order. small sits at byte 0. Three padding bytes follow. big sits at offset 4. tiny sits at byte 8. The total rounds up to 12. SameFields holds the same three fields with the default layout. It measures only 8 bytes. The compiler is free to reorder fields. It can also shrink the padding. Wrapper proves the transparent promise: it measures exactly the 4 bytes of the u32 inside it. Packed removes padding entirely. So it measures 5 bytes. We copy big into a plain variable before printing it. A packed field can sit at an odd address. Direction, a field-less enum with repr(u8), fits in 1 byte. And Marker, an empty struct, measures 0 — a type can exist purely for the compiler.

Note

Padding and reordering decide the real answer — and size_of is always there to measure it.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

A #[repr(C)] struct holds a u8 followed by a u32. Why does it measure 8 bytes and not 5?