We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Macros, FFI and Type-Driven Design step 23 of 28
Edition 2024: unsafe extern blocks and safe declarations
A pre-2024 extern block that no longer compiles, and four functions to classify.
pub fn root(x: c_double) -> c_double // sqrt
pub fn magnitude(v: c_int) -> c_int // abs
pub fn byte_len(bytes: &[u8]) -> usize // strlen
pub fn compare(a: &[u8], b: &[u8]) -> c_int // memcmp
All four bodies are written. The extern block is not.
The migration
Every tutorial written before 2024 — and most code generated by an LLM today — declares foreign functions like this:
extern "C" {
fn sqrt(x: c_double) -> c_double;
}
In edition 2024 that is a hard error. The block itself must be unsafe:
unsafe extern "C" {
fn sqrt(x: c_double) -> c_double;
}
The keyword is not decoration and it is not about the caller. It says: the author of this declaration is taking responsibility for the signature being correct.
That is a genuinely different claim from the one unsafe { } at a call site
makes. A caller writing unsafe { sqrt(x) } is asserting “I have satisfied
whatever sqrt requires”. The person writing the declaration is asserting
something the caller cannot possibly check: that sqrt really does take one
double and return one double, in the C calling convention, with that exact
symbol name. Get it wrong and every call is undefined behaviour, no matter how
carefully each caller behaves (18.22).
Before edition 2024 there was no place to write that assertion down. Now there
is. rustc’s missing_unsafe_on_extern lint automates the migration — but note
what it does and does not do: it adds unsafe mechanically without verifying a
single signature. The hard part stays manual.
safe declarations: unsafety is per function, not per category
Inside an unsafe extern block, each item may be marked:
unsafe extern "C" {
safe fn sqrt(x: c_double) -> c_double; // callable with NO unsafe block
unsafe fn strlen(s: *const c_char) -> usize; // needs one
fn memcmp(...) -> c_int; // unmarked = unsafe
}
A safe fn in an extern block is callable from safe Rust with no unsafe
block at all. Statics can be marked safe too.
This is the cleanest possible statement of a point that FFI teaching usually fudges: unsafety is a property of a particular function, not of the boundary.
-
sqrt(x)for anyf64— including negative, including NaN, including infinity — returns af64. There is no input that causes memory unsafety. There is no precondition. It is a pure function of its argument. Marking itunsafeand forcing every caller to write a block would be pure ceremony. -
strlen(p)reads memory forward frompuntil it finds a zero byte. Ifpis null, dangling, or points at a buffer with no nul in it, that is a read of unmapped memory. The caller genuinely must promise something.
The dividing line is almost always “does it take a pointer”. Not always — but if you are unsure, that heuristic is right far more often than it is wrong.
How this problem enforces the split
The four function bodies are already written, and they are written in the shape that only compiles for a correct classification:
-
rootcallssqrt(x)outside anyunsafeblock. Marksqrtas unsafe and you get E0133. -
magnitudecallsabs(v)outside any block. Same. -
byte_lenandcomparewrap their calls inunsafe { }. Mark thosesafeand rustc’s warn-by-defaultunused_unsafefires — which underclippy -D warningsfails your submission.
There is exactly one classification that satisfies both directions. You cannot
mark everything unsafe “to be safe”, and you cannot mark everything safe to
avoid blocks.
::: question abs is marked safe here. But 18.22 said abs(INT_MIN) is undefined behaviour in C. Isn’t that a lie?
Yes, and it is the honest kind of lie worth pointing at.
abs(INT_MIN) is genuinely UB by the C standard. Marking abs as safe asserts
that no input can cause unsoundness, and strictly that assertion is false for one
value out of four billion. In practice every real implementation returns
INT_MIN unchanged (the two’s-complement negation of the most negative value is
itself), so the observable consequence is a wrong number rather than memory
corruption — which is why the ecosystem, including the libc crate, treats it as
safe.
The lesson is the important part: marking something safe that is not is a
soundness bug the compiler cannot catch. safe is a claim you make, exactly
like unsafe extern is. Nothing verifies it, ever. If you are wrong, safe Rust
can now invoke undefined behaviour with no unsafe anywhere in the call chain,
which is precisely the situation Rust’s whole safety story is built to prevent.
Be conservative. If a function takes a pointer, it is not safe. If its C
documentation contains the word “undefined”, think hard.
:::
memcmp and determinism
memcmp returns “an integer greater than, equal to, or less than zero” — the
sign is specified, the magnitude is not. Different libc implementations return
different numbers for the same comparison. compare therefore normalises with
.signum(), and falls back to comparing lengths when the common prefix is equal,
so the result is a deterministic -1, 0 or 1.
That normalisation is not test-harness bureaucracy. Depending on the magnitude of
a memcmp result is a real portability bug that survives testing on one platform.
Your job
Migrate the block and classify the four declarations.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.