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
Migrate a pre-2024 extern block to Rust edition 2024 and decide which of its
four C functions may be declared safe.
The starter code declares four functions from the C standard library and wraps each one in a Rust function:
pub fn root(x: c_double) -> c_double // calls sqrt
pub fn magnitude(v: c_int) -> c_int // calls abs
pub fn byte_len(bytes: &[u8]) -> usize // calls strlen
pub fn compare(a: &[u8], b: &[u8]) -> c_int // calls memcmp
The four wrapper bodies are already written. Your task is to rewrite the
extern block that declares the C functions. Leave the wrappers as they are.
Background: extern blocks
An extern "C" block declares functions that are defined outside Rust, here in
the platform's C library. The "C" names the calling convention: the rules for
how arguments and return values are passed between caller and callee. The
compiler does not see the C code, so it has to trust that each declared name,
parameter list and return type matches the real function.
The edition 2024 migration
Code written for editions 2015, 2018 and 2021 usually declares foreign functions like this:
extern "C" {
fn sqrt(x: c_double) -> c_double;
}
Edition 2024 rejects that block with the error extern blocks must be unsafe.
The block must be written as:
unsafe extern "C" {
fn sqrt(x: c_double) -> c_double;
}
(unsafe extern is also accepted in earlier editions from Rust 1.82 onward;
edition 2024 makes it mandatory.)
The unsafe on the block is a promise made by whoever writes the declaration:
that every signature in it matches the foreign definition. That is a different
promise from the one an unsafe { ... } block at a call site makes. A caller
writing unsafe { strlen(p) } promises that it has met strlen's
preconditions, such as passing a valid pointer. A caller cannot check whether
the declaration itself is correct. If the declaration says sqrt takes an
f64 but the real symbol takes something else, calls through that declaration
are undefined behavior however careful each caller is.
The missing_unsafe_on_extern lint, part of the rust_2024_compatibility
group that cargo fix --edition applies, can add the keyword for you. It only
inserts unsafe; it does not check any signature. Checking the signatures is
still your job.
safe and unsafe items inside the block
Inside an unsafe extern block, each function or static can be marked
individually:
unsafe extern "C" {
safe fn sqrt(x: c_double) -> c_double; // callable without an unsafe block
unsafe fn strlen(s: *const c_char) -> usize; // calls need an unsafe block
fn memcmp(a: *const c_void, b: *const c_void, n: usize) -> c_int; // unmarked: same as unsafe
}
A safe fn can be called from safe Rust with no unsafe block. An unmarked
item is treated as unsafe, so calling it outside an unsafe block is error
E0133. Statics in the block can be marked safe or unsafe in the same way.
Marking a foreign function safe is a claim that no argument values the
Rust types allow can cause undefined behavior. The compiler does not check that
claim. If it is false, safe Rust code can trigger undefined behavior without
writing unsafe anywhere, which is exactly what Rust's safety rules are meant
to prevent. So whether an individual function may be safe depends on its own
contract, not on the fact that it is a foreign function.
Taking a raw pointer is a common reason a function cannot be safe, because
the caller must promise the pointer is valid. It is not the only reason: a
function can also have undefined behavior for some plain integer inputs, or
depend on global state. Read each function's C documentation.
The four functions
sqrttakes and returns adouble. Everyf64value, including negative numbers, infinities and NaN, is a valid argument: a negative input returns NaN rather than causing undefined behavior. (Some C libraries also record a domain error inerrno, which is a defined side effect.) It reads no memory through pointers, so it can be declaredsafe.abstakes and returns anint. The C standard says that if the result cannot be represented, the behavior is undefined. That happens for exactly one input,INT_MIN(c_int::MINin Rust), because its positive counterpart is one larger thanINT_MAX. Since one allowed input is undefined behavior,absmust not be declaredsafe.strlenreads bytes starting at the pointer until it finds a zero byte (the nul terminator). If the pointer is null or dangling, or no zero byte follows inside the allocation, it reads memory it must not read. The caller has to guarantee a valid, nul-terminated string, so it is notsafe.memcmpreadsnbytes from each of two pointers. The caller must guarantee both are valid fornbytes, so it is notsafe.
How the wrappers check your classification
The wrappers are written so that only one classification compiles cleanly:
rootcallssqrt(x)outside anyunsafeblock. Ifsqrtis unmarked orunsafe, compilation fails with E0133.magnitude,byte_lenandcomparecall their functions insideunsafe { ... }. If one of those functions is markedsafe, the block contains no unsafe operation and rustc emits the warn-by-defaultunused_unsafelint. The Rust grader's Clippy check treats warnings as errors, so the submission fails.
So sqrt must be safe, and the other three must be unsafe fn or unmarked
(the two spellings mean the same thing). Writing unsafe fn explicitly makes
the decision visible to readers.
What the wrappers return
The harness calls one wrapper per test case:
root(x)returns the C library'ssqrt(x)for anf64input.magnitude(v)returns the absolute value ofv. Forc_int::MIN, whichabscannot handle, it does not callabsand returnsc_int::MINunchanged, matching Rust'si32::wrapping_abs. That check is what makes callingabsinside theunsafeblock sound.byte_len(bytes)copies the bytes, appends a zero byte and callsstrlen, so it returns the number of bytes before the first zero byte inbytes(all of them if there is none). For example,[104, 105, 0, 120]gives 2.compare(a, b)compares the two byte sequences lexicographically as unsigned bytes and returns exactly-1,0or1.memcmpcompares the common prefix. If that prefix is equal, the shorter sequence is less, and two equal sequences, including two empty ones, give0.
memcmp only promises the sign of its result: "greater than, equal to, or
less than zero". The magnitude can differ between C libraries, so compare
reduces a nonzero result with .signum(). Code that depends on the exact
value memcmp returns can pass tests on one platform and fail on another.
When the common prefix is empty, compare calls memcmp with n equal to 0
and pointers from empty slices, which are non-null but dangling. Rust's core
library documents that it assumes memcmp with n == 0 is defined for any
pointers, and the comment in the code relies on that.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.