< 返回版块

Mike Tang 发表于 2020-10-15 11:09

Tags:rust,ffi,

cbindgen 是一个从 Rust 库(这个库已面向暴露 C 接口进行设计)生成 C/C++ 头文件的工具。

我们在最初 Rust 生态还没起来的时候,一般都是使用 Rust 对已有的 C 库进行封装,这时,就会用到 bindgen 多一些。但是随着 Rust 生态越来越成熟,可能大量的库直接使用 Rust 实现了。这时,反而想导出 C 接口,进而供其它语言调用,这时,就会用到 cbindgen 了。

为什么这类工作,需要用到这种辅助工具呢?因为,真的很枯燥啊!!!

其实,FFI封装、转换,熟悉了之后,知识点就那些,模式也比较固定,如果接口量很大,那就需要大量重复的 coding。量一大,人手动绑定出错的机率也大。所以这种辅助工具的意义就显露出来了。基于辅助工具生成的代码,如不完美,再适当手动修一修,几下就能搞定,大大提高生产效率。

当然,如果你的 Rust 库,只是导出一两个接口,那就没必要使用这个工具了。

如何使用 cbindgen

使用 cbindgen 有两种方式:

  1. 以命令行工具的方式使用;
  2. 以库的方式在 build.rs 中使用;

两种方式各有其方便和不方便之处。第一种形式不需要写代码,但是每次 Rust 库修改升级后,可能要重新运行一次这个命令行,以生成最新的 C 头文件。第二种形式在 Rust 库编译的过程中,就自动生成了 C 头文件。

下面我们来看第一种方式。

命令行工具方式

安装 cbindgen

cargo install --force cbindgen

对一个暴露了 C API 的 Rust crate,直接:

cbindgen --config cbindgen.toml --crate my_rust_library --output my_header.h

就可以了。my_header.h 就是生成的头文件。

要注意两点:

  1. 不是任意 Rust crate 都可以,而是已经做了暴露 C API 开发的库才行。因为 cbindgen 会去扫描整个源代码,把对接的接口抽出来;
  2. 可以通过 cbindgen.toml 这个配置文件,给 cbindgen 配置行为参数,参数很多,后面有参考链接。

build.rs 方式

build.rs 的基础知识,不在这里讲了,传送门在这里:https://doc.rust-lang.org/cargo/reference/build-scripts.html

build.rs 的功能就是在编译期间,在编译真正的 crate 之前,先编译执行 build.rs 中的代码。于是,可以在这里面做 on-fly 生成之类的工作。

按照下面这种模板使用 cbindgen 就好了:

extern crate cbindgen;

use std::env;

fn main() {
    let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();

    cbindgen::Builder::new()
      .with_crate(crate_dir)
      .generate()
      .expect("Unable to generate bindings")
      .write_to_file("bindings.h");
}

在 build.rs 方式里,也是可以配置 cbindgen.toml 参数的。这里,编译之后,生成的 bindings.h 就是我们要的 C 头文件。

生成的结果看起来是什么样子?

比如,我们有一个 Rust crate:

// trebuchet.rs

#[repr(u8)]
enum Ammo {
    Rock,
    WaterBalloon,
    Cow,
}

#[repr(C)]
struct Target {
    latitude: f64,
    longitude: f64,
}

// notice: #[repr(rust)]
struct Trebuchet { ... }

#[no_mangle]
unsafe extern "C" fn trebuchet_new() -> *mut Trebuchet { ... }

#[no_mangle]
unsafe extern "C" fn trebuchet_delete(treb: *mut Trebuchet) { ... }

#[no_mangle]
unsafe extern "C" fn trebuchet_fire(treb: *mut Trebuchet,
                                    ammo: Ammo,
                                    target: Target) { ... }

生成后的 C header 文件如下:

// trebuchet.h

#include <cstdint>
#include <cstdlib>

extern "C" {

enum class Ammo : uint8_t {
    Rock = 0,
    WaterBalloon = 1,
    Cow = 2,
};

struct Trebuchet;

struct Target {
    double latitude;
    double longitude;
};

void trebuchet_delete(Trebuchet *treb);

void trebuchet_fire(Trebuchet *treb, Ammo ammo, Target target);

Trebuchet* trebuchet_new();

} // extern "C"

可以看到,cbindgen 完整实现了我们的意图:

  1. Ammo 有正确的大小和值;
  2. Target 包含所有字段和正确的布局(字段顺序);
  3. Trebuchet 被声明为一个 opaque 结构体;
  4. 所有的函数有了对应的声明。

补充

  1. cbindgen 不但可以生成 C 头文件,还可以生成 C++ 的,甚至还可以生成 swift 可以用的头文件;
  2. cbindgen 不是一包到底的,对 C 的支持相对成熟,对 C++ 的差一些。适当的时候,还是需要手动做很多事情的;
  3. cbindgen 对 Rust 的泛型,有一定的支持;

在下一篇,我们将会使用 cbindgen 对我们之前写的库做一下生成实验

参考链接

评论区

写评论

还没有评论

1 共 0 条评论, 1 页