Foreign Function Interface (FFI)
No language is an island. Decades of critical software — operating systems, libraries, codecs, crypto — is written in C, and any systems language that couldn't talk to it would be dead on arrival. Rust's foreign function interface lets it call C code (and be called from C), which is essential for interoperating with the existing world and for adopting Rust incrementally into C/C++ codebases. Crossing that boundary means leaving Rust's safety guarantees behind, so FFI is inherently unsafe — but it's a controlled, well-defined kind of unsafe.
This post covers the foreign function interface (FFI) — how Rust interoperates with other languages, especially C. It covers calling C from Rust, exposing Rust to C, why FFI is unsafe, and the practice of wrapping FFI in safe abstractions. FFI builds directly on unsafe Rust (the previous post — crossing the language boundary is unsafe) and is essential for interoperating with the vast body of existing C code. Understanding FFI shows how Rust fits into the real, polyglot world.
Why FFI matters
FFI — calling between Rust and other languages — matters because Rust must interoperate with the huge existing body of code (mostly C) and support incremental adoption:
- The world runs on C libraries. An enormous amount of essential software — OS APIs, libraries, codecs, crypto, drivers — is written in C (or exposes a C interface). A systems language that couldn’t call C would be cut off from all of it. FFI lets Rust use the vast existing C ecosystem. Interop with C is essential. The existing world is C.
- FFI enables incremental adoption. FFI also enables incremental adoption — introducing Rust into existing C/C++ codebases piece by piece (Rust calling the existing C, or C calling new Rust), rather than rewriting everything at once. This gradual-adoption path is a big reason FFI matters (you can add Rust to a C project incrementally). FFI enables gradual Rust adoption. Rewrite piece by piece.
- C is the lingua franca. C is the common interface language — most languages can call C, and C can call them, via a stable ABI (application binary interface). So Rust’s FFI centers on C interop (the common denominator). Even calling other languages often goes through a C interface. FFI centers on C (the lingua franca). The universal boundary.
FFI (calling between Rust and other languages, centered on C) matters because Rust must interoperate with the vast existing body of C code (OS APIs, libraries) and support incremental adoption into C/C++ codebases — with C as the lingua franca of interop. The most common direction is calling C from Rust.
Calling C from Rust
To call C functions from Rust, you declare them in an extern block and call them within unsafe:
// Declare the external C function (here, abs from the C standard library).
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
// Calling a foreign function is unsafe — the compiler can't verify it.
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
- Declare foreign functions in an
externblock. Anextern "C" { ... }block declares functions from another language (their signatures) that Rust can call."C"specifies the ABI (the C ABI — how functions are called at the binary level). You declare the signature; the actual function is linked from the C library. Declare foreign functions withextern "C". Signatures for linking. - Calling them is unsafe. Calling a foreign function requires
unsafe— because the compiler can’t verify the foreign code upholds Rust’s guarantees (it’s C, outside Rust’s checks). Crossing the language boundary is inherently unsafe (the previous post’s theme). Foreign calls are unsafe. The boundary isn’t checked. - You must get the signatures right. A key responsibility: the
externdeclarations must correctly match the C function signatures (types, ABI) — a mismatch causes undefined behavior (the compiler can’t check the C side). Getting FFI signatures exactly right is crucial (and error-prone — tools likebindgenhelp generate them). Match signatures exactly. Mismatches are UB.
Calling C from Rust means declaring the foreign functions in an extern "C" block (their signatures, with the C ABI) and calling them within unsafe (crossing the language boundary is unsafe, and you must match the C signatures exactly — mismatches cause undefined behavior). The reverse direction — exposing Rust to C — has its own requirements.
Exposing Rust to C
To let C call Rust functions, you make them use the C ABI and prevent name mangling — so C can find and call them:
// Make this Rust function callable from C.
#[unsafe(no_mangle)] // don't mangle the name, so C can find "call_from_c"
pub extern "C" fn call_from_c() {
println!("Just called a Rust function from C!");
}
- Use
extern "C"on the Rust function. Marking a Rust functionextern "C"makes it use the C ABI (so C can call it with C’s calling convention). This exposes the Rust function to C.extern "C"exposes Rust to C. C-compatible functions. - Prevent name mangling. By default, Rust mangles function names (encodes them for its own use) — which C wouldn’t recognize. The
no_mangleattribute prevents mangling, keeping the name as-is so C can find it by name.no_manglekeeps the name C-findable. So C can link to it. (In current Rust this is written#[unsafe(no_mangle)], marking it as a safety-relevant attribute.) - Then Rust can be a C library. With
extern "C"andno_mangle, Rust functions become callable from C — so you can build a Rust library with a C interface (that C, or any C-compatible language, can use). This is how Rust code gets adopted into C/C++ projects (Rust exposing a C API). Rust can present a C API. Adoptable by C code. - Care at the boundary. Exposing Rust to C requires care — using C-compatible types, handling the safety of data crossing the boundary, and matching what C expects. The FFI boundary demands correctness on both sides. Correctness at the boundary matters. Both sides must agree.
Exposing Rust to C means marking functions extern "C" (C ABI) and no_mangle (keep the name C-findable) — so Rust can present a C API that C (or any C-compatible language) can call, enabling Rust’s adoption into C/C++ projects. Crossing the boundary in either direction is unsafe, so wrapping FFI in safe abstractions is the key practice.
Wrapping FFI in safe abstractions
As with all unsafe, the crucial practice is wrapping FFI in safe Rust abstractions — a safe API over the unsafe foreign calls:
- FFI is unsafe; wrap it safely. FFI calls are
unsafe(crossing the language boundary). The idiomatic practice (from the unsafe post): wrap the unsafe FFI in a safe Rust API — the unsafe foreign calls are contained inside, and Rust callers use a safe interface (that handles the boundary correctly). Wrap unsafe FFI in a safe API. Contain the boundary. - This is how Rust bindings work. Rust bindings to C libraries typically have two layers: a low-level unsafe layer (
-syscrates — rawexterndeclarations matching the C library) and a safe wrapper layer (idiomatic, safe Rust API over the unsafe bindings). You use the safe wrapper; the unsafe FFI is contained below. Bindings: unsafe layer + safe wrapper. The-sys+ safe-crate pattern. - The safe wrapper handles the boundary. The safe wrapper handles the FFI complexities — converting types, managing memory/ownership across the boundary, ensuring the unsafe calls are used correctly — so callers get a safe, idiomatic Rust API. It encapsulates the unsafe FFI. Safe wrappers encapsulate FFI complexity. Idiomatic Rust over raw C.
- Result: safe use of C libraries. The payoff: you can use C libraries from Rust safely (via safe wrappers), getting the C ecosystem’s functionality with Rust’s safety at the usage level (the unsafe FFI contained and verified below). Safe wrappers give safe access to C. The C world, used safely.
FFI — Rust’s interoperability with other languages (centered on C) — lets Rust call C (via extern "C" blocks, unsafely) and be called from C (via extern "C" + no_mangle), essential for using the vast C ecosystem and incremental adoption; and as with all unsafe, the key practice is wrapping FFI in safe abstractions (unsafe -sys layer + safe wrapper), giving safe access to C libraries. This shows how Rust fits the polyglot world. Next: closures and function pointers.
Key takeaways
- FFI (calling between Rust and other languages, centered on C) matters because Rust must interoperate with the vast existing body of C code (OS APIs, libraries, codecs, crypto) — which it would be cut off from otherwise — and because FFI enables incremental adoption of Rust into existing C/C++ codebases piece by piece; C is the lingua franca of interop.
- Calling C from Rust means declaring the foreign functions in an
extern "C" { ... }block (their signatures, specifying the C ABI) and calling them withinunsafe(the compiler can’t verify foreign code, so crossing the boundary is unsafe) — and you must match the C signatures exactly, since mismatches cause undefined behavior (tools likebindgenhelp). - Exposing Rust to C means marking functions
extern "C"(to use the C ABI/calling convention) andno_mangle(to keep the name unmangled so C can find it by name) — letting Rust present a C API that C or any C-compatible language can call, enabling Rust’s adoption into C/C++ projects. - FFI calls are unsafe (crossing the language boundary), so the key practice is wrapping them in safe Rust abstractions — Rust bindings to C libraries typically have two layers: a low-level unsafe layer (
-syscrates with rawexterndeclarations) and a safe wrapper layer (idiomatic safe Rust API over them). - The safe wrapper handles the FFI complexities (type conversion, memory/ownership across the boundary, correct use of the unsafe calls), so you get safe access to C libraries — the C ecosystem’s functionality with Rust’s safety at the usage level, the unsafe FFI contained and verified below.
Further reading
- The Rustonomicon — Foreign Function Interface
- Unsafe Rust (previous post)
- The Rust Reference — external blocks (extern)