Frontend Labs
⋯

Part VII — Expert: internals and design

Parts I–VI covered how to use Rust. This Part covers how it works underneath: what is in memory, what async compiles to, what the atomic orderings promise, and how to design APIs that others can use safely. It ends with two capstones that use the whole guide.


57. Memory layout: size, alignment, niches, repr(C), fat pointers

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

#[allow(dead_code)]
struct Default_ { a: u8, b: u32, c: u16 }          // Rust may reorder fields

#[allow(dead_code)]
#[repr(C)]
struct CLayout { a: u8, b: u32, c: u16 }           // C order, C padding

#[allow(dead_code)]
enum Shape { Circle(f32), Rect(f32, f32) }

fn main() {
    println!("{:<28}{:>5}{:>7}", "type", "size", "align");
    macro_rules! row { ($t:ty) => { println!("{:<28}{:>5}{:>7}", stringify!($t), size_of::<$t>(), align_of::<$t>()) } }
    row!(Default_);            // 8: reordered to b, c, a + 1 byte of padding
    row!(CLayout);             // 12: a, 3 pad, b, c, 2 pad (same as the C guide §8.1)
    row!(&u8);                 // 8: thin pointer
    row!(&[u8]);               // 16: pointer + length
    row!(&str);                // 16
    row!(&dyn std::fmt::Debug);// 16: pointer + vtable
    row!(Box<u64>);            // 8
    row!(Option<Box<u64>>);    // 8: None uses the null niche
    row!(Option<u32>);         // 8: needs a separate tag
    row!(Option<NonZeroU32>);  // 4: 0 is the niche
    row!(Option<bool>);        // 1: values 2..=255 are niches
    row!(Result<u32, ()>);     // 8
    row!(Shape);               // 12: tag + the larger variant
    row!(String);              // 24
    row!(Vec<u8>);             // 24
    row!(());                  // 0: zero-sized type
}
type                         size  align
Default_                        8      4
CLayout                        12      4
&u8                             8      8
&[u8]                          16      8
&str                           16      8
&dyn std::fmt::Debug           16      8
Box<u64>                        8      8
Option<Box<u64>>                8      8
Option<u32>                     8      4
Option<NonZeroU32>              4      4
Option<bool>                    1      1
Result<u32, ()>                 8      4
Shape                          12      4
String                         24      8
Vec<u8>                        24      8
()                              0      1

Three rules come out of this table:

  1. Rust's default layout is unspecified. The compiler may reorder fields to minimize padding. Default_ is 8 bytes, where the C layout of the same fields is 12. Use #[repr(C)] for anything that crosses an FFI boundary or is read from raw bytes, as in the C guide §8.1. Never transmute a default-layout struct.
  2. Niches make Option free for pointer-like types. References, Box, NonNull, and NonZero* have an invalid bit pattern (0), and None uses it. That is how Option<&T> has the same ABI as a nullable C pointer, so FFI code can use it.
  3. Zero-sized types cost nothing. HashSet<T> is a HashMap<T, ()>, and the () values take no memory. Marker types in typestate APIs (Ch 62) take no space either.

Generated from real-life-rust-guide.md · 8 Oct 2026 16:36