Foreign Function Interface
The Foreign Function Interface (FFI) is the mechanism that lets Rust call code written in other languages and lets other languages call Rust. In practice, FFI almost always means interoperating with C — the lingua franca of systems programming — because virtually every language can produce and consume C-compatible binaries.
FFI is inherently unsafe. C gives no memory-safety, lifetime, or thread-safety
guarantees. Every call across the FFI boundary must be wrapped in an unsafe block,
and the programmer is responsible for upholding every invariant that Rust would
normally enforce automatically.
Calling a C Function from Rust
Use an extern "C" block to declare the signature of a function that lives in a C
library. The "C" string specifies the calling convention — the low-level ABI that
governs how arguments and return values are passed. "C" is the standard C ABI,
supported on all platforms.
// Tell the linker that 'abs' and 'sqrt' exist in the C standard library
extern "C" {
fn abs(input: i32) -> i32;
fn sqrt(x: f64) -> f64;
}
fn main() {
// All calls to foreign functions require unsafe
unsafe {
println!("abs(-5) = {}", abs(-5));
println!("sqrt(16) = {}", sqrt(16.0));
}
}abs(-5) = 5 sqrt(16) = 4
abs yourself — this example is illustrative. For real C interop you will typically use the libc crate.The libc Crate
The libc crate provides comprehensive, cross-platform definitions for all C types
and standard C library functions. Always use it instead of manually mapping C types,
because sizes differ between platforms and architectures.
Add it to Cargo.toml:
[dependencies] libc = "0.2"
use libc::{c_int, c_double};
extern "C" {
fn abs(x: c_int) -> c_int;
fn pow(base: c_double, exp: c_double) -> c_double;
}
fn main() {
unsafe {
println!("abs(-42) = {}", abs(-42));
println!("2^10 = {}", pow(2.0, 10.0));
}
}abs(-42) = 42 2^10 = 1024
C type | libc type | Typical Rust equivalent |
|---|---|---|
int | c_int | i32 |
unsigned int | c_uint | u32 |
long | c_long | i64 on 64-bit |
char * | c_char (as *mut c_char) | *mut i8 |
void * | c_void (as *mut c_void) | *mut () |
double | c_double | f64 |
float | c_float | f32 |
size_t | size_t | usize |
Linking External Libraries
Use #[link(name = "...")] to tell the linker which native library provides the
declarations in an extern block. For most system libraries the linker finds them
automatically; for others you need to be explicit.
// Link against libm (the C math library)
#[link(name = "m")]
extern "C" {
fn sin(x: f64) -> f64;
fn cos(x: f64) -> f64;
}
fn main() {
let pi = std::f64::consts::PI;
unsafe {
println!("sin(pi/2) = {:.4}", sin(pi / 2.0));
println!("cos(pi) = {:.4}", cos(pi));
}
}sin(pi/2) = 1.0000 cos(pi) = -1.0000
build.rs script and useprintln!("cargo:rustc-link-lib=...") to instruct Cargo at build time.Exposing Rust Functions to C
To make a Rust function callable from C:
- Declare it
pub extern "C"— use the C calling convention. - Add
#[no_mangle]— prevent the Rust compiler from mangling the symbol name. Without this, the compiled symbol name becomes something C cannot recognise.
// Callable from C as: int add(int a, int b);
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
// Callable from C as: double multiply(double x, double y);
#[no_mangle]
pub extern "C" fn multiply(x: f64, y: f64) -> f64 {
x * y
}
// Returns a C-compatible bool
#[no_mangle]
pub extern "C" fn is_even(n: i32) -> bool {
n % 2 == 0
}In Cargo.toml, set the crate type to produce a C-compatible shared library:
[lib] crate-type = ["cdylib"] # .so on Linux, .dylib on macOS, .dll on Windows
C-Compatible Struct Layouts with repr(C)
By default, the Rust compiler may reorder struct fields to optimise alignment and
size. C makes no such reordering — field order matches declaration order exactly.
When passing structs across the FFI boundary, annotate them with #[repr(C)] to
guarantee C-compatible memory layout.
// Without #[repr(C)]: Rust may reorder fields — unsafe to pass to C
struct BadPoint { y: f32, x: f32 }
// With #[repr(C)]: field order is guaranteed to match the declaration
#[repr(C)]
struct Point { x: f32, y: f32 }
#[repr(C)]
struct Color { r: u8, g: u8, b: u8, a: u8 }
fn main() {
let p = Point { x: 1.0, y: 2.0 };
let c = Color { r: 255, g: 128, b: 0, a: 255 };
println!("Point ({}, {})", p.x, p.y);
println!("Color ({}, {}, {}, {})", c.r, c.g, c.b, c.a);
}Point (1, 2) Color (255, 128, 0, 255)
Passing Strings Across FFI
Rust strings (String, &str) are not null-terminated and may contain null bytes.
C strings are null-terminated and cannot contain null bytes. The bridge types are
CString (owned, null-terminated) and CStr (borrowed, null-terminated):
- Rust to C: create a
CString, then call.as_ptr()for a*const c_char - C to Rust: wrap the raw pointer with
CStr::from_ptr(ptr), then call.to_str()
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
// Rust -> C: wrap a Rust &str in a CString before passing to C
fn demonstrate_to_c(s: &str) {
// CString::new errors if the string contains a null byte
let c_string = CString::new(s).expect("interior null byte");
// as_ptr() is valid as long as c_string is alive
let ptr: *const c_char = c_string.as_ptr();
println!("raw pointer: {:?}", ptr);
// pass ptr to a C function here — c_string must stay in scope until done
}
// C -> Rust: read a *const c_char as a &str
unsafe fn receive_from_c(ptr: *const c_char) -> &'static str {
// SAFETY: caller guarantees ptr is a valid, null-terminated UTF-8 C string.
CStr::from_ptr(ptr)
.to_str()
.expect("C string contains invalid UTF-8")
}
fn main() {
demonstrate_to_c("Hello from Rust!");
}.as_ptr() after theCString that produced it is dropped — the pointer becomes dangling immediately. Always bind the CString to a named variable to ensure it lives long enough.Null Pointers
C APIs frequently use null pointers as sentinel values — "no result found" or
"optional argument not provided". In Rust, use std::ptr::null() and
std::ptr::null_mut() to create null raw pointers, and always check before
dereferencing.
use std::ptr;
fn process_optional(ptr: *const i32) {
if ptr.is_null() {
println!("received null pointer — nothing to process");
} else {
// SAFETY: we just checked for null; assume caller guarantees valid alignment.
let value = unsafe { *ptr };
println!("value: {}", value);
}
}
fn main() {
let x = 42;
let valid: *const i32 = &x;
let null: *const i32 = ptr::null();
process_optional(valid);
process_optional(null);
}value: 42 received null pointer — nothing to process
Automating Bindings with bindgen and cbindgen
Manually writing extern "C" declarations for large C APIs is tedious and
error-prone. Two tools automate this:
bindgen — reads a C header file (
.h) and generates the corresponding Rustextern "C"declarations,#[repr(C)]structs, and type aliases. Run it from abuild.rsscript or as a one-off CLI tool (cargo install bindgen-cli).cbindgen — the reverse: reads a Rust library and generates a C header file that declares all
pub extern "C"functions and#[repr(C)]types. Useful when distributing a Rust library for C consumers.
# Cargo.toml — add bindgen as a build dependency [build-dependencies] bindgen = "0.69"
// build.rs — auto-generate bindings from a C header at build time
fn main() {
println!("cargo:rerun-if-changed=src/mylib.h");
let bindings = bindgen::Builder::default()
.header("src/mylib.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("failed to generate bindings");
let out_path = std::path::PathBuf::from(
std::env::var("OUT_DIR").unwrap()
);
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("failed to write bindings");
}// src/lib.rs — include the generated bindings
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));FFI Safety Checklist
Annotate every
unsafeblock that calls foreign code with a// SAFETY:comment.Use
#[repr(C)]on every struct passed across the boundary.Check raw pointers for null before dereferencing.
Null-terminate strings with
CStringbefore passing them to C.Never return Rust references or Box pointers to C — pass raw pointers and document ownership clearly.
Match the C library's ownership model: if C expects to free memory you allocated, expose a dedicated
free_xxxfunction.Use
libctypes (c_int,c_char, etc.) rather than assuming Rust primitives match.
#[repr(C)], CString/CStr,bindgen, and careful unsafe discipline, Rust can interoperate with virtually any C library while still providing a safe wrapper layer that protects the rest of your codebase.