🦀 Functional Rust

722: Memory Layout — repr(C), repr(packed), repr(align(N))

Difficulty: 4 Level: Expert Control struct field order, padding, and alignment for FFI, serialisation, and SIMD work.

The Problem This Solves

Rust's default struct layout (`repr(Rust)`) makes no guarantees about field order or padding. The compiler may reorder fields to minimise padding, and that reordering can change between compiler versions. For application code that never crosses an ABI boundary, this is fine — the compiler does the right thing. For FFI, network protocols, file formats, or SIMD buffers, you need byte-exact layout control. Three attributes give you that control. `#[repr(C)]` matches C's layout rules: fields in declaration order, each field padded to its natural alignment, the struct padded to the alignment of its largest field. `#[repr(packed)]` strips all padding — every field is adjacent in memory, regardless of alignment. `#[repr(align(N))]` forces the struct to be aligned to at least N bytes, which is required for SIMD types and some MMIO register maps. Combining them is allowed — `#[repr(C, packed)]` gives you C field order with no padding, which is common for wire-format structs. But mixing `repr(packed)` with references is an instant UB trap: taking a reference to an unaligned field violates Rust's aliasing rules.

The Intuition

Memory layout is like packing a suitcase with rigid dividers. The default `repr(Rust)` lets the compiler move things around to fill gaps efficiently. `repr(C)` puts in the same dividers as the C compiler — both sides of the FFI boundary see the same gaps in the same places. `repr(packed)` removes all dividers — maximum density, but you might need special unaligned reads. `repr(align(16))` is like choosing a suitcase whose handle is always in the right position for the luggage carousel (SIMD lane alignment).

How It Works in Rust

// repr(Rust) — default, do NOT use for FFI.
struct DefaultLayout { a: u8, b: u32, c: u16 }
// Likely 8 bytes: compiler reorders to b(4)+c(2)+a(1)+pad(1)

// repr(C) — C-compatible, field order preserved.
#[repr(C)]
pub struct CLayout { pub a: u8, pub b: u32, pub c: u16 }
// Exactly 12 bytes: a(1)+pad(3)+b(4)+c(2)+pad(2)

// repr(packed) — no padding, potentially unaligned.
#[repr(C, packed)]
pub struct WireFrame { pub a: u8, pub b: u32, pub c: u16 }
// Exactly 7 bytes: a(1)+b(4)+c(2), no padding

// repr(align(16)) — for SIMD and cache-line alignment.
#[repr(C, align(16))]
pub struct SimdVec4 { pub x: f32, pub y: f32, pub z: f32, pub w: f32 }
// 16 bytes, 16-byte aligned — maps to XMM register directly.
Inspect at compile time: `std::mem::size_of::<T>()`, `std::mem::align_of::<T>()`. Use `std::mem::offset_of!(T, field)` (stabilised in 1.77) to get field byte offsets. Critical rule: Never take a reference to a field of a `repr(packed)` struct. Use `ptr::read_unaligned` / `ptr::write_unaligned` instead.

What This Unlocks

Key Differences

ConceptOCamlRust
Default struct layoutFields in declaration order (GC-managed)Compiler may reorder (unspecified)
C-compatible layout`ctypes` library`#[repr(C)]`
Packed layoutNot in stdlib`#[repr(packed)]`
Forced alignmentNot available`#[repr(align(N))]`
Inspect layout at compile timeNot available`size_of!`, `align_of!`, `offset_of!`
Reference to packed fieldNot applicableUB — use `read_unaligned` instead
// 722. repr(C), repr(packed), repr(align(N)) layouts
//
// Shows how to control struct layout and inspect it at compile time.

use std::mem;

// ── repr(Rust) — default, unspecified order ───────────────────────────────────

/// Default layout: compiler may reorder fields to minimise padding.
/// Do NOT use for FFI or when byte layout matters.
struct DefaultLayout {
    a: u8,   // 1 byte
    b: u32,  // 4 bytes — compiler will likely place this first to avoid padding
    c: u16,  // 2 bytes
}
// Typical: b(4) + c(2) + a(1) + pad(1) = 8 bytes

// ── repr(C) — C-compatible, field order preserved ─────────────────────────────

/// Fields are laid out in declaration order with C padding rules.
/// Safe to pass across FFI boundaries.
#[repr(C)]
pub struct CLayout {
    pub a: u8,   // offset 0, size 1
    // 3 bytes padding (to align b to 4)
    pub b: u32,  // offset 4, size 4
    pub c: u16,  // offset 8, size 2
    // 2 bytes padding (to align struct to max(4) = 4)
}
// Total: 12 bytes

