| |

Rust 93 🦀 Foreign Function Interface (bindgen and cbindgen)

⏱️ Estimated read: 11 min | |

Writing FFI bindings by hand is tedious and error-prone. The signatures must match the C ABI exactly, the types must map correctly, and a single mistake produces undefined behavior that may not surface until runtime. bindgen and cbindgen automate the two directions of this translation: bindgen reads a C header and generates Rust extern declarations, while cbindgen reads a Rust crate and generates a C header. The tools are complementary, and a project that calls C and is called by C uses both.

Key point: bindgen and cbindgen translate signatures. They do not verify that the C side and the Rust side agree on semantics, memory ownership, or lifetime. The generated bindings are the starting point for the unsafe proof, not a replacement for it.


Why binding generators exist

The signature drift problem. A C header changes, and the Rust extern block does not. The compiler cannot detect the mismatch because the extern block is a declaration, not a definition. The program compiles and links, and the call is undefined behavior. bindgen eliminates this class of error by generating the Rust declarations from the header itself. If the header changes and bindgen re-runs, the Rust declarations change with it.

The type mapping problem. C has int, long, unsigned char, and platform-dependent sizes. Rust has i32, i64, u8, and the libc type aliases. Mapping between them by hand is a source of subtle bugs. bindgen performs the mapping using the target platform’s ABI, so the generated c_int is the correct size for the target .

The header maintenance problem. A Rust library exposed to C needs a header file that C programmers can include. Writing it by hand means duplicating the type definitions and keeping them synchronized with the Rust code. cbindgen generates the header from the Rust source, so the two cannot drift .


a. bindgen: From C Header to Rust

bindgen is a Rust library and a command-line tool. It uses libclang to parse the C or C++ header and produces a bindings.rs file containing extern "C" blocks and #[repr(C)] structs . The library form is used in a build.rs script so that the bindings are regenerated whenever the header changes.

// build.rs — generate bindings from a C header
use std::env;
use std::path::PathBuf;

fn main() {
    // Re-run if the header changes
    println!("cargo:rerun-if-changed=wrapper.h");

    let bindings = bindgen::Builder::default()
        .header("wrapper.h")
        .generate()
        .expect("Unable to generate bindings");

    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings!");
}

The wrapper.h header is a single file that includes the C library’s headers. bindgen parses it and generates the Rust declarations. The OUT_DIR is a Cargo-provided directory where build artifacts are written. The generated file is included in the crate with include! .

// src/main.rs — use the generated bindings
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]

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

fn main() {
    // SAFETY: the C library has been initialized.
    unsafe {
        let result = example_function(2, 3);
        println!("{}", result);
    }
}

The allow attributes are necessary because bindgen preserves the C naming conventions. A C function named example_function becomes a Rust function with the same name, which violates Rust’s snake_case convention. The attributes suppress the warnings without changing the generated names.

The command-line form is used when the bindings are generated once and committed to the repository. bindgen input.h -o bindings.rs produces the same output without a build.rs . The trade-off is that the bindings must be regenerated manually when the header changes.


b. cbindgen: From Rust to C Header

cbindgen analyzes a Rust crate and generates a C or C++ header. It requires a cbindgen.toml configuration file, which can be empty to start, and a Rust crate with a public C API—functions marked #[no_mangle] and extern "C", and types marked #[repr(C)] .

# cbindgen.toml — minimal configuration
language = "C"
include_guard = "MYLIB_H"
# Generate the header
cbindgen --config cbindgen.toml --crate my_rust_library --output my_header.h

The --crate flag specifies the crate name, and --output specifies the header path. The --lang c flag selects C output; the default is C++ . The --verify flag generates the bindings and compares them to the existing file, erroring if they differ. This is useful in CI to ensure that the committed header is up to date .

cbindgen can also be used as a library in a build.rs script, which means the header is generated as part of the build rather than as a separate step . The library form is appropriate when the header is not committed and is consumed by a build system that runs cargo build before compiling the C code.


c. The Boundary Between the Tools

