๐ŸŽ“ A1Learner
๐Ÿ”

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?