Rust bindgen:为 C 头文件生成 Rust 调用绑定

2025年8月26日Elecmonkey

基本用法

命令行方式

首先安装 bindgen 可执行文件:

cargo install bindgen-cli

然后运行绑定生成:

bindgen input.h -o bindings.rs

其中 input.h 是你的 C 头文件,bindings.rs 是生成的 Rust 绑定。如果需要传递包含目录或宏定义,可以在命令后加 -- 及相应 clang 参数(例如 -I)。

库调用方式(build.rs)

Cargo.toml[build-dependencies] 中加入 bindgen,然后在 build.rs 中使用:

use std::env;
use std::path::PathBuf;
use bindgen;

fn main() {
    let bindings = bindgen::Builder::default()
        .header("wrapper.h")
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        .generate()
        .expect("无法生成绑定");
    
    bindings.write_to_file(PathBuf::from(env!("OUT_DIR")).join("bindings.rs"))
        .expect("无法写入绑定文件");
}

在 Rust 代码中通过 include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 来包含生成的绑定。

Clang/Libclang 依赖

bindgen 依赖 libclang(Clang 9.0+)

  • Windows: 使用 winget install LLVM.LLVM,设置 LIBCLANG_PATH 为 LLVM 安装目录的 bin 路径
  • Linux (Debian/Ubuntu): apt install libclang-dev
  • macOS: 使用 Homebrew 安装 llvm

复杂 C 类型支持

结构体和联合体

  • 结构体: 直接翻译为带 #[repr(C)] 的 Rust 结构体
  • 联合体: 如果所有字段都可 Copy,生成原生 union;否则生成 __BindgenUnionField 包装

枚举处理

// 生成新类型加关联常量
.newtype_enum("MyEnum")

// 生成真正的 Rust enum(注意越界风险)
.rustified_enum("MyEnum")

// 位标志枚举
.bitfield_enum("MyFlags")

函数指针

C 中的函数指针类型会转换为 Option<extern "C" fn(...)>,支持空指针情况。

位域处理

Rust 不原生支持 C 结构体中的位域,bindgen 会生成访问器方法:

// C: struct { unsigned a:1; unsigned b:1; unsigned c:2; };
let mut bf = unsafe { create_bitfield() }; 
bf.set_a(1);
println!("a={}", bf.a());
bf.set_b(1);
bf.set_c(3);

构建时自动生成绑定

build.rs 中动态生成绑定:

use std::env;
use std::path::PathBuf;
use bindgen;

fn main() {
    let header = "wrapper.h";
    let bindings = bindgen::Builder::default()
        .header(header)
        .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
        // 可选:指定 clang 参数
        //.clang_args(&["-I/path/to/include"])
        // 可选:只生成需要的符号
        //.allowlist_function("foo_.*")
        //.allowlist_type("MyStruct")
        .generate()
        .expect("无法生成绑定");
    
    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("bindings.rs");
    bindings.write_to_file(out_path).expect("写入绑定文件失败");

    // 链接 C 库
    println!("cargo:rustc-link-search=native=/path/to/lib");
    println!("cargo:rustc-link-lib=mylib");
}

FFI 安全性

unsafe 使用

外部函数和指针操作放在 unsafe { } 块中:

unsafe {
    let result = c_function(ptr);
}

签名正确性

Rust 无法在编译时验证 FFI 函数签名,必须使用正确的 C 类型(使用 libc 里的 c_intc_char 等别名)。

内存和所有权

处理指针时明确内存所有权和生命周期:

// 字符串转换示例
use std::ffi::{CString, CStr};

let c_string = CString::new("Hello").unwrap();
let ptr = c_string.as_ptr();
// 调用 C 函数...

空指针检查

指针处理为 Option<fn> 返回,强迫调用方进行空指针检查。

错误处理

把 C 函数的错误表示包装成 Rust 中常用的错误处理枚举 —— ResultOption