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:

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));
    }
}

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!");
}

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 — 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

Further reading

Sources & References