Mehdi Akiki
Rust Failure Atlas / FFI and targets

RFA-514 · Case file with fixtures · Case 486 of 694 · Compiler evidence

Rust Foreign Function Parameters Cannot Use Destructuring Patterns

An extern declaration describes an ABI boundary without a Rust body where destructuring could run. Receive one FFI-safe representation, then destructure or convert it inside safe Rust code.

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
An external declaration describes a symbol's ABI without a local Rust body in which destructuring could execute, and tuple layout is not an implicit C contract.
First discriminating check
Match the authoritative foreign signature with FFI-safe named representations, then destructure and validate inside a Rust wrapper after the boundary.

I wrote a tuple destructuring pattern directly in an extern "C" declaration. Rust emitted E0130 because a foreign function declaration describes a callable symbol and ABI signature; it contains no Rust body where parameter destructuring would execute.

The failing fixture is syntactically Rust-like, but the boundary has stricter representation rules than an ordinary Rust function definition.

A foreign declaration has no local body

In a normal function definition, a parameter pattern can bind and destructure the incoming Rust value before the body uses its parts. An external block only declares that code elsewhere provides the function.

There is no local entry block for Rust to perform that pattern match. The declaration needs an identifier and a type describing what crosses the ABI.

The official E0130 page recommends replacing the pattern with one regular parameter.

The repaired fixture names an FFI representation

The repaired fixture declares a #[repr(C)] Pair and accepts it as one named parameter. This makes field order follow the chosen representation contract instead of Rust's default struct layout freedom.

In real code, I verify the matching C declaration, integer widths, alignment, calling convention, and platform assumptions. Successful Rust compilation alone cannot prove two languages agree.

The fixture validates declaration shape, not a live foreign symbol.

Destructure inside a Rust wrapper

I usually keep the raw extern declaration small and private, then expose a safe Rust function. The wrapper constructs or receives the FFI-safe value, calls the unsafe boundary under documented conditions, and converts the result into domain types.

If Rust implements a callback, an extern "C" fn with a body can use ordinary local statements to destructure after receiving its ABI-safe parameters.

This separation keeps pattern convenience on the Rust side of the boundary.

Rust tuples are usually not an FFI contract

Even without destructuring syntax, a tuple type may not be FFI-safe. Its default layout is not the same promise as a C struct.

I use explicit #[repr(C)] structs, fixed-width integer types, pointer contracts, and documented ownership. Enums, booleans, strings, slices, and trait objects require particular care because their Rust representations may not match the foreign language.

The Rustonomicon FFI chapter is the primary guide for these unsafe boundaries.

Edition 2024 makes the unsafe boundary visible

External blocks are written as unsafe extern in the current edition because the declarations assert facts Rust cannot verify. Individual functions may carry additional safe or unsafe qualification under the language rules.

This syntax does not make an incorrect ABI safe. It identifies where the programmer must uphold symbol, signature, and calling invariants.

I keep those declarations close to their safety documentation and wrapper tests.

Bindgen is often safer than hand transcription

For substantial C headers, generated bindings reduce spelling and layout drift. Generation still needs version control, target-aware CI, and review of opaque or unsupported constructs.

I test sizes and alignments when both sides expose them, run calls against representative libraries, and pin header/library versions. E0130 catches one Rust syntax issue; integration evidence must cover the actual ABI.

Passing by value and passing by pointer are different ABIs

Replacing a tuple pattern with two scalar parameters or one struct pointer is not automatically equivalent to passing a struct by value. Each form can use different registers, stack layout, nullability, and ownership conventions. I match the authoritative foreign header exactly and let the Rust wrapper provide the ergonomic shape.

For pointers, I document whether null is allowed, how many elements are valid, who owns the allocation, and how long the pointed data remains usable. For output parameters, I initialise and validate according to the foreign contract. The syntactic E0130 repair is only complete when the selected parameter representation is the real ABI, not merely an FFI-safe Rust type.

A symbol can link even when Rust declared the wrong parameter layout. The resulting call may corrupt data or crash. Cross-language integration tests and header-derived bindings are therefore essential evidence beyond the compiler fixture.

My E0130 checklist

  • Is this a foreign declaration or a Rust function definition?
  • Did I place a pattern where only a name and type belong?
  • Is the parameter type actually FFI-safe?
  • Does a repr(C) struct match the foreign declaration exactly?
  • Can destructuring happen inside a safe Rust wrapper?
  • Are calling convention, widths, alignment, and ownership documented?
  • Should bindings be generated from the authoritative header?
  • Do target-specific tests exercise the real symbol boundary?

The core principle is that an extern declaration describes bytes and calling rules crossing languages. Rust patterns belong after that boundary, where a body and Rust's type semantics can perform the destructuring safely.