We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Macros, FFI and Type-Driven Design step 24 of 28
Edition 2024: #[unsafe(no_mangle)] and calling Rust from C in one file
Export two Rust functions to the C world, then call them back through the C ABI from inside the same file.
pub fn run(xs: Vec<i32>) -> (i64, i32)
Exporting Rust to C is half of all real FFI work — embedding Rust in an existing C, Python, Ruby or Node codebase — and the half that tutorials skip because it usually needs a second language and a build system. Here it does not: if a function publishes a real linker symbol, and you declare that symbol and call it, the call goes out through the genuine C ABI and comes back. You can watch it happen.
Publishing a symbol
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_sum(ptr: *const i32, len: usize) -> i64 { ... }
Two attributes-worth of meaning:
-
extern "C"fixes the calling convention (18.21), so C-compiled code knows where to put the arguments. -
#[unsafe(no_mangle)]suppresses Rust’s name mangling, so the symbol in the object file is literallyrust_sumrather than_ZN4demo8rust_sum17h…E.
#[unsafe(export_name = "cracked_clamp")] does the same job but lets you choose
the name, which is how you give a C-facing API a prefix without renaming the Rust
function.
Why unsafe(...) around the attribute
In edition 2024, a bare #[no_mangle] is a hard error:
error: unsafe attribute used without unsafe
no_mangle, export_name and link_section must be written inside an
unsafe(...) wrapper. The rationale is worth understanding rather than
memorising, because it is not the usual “this might read bad memory” kind of
unsafe.
These attributes operate in the global linker namespace, where the compiler
cannot check for collisions. Two crates in your dependency graph that both export
init produce a symbol clash the compiler never sees. Worse, an exported
malloc silently replaces the system one, process-wide — for your code, for
your dependencies, and for the C libraries linked into the process. There is no
diagnostic. unsafe(...) is where you acknowledge that you are writing into a
namespace shared by the entire program.
rustc’s unsafe_attr_outside_unsafe lint automates the migration for pre-2024
code.
Two more rules while you are here:
-
no_mangleon a generic function is meaningless and rustc’sno_mangle_generic_itemslint says so — there is no single symbol to emit, because there is no single function until it is monomorphised. -
Forget
extern "C"on ano_manglefunction and clippy’sno_mangle_with_rust_abicatches it. You would have published a plain symbol using the unstable Rust convention: the worst of both worlds, and silently wrong.
E0428: you cannot declare and define in the same module
This one is mandatory, not stylistic. Write
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_sum(...) -> i64 { ... }
unsafe extern "C" {
pub fn rust_sum(ptr: *const i32, len: usize) -> i64; // E0428
}
and you get:
error[E0428]: the name `rust_sum` is defined multiple times
From Rust’s point of view these are two items with the same name in one module, which is simply illegal — the fact that one is a definition and the other a declaration of the same underlying symbol is a linker-level truth the module system does not model.
Two ways out, and you will use both here:
-
Put the declaration in a nested module.
mod c_side { unsafe extern "C" { … } }. Different module, no collision, and the linker still resolves the symbol because symbols are global. -
Use
#[unsafe(export_name = "…")]so the Rust name and the symbol name differ. Then declare the symbol name, which no Rust item is using.
Panics are not an option
An exported extern "C" function must be panic-free, because a panic
unwinding out of an extern "C" boundary aborts the process (18.21). Not an
error, not a caught exception — SIGABRT.
That is why rust_clamp checks for an inverted range and returns early instead of
calling v.clamp(lo, hi) unconditionally: clamp panics when lo > hi, and a
panic here would kill the process. In production code the belt-and-braces version
wraps the whole body in std::panic::catch_unwind (18.27).
FFI-safe types only
Everything crossing the boundary must have a defined C representation. rustc’s
improper_ctypes_definitions lint checks the ones you define, and it is
warn-by-default — so under clippy -D warnings it is effectively a hard error
here. *const i32, usize, i64 and c_int are all fine. A &str, a tuple or
a Vec would not be (18.25).
Note the shape of rust_sum‘s signature: a pointer plus a length, which is
exactly how you spell “slice” in C. Reconstructing the slice on the Rust side is
std::slice::from_raw_parts, and the null-and-zero case has to be handled
explicitly because C callers pass null for empty far more often than Rust ones
do.
::: question rust_sum is unsafe fn, but its body still needs an unsafe { } block around from_raw_parts. Why twice?
Because edition 2024 changed what unsafe fn means, and the change is an
improvement.
Before, the body of an unsafe fn was implicitly an unsafe block: you could do
anything anywhere inside it without further ceremony. That conflated two
different things — “this function has a precondition callers must satisfy” and
“this function performs unsafe operations” — and it meant a 200-line unsafe fn
gave you no visual indication of which three lines were actually dangerous.
In edition 2024, unsafe_op_in_unsafe_fn is deny by default. The unsafe in
the signature now says only “callers must uphold my contract”; performing an
unsafe operation still needs a block, still wants a // SAFETY: comment, and is
still visible when you skim.
So the two unsafes mean genuinely different things: one is a promise you
extract from your callers, the other is a promise you make to the compiler.
:::
Your job
The starter has a bare #[no_mangle] and an extern block in the wrong module.
Fix both.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.