25.5Calling Rust from C
Last lesson (25.4) called C from Rust. This one runs the boundary the other way: making a Rust function that C can call, or Python, or Ruby, or Node, or any language that can call C, which is essentially all of them. This is how Rust spreads into existing systems without a rewrite: you write a performance-critical or safety-critical piece in Rust and expose it as if it were a C function, and the rest of a large program in some other language calls it with no idea it's talking to Rust. It's the closing pitch of the chapter, and of Rust's systems story: Rust in anything.
Exposing a function to C
To make a Rust function callable from C, you need to do two things to it: tell Rust to use C's calling convention, and tell Rust not to rename it.
#[no_mangle]
pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 {
a + b
}
Two annotations carry the weight. extern "C" (the same "C" ABI from last lesson) tells Rust to use C's calling convention for this function, so C code knows how to pass it arguments and collect its result. And #[no_mangle] tackles a problem you haven't met: by default, the Rust compiler mangles names, it rewrites every function's name into a long, unique, encoded symbol (to support generics, modules, and overloading at the linker level). C code looking for a function called add_numbers would never find the mangled gibberish. #[no_mangle] switches mangling off for this function, so its symbol stays exactly add_numbers, the name C will look for. The pub makes it public. Together: a Rust function that, to the linker and to C, is indistinguishable from a C function named add_numbers.
Notice the body is ordinary, safe Rust. The function itself isn't unsafe, exposing it to C doesn't make its insides dangerous. The danger lives entirely on the C caller's side (they have to call it correctly, with the right types), just as calling C from Rust put the danger on the Rust side. The boundary is unsafe; the Rust implementation behind it is as safe as any other Rust.
Building it as a C-callable library
Writing the function is half of it; you also have to build it into something C can link against. That's a setting in Cargo.toml: you tell Cargo to produce a cdylib, a "C dynamic library," the .so/.dylib/.dll shared-library format C tools expect, rather than a normal Rust binary or library:
[lib]
crate-type = ["cdylib"]
With that, cargo build produces a shared library exporting add_numbers as a plain C symbol. A C program (or a Python script using ctypes, or any FFI-capable language) can then load that library and call add_numbers as though a C programmer had written it. The Rust-ness is completely invisible from the outside; what crosses the boundary is a C function symbol with a C calling convention, and behind it runs your Rust.
Keeping the boundary C-compatible
The same representation mismatch from last lesson applies, now from the other direction: a function exposed to C can only traffic in types C understands. Plain integers and floats cross freely, they mean the same thing everywhere. But you can't hand C a Rust String, a Vec, an Option, or a struct with Rust's memory layout and expect it to make sense, those are Rust concepts with no C equivalent. Across the boundary you stick to C-compatible types: the fixed-width integers, raw pointers (*const/*mut), c_char, and structs explicitly laid out in C's format (marked #[repr(C)] so Rust uses C's field ordering instead of its own). To return a string to C, you'd hand back a raw pointer to a null-terminated buffer, the C-string shape from last lesson, and agree on who frees it. The discipline is the same in both directions: at the boundary, think in C's vocabulary; inside, write normal Rust.
Rust in anything: the closing pitch
This is the quiet superpower of Rust's FFI. Because Rust can present itself as a C library, you can drop Rust into the middle of any existing system, a Python data pipeline, a Ruby web app, a C++ game engine, a Node service, and have it call your Rust as if it were native C. You don't rewrite the whole application; you replace one hot or risky piece with safe, fast Rust and expose it through the C boundary. That incremental path, "rewrite the one function that matters, not the program", is how Rust actually gets adopted in large codebases, and it's possible only because of the FFI this chapter covered.
A note on safety at this boundary
One honest caution to close on. When you expose a Rust function to C, you give up the guarantee that callers behave. C code can call your extern "C" function with the wrong types, pass a null pointer where you expected a valid one, or violate any assumption your Rust code makes, and Rust can't stop it, because the caller isn't Rust. So a Rust function exposed to FFI must be defensive: validate pointers before dereferencing them, never assume inputs are well-formed, and treat data arriving from C with the same suspicion you'd treat raw user input. The safety inside your Rust is real, but the inputs arrive from outside the safe world, and bridging that gap responsibly is the final piece of using unsafe well.
Quiz time
Question #1
What do extern "C" and #[no_mangle] each do when exposing a Rust function to C?
Show solution
extern "C" makes the function use C's calling convention (the C ABI), so C code knows how to pass arguments and receive the result. #[no_mangle] turns off Rust's name mangling for the function, by default Rust encodes function names into long unique symbols, which C couldn't find, so #[no_mangle] keeps the symbol as the plain name (e.g. add_numbers) that C looks for. Together they make the Rust function look like a C function to the linker.
Question #2
What is a cdylib, and why do you need it to call Rust from C?
Show solution
A cdylib ("C dynamic library") is a shared-library build (.so/.dylib/.dll) that exports C-compatible symbols, the format C tools (and other languages' FFI) expect to link against or load. You set crate-type = ["cdylib"] in Cargo.toml. A normal Rust binary or rlib isn't in a form C can use; the cdylib produces the shared library exposing your #[no_mangle] extern "C" functions as plain C symbols.
Question #3
A Rust function exposed to C is itself written in safe Rust. Why must it still be written defensively?
Show solution
Because its callers are C (or another language), which Rust can't check or constrain: C code may pass wrong types, null or dangling pointers, or otherwise violate the function's assumptions, and Rust has no way to prevent it at the boundary. So the function must validate inputs (check pointers before dereferencing, not assume well-formed data) and treat everything arriving from C with suspicion, like untrusted input. The Rust insides are safe, but the inputs come from outside the safe world.
That completes the FFI tour and the unsafe chapter. The summary and quiz (25.x) pull together what unsafe means, the five powers, safe abstractions, and both directions of FFI. After it comes the finale: chapter 26 builds a complete multithreaded web server, putting the whole course to work in one real program, and it does it entirely in safe Rust.