/// A C-compatible packet header — safe to `memcpy` to/from a C struct.
#[repr(C)]
pub struct PacketHeader {
    pub magic:    u32,
    pub version:  u8,
    pub flags:    u8,
    pub length:   u16,
    pub checksum: u32,
}

// ── repr(packed) — no padding ─────────────────────────────────────────────────

/// All padding removed. Fields may be unaligned.
///
/// # Warning
/// Never take a reference (`&`) to a field — that would be UB on strict-align
/// architectures. Use `ptr::read_unaligned` / `ptr::write_unaligned` instead.
#[repr(C, packed)]
pub struct PackedLayout {
    pub a: u8,
    pub b: u32,
    pub c: u16,
}
// Total: 1 + 4 + 2 = 7 bytes — no padding

/// Read a packed field safely (no reference taken to unaligned field).
impl PackedLayout {
    pub fn b_value(&self) -> u32 {
        // SAFETY: `self` is a valid `PackedLayout` instance. We use
        // `read_unaligned` because `b` may not be 4-byte aligned due to packed.
        unsafe { std::ptr::addr_of!(self.b).read_unaligned() }
    }

    pub fn c_value(&self) -> u16 {
        // SAFETY: same reasoning as `b_value`.
        unsafe { std::ptr::addr_of!(self.c).read_unaligned() }
    }
}

// ── repr(align(N)) — forced minimum alignment ─────────────────────────────────

/// Align to 64 bytes (one full cache line).
/// Prevents false sharing between CPU cores in concurrent data structures.
#[repr(C, align(64))]
pub struct CacheLinePadded<T> {
    pub value: T,
    // Implicit padding to reach 64-byte size if needed.
}

/// Two u64s, each on their own cache line.
pub struct NoPseudoSharing {
    pub a: CacheLinePadded<u64>,
    pub b: CacheLinePadded<u64>,
}

/// Align to 32 bytes for AVX2 SIMD loads (requires 32-byte alignment).
#[repr(C, align(32))]
pub struct Avx2Aligned {
    pub data: [f32; 8], // 256-bit = 8 × f32
}

// ── repr(transparent) — single-field newtype ──────────────────────────────────

/// Same layout as the inner type — zero-cost newtype pattern.
#[repr(transparent)]
pub struct Meters(pub f64);

#[repr(transparent)]
pub struct Seconds(pub f64);

// ── Compile-time layout assertions ───────────────────────────────────────────

const _: () = {
    assert!(mem::size_of::<PackedLayout>() == 7);
    assert!(mem::size_of::<PacketHeader>() == 12);
    assert!(mem::align_of::<CacheLinePadded<u64>>() == 64);
    assert!(mem::align_of::<Avx2Aligned>() == 32);
    assert!(mem::size_of::<Meters>() == mem::size_of::<f64>());
};

// ── main ──────────────────────────────────────────────────────────────────────

fn print_layout(name: &str, size: usize, align: usize) {
    println!("  {name:<30} size={size:3} align={align:3}");
}

