作为Rust开发者,我们难免会遇到需要调用C/C++库的场景——不管是系统自带的经典库(比如bzip2、zlib),还是公司内部的C/C++老代码。手动写FFI(外部函数接口)绑定代码不仅繁琐,还容易因为内存布局、类型匹配出错,而bindgen就是来解决这个问题的:它能自动把C/C++头文件转换成Rust能直接调用的FFI代码,帮我们省去大量重复工作。
一、先搞懂:bindgen到底能干嘛?
简单说,bindgen是个“翻译工具”——输入C/C++头文件,它会输出对应的Rust代码:把C的结构体、函数、宏都转换成Rust能识别的形式,还会自动处理类型映射(比如C的int转Rust的c_int)、内存布局(保证Rust结构体和C结构体占一样的内存空间)。
比如C里有个简单的结构体和函数:
typedef struct CoolStruct {
int x;
int y;
} CoolStruct;
void cool_function(int i, CoolStruct* cs);
bindgen会自动生成这样的Rust代码,我们不用手动写extern "C",不用纠结指针和类型的问题:
#[repr(C)] // 保证内存布局和C一致
pub struct CoolStruct {
pub x: ::std::os::raw::c_int,
pub y: ::std::os::raw::c_int,
}
extern "C" {
pub fn cool_function(i: ::std::os::raw::c_int, cs: *mut CoolStruct);
}
二、准备工作:安装必要依赖
bindgen依赖libclang(Clang的底层库)来解析C/C++头文件,所以第一步要装Clang(版本要求9.0及以上),不同系统安装方式如下:
Windows
- 方法1(推荐):用winget安装
winget install LLVM.LLVM - 方法2:从LLVM官网下载预编译包安装
安装后记得设置环境变量LIBCLANG_PATH,指向LLVM安装目录的bin文件夹(比如D:\Program Files\LLVM\bin)。如果用MinGW64,直接装:
pacman -S mingw64/mingw-w64-x86_64-clang
macOS
用Homebrew一键安装:
brew install llvm
Linux
- Debian/Ubuntu:
sudo apt install libclang-dev - Arch Linux:
sudo pacman -S clang - Fedora:
sudo dnf install clang-devel
三、实操环节:从零创建项目,调用bzip2
我们的目标:用bindgen自动生成bzip2的Rust绑定,然后写代码测试“压缩-解压缩”功能,验证调用是否成功。
步骤1:创建Rust库项目
首先打开终端,创建一个新的Rust库项目(FFI绑定通常做成库给其他项目用,更贴合实际场景):
cargo new --lib bindgen-bzip2-demo
cd bindgen-bzip2-demo
步骤2:配置Cargo.toml
打开项目根目录的Cargo.toml,添加两部分内容:一是告诉cargo我们有编译前置脚本build.rs,二是添加bindgen作为构建依赖(构建依赖只在编译时生效,不会打包到最终产物里)。
完整的Cargo.toml如下:
[package]
name = "bindgen-bzip2-demo"
version = "0.1.0"
edition = "2021"
# 告诉cargo:编译前先运行build.rs脚本
build = "build.rs"
[build-dependencies]
# 指定bindgen版本(选一个稳定版即可)
bindgen = "0.66.1"
步骤3:创建wrapper.h头文件
为什么要写wrapper.h?因为bindgen需要一个“入口头文件”——如果要绑定的C库有多个头文件,我们可以把所有需要的头文件都#include到wrapper.h里,bindgen只需要处理这一个文件就够了。
在项目根目录新建wrapper.h,内容只有一行(引入bzip2的核心头文件):
#include <bzlib.h>
步骤4:编写build.rs构建脚本
build.rs是cargo的“编译前置脚本”——编译项目前,cargo会先编译并运行这个脚本,我们在这里调用bindgen生成绑定代码。
在项目根目录新建build.rs,代码如下(每行都加了注释,一看就懂):
extern crate bindgen;
use std::env;
use std::path::PathBuf;
fn main() {
// 1. 告诉cargo:编译时链接系统的bzip2库
// 这样Rust代码才能找到bzip2的实际实现
println!("cargo:rustc-link-lib=bz2");
// 2. 配置bindgen,生成绑定代码
let bindings = bindgen::Builder::default()
// 不生成需要夜间版Rust的代码,保证兼容性
.no_unstable_rust()
// 指定要处理的头文件(就是我们写的wrapper.h)
.header("wrapper.h")
// 生成绑定代码,如果失败就直接报错
.generate()
.expect("生成bzip2绑定代码失败!");
// 3. 把生成的代码写到cargo的临时输出目录
// OUT_DIR是cargo自动设置的环境变量,不用手动改
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("写入绑定代码文件失败!");
}
步骤5:引入生成的绑定代码
打开src/lib.rs,替换原有内容:
// 屏蔽命名规范警告(C库的命名和Rust不一样,不用管)
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
// 引入bindgen生成的绑定代码
// concat!把路径拼起来,OUT_DIR是cargo的临时目录
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
现在运行cargo build,如果没有报错,说明绑定代码生成成功了!cargo会把生成的bindings.rs放在target/debug/build/xxx/out/目录下,我们不用手动查看,只要能正常引入就行。
此时我们可以再次运行cargo build,验证绑定代码本身是否能正常编译:
$ cargo build
Compiling libbindgen-tutorial-bzip2-sys v0.1.0
Finished debug [unoptimized + debuginfo] target(s) in 62.8 secs
也可以运行cargo test,验证 bindgen 生成的 Rust FFI 结构体的内存布局、大小和对齐方式是否符合预期:
$ cargo test
Compiling libbindgen-tutorial-bzip2-sys v0.1.0
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/debug/deps/bzip2_sys-10413fc2af207810
running 14 tests
test bindgen_test_layout___darwin_pthread_handler_rec ... ok
test bindgen_test_layout___sFILE ... ok
test bindgen_test_layout___sbuf ... ok
test bindgen_test_layout__bindgen_ty_1 ... ok
test bindgen_test_layout__bindgen_ty_2 ... ok
test bindgen_test_layout__opaque_pthread_attr_t ... ok
test bindgen_test_layout__opaque_pthread_cond_t ... ok
test bindgen_test_layout__opaque_pthread_mutex_t ... ok
test bindgen_test_layout__opaque_pthread_condattr_t ... ok
test bindgen_test_layout__opaque_pthread_mutexattr_t ... ok
test bindgen_test_layout__opaque_pthread_once_t ... ok
test bindgen_test_layout__opaque_pthread_rwlock_t ... ok
test bindgen_test_layout__opaque_pthread_rwlockattr_t ... ok
test bindgen_test_layout__opaque_pthread_t ... ok
test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured
Doc-tests libbindgen-tutorial-bzip2-sys
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
步骤6:写测试,验证调用是否成功
光生成代码还不够,我们写个“压缩-解压缩”的测试,验证能不能真的调用bzip2的功能。
第一步:准备测试文本
在项目根目录新建futurama-quotes.txt,随便写点内容(比如《飞出个未来》的经典台词):
Bite my shiny metal ass!
I'm already a cyborg, what more do you want?
第二步:添加测试代码
在src/lib.rs的末尾添加测试模块:
#[cfg(test)]
mod tests {
use super::*;
use std::mem;
#[test]
fn test_compress_decompress() {
// 调用C库的FFI必须用unsafe块(Rust无法保证C代码的安全性)
unsafe {
// 读取测试文本,转成字节数组
let input = include_str!("../futurama-quotes.txt").as_bytes();
// 准备压缩/解压缩的输出缓冲区(先设成和输入一样大)
let mut compressed = vec![0; input.len()];
let mut decompressed = vec![0; input.len()];
// 1. 初始化压缩流
let mut stream: bz_stream = mem::zeroed(); // 把结构体初始化为0
let init_result = BZ2_bzCompressInit(
&mut stream as *mut _,
1, // 压缩块大小(1=100KB,越小越快)
4, // 日志详细程度(4最详细,0不输出)
0, // 默认工作因子
);
// 检查初始化是否成功
match init_result {
r if r == BZ_CONFIG_ERROR as _ => panic!("配置错误!"),
r if r == BZ_PARAM_ERROR as _ => panic!("参数错误!"),
r if r == BZ_MEM_ERROR as _ => panic!("内存不足!"),
BZ_OK as _ => (), // 成功就继续
r => panic!("未知错误:{}", r),
}
// 2. 执行压缩
stream.next_in = input.as_ptr() as *mut _; // 输入数据指针
stream.avail_in = input.len() as _; // 输入数据长度
stream.next_out = compressed.as_mut_ptr() as *mut _; // 输出缓冲区指针
stream.avail_out = compressed.len() as _; // 输出缓冲区长度
let compress_result = BZ2_bzCompress(&mut stream as *mut _, BZ_FINISH as _);
match compress_result {
r if r == BZ_RUN_OK as _ => panic!("压缩未完成!"),
r if r == BZ_FLUSH_OK as _ => panic!("刷新缓冲区失败!"),
r if r == BZ_FINISH_OK as _ => panic!("收尾失败!"),
r if r == BZ_SEQUENCE_ERROR as _ => panic!("调用顺序错误!"),
BZ_STREAM_END as _ => (), // 压缩完成
r => panic!("压缩错误:{}", r),
}
// 3. 结束压缩流
let end_compress_result = BZ2_bzCompressEnd(&mut stream as *mut _);
if end_compress_result != BZ_OK as _ {
panic!("关闭压缩流失败!");
}
// 4. 初始化解压缩流
let mut decompress_stream: bz_stream = mem::zeroed();
let decompress_init_result = BZ2_bzDecompressInit(
&mut decompress_stream as *mut _,
4, // 日志详细程度
0, // 默认小因子
);
match decompress_init_result {
r if r == BZ_CONFIG_ERROR as _ => panic!("解压缩配置错误!"),
r if r == BZ_PARAM_ERROR as _ => panic!("解压缩参数错误!"),
r if r == BZ_MEM_ERROR as _ => panic!("解压缩内存不足!"),
BZ_OK as _ => (),
r => panic!("解压缩初始化错误:{}", r),
}
// 5. 执行解压缩
decompress_stream.next_in = compressed.as_ptr() as *mut _;
decompress_stream.avail_in = compressed.len() as _;
decompress_stream.next_out = decompressed.as_mut_ptr() as *mut _;
decompress_stream.avail_out = decompressed.len() as _;
let decompress_result = BZ2_bzDecompress(&mut decompress_stream as *mut _);
match decompress_result {
r if r == BZ_PARAM_ERROR as _ => panic!("解压缩参数错误!"),
r if r == BZ_DATA_ERROR as _ => panic!("压缩数据损坏!"),
r if r == BZ_MEM_ERROR as _ => panic!("解压缩内存不足!"),
BZ_STREAM_END as _ => (), // 解压缩完成
r => panic!("解压缩错误:{}", r),
}
// 6. 结束解压缩流
let end_decompress_result = BZ2_bzDecompressEnd(&mut decompress_stream as *mut _);
if end_decompress_result != BZ_OK as _ {
panic!("关闭解压缩流失败!");
}
// 7. 验证结果:解压缩后的内容和原内容一致
assert_eq!(input, &decompressed[..]);
println!("测试成功!压缩解压缩内容完全一致~");
}
}
}
第三步:运行测试
在终端执行:
$ cargo test
Compiling libbindgen-tutorial-bzip2-sys v0.1.0
Finished debug [unoptimized + debuginfo] target(s) in 0.54 secs
Running target/debug/deps/libbindgen_tutorial_bzip2_sys-1c5626bbc4401c3a
running 15 tests
test bindgen_test_layout___darwin_pthread_handler_rec ... ok
test bindgen_test_layout___sFILE ... ok
test bindgen_test_layout___sbuf ... ok
test bindgen_test_layout__bindgen_ty_1 ... ok
test bindgen_test_layout__bindgen_ty_2 ... ok
test bindgen_test_layout__opaque_pthread_attr_t ... ok
test bindgen_test_layout__opaque_pthread_cond_t ... ok
test bindgen_test_layout__opaque_pthread_condattr_t ... ok
test bindgen_test_layout__opaque_pthread_mutex_t ... ok
test bindgen_test_layout__opaque_pthread_mutexattr_t ... ok
test bindgen_test_layout__opaque_pthread_once_t ... ok
test bindgen_test_layout__opaque_pthread_rwlock_t ... ok
test bindgen_test_layout__opaque_pthread_rwlockattr_t ... ok
test bindgen_test_layout__opaque_pthread_t ... ok
block 1: crc = 0x47bfca17, combined CRC = 0x47bfca17, size = 2857
bucket sorting ...
depth 1 has 2849 unresolved strings
depth 2 has 2702 unresolved strings
depth 4 has 1508 unresolved strings
depth 8 has 538 unresolved strings
depth 16 has 148 unresolved strings
depth 32 has 0 unresolved strings
reconstructing block ...
2857 in block, 2221 after MTF & 1-2 coding, 61+2 syms in use
initial group 5, [0 .. 1], has 570 syms (25.7%)
initial group 4, [2 .. 2], has 256 syms (11.5%)
initial group 3, [3 .. 6], has 554 syms (24.9%)
initial group 2, [7 .. 12], has 372 syms (16.7%)
initial group 1, [13 .. 62], has 469 syms (21.1%)
pass 1: size is 2743, grp uses are 13 6 15 0 11
pass 2: size is 1216, grp uses are 13 7 15 0 10
pass 3: size is 1214, grp uses are 13 8 14 0 10
pass 4: size is 1213, grp uses are 13 9 13 0 10
bytes: mapping 19, selectors 17, code lengths 79, codes 1213
final combined CRC = 0x47bfca17
[1: huff+mtf rt+rld {
0x47bfca17, 0x47bfca17}]
combined CRCs: stored = 0x47bfca17, computed = 0x47bfca17
test tests::round_trip_compression_decompression ... ok
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured
Doc-tests libbindgen-tutorial-bzip2-sys
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
如果输出test tests::test_compress_decompress ... ok,说明整个流程跑通了——我们成功用Rust调用了C的bzip2库!
不管是调用系统库还是自定义C库,这套流程基本都适用——只要把wrapper.h里的头文件换成你要绑定的,再调整测试代码就行。