Mehdi Akiki
Rust Failure Atlas / FFI and targets

RFA-117 · Case file with fixtures · Case 89 of 694 · Compiler evidence

Why Rust Rejects repr(packed) Together With repr(align)

repr(packed) and repr(align) express conflicting layout policies and Rust forbids combining them. Model wire bytes separately from aligned in-memory values instead of forcing one struct to serve both contracts.

Reviewed
Rust
Rust 1.98.1
Targets
all targets; exact layout is target-dependent
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
Packed representation lowers alignment while align(N) raises it, so one type is being asked to satisfy opposing memory-layout policies.
First discriminating check
Separate the external byte-layout requirement from the in-memory alignment requirement before choosing which representation belongs on the type.

The failing program places packed and align(8) on one repr attribute. Rust reports E0587 because the type asks for two opposing alignment policies.

packed reduces alignment and removes or limits padding. align increases alignment. A structure cannot truthfully promise both arbitrary compact placement and an increased type alignment through these hints.

Representation attributes are contracts

The Rust Reference describes packed and align as alignment modifiers. They influence type alignment and field placement; they are not optimizer suggestions that rustc may ignore.

#[repr(align(8))] says instances require at least eight-byte alignment. This can be useful for ABI requirements, cache-line strategies, or hardware descriptors.

The packed representation lowers alignment and can place fields without their natural alignment. That may match an external byte layout, but accessing fields through references becomes dangerous because Rust references must be aligned.

When both appear, the compiler refuses to invent precedence.

One struct is often serving two jobs

I usually see this combination when a type is expected to be both:

  • an exact compact wire or file format;
  • a convenient aligned in-memory structure.

These jobs have different rules. A network header is a sequence of bytes with specified offsets and endianness. An in-memory Rust struct follows reference validity, alignment, and target layout rules.

I split them. The wire form is parsed from a byte slice with explicit offsets and endian conversion. The domain form uses normal or explicitly aligned Rust fields. Conversion validates lengths, tag values, reserved bits, and numeric ranges.

This avoids borrowing unaligned fields and makes the trust boundary visible.

The repaired fixture chooses one contract

The repaired program removes packed and retains repr(C, align(8)). It asserts the type alignment. This is correct for the fixture's in-memory requirement.

For a true packed external structure, the repair might instead retain repr(C, packed) and remove align. Then field access must respect unaligned memory. Copying a Copy field by value can be safe in cases where creating a reference would not be. Raw pointers and read_unaligned require a carefully audited unsafe boundary.

I do not choose between these repairs only to satisfy the compiler. I identify the external contract first.

repr(C) does not mean serialized

repr(C) gives C-compatible field ordering and layout rules for the target ABI. It does not define byte order, cross-platform integer width for every C type, pointer meaning, or a stable file format across all targets.

Combining repr(C) with packing may match a particular C declaration under particular compiler pragmas, but I verify against the actual header and target ABI. C compilers have their own packing options, and a copied declaration can still disagree.

For disk or network data, an explicit serializer is normally more portable than transmuting a structure.

Alignment can affect containing types

An align(64) inner type can increase padding in an outer struct and every array element. The memory cost may be significant. A packed inner type can lower field alignment and create unaligned addresses inside containers.

I measure size_of, align_of, and field offsets for supported targets when layout matters. Compile-time assertions can protect a specific ABI, but the asserted values must be correct for each target configuration.

Layout tests are evidence, not a substitute for safety analysis. A struct can have the expected size and still contain invalid bit patterns or unsafe reference access.

Generated declarations can add the conflict

When bindings or macros generate the type, I inspect expanded output and generator settings before editing generated Rust by hand. The foreign build may apply packing through a header pragma while a local wrapper macro adds alignment. Fixing the generator preserves the decision across regeneration and makes the target-specific source of each attribute reviewable.

My debugging sequence

When E0587 appears, I follow this order:

  1. Find all representation attributes, including macro-generated ones.
  2. Write down the required external layout and alignment separately.
  3. Decide whether the type is a wire view, FFI object, or normal domain value.
  4. Remove the conflicting policy and split representations when two jobs exist.
  5. Audit packed field access for accidental references.
  6. Verify size, alignment, offsets, endianness, and target ABI where relevant.

Rust rejects the combination because alignment is part of memory safety, not decoration. A good repair makes one type express one layout contract and uses explicit conversion where compact external bytes meet aligned Rust values.