Rust FFI 编程 - 实现 C 库的 Rust 的绑定
虽然 crates.io 上提供了超过 164,780 个包(截至 2024 年 12 月),但在某些情况下,我们可能会遇到 Rust 生态系统中没有现成的包。为了解决这个问题,我们可以通过绑定 C 库并使用 FFI(Foreign Function Interface)来调用这些库。实际上,许多流行的 Rust 包也依赖于 FFI 来调用 C 库,例如 openssl 和 git,它们并不是纯 Rust 实现的。
接下来,我们将介绍如何绑定 C 库并在 Rust 中使用它。
在 Rust 中,所有对外部库的调用都被视为不安全的操作,因此需要使用 unsafe 关键字来声明 unsafe 代码块。extern 块用于声明外部库中的函数签名。一个典型的 extern 块看起来像这样:
unsafe extern "C" {
///...
}
这里的C表示这些函数或变量遵循 C 语言的调用约定。
虽然我们可以完全手动地实现 C 库的绑定,但是使用 bindgen 这个工具可以省很多事。
这里我们以pdfio 这个库为例子。
安装 pdfio
pdfio 需要从源码上编译,因此先下载源码包。
wget https://github.com/michaelrsweet/pdfio/releases/download/v1.3.2/pdfio-1.3.2.tar.gz
tar -zxvf pdfio-1.3.2.tar.gz
cd pdfio-1.3.2
在 Unix 系统上,PDFio 使用配置脚本生成一个 makefile。
./configure
# 或者使用动态库
./configure --enable-shared
默认安装位置是 /usr/local。传递--prefix选项以将其安装到其他位置。
配置完成后,运行以下命令来构建库:
make all
运行安装命令:
sudo make install
pdfio 默认是依赖 ZLIB (https://www.zlib.net) 1.1,因此我们还需要安装 zlib。
这里以 macos 为例子:
brew install zlib
export LDFLAGS="-L/opt/homebrew/opt/zlib/lib"
export CPPFLAGS="-I/opt/homebrew/opt/zlib/include"
至此,pdfio 已经安装好了。
准备项目
我们先创建一个项目,就叫 pdfio-rs 好了。
cargo new pdfio-rs --lib
打开 Cargo.toml 文件,添加依赖:
[build-dependencies]
bindgen = "0.71.0"
我们需要 bindgen来生成绑定代码。
创建 wrapper.h 头
我们需要一个头文件来包含所有的头文件,因此我们创建一个 wrapper.h 文件,内容如下:
#include </usr/local/include/pdfio.h>
如果有多个头文件,都可以在这个文件里列出来。
构建脚本
我们还需要创建 build.rs 文件,用来在构建时生成绑定代码。内容如下:
//build.rs
use std::env;
use std::path::PathBuf;
fn main() {
// 告诉 cargo 在指定目录中查找静态库
println!("cargo:rustc-link-lib=z");
println!("cargo:rustc-link-search=native=/opt/homebrew/opt/zlib/lib");
// 告诉 cargo 在指定目录中查找共享库
println!("cargo:rustc-link-search=/usr/local/lib");
// 告诉 rustc 链接系统 共享库。
// 库名称是 libpdfio.so 或者 libpdfio.a
println!("cargo:rustc-link-lib=pdfio");
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
}
这里需要处理的是zlib库和pdfio库的链接。使用println! 告诉 rustc 链接系统共享库的位置。
需要注意的是,cargo:rustc-link-lib={} 中,库名称是不带前缀的,比如 libpdfio.so 的话,只需要写 pdfio 即可。
这里 cargo:rustc-link-lib=z 的原因是,在macos 下 zlib 的名称是 libz.dylib。
在库中引用生成绑定代码
我们可以使用include!宏将生成的绑定直接导入到我们的 crate 的主入口点src/lib.rs中.
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
因为 bindgen 生成的代码命名基本不符合rust的命名规范,因此需要使用 #![allow(non_upper_case_globals)] 等宏来忽略警告。
事实上,这里的代码已经写好了,可以直接使用。
但是,如果仅此而已,对外调用时会存在大量的不安全代码。而且这些代码使用的参数来源于等效的 C 中的参数,并不是常规的rust 类型,也会让调用者为难。
作为库作者,我们应该尽可能地避免调用者使用不安全代码并且让库变得更rust化。因此,我们需要在 src/lib.rs 中添加一些封装代码。
二次封装
这里为了增加易用性,我们添加了以下封装代码,定义一个结构体:
struct Pdfio {
file: *mut pdfio_file_t,
}
限于限篇幅,这里只列出了封闭打开pdf和获取pdf页数功能。
打开pdf
我们来看看原始的C 代码签名:
pdfio_file_t *pdfioFileOpen(const char *filename, pdfio_password_cb_t password_cb, void *password_cbdata, pdfio_error_cb_t error_cb, void *error_cbdata);
参数含义如下:
filename: 文件名password_cb: 密码回调,NULL表示无password_cbdata: 密码回调数据error_cb: 错误回调,NULL表示无error_cbdata: 错误回调数据
绑定的签名如下:
unsafe extern "C" {
pub fn pdfioFileOpen(
filename: *const ::std::os::raw::c_char,
password_cb: pdfio_password_cb_t,
password_data: *mut ::std::os::raw::c_void,
error_cb: pdfio_error_cb_t,
error_data: *mut ::std::os::raw::c_void,
) -> *mut pdfio_file_t;
}
可以看到,如果不二次封装,是很难让调用者使用的。
为了简化,我们不处理密码问题和错误问题,只做最简单的文件打开。
pub fn file_open(filename:&str) ->Self {
unsafe {
let filename_cstr = std::ffi::CString::new(filename).unwrap();
let filename_ptr = filename_cstr.as_ptr();
let password_cb = None;
let password_data: *mut ::std::os::raw::c_void = std::ptr::null_mut();
let error_cb = None;
let error_data: *mut ::std::os::raw::c_void = std::ptr::null_mut();
let f = pdfioFileOpen(filename_ptr, password_cb, password_data, error_cb, error_data);
Self { file: f }
}
}
获取 pdf 页数
这个比较简单,直接调用 pdfioFileGetNumPages 函数,返回页数。
pub fn get_page_count(&self) -> usize {
unsafe {
pdfioFileGetNumPages(self.file) as usize
}
}
析构器
当 PdfFile 结构体被释放时,需要释放内部的文件指针。记住一个原则: 谁创建了指针,谁负责释放它。
我们需要实现 Rust 的析构器来提供安全并保证这些资源的释放.
impl Drop for Pdfio {
fn drop(&mut self) {
unsafe {
pdfioFileClose(self.file);
}
}
}
最后添加测试:
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_get_page_count() {
let pdf = Pdfio::file_open("test.pdf");
assert_eq!(pdf.get_page_count(), 1);
}
}
运行测试:
cargo test --package pdfio-rs --lib -- test --show-output