Skip to content

← Performance and Data Layout step 6 of 20

Medium Primitives

repr(C): computing struct layout by hand

Layout arithmetic is the foundation of all struct-level FFI, and unlike most of what this track covers it is completely deterministic: given the field types and their order, there is exactly one right answer, and you can compute it on paper before the compiler tells you.

The algorithm, exactly

For a #[repr(C)] struct:

  1. Start at offset 0.
  2. For each field in declaration order: round the current offset up to the next multiple of that field’s alignment (the gap is padding), place the field there, then advance by the field’s size.
  3. The struct’s alignment is the maximum of its fields’ alignments.
  4. The struct’s size is the final offset rounded up to a multiple of the struct’s alignment.

Two things trip people up every time.

Trailing padding is real. Step 4 exists so that arr[1] in an array of your struct is still correctly aligned. If your struct ends on a byte and its alignment is 4, three bytes of nothing get appended. Learners routinely compute steps 1–3 perfectly and then forget step 4.

Alignment is the max of the fields’ alignments, not of their sizes. A [u16; 3] field is 6 bytes long and has alignment 2, not 6 and not 8. An array’s alignment is its element’s alignment.

The four structs

#[repr(C)] struct A { a: u8,   b: u32,      c: u8 }
#[repr(C)] struct B { inner: A, d: u16 }
#[repr(C)] struct C { a: u8,   arr: [u16; 3], b: u8 }
#[repr(C)] struct D { flag: bool, count: u32, tag: u8, code: u16 }

Work A through as the model. a is a u8 at offset 0. b is a u32 with alignment 4, so the offset rounds 1 → 4: three bytes of padding, b at 4, ending at 8. c at 8, ending at 9. The struct’s alignment is max(1, 4, 1) = 4, so 9 rounds up to 12. Answer: size 12, align 4, offsets [0, 4, 8].

B is the nesting case — A is 12 bytes with alignment 4, so inner is at 0..12 and d (alignment 2) goes at 12, ending at 14, rounded up to a multiple of 4. C has the array. D has four fields and a bool (size 1, align 1, and exactly two valid bit patterns).

What to write

pub fn layout() -> Vec<(usize, usize, Vec<usize>)>

Return (size, align, field offsets) for A, B, C, D in that order. Compute them with size_of, align_of and offset_of!, not by typing the numbers you worked out on paper. The starter does exactly that — it hardcodes four hand-written guesses, every one of which makes the same mistake — and the point of the exercise is to check your arithmetic against the compiler, not to replace one set of literals with another.

offset_of!(A, a) is std::mem::offset_of!, stable since 1.77. Before it existed, getting a field offset in safe Rust was genuinely awkward.

Why this matters beyond FFI

Everything above explains why field ordering affects memory usage. Declare a repr(C) struct widest-alignment-first and the padding mostly vanishes; declare it in the order the fields occurred to you and you can easily waste a third of the struct. repr(Rust) does the sorting for you, which is why you never think about it — until the day you write #[repr(C)] and inherit the responsibility.

A portability warning worth taking seriously

Do not assume u64 has alignment 8 everywhere. On 32-bit x86 (the i686-unknown-linux-gnu ABI) it is 4, so a struct containing one lays out differently there than on x86-64 or aarch64. That is exactly the class of bug that makes hand-written FFI structs fail on one platform only. None of the four structs here contain a u64, deliberately — their layouts are the same on every target this course runs on.

Related lints

manual_slice_size_calculation (you wrote slice.len() * size_of::<T>() where size_of_val says it better) and size_of_in_element_count, which is in clippy’s correctness group and denied by default — passing a byte count where an element count is wanted is a memory-safety bug waiting to happen.

Remember the grade is compile + tests + clippy -D warnings.