手把手教你用bindgen:让Rust轻松调用C库

简介: bindgen是Rust官方推荐的FFI绑定生成工具,可自动将C/C++头文件转换为安全、正确的Rust FFI代码,精准处理类型映射、内存布局与符号导出。本文以bzip2为例,详解从环境配置、build.rs脚本编写到压缩/解压缩功能验证的完整实践流程。

作为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库有多个头文件,我们可以把所有需要的头文件都#includewrapper.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里的头文件换成你要绑定的,再调整测试代码就行。

相关文章
|
存储 算法 安全
PKCS#1、PKCS#5、PKCS#7、PKCS#8到底是什么?
PKCS#1、PKCS#5、PKCS#7、PKCS#8到底是什么?
3397 0
|
存储 iOS开发 Windows
利用Dism修复系统步骤,以及dism找不到源文件解决方案
利用Dism修复系统步骤,以及dism找不到源文件解决方案
17983 0
利用Dism修复系统步骤,以及dism找不到源文件解决方案
|
6月前
|
JSON Java 编译器
Protobuf 是什么?一篇文章搞懂这个高性能序列化神器
Protobuf是Google开源的高效二进制序列化协议,体积小、速度快,支持跨语言、向后兼容。相比JSON,更适合RPC等高性能场景,广泛应用于微服务通信。通过`.proto`文件定义结构,自动生成代码,实现数据的快速序列化与反序列化。
2599 158
|
5月前
|
Rust 安全 C语言
Rust Bindgen入门教程--搞定C的联合、位域与柔性数组
本文详解bindgen处理C语言三大特殊结构:联合(union)、位域(bitfield)和柔性数组(flexible array)。涵盖Rust 1.19+原生union用法、位域的自动getter/setter生成,以及柔性数组的`__IncompleteArrayField`与nightly版DST两种绑定策略,助你安全高效对接C库。
|
5月前
|
域名解析 缓存 网络协议
Linux dnsmasq完全教程
dnsmasq是轻量级DNS+DHCP一体化工具,专为小型局域网设计。它能实现DNS缓存加速、内网域名解析(如用“server.local”代替IP)、自动分配IP地址,配置简单、资源占用低,树莓派或老旧服务器也能流畅运行,是家庭、工作室网络管理的“全能小管家”。
|
运维 程序员 开发者
Docker凉了,国内镜像站全军覆没!
近期在使用Docker部署时,因网络问题导致镜像拉取失败。尽管尝试了阿里云、清华、中科大等国内镜像站均无效,最终找到仍可用的镜像源并分享解决方案。文中提供可正常访问的镜像地址及配置方法,帮助开发者快速恢复开发环境,解决燃眉之急。
4004 1
Docker凉了,国内镜像站全军覆没!
|
9月前
|
消息中间件 Kafka Linux
Linux下安装Kafka 3.9.1
本文介绍Kafka 3.9.1版本的安装与配置,包括通过ZooKeeper或KRaft模式启动Kafka。涵盖环境变量设置、日志路径修改、集群UUID生成、存储格式化及服务启停操作,适用于Linux环境下的部署实践。
1168 0
|
人工智能 C++ iOS开发
ollama + qwen2.5-coder + VS Code + Continue 实现本地AI 辅助写代码
本文介绍在Apple M4 MacOS环境下搭建Ollama和qwen2.5-coder模型的过程。首先通过官网或Brew安装Ollama,然后下载qwen2.5-coder模型,可通过终端命令`ollama run qwen2.5-coder`启动模型进行测试。最后,在VS Code中安装Continue插件,并配置qwen2.5-coder模型用于代码开发辅助。
30549 71