🎓 A1Learner
🔍
0 of 149 lessons completed

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?