Using system libraries, embedding Rust in C/C++/Python apps, or gradually rewriting C code all go through FFI.
Rust talks to other languages through the C ABI. To call C, declare the functions in an extern block. In the 2024 edition the block itself must be marked unsafe, and each item can be declared safe or unsafe:
unsafe extern "C" {
pub safe fn abs(x: i32) -> i32; // callable without unsafe
pub unsafe fn strlen(s: *const c_char) -> usize;
}Declaring a signature is a promise the compiler cannot check; a wrong type here is undefined behaviour. For real libraries, generate the declarations with bindgen, link with #[link(name = "foo")] or a build.rs, and compile bundled C with the cc crate.
To be called from C, export a function: #[unsafe(no_mangle)] pub extern "C" fn add(a: i32, b: i32) -> i32, and build a cdylib or staticlib crate type. cbindgen generates the C header.
Types across the boundary must have a C layout: integers, floats, raw pointers, #[repr(C)] structs and enums, and Option<extern "C" fn(...)> for nullable callbacks (a niche makes None the null pointer). Use std::ffi::{c_int, c_char, c_void} for C types.
Strings: C strings are NUL-terminated bytes. `CString` owns one you can pass as .as_ptr() (keep the CString alive while C uses the pointer); `CStr` borrows one received from C, and c"literal" makes a &CStr at compile time.
Ownership rule: memory must be freed by the allocator that created it. Hand Rust objects to C with Box::into_raw and provide a free_thing(ptr) function that calls Box::from_raw.
Panics must not unwind into C. Since Rust 1.81, a panic escaping an extern "C" function aborts the process; catch panics with std::panic::catch_unwind at the boundary, or use the "C-unwind" ABI when unwinding across languages is intended.
Example
use std::ffi::{c_char, c_int, c_void, CStr, CString};
unsafe extern "C" {
safe fn abs(x: c_int) -> c_int;
unsafe fn strlen(s: *const c_char) -> usize;
unsafe fn qsort(base: *mut c_void, n: usize, size: usize,
cmp: Option<unsafe extern "C" fn(*const c_void, *const c_void) -> c_int>);
}
unsafe extern "C" fn cmp_i32(a: *const c_void, b: *const c_void) -> c_int {
// SAFETY: qsort passes pointers to elements of our [i32] array
unsafe { (*(a as *const i32)).cmp(&*(b as *const i32)) as c_int }
}
#[unsafe(no_mangle)]
pub extern "C" fn rust_greeting_len(name: *const c_char) -> usize {
// SAFETY: documented contract: name is a valid, non-null, NUL-terminated string
unsafe { CStr::from_ptr(name) }.to_bytes().len() + 7 // "Hello, " + name
}
fn main() {
let (owned, mut nums) = (CString::new("ferris").unwrap(), [5, -2, 9, 0]); // owned outlives its pointer
// SAFETY: valid pointer, length and element size, and a matching comparator
unsafe { qsort(nums.as_mut_ptr().cast(), nums.len(), size_of::<i32>(), Some(cmp_i32)) };
println!("{} {} {:?}", abs(-4), unsafe { strlen(owned.as_ptr()) }, nums); // 4 6 [-2, 0, 5, 9]
println!("{}", rust_greeting_len(c"Rustacean".as_ptr())); // 16
}Remember
- 2024 edition:
unsafe extern "C" { safe fn ...; unsafe fn ...; }and#[unsafe(no_mangle)] - Only C-layout types cross the boundary; use
CString/CStrfor strings - Free memory with the allocator that allocated it; never let panics unwind into C
Watch out for
- Passing
CString::new(s).unwrap().as_ptr()directly: the temporary is dropped and the pointer dangles.