---
title: C ABI and bindgen
url: https://doc.liz6.com/en/rust/11-ffi-and-system-interaction/01-c-abi-and-bindgen
locale: en
area: rust
tags:
- rust
- ffi-and-system-interaction
date: 2026-06-30
modified: 2026-07-16
description: '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 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 type | Rust type (libc) | Notes |
|--------|-----------------|-------|
| `int` | `c_int` (i32) | |
| `long` | `c_long` | 32-bit on Windows, 64-bit on Linux! |
| `size_t` | `usize` | |
| `char *` (string) | `*const c_char` → `CStr::from_ptr()` | |
| `void *` | `*mut c_void` | |
| struct pointer (opaque) | `*mut MyStruct` | Do not dereference; only pass around |
| enum | `#[repr(C)] enum` | C 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:

```rust
// 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

```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)]*