The two tools solve opposite problems, and a project that both calls C and is called by C uses both. bindgen is for consuming a C API; cbindgen is for exposing a Rust API. They do not interact, and they are not alternatives.

DirectionToolInputOutput
C → RustbindgenC headerbindings.rs
Rust → CcbindgenRust crate.h header

The generated bindings are unsafe by construction. bindgen produces extern "C" blocks with unsafe functions, and the caller must provide the SAFETY reasoning. cbindgen produces a header that declares the C functions, but it cannot enforce that the C caller respects the ownership rules that the Rust implementation assumes. The tools translate the types, not the contracts.

A common pattern is to use bindgen to generate bindings for a system library, and cbindgen to generate a header for a Rust wrapper that the rest of the C codebase can call. The wrapper is where the memory management and the error handling are implemented, and the generated header is the contract that the C side sees.


Complete Example Session

// ============================================
// PART 1: C HEADER TO CONSUME
// ============================================
// wrapper.h
// int add(int a, int b);
// typedef struct { int x; int y; } Point;
// void move_point(Point* p, int dx, int dy);


// ============================================
// PART 2: BINDGEN IN BUILD.RS
// ============================================
// build.rs
use std::env;
use std::path::PathBuf;

fn main() {
    println!("cargo:rerun-if-changed=wrapper.h");
    let bindings = bindgen::Builder::default()
        .header("wrapper.h")
        .generate()
        .expect("Unable to generate bindings");
    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    bindings.write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings!");
}


// ============================================
// PART 3: USING THE GENERATED BINDINGS
// ============================================
// src/main.rs
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

fn main() {
    // SAFETY: the C library is linked and initialized.
    unsafe {
        println!("{}", add(2, 3));
        let mut p = Point { x: 0, y: 0 };
        move_point(&mut p, 1, 1);
    }
}


// ============================================
// PART 4: RUST LIBRARY TO EXPOSE
// ============================================
// src/lib.rs
#[repr(C)]
pub struct Rectangle {
    pub width: f64,
    pub height: f64,
}

#[no_mangle]
pub extern "C" fn rectangle_area(r: *const Rectangle) -> f64 {
    if r.is_null() {
        return 0.0;
    }
    // SAFETY: r is non-null and points to a valid Rectangle.
    let r = unsafe { &*r };
    r.width * r.height
}


// ============================================
// PART 5: CBINDGEN CONFIGURATION
// ============================================
// cbindgen.toml
language = "C"
include_guard = "RECTANGLE_H"
autogen_warning = "/* Warning: this file is autogenerated by cbindgen. */"


// ============================================
// PART 6: GENERATE THE HEADER
// ============================================
// cbindgen --config cbindgen.toml --crate rectangle --output rectangle.h


// ============================================
// PART 7: THE GENERATED HEADER
// ============================================
// rectangle.h
// #ifndef RECTANGLE_H
// #define RECTANGLE_H
// typedef struct Rectangle {
//     double width;
//     double height;
// } Rectangle;
// double rectangle_area(const Rectangle* r);
// #endif


// ============================================
// PART 8: CBINDGEN VERIFY IN CI
// ============================================
// cbindgen --verify --config cbindgen.toml --crate rectangle --output rectangle.h
// Fails if the header is out of date


// ============================================
// PART 9: CARGO.TOML FOR CDYLIB
// ============================================
// [lib]
// crate-type = ["cdylib", "staticlib"]


// ============================================
// PART 10: THE COMPLETE BOUNDARY
// ============================================
// C → Rust: bindgen generates bindings.rs from wrapper.h
// Rust → C: cbindgen generates rectangle.h from lib.rs
// Both tools translate signatures, not contracts

The ten parts trace the two directions. Parts 1 through 3 show bindgen consuming a C header. Parts 4 through 7 show cbindgen exposing a Rust library. Part 8 shows the CI verification step.


Quick Reference

bindgen

AspectDetail
PurposeGenerate Rust bindings from C header
InputC/C++ header
Outputbindings.rs with extern "C" and #[repr(C)]
Dependencylibclang
Build integrationbuild.rs with bindgen::Builder
CLIbindgen input.h -o bindings.rs
Annotation/// <div rustbindgen opaque></div>