fn main() {
    println!("=== Layout report ===");
    print_layout("DefaultLayout",        mem::size_of::<DefaultLayout>(),        mem::align_of::<DefaultLayout>());
    print_layout("CLayout (repr(C))",    mem::size_of::<CLayout>(),              mem::align_of::<CLayout>());
    print_layout("PackedLayout",         mem::size_of::<PackedLayout>(),         mem::align_of::<PackedLayout>());
    print_layout("PacketHeader",         mem::size_of::<PacketHeader>(),         mem::align_of::<PacketHeader>());
    print_layout("CacheLinePadded<u64>", mem::size_of::<CacheLinePadded<u64>>(), mem::align_of::<CacheLinePadded<u64>>());
    print_layout("Avx2Aligned",          mem::size_of::<Avx2Aligned>(),          mem::align_of::<Avx2Aligned>());
    print_layout("Meters (transparent)", mem::size_of::<Meters>(),               mem::align_of::<Meters>());

    println!("\n=== Field offsets (CLayout, repr(C)) ===");
    println!("  a @ offset {}", std::mem::offset_of!(CLayout, a));
    println!("  b @ offset {}", std::mem::offset_of!(CLayout, b));
    println!("  c @ offset {}", std::mem::offset_of!(CLayout, c));

    // Packed: read via unaligned helpers
    let packed = PackedLayout { a: 0xAA, b: 0x1234_5678, c: 0xBEEF };
    println!("\n=== PackedLayout unaligned reads ===");
    println!("  a=0x{:02X}  b=0x{:08X}  c=0x{:04X}", packed.a, packed.b_value(), packed.c_value());

    // Cache-line padding
    let cs = NoPseudoSharing {
        a: CacheLinePadded { value: 1 },
        b: CacheLinePadded { value: 2 },
    };
    println!("\n=== No false sharing ===");
    let a_addr = &cs.a as *const _ as usize;
    let b_addr = &cs.b as *const _ as usize;
    println!("  &a=0x{a_addr:016X}  &b=0x{b_addr:016X}  diff={} bytes", b_addr - a_addr);
    assert!(b_addr - a_addr >= 64, "must be on different cache lines");
}

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn packed_size_is_seven() {
        assert_eq!(mem::size_of::<PackedLayout>(), 7);
    }

    #[test]
    fn cache_line_align() {
        assert_eq!(mem::align_of::<CacheLinePadded<u64>>(), 64);
        assert!(mem::size_of::<CacheLinePadded<u64>>() >= 64);
    }

    #[test]
    fn packet_header_size() {
        assert_eq!(mem::size_of::<PacketHeader>(), 12);
    }

    #[test]
    fn transparent_same_size() {
        assert_eq!(mem::size_of::<Meters>(), mem::size_of::<f64>());
        assert_eq!(mem::align_of::<Meters>(), mem::align_of::<f64>());
    }

    #[test]
    fn packed_unaligned_read() {
        let p = PackedLayout { a: 1, b: 0xDEAD_BEEF, c: 0x1234 };
        assert_eq!(p.b_value(), 0xDEAD_BEEF);
        assert_eq!(p.c_value(), 0x1234);
    }

    #[test]
    fn c_layout_offsets() {
        assert_eq!(std::mem::offset_of!(CLayout, a), 0);
        assert_eq!(std::mem::offset_of!(CLayout, b), 4);
        assert_eq!(std::mem::offset_of!(CLayout, c), 8);
    }
}
(* OCaml: Memory layout inspection
   OCaml doesn't expose repr attributes, but we can observe sizes
   using Obj and compare with C layouts via ctypes (not used here — std only). *)

(* OCaml's Obj module lets us inspect the runtime layout *)

(* --- Field ordering and padding in OCaml --- *)
(* OCaml records are laid out in declaration order; each field is one word.
   There is no padding between fields (they're always word-sized). *)

type compact = { a: int; b: bool; c: int }
(* In OCaml: a=1 word, b=1 word (bool is boxed as int), c=1 word = 3 words *)

type c_like = { x: float; y: float; z: float }
(* float array is unboxed; float record fields are also unboxed *)

let () =
  (* Obj.size gives number of fields (words, not bytes) *)
  let v = { a = 1; b = true; c = 3 } in
  Printf.printf "compact: Obj.size=%d words\n" (Obj.size (Obj.repr v));

  let p = { x = 1.0; y = 2.0; z = 3.0 } in
  Printf.printf "c_like: Obj.size=%d words\n" (Obj.size (Obj.repr p));

  (* On 64-bit: 1 word = 8 bytes *)
  let word_size = Sys.int_size / 8 + 1 in
  Printf.printf "Word size: %d bytes\n" word_size

(* --- Simulated packed / aligned structs --- *)
(* OCaml has no packed or align attributes.
   For FFI, you'd use the `ctypes` library which has Ctypes.Struct. *)

(* ctypes conceptual example (not compiled — just illustration):
   open Ctypes
   let my_struct = structure "my_struct"
   let f1 = field my_struct "field1" uint8_t
   let f2 = field my_struct "field2" uint32_t
   let () = seal my_struct
   (* ctypes computes the right C layout with padding *)
*)

(* --- Byte-level layout via Bytes --- *)
(* The closest OCaml can get to packed repr is Bytes/Bigarray *)
let pack_u8_u32 (a : int) (b : int32) : bytes =
  let buf = Bytes.create 5 in
  Bytes.set buf 0 (Char.chr (a land 0xFF));
  (* Little-endian u32 *)
  Bytes.set buf 1 (Char.chr (Int32.to_int (Int32.logand b 0xFFl)));
  Bytes.set buf 2 (Char.chr (Int32.to_int (Int32.logand (Int32.shift_right_logical b 8) 0xFFl)));
  Bytes.set buf 3 (Char.chr (Int32.to_int (Int32.logand (Int32.shift_right_logical b 16) 0xFFl)));
  Bytes.set buf 4 (Char.chr (Int32.to_int (Int32.logand (Int32.shift_right_logical b 24) 0xFFl)));
  buf

let () =
  let packed = pack_u8_u32 0xAB 0x12345678l in
  Printf.printf "Packed bytes: %s\n"
    (String.concat " " (List.init (Bytes.length packed)
       (fun i -> Printf.sprintf "%02X" (Char.code (Bytes.get packed i)))))