Skip to content

← Performance and Data Layout step 7 of 20

Hard Primitives

The other reprs: transparent, packed, align, integer enums

#[repr(C)] gets you the standard ABI. Matching an existing C header usually needs more than that, and the remaining representation attributes are the tools. They are also where alignment stops being an abstract property and starts being a compile error.

The header you are matching

#pragma pack(push, 1)
struct wire_header {
    uint8_t  version;
    uint32_t length;
    uint8_t  flags;
    uint16_t checksum;
};
#pragma pack(pop)

typedef struct { int32_t raw; } handle_t;

enum status : uint8_t { STATUS_OK = 0, STATUS_RETRY = 1, STATUS_FATAL = 2 };

#[repr(C, packed)]

packed removes all padding: every field goes at the next byte, and the struct’s alignment becomes 1. wire_header under normal C rules would be 12 bytes; packed it is exactly 8 — the sum of its fields, which is what a wire format needs.

Write #[repr(C, packed)], not #[repr(packed)]. packed on its own removes the padding but leaves the field order unspecified, so you get a compact struct that still does not match the header. clippy’s repr_packed_without_abi is on by default and says exactly this.

The error that makes the lesson stick

let r = &h.length;   // error[E0793]
error[E0793]: reference to packed field is unaligned
= note: packed structs are only aligned by one byte, and many modern architectures
        penalize unaligned field accesses
= note: creating a misaligned reference is undefined behavior (even if that
        reference is never dereferenced)

Every &T in Rust carries a promise that the address is aligned for T, and the optimiser relies on it. In a packed struct that promise cannot be kept, so the reference is forbidden — even if you never dereference it.

Note what this catches that you would not expect: println!("{}", h.length) fails too, because the formatting macros take references to their arguments.

Two ways out:

  1. Copy the value out. let n = h.length; is fine — reading a Copy field by value creates no reference. This is the idiomatic fix and it is entirely safe.

  2. Take a raw pointer and read it unaligned. For fields you cannot copy out, or when you need the address:

    unsafe { (&raw const h.checksum).read_unaligned() }

    &raw const place (stable syntax since 1.82, replacing addr_of!) makes a raw pointer without ever forming a reference, and read_unaligned reads through it with no alignment requirement.

Both appear in this problem, deliberately: read_length uses the safe copy-out, read_checksum uses the raw-pointer form so you have written it once.

Packed access can be slower, and on some architectures it traps. x86 and aarch64 tolerate unaligned loads with a penalty; not every target does. Pack for wire formats, not for compactness in memory you own.

#[repr(transparent)]

#[repr(transparent)]
pub struct Handle(pub i32);

“This newtype has exactly the layout and ABI of its single field.” Size 4, alignment 4, passed in registers identically to a bare i32. It is the idiomatic zero-cost way to add type safety to an FFI handle: the C side sees an int32_t, your side cannot confuse a Handle with a FileNo.

The rule is that the type may contain exactly one field of non-zero size. Zero-sized fields (PhantomData, ()) are allowed alongside it — which is how phantom-typed handles work.

#[repr(u8)] on an enum

#[repr(u8)]
pub enum Status { Ok = 0, Retry = 1, Fatal = 2 }

Fixes the discriminant to a u8: size 1, alignment 1, and the values are exactly the ones you wrote. Without it, rustc picks whatever width it likes. s as u8 gives you the discriminant back.

And a small piece of magic to notice: Option<Status> is also 1 byte. The enum uses three of 256 patterns, so None hides in one of the other 253 — a niche, exactly as in the previous item.

What to write

Define the three types with the right representations, then:

pub fn read_length(h: &WireHeader) -> u32       // copy the field out
pub fn read_checksum(h: &WireHeader) -> u16     // &raw const + read_unaligned
pub fn status_code(s: Status) -> u8
pub fn handle_raw(h: Handle) -> i32
pub fn describe() -> Vec<(String, usize, usize)>

describe returns (name, size, align) for WireHeader, Handle, Status and Option<Status>, in that order. These are repr-pinned types, so unlike the previous items the exact numbers are guaranteed and the test asserts them: 8/1, 4/4, 1/1, 1/1.

Errors and lints in this neighbourhood

  • E0793 — reference to a packed field. Covered above.
  • E0587 — a type cannot have both packed and align representation hints. They contradict each other, and rustc says so.
  • E0517#[repr(..)] applied to something that cannot carry it (a function, a use, a statement).
  • E0084 — an enum with no variants cannot have a discriminant representation.
  • enum_clike_unportable_variant — clippy correctness, denied by default: a C-like enum whose discriminant does not fit in the target’s isize.
  • default_union_representation, trailing_empty_array — pedantic, but the FFI-shaped mistakes they catch are real.

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