On this page

C ABI and bindgen

The first step in calling C from Rust is to ensure consistent type layout—repr(C) forces Rust structs to lay out fields according to the C ABI, aligning byte-for-byte with structs in C headers. bindgen automatically generates corresponding Rust types and function signatures from C headers, automating the tedious and error-prone manual maintenance of Rust/C type mappings. The core of safe callbacks is: panics must not unwind across FFI boundaries when C calls back into Rust.

C ↔ Rust Type Mapping

FFI type correspondence is not automatic—you must explicitly use C-compatible types from std::os::raw or the libc crate:

C typeRust type (libc)Notes
intc_int (i32)
longc_long32-bit on Windows, 64-bit on Linux!
size_tusize
char * (string)*const c_charCStr::from_ptr()
void **mut c_void
struct pointer (opaque)*mut MyStructDo not dereference; only pass around
enum#[repr(C)] enumC enums are c_int sized

Key pitfall: The size of long is 4 bytes on LLP64 (Windows) and 8 bytes on LP64 (Linux). If your C code relies on long being 8 bytes, use i64 instead on Windows.

bindgen Automation

bindgen automatically generates Rust extern function declarations, structs, and constants from C headers:

// build.rs
fn main() {
    println!("cargo:rerun-if-changed=wrapper.h");

    let bindings = bindgen::Builder::default()
        .header("wrapper.h")
        .allowlist_function("my_lib_.*")     // Only bind these functions
        .allowlist_type("MyStruct")          // Only bind these types
        .generate()
        .expect("Unable to generate bindings");

    bindings.write_to_file(
        std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap())
            .join("bindings.rs"),
    ).expect("Couldn't write bindings");
}

// src/lib.rs
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

bindgen handles: basic type mapping, struct alignment, nested structs, function pointers, conditional defines. It cannot handle: inline functions (must be manually ported to Rust), C++ templates.

Callbacks: Safe Boundaries for C Calling Rust

extern "C" fn callback(x: i32) {
    println!("C called with {}", x);
}

unsafe { register_callback(callback as unsafe extern "C" fn(i32)); }

Panics in callbacks must not cross FFI boundaries—doing so results in undefined behavior. Setting panic = "abort" in Cargo.toml is the most straightforward way to eliminate this risk. If you must retain unwinding, wrap all logic inside the callback with catch_unwind.

References

  • bindgen: rust-lang.github.io/rust-bindgen
  • Rustonomicon: FFI
  • libc crate: docs.rs/libc

Keywords: C ABI, bindgen, c_int, c_long, c_void, callback, panic across FFI, #[repr(C)]