Memory Layout and Representation

Value Representation

Value representation defines how an instance of a Rust type is encoded as a sequence of bytes in memory, which bit patterns are valid for that type, and whether any bytes remain uninitialized. Every value of a Sized type T occupies std::mem::size_of::<T>() contiguous bytes in memory.

Byte-Level Encoding of Primitives

Primitive types have fixed, architecture-defined bit representations:

Type Size (Bytes) Valid Bit Patterns Representation
u8, u16, u32, u64, u128 1, 2, 4, 8, 16 All bit patterns (0..=2^N - 1) Unsigned binary (native endianness)
i8, i16, i32, i64, i128 1, 2, 4, 8, 16 All bit patterns Two's complement (native endianness)
f32, f64 4, 8 All bit patterns IEEE 754 binary32 / binary64
bool 1 0x00 (false) or 0x01 (true) only Single byte; 0x02..=0xFF are invalid
char 4 0x0000..=0xD7FF and 0xE000..=0x10FFFF 32-bit Unicode scalar value
&T, &mut T, NonNull<T> 8 (on 64-bit) Non-null (!= 0), aligned for T Virtual memory address
*const T, *mut T 8 (on 64-bit) Any initialized bit pattern (including 0) Raw memory address
() 0 Single trivial state Zero bytes

Endianness

Multi-byte integers and char values are stored in the target CPU's native endianness. On little-endian architectures (x86_64, AArch64, RISC-V, WebAssembly), the least significant byte is stored at the lowest memory address.

Value: 0x12345678_u32

Little-Endian Memory Layout (lowest address first):
+------+------+------+------+
| 0x78 | 0x56 | 0x34 | 0x12 |
+------+------+------+------+
  +0     +1     +2     +3

Explicit byte conversion methods (to_le_bytes, to_be_bytes, to_ne_bytes) expose the byte representation without unsafe code:

fn main() {
    let number: u32 = 0x1234_5678;

    println!("Native bytes: {:02x?}", number.to_ne_bytes());
    println!("Little-endian: {:02x?}", number.to_le_bytes());
    println!("Big-endian:    {:02x?}", number.to_be_bytes());
}
Native bytes: [78, 56, 34, 12]
Little-endian: [78, 56, 34, 12]
Big-endian:    [12, 34, 56, 78]

Validity Invariants and Uninitialized Memory

A type's validity invariant specifies which byte states the compiler assumes a value can hold at all times. Constructing a value whose bytes violate its validity invariant causes immediate undefined behavior, even if the value is never read.

  • Uninitialized bytes: Before memory is written, its bytes are conceptually undef (uninitialized), not an arbitrary fixed integer. Reading uninitialized bytes as an integer (u8) or boolean (bool) is undefined behavior. std::mem::MaybeUninit<T> is the only safe wrapper for holding potentially uninitialized bytes.
  • Padding bytes: Padding bytes between struct fields are treated as uninitialized when a struct is copied or moved.
  • Bit-validity: u8 accepts any initialized 8-bit value (0x00..=0xFF), whereas bool only accepts 0x00 and 0x01.
use std::mem::MaybeUninit;

fn main() {
    let mut slot: MaybeUninit<u32> = MaybeUninit::uninit();
    slot.write(0xDEAD_BEEF);

    // SAFETY: `slot` was initialized by `slot.write` above.
    let initialized: u32 = unsafe { slot.assume_init() };
    println!("initialized = {:#x}", initialized);
}
initialized = 0xdeadbeef

Zero-Sized Types (ZSTs)

A zero-sized type (ZST) has a size of 0 bytes (size_of::<T>() == 0). Unit types (()), empty structs (struct Marker;), arrays of length zero ([T; 0]), and PhantomData<T> occupy no space in memory and generate no load or store instructions at runtime.

use std::mem::size_of;

struct EmptyMarker;

fn main() {
    let markers = [EmptyMarker, EmptyMarker, EmptyMarker];
    println!("size of EmptyMarker:    {} bytes", size_of::<EmptyMarker>());
    println!("size of [EmptyMarker; 3]: {} bytes", size_of_val(&markers));
    println!("size of [u64; 0]:       {} bytes", size_of::<[u64; 0]>());
}
size of EmptyMarker:    0 bytes
size of [EmptyMarker; 3]: 0 bytes
size of [u64; 0]:       0 bytes