Mehdi Akiki
Rust Failure Atlas / Upgrades and compatibility

RFA-083 · Case file with fixtures · Case 55 of 694 · Compiler evidence

Rust 2024: Extern Blocks Must Be Unsafe

An extern block is a set of unchecked claims about foreign code. Rust 2024 requires unsafe extern so the declaration-side proof is visible before any call site tries to rely on it.

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

Direct answer

What this Rust failure means

Why it happens
Writing foreign signatures creates an unchecked contract with code outside Rust, and edition 2024 makes the declaration-side proof obligation explicit.
First discriminating check
Add `unsafe extern`, then compare every declared function, static, ABI, and platform type against the authoritative foreign header.

Rust 2024 rejects this declaration even when no foreign function is called:

extern "C" {
    fn atlas_foreign_status() -> i32;
}

The compiler says extern blocks must be unsafe. The failing fixture reproduces it with Rust 1.98.1.

This is not a claim that declaring a function immediately executes unsafe code. It is recognition that the declaration itself can be wrong in ways Rust cannot check. A foreign block is close to writing type signatures for code the compiler cannot see.

The unchecked claim starts at the declaration

Suppose a C header declares a function accepting a pointer and length, but the Rust block swaps their order or chooses the wrong integer width. Every call site can look clean. The unsafe block around a call can be carefully documented. The program is still based on a false signature.

The person writing the extern block is responsible for the name, ABI, parameter types, return type, variadic status, and static declarations. Platform aliases matter too: C long is not the same width on every common data model. Structure layout and calling convention details can change by target.

The Rust 2024 Edition Guide explains why the block must now carry unsafe. The Reference defines external blocks and notes that their declarations are the basis of Rust's FFI.

The compiling form is explicit

// Safety: the host provides this symbol with the documented C signature.
unsafe extern "C" {
    fn atlas_foreign_status() -> i32;
}

The repaired fixture uses this form. Adding unsafe makes the declaration legal, but it cannot validate the comment. I treat the keyword as a review marker attached to the whole foreign contract.

Rust 2024 also allows individual items in an unsafe extern block to be marked safe when calling them has no extra requirements. That decision needs care. A function such as a mathematical operation on plain values may be safe if every bit pattern is accepted and the foreign implementation meets its contract. A function accepting a raw pointer will usually remain unsafe.

Declaration safety and call safety are different

There are two proof locations:

  • The extern block author proves that the signature truthfully describes the foreign item.
  • The caller proves any per-call preconditions, such as pointer validity or buffer length.

Marking the block unsafe addresses the first responsibility. An unsafe function declaration still requires an unsafe call. Marking an item safe removes the call-site unsafe block, so the block author must be confident that all values allowed by its Rust signature are valid inputs.

Conflating these responsibilities produces weak comments. “Safe because called in unsafe” says nothing about the signature. “Signature copied from header version 3.2 for x86_64 Linux” is evidence that can be checked.

Bindgen reduces transcription, not responsibility

Generated bindings are often more reliable than hand-written translations, especially for target-specific layout. They are still generated from a particular header set, flags, target, and tool version. Stale bindings can disagree with a newly installed native library.

I record these inputs and regenerate in a controlled environment. For important boundaries I add size, alignment, and constant checks or compare a small C probe with the Rust view. The exact proof depends on the interface, but “generated” alone is not enough.

Migration can expose old debt

cargo fix --edition can add unsafe to extern blocks. It cannot know whether usize should have been c_ulong, whether a function may unwind, or whether a static is initialized before Rust begins. I keep the mechanical edit, then audit the declarations against their source of truth.

I also look for duplicate handwritten declarations spread across modules. One boundary module is easier to review than several local copies of the same foreign API.

Foreign statics deserve an even stricter pass. Reading one assumes the foreign side initialized a bit pattern valid for the declared Rust type before Rust code began using it. Mutation, thread-local storage, and library initialization order can change that promise. I avoid exposing a foreign static through a safe Rust reference unless the complete lifetime and synchronization story is known; a small accessor wrapper makes the assumptions easier to contain.

The Rust 2024 error is therefore more than ceremony. It points to the earliest place where the compiler must trust a human statement about foreign code. Once that statement is verified, unsafe extern describes the architecture honestly: the block owns the declaration proof, and callers own the remaining operational preconditions.