FFI: calling C and being called from C

Rust · Expert · 20 min

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/CStr for 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.

Check yourself

  1. What happens if a Rust panic reaches the edge of an `extern "C"` function (Rust 1.81+)?
  2. Rust hands a `Box<Parser>` to C with `Box::into_raw`. How should it be freed?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: doc.rust-lang.org/nomicon/ffi.html

Builds on

Unlocks

Nothing builds on this yet.

FFI: calling C and being called from C · Rust · Topolearn