How to Use bindgen to Generate Rust FFI Bindings

Use bindgen to automatically generate safe Rust FFI bindings from C header files with a single command.

When C headers get in the way

You inherited a C library that does exactly what your project needs. Maybe it is a decades-old image processing engine, a proprietary hardware driver, or a math library your team has trusted for years. Rewriting it in Rust takes months. You just want to call it. Writing the Rust function signatures and struct definitions by hand is tedious and invites subtle bugs. One wrong u32 instead of u64 and your memory layout breaks. A missing #[repr(C)] and the fields shift by four bytes. bindgen solves this. It reads the C headers and writes the Rust code for you.

Don't write bindings by hand. Let the machine do the boring work.

The adapter plate analogy

C and Rust speak different binary languages. C assumes the compiler packs structs one way. Rust packs them another. C uses a specific calling convention for stack cleanup. Rust uses a different one. If you call C code directly with Rust types, the stack gets corrupted. The program crashes.

bindgen builds the adapter plate. It generates the extern "C" blocks and struct definitions that match the C ABI exactly. You don't write the adapter. The tool writes it. You just plug it in. The generated code tells the Rust compiler to lay out memory like C and to call functions like C. The adapter is generated, but the plug is yours. You still have to handle the safety implications.

Minimal example

Start with a C header file. This defines a struct and a function.

// example.h
// A simple point structure
typedef struct {
    int x;
    int y;
} Point;

// Creates a point
Point make_point(int x, int y);

Run bindgen from the command line. It parses the header and outputs a Rust file.

bindgen example.h --output bindings.rs

The output file contains the Rust types. It looks like this.

// bindings.rs
// Generated by bindgen. Do not edit manually.
pub type Point = [u8; 0]; // Opaque if not configured, or struct if visible
// Actually, bindgen usually expands this:
#[repr(C)]
pub struct Point {
    pub x: ::std::os::raw::c_int,
    pub y: ::std::os::raw::c_int,
}
extern "C" {
    pub fn make_point(x: ::std::os::raw::c_int, y: ::std::os::raw::c_int) -> Point;
}

Include the bindings in your Rust code and call the function. You must use unsafe because FFI is inherently unsafe.

// main.rs
mod bindings {
    // Include the generated file so the compiler sees the code
    include!("bindings.rs");
}

fn main() {
    // SAFETY: make_point is a pure function that returns a valid Point.
    // It does not take pointers or mutate global state.
    let p = unsafe { bindings::make_point(10, 20) };
    println!("x: {}, y: {}", p.x, p.y);
}

Trust the generated code for layout, but never trust FFI for safety.

How bindgen builds the bridge

When you run the command, bindgen invokes Clang under the hood. Clang parses the C headers and builds an Abstract Syntax Tree. bindgen walks that tree and maps C concepts to Rust.

It sees a struct Point and emits a Rust struct with #[repr(C)]. That attribute tells the Rust compiler to lay out the fields exactly like C would. Rust normally packs structs to optimize access. #[repr(C)] disables that optimization. It ensures the offset of x is zero and y is four bytes later, matching the C expectation.

It sees a function make_point and wraps it in extern "C". This tells Rust to use the C calling convention. Arguments go on the stack or in registers according to C rules. The caller cleans up the stack. Without extern "C", Rust might use a different convention and the call would fail.

It maps types carefully. int becomes c_int. char becomes c_char. These are aliases defined in std::os::raw. They match the C types on the current platform. On Linux x86_64, c_int is i32. On some embedded targets, it might differ. Using the aliases keeps your code portable.

Convention aside: bindgen generates code that often triggers Clippy lints. The community convention is to add #[allow(clippy::all)] or #[allow(non_snake_case)] to the bindings module. Generated code follows C naming rules, not Rust style. Suppressing lints on the bindings module keeps your CI clean.

Realistic build integration

In a real project, you don't run bindgen from the shell. You integrate it into the build process. This ensures bindings regenerate when headers change. You use a build.rs script.

Add bindgen as a build dependency in Cargo.toml.

# Cargo.toml
[build-dependencies]
bindgen = "0.69"

Create build.rs in the project root. This script runs before compilation.

// build.rs
fn main() {
    // Tell Cargo to rerun this script if the header changes
    println!("cargo:rerun-if-changed=src/c_lib/example.h");

    // Configure bindgen with the Builder pattern
    let bindings = bindgen::Builder::default()
        // Point to the header file
        .header("src/c_lib/example.h")
        // Allowlist only the symbols you need.
        // This keeps the generated file small and compilation fast.
        .allowlist_function("make_point")
        .allowlist_type("Point")
        // Generate layout tests to verify struct sizes at compile time.
        // This catches ABI mismatches early.
        .layout_tests(true)
        // Generate the bindings
        .generate()
        .expect("Unable to generate bindings");

    // Write to a file in src/
    bindings
        .write_to_file("src/bindings.rs")
        .expect("Couldn't write bindings");
}

The Builder pattern lets you customize the output. allowlist_function filters functions. layout_tests adds compile-time assertions. detect_include_paths helps bindgen find system headers.

Include the bindings in your code. Use include! to embed the generated file.

// main.rs
#[allow(clippy::all)]
#[allow(non_snake_case)]
mod bindings {
    include!("bindings.rs");
}

fn main() {
    // SAFETY: Verified that make_point returns a valid Point.
    let p = unsafe { bindings::make_point(10, 20) };
    println!("x: {}", p.x);
}

Automate the generation. Manual bindings rot faster than code.

Pitfalls and compiler errors

The compiler will reject calls to generated functions without unsafe. You get E0133 (dereference of raw pointer requires unsafe) or a call error. You must wrap calls in unsafe { ... }. The unsafe block is your promise that you have verified the preconditions.

Watch the types. bindgen uses c_int, c_char, and other aliases. Don't assume c_int is always i32. On some platforms it is i16 or i64. Use the aliases bindgen provides. If you cast blindly, you might get E0308 (mismatched types) or subtle runtime bugs.

bindgen struggles with complex C macros. It can handle simple #define constants. It cannot parse macros that expand to complex expressions. You might need to use clang_macro_fallback or define the constants manually.

Never edit bindings.rs manually. The file is generated. If you add a field to a struct, the next build wipes it. Use build.rs filters or #[allow] attributes instead. If you need to wrap unsafe logic, write a safe wrapper in a separate module.

Treat bindings.rs as immutable. If you need to change it, change the C header or the bindgen config.

Decision: bindgen vs alternatives

Use bindgen when you have C headers and need to call C functions or access C structs. It handles the tedious mapping and ensures ABI correctness.

Write bindings by hand when the C interface is tiny and bindgen overhead isn't worth it. A single function call might not justify a build.rs script. Hand-written bindings are easier to inspect and wrap immediately.

Filter with bindgen when the C library is massive and you only need a few symbols. Use allowlist_function and allowlist_type to keep the generated file small. This speeds up compilation and reduces noise.

Publish a sys crate when you are building a library for others. Generate bindings in a sys crate and provide a safe wrapper in the main crate. This separates the unstable FFI layer from the stable Rust API.

Generate the raw bindings. Wrap them in safe Rust. That's the pattern.

Where to go next