Skip to content

← Performance and Data Layout step 6 of 20

Medium Primitives

repr(C): computing struct layout by hand

Compute the size, alignment and field offsets of four supplied #[repr(C)] structs. First work through the padding by hand, then use Rust's layout queries to return the compiler's results.

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

Return exactly four tuples in the order A, B, C, D. Each contains (size in bytes, alignment in bytes, field offsets in declaration order). Keep the supplied struct definitions. Use std::mem::size_of, align_of and offset_of! rather than replacing the starter's guesses with new literals.

The types and target assumptions

#[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 }

This exercise's expected results assume alignment 1 for u8 and bool, alignment 2 for u16, and alignment 4 for u32. Their sizes are 1, 1, 2 and 4 bytes respectively. The structs have no packed or explicit align modifier. Layout is deterministic once the target's field sizes and alignments, representation and declaration order are fixed.

repr(C) does not make every field's layout identical on all platforms. When working on a different target, query that target's sizes and alignments rather than assuming host results apply. It also does not recursively change a nested type's representation: A has its own repr(C) declaration here.

Place fields, then round the total size

For these structs:

  1. Start at byte offset zero.
  2. In declaration order, round the current offset up to a multiple of the next field's alignment. An already aligned offset stays unchanged.
  3. Record that field's offset, then advance by its size.
  4. Use the largest field alignment as the struct's alignment.
  5. Round the final offset up to a multiple of the struct's alignment to get the total size.

The gaps between fields are interior padding. The final rounding may add trailing padding. The padded total size is also the stride between elements in an array, keeping each element aligned. Padding bytes are not field values and should not be assumed to contain zeros or be safe to serialize directly.

An array's alignment is its element alignment. Under these assumptions, [u16; 3] occupies six bytes but needs alignment two, not six or eight.

Work through A

Field Alignment Offset End offset
a: u8 1 0 1
b: u32 4 4 8
c: u8 1 8 9

Three padding bytes precede b. The largest alignment is four, so the final offset nine rounds up to twelve. A therefore has size 12, alignment 4 and offsets [0, 4, 8].

For B, use all twelve bytes of A, including its trailing padding, before placing d. For C, treat the array as one six-byte field with alignment two. For D, place the final u16 at an aligned offset and include both of its bytes before checking whether any trailing padding is needed.

The starter contains incorrect sizes, but not all errors have the same cause. Check each field's end offset as well as the final rounding step.

Ask the compiler

size_of::<A>() returns the full size including padding. align_of::<A>() returns the required alignment. offset_of!(A, b) returns the byte offset of b within A; unlike the first two queries, it is a macro.

The harness calls layout() and inspects one tuple at a time. It also checks that the returned vector contains four tuples. Your function itself takes no input and must return all four layouts on every call.

Why field order matters

For repr(C), declaration order is preserved. Placing more strongly aligned fields earlier can reduce padding, but do not reorder these exercise structs or an existing foreign ABI just to save space.

The default Rust representation may choose a different field order. It does not promise a particular sorting algorithm or a layout stable across builds. For FFI, layout is only part of compatibility: field validity, ownership and the function calling convention also matter. For example, Rust's bool accepts only the byte representations 0 and 1.

Check the computed tuples and address Clippy warnings. Passing requires compilation, correct layout results and the lint gate.