cbindgen

AspectDetail
PurposeGenerate C/C++ header from Rust crate
InputRust crate with public C API
Output.h or .hpp header
Configurationcbindgen.toml
CLIcbindgen --crate mylib --output mylib.h
Language flag--lang c or --lang c++
CI verification--verify fails if header is stale

Rust Requirements for cbindgen

RequirementDetail
#[no_mangle]Prevents name mangling
extern "C"C calling convention
#[repr(C)]C-compatible struct layout
cdylib / staticlibCrate type for C linkage

Best Practices

✅ Do This:

// Use build.rs for bindgen so bindings stay in sync
println!("cargo:rerun-if-changed=wrapper.h");                          // ✅
// Configure cbindgen with a committed cbindgen.toml
language = "C"                                                          // ✅
# Verify headers in CI
cbindgen --verify --crate mylib --output mylib.h                       // ✅
// Mark the public API with #[repr(C)] and extern "C"
#[repr(C)] pub struct Point { x: f64, y: f64 }                         // ✅

❌ Don’t Do This:

// Commit generated bindings and never regenerate
// Header changes silently break the Rust side                              // ❌
// Use cbindgen without #[no_mangle]
pub extern "C" fn f() {} // symbol is mangled; C cannot find it        // ❌
// Expect bindgen to generate safe functions
// The generated extern block is unsafe by default                       // ❌
// Assume the generated header enforces ownership
// The C side can still double-free or use-after-free                    // ❌

Common Pitfalls

PitfallWhy It HappensFix
Linker error: undefined symbol#[no_mangle] missing on exported functionAdd the attribute
bindgen fails to parselibclang not installedInstall Clang/LLVM
Header out of datecbindgen not run after Rust changeUse build.rs or --verify in CI
Wrong struct layoutRust struct not #[repr(C)]Add the attribute
Generated code has warningsC naming conventionsAdd #![allow(non_snake_case)]

Real-World Examples

1. bindgen build.rs

bindgen::Builder::default().header("wrapper.h").generate().unwrap();

2. bindgen CLI

bindgen input.h -o bindings.rs

3. Include Generated Bindings

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

4. cbindgen CLI

cbindgen --crate mylib --output mylib.h

5. cbindgen C Output

cbindgen --lang c --crate mylib --output mylib.h

6. cbindgen Verify

cbindgen --verify --crate mylib --output mylib.h

7. Rust Struct for C

#[repr(C)]
pub struct Point { pub x: f64, pub y: f64 }

8. Rust Function for C

#[no_mangle]
pub extern "C" fn area(r: *const Rectangle) -> f64 { ... }

9. Cargo cdylib

[lib]
crate-type = ["cdylib"]

10. cbindgen.toml

language = "C"
include_guard = "MYLIB_H"

Visual

The Two Directions

┌──────────────────────────────────────────────────────────────┐
│  C → RUST                     RUST → C                       │
│                                                              │
│  ┌─────────────────┐          ┌─────────────────┐           │
│  │ wrapper.h       │          │ lib.rs          │           │
│  │ int add(...);   │          │ #[no_mangle]    │           │
│  │                 │          │ extern "C" fn   │           │
│  └────────┬────────┘          └────────┬────────┘           │
│           │ bindgen                    │ cbindgen            │
│           ▼                            ▼                     │
│  ┌─────────────────┐          ┌─────────────────┐           │
│  │ bindings.rs     │          │ mylib.h         │           │
│  │ extern "C" { }  │          │ int add(...);   │           │
│  │ #[repr(C)]      │          │                 │           │
│  └─────────────────┘          └─────────────────┘           │
│                                                              │
│  Both translate signatures. Neither verifies semantics.      │
│                                                              │
└──────────────────────────────────────────────────────────────┘

bindgen in the Build

