Mehdi Akiki
Rust Failure Atlas / FFI and targets

RFA-125 · Case file with fixtures · Case 97 of 694 · Linker evidence

Why an extern Function Compiles but Fails With an Undefined Symbol

An extern block declares a foreign contract; it does not provide the implementation or link the library. Trace declaration, symbol spelling, link directives, artifact architecture, and final linker inputs as separate evidence.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
x86_64-unknown-linux-gnu evidence, native-linking targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
The extern block declares an assumed ABI contract but no linked object or library actually exports the symbol with the required spelling.
First discriminating check
Copy the exact undefined name, inspect exports in the intended library, and compare them with the final linker command and target architecture.

The failing program declares rfa_missing_symbol in an unsafe extern "C" block and calls it correctly inside unsafe. Rust accepts the declaration and type-checks the call. Final linking fails because no object in the link defines that symbol.

This separation is essential: an extern declaration tells rustc what the function is assumed to look like. It does not create the function.

Declaration, safety, and linkage are three checks

The external blocks reference describes foreign function declarations and their unsafe requirements. The declaration gives a name, ABI, parameter types, and return type.

The unsafe call promises that the foreign contract is correct for this invocation. That includes pointer validity, ownership, and other preconditions not represented by the signature.

The linker later asks a different question: which object or library provides a compatible symbol with this name? The Rust linkage reference describes how Rust artifacts and native libraries participate in final linkage.

I debug these layers separately:

rustc parsing/type checking -> is the declaration usable as Rust?
unsafe contract             -> is this call semantically valid?
linker                      -> is a matching implementation present?

Passing one layer does not prove the next.

Read the final undefined name

The evidence runner performs a real link, not only --emit=metadata. The linker reports undefined symbol: rfa_missing_symbol. That exact spelling is the starting point.

I inspect the supposed library with platform tools such as nm, readelf, or dumpbin and compare exported names. C++ name mangling, leading underscores, symbol versions, visibility, and calling-convention decoration can all change what the linker sees.

If the library contains a similar but different symbol, adding more search paths does not help. If it contains the exact symbol but the linker never receives the library, changing Rust types does not help.

Native dependencies are commonly connected through a build script emitting cargo::rustc-link-lib and cargo::rustc-link-search, or through #[link] attributes for appropriate cases. Sys crates centralize this discovery.

I run Cargo verbosely and inspect the final rustc and linker command. The library may be found during a build script probe but absent from the final link line. Static library order, --as-needed, feature gates, and target-specific manifest sections can affect inclusion.

Absolute local paths are poor repairs because they do not travel to CI or release hosts. I define how the native artifact is installed, vendored, or built for each target.

The repaired fixture supplies the implementation

The repaired program defines a C-ABI function and exports an unmangled symbol with edition-2024 unsafe attribute syntax. The program calls the Rust item directly and verifies the result.

This proves the linker can resolve a supplied symbol in the minimal environment. In a real FFI case, the repaired form would normally link the actual external library and retain an extern declaration matching its header.

I keep generated or manually reviewed bindings tied to a known library version. A resolved symbol can still use an incompatible signature and cause undefined behavior at runtime.

Architecture and format must match

A library can exist at the path and still be unusable: 32-bit versus 64-bit, wrong operating-system object format, incompatible C runtime, static versus dynamic variant, or missing transitive native dependencies.

The linker often prints “file format not recognized,” “wrong architecture,” or secondary undefined symbols in these cases. I record target triple and inspect the artifact rather than only checking that a filename exists.

Cross-compilation needs target libraries, while proc macros and build scripts execute on the host. Mixing their search paths is a common source of misleading success and failure.

Dynamic loading moves failure to runtime

Using a dynamic loading API can avoid a static undefined-symbol link error, but it moves discovery and type agreement to runtime. The application then needs explicit error handling for missing files, missing symbols, version mismatch, and library lifetime.

This can be correct for plugins. It is not a free fix for a mandatory dependency. I choose static linkage, dynamic linkage, or runtime loading from the product lifecycle and deployment model.

My debugging sequence

When an extern call ends in an undefined symbol, I do this:

  1. Copy the exact missing symbol from the final linker diagnostic.
  2. Verify an implementation exists in the intended artifact with symbol-inspection tools.
  3. Compare ABI, name mangling, signature, and visibility with the foreign header.
  4. Inspect the actual final linker inputs and search paths.
  5. Check artifact architecture, target, and transitive native dependencies.
  6. Add a clean link-and-run test on every supported platform.

An extern block is a declaration of trust, not evidence that code exists. A reliable FFI boundary proves both halves: Rust's declaration matches the foreign contract, and the build delivers the exact implementation to the final linker.