Rust FFI
FFI — Foreign Function Interface — is the bridge between Rust and other languages. FFI lets Rust call C functions, and C code can also call Rust functions.
Declare a C function
Current Rust marks the whole block unsafe — the compiler cannot check anything declared inside it. One plainly safe function may opt out as safe fn:
unsafe extern "C" { fn abs(n: i32) -> i32; }Call it inside unsafe
The declaration is only a promise written by you. The call itself sits in an unsafe block:
let value = unsafe { abs(-7) };
println!("abs(-7) = {value}");Export a Rust function to C
Mark your function extern "C" and keep its plain name with #[unsafe(no_mangle)], so C can find it:
#[unsafe(no_mangle)]
pub extern "C" fn double(n: i32) -> i32 { n * 2 }
println!("double(21) = {}", double(21));Match C's struct layout
#[repr(C)] guarantees a struct is laid out in memory exactly as C would lay it out:
#[repr(C)]
struct Point { x: i32, y: i32 }
let p = Point { x: 1, y: 2 };
println!("point = ({}, {})", p.x, p.y);Text needs a NUL at the end
C text ends with a NUL byte. Rust text does not. CString adds the byte for the trip across:
let text = std::ffi::CString::new("hello").unwrap();
println!("bytes = {:?}", text.as_bytes_with_nul());C can call back: function pointers
C libraries often take a callback — a function they promise to call later. An extern "C" fn is handed over as a plain function pointer. Here apply stands in for the C side:
extern "C" fn double(n: i32) -> i32 { n * 2 }
fn apply(f: extern "C" fn(i32) -> i32, value: i32) -> i32 { f(value) }
fn main() {
println!("result = {}", apply(double, 21));
}Example
unsafe extern "C" { fn abs(input: i32) -> i32; }
fn main() {
let result = unsafe { abs(-7) }; // SAFETY: abs is a pure C function taking i32 by value.
println!("C abs(-7) = {}", result);
}Key points
- Calling a foreign function is unsafe — Rust cannot verify its signature or its promises
- The "C" in extern "C" names the ABI.
- The ABI is the low-level calling rules both sides agree on.
- C is the most common one..
- A calling rule settles the details.
- It says where each value sits during a call.
- It also says how the answer comes back.
- Helper crates do the heavy lifting.
- libc covers system types.
- bindgen generates declarations from C headers.
- cxx gives a safer C++ bridge.
- Wrap each foreign function in a safe Rust function that checks the inputs first.
- Callers then see an ordinary safe API.
- All the unsafe stays in one small, easy-to-check place.
- Anything a callback needs must travel as plain data.
- Often that data is a raw pointer.
- Text crosses in a CString.
- It owns the bytes and adds the NUL byte C expects.
- C knows nothing of Rust closures, captures, or references.
- FFI is the bridge that lets Rust and C call each other.
- Never let a panic cross the FFI border.
- Operating systems, libraries, and decades of existing code speak C.
In this example, the extern "C" block declares abs, a function from the C standard library that is linked automatically. The declaration is only a promise written by the programmer — Rust trusts it. The call happens inside unsafe, with a SAFETY note. abs is a pure C function. It takes an i32 by value. So calling it can break no Rust rules. It returns 7 for -7, and the println! shows C abs(-7) = 7. Rust code just called forty-year-old C, safely fenced.
Note
Unwinding into C is undefined behaviour — catch errors on the Rust side and return an error code.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
Why are FFI calls unsafe?