Rust 93 🦀 Foreign Function Interface (bindgen and cbindgen)
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.
| Direction | Tool | Input | Output |
|---|---|---|---|
| C → Rust | bindgen | C header | bindings.rs |
| Rust → C | cbindgen | Rust 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
| Aspect | Detail |
|---|---|
| Purpose | Generate Rust bindings from C header |
| Input | C/C++ header |
| Output | bindings.rs with extern "C" and #[repr(C)] |
| Dependency | libclang |
| Build integration | build.rs with bindgen::Builder |
| CLI | bindgen input.h -o bindings.rs |
| Annotation | /// <div rustbindgen opaque></div> |
cbindgen
| Aspect | Detail |
|---|---|
| Purpose | Generate C/C++ header from Rust crate |
| Input | Rust crate with public C API |
| Output | .h or .hpp header |
| Configuration | cbindgen.toml |
| CLI | cbindgen --crate mylib --output mylib.h |
| Language flag | --lang c or --lang c++ |
| CI verification | --verify fails if header is stale |
Rust Requirements for cbindgen
| Requirement | Detail |
|---|---|
#[no_mangle] | Prevents name mangling |
extern "C" | C calling convention |
#[repr(C)] | C-compatible struct layout |
cdylib / staticlib | Crate 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Linker error: undefined symbol | #[no_mangle] missing on exported function | Add the attribute |
| bindgen fails to parse | libclang not installed | Install Clang/LLVM |
| Header out of date | cbindgen not run after Rust change | Use build.rs or --verify in CI |
| Wrong struct layout | Rust struct not #[repr(C)] | Add the attribute |
| Generated code has warnings | C naming conventions | Add #![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
| Item | Value |
|---|---|
| bindgen purpose | C header → Rust bindings |
| bindgen dependency | libclang |
| bindgen build integration | build.rs + bindgen::Builder |
| bindgen CLI | bindgen input.h -o bindings.rs |
| cbindgen purpose | Rust crate → C header |
| cbindgen configuration | cbindgen.toml |
| cbindgen CLI | cbindgen --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:
bindgenautomates the C-to-Rust direction. It parses the C header withlibclangand generatesextern "C"declarations and#[repr(C)]structs. The generated bindings are unsafe by default because the compiler cannot verify the C library’s contracts .cbindgenautomates 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]andextern "C"and the types are#[repr(C)].- The tools translate signatures, not semantics.
bindgencannot know whether the C function expects a non-null pointer, whether it takes ownership of the memory, or whether it is thread-safe.cbindgencannot know whether the C caller will respect the ownership rules. Theunsafeproof remains the programmer’s responsibility. build.rsis the integration point for both tools.bindgenruns inbuild.rsto generate bindings intoOUT_DIR, andcbindgencan run inbuild.rsor as a separate step. Thecbindgen --verifyflag is the CI check that ensures the committed header is not stale .- The generated code is a starting point, not a finished product.
bindgenoutput requires#![allow(non_snake_case)]and similar attributes to compile without warnings.cbindgenoutput 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.
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!