Dec 10, 2024
3 min read
Rust,
FFI,

Rust FFI 编程 - 实现 C 库的 Rust 的绑定

如何使用 Rust 通过 FFI 绑定 C 库,以 pdfio 为例,详细说明了从安装 C 库、生成绑定代码到二次封装和测试的整个过程。

Rust FFI 编程 - 实现 C 库的 Rust 的绑定

虽然 crates.io 上提供了超过 164,780 个包(截至 2024 年 12 月),但在某些情况下,我们可能会遇到 Rust 生态系统中没有现成的包。为了解决这个问题,我们可以通过绑定 C 库并使用 FFIForeign Function Interface)来调用这些库。实际上,许多流行的 Rust 包也依赖于 FFI 来调用 C 库,例如 opensslgit,它们并不是纯 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

总结

pdfio: https://github.com/michaelrsweet/pdfio