34. FFI: calling C from Rust and Rust from C
34.1 Calling C
use std::ffi::{c_char, c_int, CStr, CString};
unsafe extern "C" { // edition 2024: extern blocks are `unsafe extern`
fn strlen(s: *const c_char) -> usize; // from libc, linked by default
fn getpid() -> c_int;
safe fn abs(x: c_int) -> c_int; // declared `safe`: callable without an unsafe block
}
fn main() {
let owned = CString::new("hello, C").expect("no interior NUL bytes"); // adds the trailing \0
// SAFETY: `owned` is a valid NUL-terminated string that lives across the call.
let n = unsafe { strlen(owned.as_ptr()) };
// SAFETY: getpid has no preconditions.
let pid = unsafe { getpid() };
println!("strlen={n} pid={pid} abs={}", abs(-7));
// C string -> Rust: borrow with CStr, then validate as UTF-8.
let back: &CStr = owned.as_c_str();
println!("{:?}", back.to_str());
}
| Rust | C |
|---|---|
CString / &CStr |
owned / borrowed char * with a NUL terminator |
*const T / *mut T |
const T * / T * |
#[repr(C)] struct |
struct with C layout (Ch 57) |
Option<extern "C" fn(..)> |
nullable function pointer |
std::ffi::c_int, c_long, ... |
int, long, ... (platform sizes) |
The libc crate declares the whole C
library for each platform, so you rarely write declarations like the ones
above yourself. bindgen generates them from a C header automatically.
34.2 Building and linking your own C code
To reuse code from the C guide, such as its
JSON parser,
compile it with the cc crate from a build script:
// fragment: build.rs (Cargo.toml: [build-dependencies] cc = "1")
fn main() {
cc::Build::new().file("c/json.c").flag_if_supported("-std=c17").compile("cjson"); // libcjson.a
println!("cargo::rerun-if-changed=c/json.c");
}
34.3 Exposing Rust to C: the opaque-pointer pattern
C §40.1
designs a stable C API around opaque handles (CURL *). In Rust you create
the handle with Box::into_raw and destroy it with Box::from_raw:
use std::ffi::{c_char, CStr};
pub struct Counter { hits: u64, last: String }
#[unsafe(no_mangle)] // keep the symbol name `counter_new`
pub extern "C" fn counter_new() -> *mut Counter {
Box::into_raw(Box::new(Counter { hits: 0, last: String::new() })) // ownership passes to C
}
/// # Safety
/// `c` must come from `counter_new`, not be freed, and not be used concurrently.
/// `key` must be a valid NUL-terminated string.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn counter_hit(c: *mut Counter, key: *const c_char) -> u64 {
// Never let a panic unwind into C: catch it and return an error value.
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
// SAFETY: the caller upholds the contract documented above.
let (c, key) = unsafe { (&mut *c, CStr::from_ptr(key)) };
c.hits += 1;
c.last = key.to_string_lossy().into_owned();
c.hits
}))
.unwrap_or(u64::MAX)
}
/// # Safety
/// `c` must come from `counter_new` and must not be used after this call.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn counter_free(c: *mut Counter) {
if !c.is_null() {
// SAFETY: per the contract, `c` is a live pointer from Box::into_raw.
drop(unsafe { Box::from_raw(c) }); // ownership returns to Rust; Drop runs
}
}
fn main() {
let c = counter_new();
let key = std::ffi::CString::new("GET /").unwrap();
// SAFETY: `c` is live, `key` is NUL-terminated.
let n = unsafe { counter_hit(c, key.as_ptr()) };
// SAFETY: last use of `c`.
unsafe { counter_free(c) };
println!("hits={n}");
}
Build it as a C library with crate-type = ["cdylib", "staticlib"] under
[lib], and generate the header with cbindgen. This is how
Rust components go into C and C++ codebases: rustls's C API, librsvg,
the Rust code in Firefox, and Rust in the Linux kernel.