Frontend Labs
⋯

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.


Generated from real-life-rust-guide.md · 8 Oct 2026 16:36