Representative interview topic

Rust 2024 Interview: Why must FFI use unsafe extern?

CodingHard
Offer.cc Editorial TeamPublished Updated

Question

A Rust crate fails to compile after moving from the 2021 to the 2024 Edition because of an extern block. Explain the new rule, how you audit a C ABI declaration, and how to wrap the unsafe boundary in a verifiable safe API.

Prompt and Applicable Context

A Rust crate fails to compile after moving from the 2021 to the 2024 Edition because its existing extern block is no longer accepted. Explain why unsafe extern is required, how to audit the ABI declaration, and how to wrap the unsafe boundary in a verifiable safe API.

Rust 2024 requires foreign blocks to use the unsafe keyword. Rust cannot prove the signatures, calling convention, global variables, or pointer contracts supplied by an external library, so the declaration author must own those assumptions.

What the Interviewer Evaluates

The interviewer is looking for a distinction between an unsafe declaration and an API that exposes unsafe at every call site. You should cover ABI, integer widths, layout, nullable pointers, ownership, thread constraints, initialization, and a small auditable FFI module.

Clarifying Questions

Confirm the crate's Edition, target platforms, foreign ABI, and header version. Ask whether functions return owned resources, which pointers may be null, who frees them, whether callbacks cross threads, and whether dynamic library versions can drift. A syntax-only fix is incomplete without these answers.

30-Second Answer Framework

“Rust 2024 marks the extern declaration itself as unsafe because the compiler cannot verify the foreign ABI contract. I would write unsafe extern, audit calling conventions, layout, integer widths, pointer validity, destruction functions, and thread rules, and mark a function safe only when its preconditions are proven by the wrapper. I would keep unprovable conditions unsafe, then validate the migration with cross-target builds and ABI regressions.”

Step-by-Step Deep Dive

Step 1: State the responsibility of unsafe extern

unsafe extern says that a declaration can enable undefined behavior and that the declaration author is responsible for its contract. It does not validate the C implementation or check arguments for callers. The 2024 Edition makes that responsibility visible in source.

Step 2: Audit ABI and layout

Check calling conventions such as extern "C", struct layout, enum representation, alignment, integer widths, and return-value rules. Headers, generated bindings, and the linked library must describe the same versioned contract; a local successful run is not cross-target evidence.

Step 3: Separate safe and unsafe foreign functions

Functions in a foreign block are unsafe by default. A function may be declared safe when its public preconditions have been proved; callers then need no unsafe block, but the declaration author still owns that proof. Do not mark an unknown function safe merely to reduce unsafe syntax.

rust
unsafe extern "C" {
    safe fn library_version() -> u32;
    fn library_parse(ptr: *const u8, len: usize) -> i32;
}

Step 4: Wrap pointers, ownership, and destruction

Before turning a raw pointer into a reference, validate non-nullness, alignment, length, and lifetime. Resources returned by a foreign library normally must be destroyed by its matching free function; Rust's default destructor or another allocator must not be used across that boundary.

Step 5: Check thread and callback constraints

Determine whether handles can cross threads, whether callbacks run on library-owned threads, whether callbacks can re-enter, and whether destruction waits for callbacks. If those properties cannot be statically guaranteed, constrain the wrapper's threading model and provide a shutdown barrier.

Step 6: Put safety preconditions in the wrapper

A safe function should accept Rust types that express its constraints, such as slices, enums, or owned handles, rather than making every caller pass a raw pointer and length. Centralize checks, keep the unsafe operation to a few lines, and document and test every precondition.

Step 7: Validate with migration tools and multiple targets

Run the Edition migration checks and cargo fix --edition, then manually review the generated extern changes. CI should cover host and target platforms, debug and release builds, static and dynamic linking, and ABI regressions against the real library version.

Step 8: Handle version drift and rollback

If headers, bindings, and the dynamic library disagree, pin versions or regenerate bindings instead of hiding the mismatch with casts. Roll out in stages, preserve a rollback path, and monitor load failures, error-code changes, and resource leaks.

High-Quality Sample Answer

I would separate declaration review from call-site encapsulation. First, change each foreign block to unsafe extern "C" and compare the ABI, layout, nullability, ownership, and free functions with the exact header and library version. Mark only functions with proven public preconditions as safe. Then place raw pointers behind a Rust handle and slice-based FFI module that checks lengths, initialization, threading, and callback shutdown, and always frees resources with the library's function. Finally, use cargo fix --edition for mechanical changes, review the diff, and run ABI, error-path, concurrent-shutdown, and dynamic-library compatibility tests on multiple targets. This keeps unsafe visible and auditable without forcing every caller to reproduce the foreign library's hidden contract.

Common Mistakes

Adding unsafe to extern and stopping there

That fixes syntax only. Incorrect signatures, layouts, or destruction protocols can still cause undefined behavior, so contract review and runtime regressions remain necessary.

Marking every foreign function safe

safe is a guarantee to callers, not a compiler hint. Use it only when the wrapper and types continually enforce the preconditions; unknown or globally stateful functions should remain unsafe.

Freeing a C resource with Rust's Box

Cross-allocator destruction can corrupt the heap. The creating library must provide the free function, and the wrapper's Drop implementation should call it with the correct shutdown order.

Follow-Up Questions and Responses

What if the C header does not document struct layout?

Treat layout as an unverified contract. Prefer official bindings or opaque handles. If a struct must cross the boundary, pin compiler, platform, and library versions and verify size, alignment, and end-to-end behavior instead of guessing fields.

What if a library upgrade changes behavior after a safe declaration?

Re-audit the safe guarantee for every version upgrade. Pin library and binding versions and add compatibility tests for error codes, threading, and resource semantics. If the same preconditions no longer hold, remove safe and handle the condition explicitly in the wrapper.

How should a Rust API handle callbacks that can run on any thread?

Do not pass an arbitrary Rust closure with shared mutable state directly to C. Use a thread-safe channel or controlled executor, define callback lifetime and a shutdown barrier, and expose a safe interface only when Send, synchronization, and lifetime requirements are enforced.

Public sources

Related questions

Related interview tool

Use Screenshot for a coding prompt

Capture the problem, then work through the constraints, solution, code, edge cases, and complexity in order.

View the tool