手把手教你用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里的头文件换成你要绑定的,再调整测试代码就行。

相关文章
|
关系型数据库 开发工具 C语言
PostgreSQL libpq开发入门
简单入门C语言开发基于PostgreSQL libpq应用
|
17天前
|
网络协议 安全 Linux
Rocky Linux 9:Samba服务安装配置全攻略
本文手把手教你用Rocky Linux 9从零搭建Samba文件共享服务:详解防火墙/SELinux配置、smb.conf核心参数(global与共享段)、目录权限与SELinux上下文设置、Samba用户创建及Win/Linux双端访问,新手也能轻松实现跨系统无缝传文件!
|
2月前
|
SQL 关系型数据库 数据库
Postgresql入门之psql用法详解(一)- 命令行参数详解
`psql` 是 PostgreSQL 的命令行客户端,支持交互式或批量执行 SQL 查询。它提供丰富的元命令、脚本自动化、格式化输出(如 CSV、HTML)、连接 URI/服务配置及 LDAP 集成,并可通过命令行选项控制连接、事务与错误处理,适用于日常操作与系统管理。
|
25天前
|
人工智能 JSON Linux
玩转Ollama函数调用:让AI从“光说不练”到“动手解决问题”
你是否厌倦AI瞎编答案?Ollama函数调用功能为AI装上“小手”,让它能调用天气查询、计算器等自定义工具,先做事、再回答,告别胡说八道!本文手把手教你从零实现单次调用、并行调用、多轮智能体循环及流式响应,全程Python实战,小白也能轻松上手。
|
24天前
|
Windows 自然语言处理
Ollama Modelfile 详细使用手册
想用Ollama打造专属模型?Modelfile就是你的“模型食谱”!本文以做菜为喻,零基础手把手教你写Modelfile:FROM选基模、PARAMETER调温度/记忆、SYSTEM定角色(如马里奥)、TEMPLATE规范格式、MESSAGE给示例。全程无术语,附实操步骤与避坑指南,看完即能创建并运行自己的第一个自定义模型。
|
28天前
|
SQL 监控 关系型数据库
PL/pgSQL 入门教程(五):触发器
PostgreSQL触发器是数据库的“自动服务员”,可在INSERT/UPDATE/DELETE等操作时自动执行校验、日志记录、汇总更新等逻辑。支持BEFORE/AFTER/INSTEAD OF时机,ROW/STATEMENT级别,配合NEW/OLD变量实现灵活数据管控,大幅提升数据一致性与运维效率。
|
1月前
|
SQL 存储 关系型数据库
PL/pgSQL 入门教程(一):语法篇
本教程为PL/pgSQL入门首篇,系统讲解其核心基础与语法规则。涵盖函数创建、块结构、变量声明、参数传递、返回类型及排序规则等关键知识点,助你掌握在PostgreSQL中编写高效存储过程与函数的必备技能,提升数据库逻辑处理能力。
|
1月前
|
SQL 存储 关系型数据库
PostgreSQL SQL函数语法详解
本文深入讲解PostgreSQL中SQL语言函数的编写,涵盖参数引用、返回类型(基类型/复合类型/集合)、输出参数、可变参数、默认值、多态函数及排序规则等核心特性,系统阐述其语法、行为与最佳实践。
|
2月前
|
SQL 关系型数据库 Shell
Postgresql入门之psql用法详解(四)- 高级功能
psql 是 PostgreSQL 的交互式命令行工具,支持模式匹配、变量替换、SQL 插值、自定义提示符及行编辑功能。通过 `\d` 等元命令可按名称模式查看对象,支持通配符与正则表达式。变量可动态设置并安全插值到 SQL 中,提升脚本灵活性。提供丰富的环境变量与配置文件(如 `.psqlrc`)来自定义行为,兼容不同终端与编码环境,适用于本地或远程数据库管理。
|
1月前
|
存储 NoSQL 安全
如何保存并分析Linux内核转储(coredump)文件
在Linux中,生成coredump需配置系统参数并满足程序条件。通过ulimit或limits.conf设置核心文件大小,修改core_pattern定义存储路径与命名格式,确保程序无信号屏蔽、权限限制,并留足磁盘空间,最后用gdb分析崩溃堆栈,便于调试定位问题。