┌──────────────────────────────────────────────────────────────┐
│  cargo build                                                 │
│         │                                                    │
│         ▼                                                    │
│  ┌─────────────────┐                                        │
│  │ build.rs        │  Runs before compiling                  │
│  │ bindgen::Builder│                                        │
│  └────────┬────────┘                                        │
│           │ parses wrapper.h                                 │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ OUT_DIR/        │  Generated bindings                    │
│  │ bindings.rs     │                                        │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ src/main.rs     │  include! the bindings                 │
│  │                 │  call unsafe functions                 │
│  └─────────────────┘                                        │
│                                                              │
│  The bindings are regenerated when wrapper.h changes.        │
│                                                              │
└──────────────────────────────────────────────────────────────┘

cbindgen in CI

┌──────────────────────────────────────────────────────────────┐
│  CI PIPELINE                                                 │
│                                                              │
│  ┌─────────────────┐                                        │
│  │ Rust code       │                                        │
│  │ changed         │                                        │
│  └────────┬────────┘                                        │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ cbindgen        │                                        │
│  │ --verify        │                                        │
│  └────────┬────────┘                                        │
│           │                                                  │
│     ┌─────┴─────┐                                           │
│     ▼           ▼                                           │
│  ┌──────┐   ┌──────┐                                       │
│  │Match │   │Diff  │                                       │
│  │      │   │      │                                       │
│  │Pass  │   │Fail  │                                       │
│  └──────┘   └──────┘                                       │
│                                                              │
│  The committed header must match the generated header.       │
│  Stale headers fail the build.                              │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Type Mapping

┌──────────────────────────────────────────────────────────────┐
│  C TYPE              RUST TYPE (via bindgen)                 │
│                                                              │
│  int                 c_int (i32 on most platforms)           │
│  long                c_long                                  │
│  char                c_char                                  │
│  char*               *mut c_char / *const c_char             │
│  struct Point        #[repr(C)] struct Point                 │
│  void                c_void                                  │
│                                                              │
│  bindgen uses the target platform's ABI.                     │
│  The mapping is correct for the target, not universal.       │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
bindgen purposeC header → Rust bindings
bindgen dependencylibclang
bindgen build integrationbuild.rs + bindgen::Builder
bindgen CLIbindgen input.h -o bindings.rs
cbindgen purposeRust crate → C header
cbindgen configurationcbindgen.toml
cbindgen CLIcbindgen --crate mylib --output mylib.h
cbindgen language flag--lang c
cbindgen CI verification--verify
Rust export requirements#[no_mangle], extern "C", #[repr(C)]

Key takeaways:

  • bindgen automates the C-to-Rust direction. It parses the C header with libclang and generates extern "C" declarations and #[repr(C)] structs. The generated bindings are unsafe by default because the compiler cannot verify the C library’s contracts .
  • cbindgen automates the Rust-to-C direction. It analyzes the Rust crate and generates a C or C++ header. The header is only correct if the Rust functions are marked #[no_mangle] and extern "C" and the types are #[repr(C)] .
  • The tools translate signatures, not semantics. bindgen cannot know whether the C function expects a non-null pointer, whether it takes ownership of the memory, or whether it is thread-safe. cbindgen cannot know whether the C caller will respect the ownership rules. The unsafe proof remains the programmer’s responsibility.
  • build.rs is the integration point for both tools. bindgen runs in build.rs to generate bindings into OUT_DIR, and cbindgen can run in build.rs or as a separate step. The cbindgen --verify flag is the CI check that ensures the committed header is not stale .
  • The generated code is a starting point, not a finished product. bindgen output requires #![allow(non_snake_case)] and similar attributes to compile without warnings. cbindgen output requires a configuration file to control the include guard, the language, and the formatting.

Remember: bindgen and cbindgen are the mechanical part of FFI. They translate the types and generate the declarations, and they eliminate the drift that occurs when a header and a binding are maintained separately. But they do not replace the reasoning that FFI requires. A generated binding is a promise that the signature is correct; it is not a promise that the memory is valid, the pointer is non-null, or the ownership is respected. The tools make the boundary easier to maintain; they do not make it safe.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!