FFI & Interop with C
Prerequisites: Unsafe Rust
C's calling convention is the lingua franca of systems software. Rust can
call C libraries (and anything exposing a C ABI) and can expose functions that
C, Python, Node.js or C# can call. This boundary is the most common place
for unsafe in real projects.
Calling C from Rust
Declare foreign functions in an extern "C" block. In the 2024 edition the
block itself must be written unsafe extern, because you are asserting
the signatures are correct — the compiler cannot check them against the C
header. Items inside can be marked safe when calling them can't cause UB
for any argument.
use std::ffi::{CString, c_char, c_int};
unsafe extern "C" {
// abs is defined for every int, so it can be declared safe.
safe fn abs(x: c_int) -> c_int;
// strlen reads until a NUL byte: the caller must pass a valid C string.
fn strlen(s: *const c_char) -> usize;
}
fn main() {
println!("{}", abs(-7));
let s = CString::new("hello").expect("no interior NUL bytes");
// SAFETY: `s` is a valid NUL-terminated string that outlives the call.
let n = unsafe { strlen(s.as_ptr()) };
println!("{n}");
}
Expected output:
7
5
Both functions come from the C standard library, which Rust programs already
link. For your own C library you tell Cargo what to link, typically from a
build.rs script:
// build.rs
fn main() {
println!("cargo::rustc-link-search=native=vendor/lib");
println!("cargo::rustc-link-lib=static=mylib");
}
The cc crate can compile bundled C sources from build.rs as well.
Strings across the boundary
Rust strings are length-prefixed UTF-8 without a terminator; C strings are NUL-terminated bytes. Two std types bridge them:
CString— an owned, NUL-terminated string you create in Rust and pass to C (as_ptr()).CString::newfails if the input contains\0.CStr— a borrowed view of a C string you received from C.CStr::from_ptris unsafe;to_str()checks UTF-8,to_string_lossy()replaces invalid bytes.
Use the c_int, c_char, c_long, ... aliases from std::ffi rather than
guessing sizes: long is 32 bits on Windows and 64 bits on 64-bit Linux.
Exposing Rust to C
Mark a function extern "C" so it uses the C calling convention, and
#[unsafe(no_mangle)] so its symbol keeps its plain name. (The attribute
is written with unsafe(...) in the 2024 edition because two exported
symbols with the same name are undefined behaviour.)
[lib]
crate-type = ["cdylib"] # a .so / .dylib / .dll; use "staticlib" for a .a / .lib
use std::ffi::{CStr, CString, c_char};
#[unsafe(no_mangle)]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a.wrapping_add(b)
}
/// Returns a newly allocated greeting. Free it with `greeting_free`.
///
/// # Safety
///
/// `name` must be null or a valid NUL-terminated string.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn greeting_new(name: *const c_char) -> *mut c_char {
if name.is_null() {
return std::ptr::null_mut();
}
// SAFETY: checked for null above; the caller guarantees validity.
let name = unsafe { CStr::from_ptr(name) }.to_string_lossy();
CString::new(format!("Hello, {name}!"))
.map(CString::into_raw)
.unwrap_or(std::ptr::null_mut())
}
/// # Safety
///
/// `s` must be null or a pointer returned by `greeting_new`, freed only once.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn greeting_free(s: *mut c_char) {
if !s.is_null() {
// SAFETY: the pointer came from CString::into_raw.
drop(unsafe { CString::from_raw(s) });
}
}
The matching C header and usage:
#include <stdint.h>
#include <stdio.h>
int32_t add(int32_t a, int32_t b);
char *greeting_new(const char *name);
void greeting_free(char *s);
int main(void) {
printf("%d\n", add(2, 3));
char *g = greeting_new("C");
printf("%s\n", g);
greeting_free(g); /* never call free() on memory Rust allocated */
return 0;
}
The rule of ownership across FFI: whoever allocates, frees. Memory from
Rust's allocator goes back to Rust; memory from malloc goes back to free.
Generating bindings
Writing declarations by hand is error-prone for large APIs:
- bindgen reads C headers and generates the Rust
externdeclarations (usually frombuild.rs). Wrap the raw-syscrate in a safe crate. - cbindgen does the reverse: it generates a C header from your Rust
extern "C"functions and#[repr(C)]types.
Structs passed by value across the boundary must be #[repr(C)] so their
field order and padding match C's.
Common mistakes
- Writing
extern "C" { ... }in the 2024 edition:
error: extern blocks must be unsafe
- Passing
CString::new(s).unwrap().as_ptr()directly. The temporaryCStringis dropped at the end of the statement, leaving a dangling pointer. Bind theCStringto a variable first. - Letting a panic reach C. A panic that tries to unwind out of an
extern "C"function aborts the process; catch it withstd::panic::catch_unwindif the caller must see an error code. - Using Rust types without a stable layout (
String,Vec, enums with data, structs without#[repr(C)]) in C signatures. - Mismatched integer sizes. Declare what the C header says, via
std::ffialiases.
Practice
Sort an array of i32 with the C standard library's qsort. Declare it in
an unsafe extern "C" block and write the comparison callback as an
extern "C" fn.
Show solution
use std::ffi::{c_int, c_void};
unsafe extern "C" {
fn qsort(
base: *mut c_void,
count: usize,
size: usize,
compare: extern "C" fn(*const c_void, *const c_void) -> c_int,
);
}
extern "C" fn compare_i32(a: *const c_void, b: *const c_void) -> c_int {
// SAFETY: qsort passes pointers to elements of our i32 array.
let (a, b) = unsafe { (*(a as *const i32), *(b as *const i32)) };
a.cmp(&b) as c_int
}
fn main() {
let mut values = [42, -3, 17, 0, 8];
// SAFETY: base/count/size describe `values` exactly, and the callback
// matches the element type.
unsafe {
qsort(
values.as_mut_ptr().cast(),
values.len(),
std::mem::size_of::<i32>(),
compare_i32,
);
}
println!("{values:?}");
}
This prints [-3, 0, 8, 17, 42]. Ordering converts to -1, 0 or 1,
which is exactly what qsort expects.