Mehdi Akiki
Rust Failure Atlas / FFI and targets

RFA-604 · Case file with fixtures · Case 576 of 694 · Compiler evidence

Rust repr(transparent) Cannot Be Combined with repr(C)

Choose whether the wrapper delegates ABI to one field or owns a C record layout. Combining both does not strengthen compatibility; it contradicts it.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all Rust targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
Representation hints were stacked as confidence markers even though they select different authorities for the type's layout and ABI.
First discriminating check
Determine whether the foreign side expects the inner scalar or a C record, choose that one model, and verify real cross-language calls.

repr(transparent) and repr(C) answer different layout questions. Transparent delegates layout and ABI to one field. C representation lays out the Rust item according to documented C-compatible rules for its kind. The failing fixture requests both and gets E0692.

More repr hints do not mean more compatibility

Representation attributes are not safety badges that can be stacked. Each imposes a concrete rule. If rules have incompatible models, the compiler must reject the type rather than choose one.

The official E0692 explanation says a transparent type delegates all representation concerns to another type, which contradicts adding another representation hint such as C.

I begin from the foreign contract. Is the external API expecting exactly the underlying scalar ABI, or a C struct containing that scalar? These may differ in calling convention even when size and alignment look equal.

The repair chooses transparent delegation

The repaired fixture keeps only repr(transparent) around u64 and asserts size equality. This matches the intent of a type-safe request identifier represented like its underlying integer.

If a header declares struct RequestId { uint64_t value; };, I would model and test that C struct contract instead. Adding an intermediate repr(C) inner wrapper and then transparently wrapping it produces the ABI of that struct wrapper, not necessarily the scalar. E0692's documentation explicitly cautions that these representations are not interchangeable.

The transparent layout rules are the source of truth. I avoid relying only on current size_of results.

ABI includes calls, not only memory

Two types can share size and alignment yet be passed differently in registers under a platform ABI. This is particularly important for single-field structs, floats, vectors, and aggregates.

I compile a small C or foreign-language harness against the produced library and call both directions. Static layout assertions are necessary evidence for shared memory, but a real call checks symbol, calling convention, and argument classification.

Target triples matter. A result on x86-64 Linux does not prove Windows, ARM, or WebAssembly behaviour. I limit support claims to the tested matrix.

Transparent wrappers still enforce Rust invariants

A RequestId can prevent mixing unrelated u64 values, hide construction, and provide formatting without runtime field overhead. Transparent representation does not automatically permit creating it from every integer if the domain rejects zero or reserves ranges.

Safe constructors validate. Unsafe conversion functions document validity obligations. If every bit pattern of the inner type is valid for the wrapper, by-value conversion can be straightforward, but that is a domain proof separate from layout.

Auto traits, drop behaviour, and additional zero-sized fields also deserve review. I keep FFI wrapper types simple and avoid custom Drop unless ownership is explicitly part of the foreign contract.

C representation belongs where the record is defined

When the inner type is a multi-field C record, I place repr(C) on that record. A transparent domain wrapper around it may be valid if the wrapper has one representation field. Each layer then has one clear job.

I do not introduce layers only to satisfy attribute syntax. Every wrapper can affect API identity and validity even if layout is preserved. The foreign header, Rust public API, and conversion functions should explain why each exists.

Changing either representation is an ABI change. I use artifact tests and versioned interfaces rather than treating it as an internal refactor.

My E0692 checklist

  • Does the foreign side expect an underlying scalar or a C record?
  • Which single representation rule matches that declaration?
  • Is transparent being stacked with C as an unsupported “extra safety” hint?
  • Could an inner repr(C) wrapper change call ABI from the raw field?
  • Are size, alignment, offsets, and actual foreign calls tested per target?
  • Does the wrapper have stricter valid values than its inner type?
  • Are zero-sized fields and Drop behaviour compatible with the contract?
  • Would changing this attribute break an existing binary interface?

The core principle is that ABI attributes select models, not confidence levels. E0692 prevents two incompatible layout authorities. I choose one from the actual external contract and verify the final artifact where it will run.