Rust FFI 工具 & 其他

  • 资料来源:

    <>

  • 更新

    1
    2023.11.28 初始

导语

还是转载 Rust 中文论坛的精华帖,帮助很大,感谢原作者!! 😂😂 原帖写的是真的深入浅出 !!

Bindgen 工具介绍

前面我们经历了《Rust FFI 编程 - 基础知识》、《Rust FFI 编程 - 手动绑定 C 库》和《Rust FFI 编程 - Rust 导出共享库》三个大的子系列,Rust FFI 编程的基础部分算靠以段落。可能仍然有未覆盖到的地方,可以在未来以补充文章的形式发布。

学以致用,现在我们开始进入新的阶段——应用。我们暂时规划了如下一些大的主题:

  • 自动化工具 bindgen 和 cbindgen 介绍;
  • Rust 交叉编译相关,Rust 在 Windows 平台的编译相关问题;
  • 某个 *-sys 库解析;
  • 使用 Rust 为其它动态语言写调用库:Python, Php, Nodejs, Ruby, Go, Java,Objective-C;、
  • 附录:Rust 二进制瘦身等;

另外,会单独开若干大的后续系列,不会放在本 FFI 系列中。

  • Rust 嵌入式开发系列
  • Rust 与 Wasm/WebAssembly 开发系列
  • Rust 高性能计算系列(待定)

Bindgen 介绍

本篇,我们来介绍 rust-bindgen 这个项目。项目地址为:https://github.com/rust-lang/rust-bindgen。可以看到,这是一个> Rust 官方孵化的项目,目前已经比较成熟。

本篇只是一个总体的介绍,并不是对 bindgen 项目的完整讲解,也不是对 bindgen tutorial 的全面翻译。因此,真正动手操作的时候,需要仔细阅读 Tutorial 原文和查阅相关的 API

bindgen 是一个能自动为 C(或 C++)库生成 Rust 绑定的辅助库和命令行工具。C++ (目前)的支持并不完整。

也就是说,bindgen 可以作为一个 crate,与 Cargo 的 build.rs 机制配合,根据 C/C++ 的头文件(.h, .hpp),在构建阶段,编译主体 Rust 代码之前,自动生成 Rust 绑定文件。具体,可查看示例:https://rust-lang.github.io/rust-bindgen/tutorial-0.html

bindgen 还有另一种用法,它本身也提供一个命令行工具。执行:

1
cargo install bindgen-cli

便可安装。 然后,在命令行下,使用

1
bindgen input.h -o bindings.rs

便可根据 C 头文件 input.h 动态生成 Rust 绑定文件 bindings.rs。非常简单。

但真实的场景远不如想象中这么纯洁。于是就有一堆可能的修补工作。bindgen 为我们提供了各种修补之法(这才是精华)。具体来说,有如下一些措施:

  • 白名单
  • 黑名单
  • Opaque 对象
  • 类型替换
  • 其它

这些措施有的可以用在库的模式下,命令行模式下,以及在源代码(C)的注释中添加标注的模式下。有的三种都能覆盖,有的只能覆盖其中一种或两种。具体可在 https://rust-lang.github.io/rust-bindgen/customizing-generated-bindings.html 页面找到详细解释。

同时,bindgen 对 C 的 Union 和 Bitfields 的特性,也有对应的处理策略。这些在实际封装的过程中,可能会经常碰到。

bindgen 本身知识点,并不多。重点在于实战,在实战中体会各种细节。后面我们也会拿一到两篇进行 bindgen 实战的讲解。

本篇内容就介绍到这里。

Bindgen 使用示例

当我们拥有一组具有良好声明的头文件时,自己定义 C 库的 Rust FFI 绑定函数是毫无意义的。我们可以使用 bindgen 这种工具从 C 库的头文件生成 Rust FFI 绑定函数。然后,我们运行一些测试代码以验证其是否正常运行,并对它们进行调整,直到正确为止。

本文我们将通过一个示例,讨论如何使用 bindgen 将 C 库中的函数公开给 Rust。我们的目标是创建一个 crate 项目,其中包含一个 bindings.rs 文件,该文件代表 C 库的公共 API(包括函数,结构体,枚举等),然后通过将该 crate 导入其它项目中来调用原 C 库的功能。

上一篇我们介绍了使用 bindgen 为 C 库创建 Rust FFI 绑定有两种方式:使用 bindgen 命令行和使用 build.rs。本文我们使用 build.rs 这种方式作为示例进行说明。

设置 Crate 项目

一般 Rust FFI 绑定的 crate 项目会包含构建和导出 C 库的 unsafe 函数, crate 的 Rust 标准命名约定为 lib<XXXX>-sys,我们本次示例,针对 C 实现的 secp256k1 库生成 Rust FFI 绑定。

首先是设置 Cargo.toml,添加 bindgen 作为构建时的依赖项,如下所示:

1
2
[build-dependencies]
bindgen = "0.55.1"

Cargo.toml 文件的 [build-dependencies] 部分,这样就声明了对 bindgen 的构建时依赖并使用了最新版本 v0.55.1,可随时通过 crates.io bindgen 页面获取最新的版本信息。

其次在 crate 项目的根目录下创建一个 build.rs 文件,用来编译和链接 bindgen 的导出。我们可以通过 C 库的源代码,也可以直接通过链接库,本文选择通过链接库的方式。创建 wrapper.h 文件内容如下:

1
#include <secp256k1.h>

创建 build.rs 文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() {
println!("cargo:rustc-link-lib=secp256k1");
println!("cargo:rerun-if-changed=wrapper.h");
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
.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!");
}

其中:rustc-link-lib = [KIND =] NAME 用来指定 C 库,传递给 cargo 告知 Rust 编译器 rustc 链接 secp256k1 共享库,可选的 KIND 可以是 staticdylib,默认值是动态库 dylib,有关更多详细信息,请参见 rustc --help

bindgen::Builderbindgen 的主要入口点,可让为生成的绑定配置各种选项。.header 用来指定要生成绑定的头文件。.parse_callbacks 是指当更改包含的任何头文件时,生成的 crate 无效。

可以通过 bindings.write_to_file 将绑定写入指定的文件,比如:$OUT_DIR/bindings.rs

生成绑定

现在直接运行 cargo build,将立即生成与 secp256k1 的 Rust FFI 绑定。生成的绑定文件位于 OUT_DIR/bindings.rs,其中 $OUT_DIR 由 cargo 根据 build.rs 确定,默认类似于 ./target/debug/build/crate-package-name-afc7747d7eafd720/out/

bindings.rs 中有如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct secp256k1_context_struct {
_unused: [u8; 0],
}
pub type secp256k1_context = secp256k1_context_struct;

#[repr(C)]
#[derive(Copy, Clone)]
pub struct secp256k1_pubkey {
pub data: [::std::os::raw::c_uchar; 64usize],
}

由于 Rust 与 C 不同,不允许对结构体进行单独的声明和定义。 我们可以看到 bindgen 用了一个私有的大小为零的类型字段,这是其默认执行的操作。

同时,bindgen 会将 C 中的 const 指针转换为 Rust 中的 const *,并将没有修饰符的 C 指针转换为 mut *。如下所示:

1
2
3
4
5
6
7
8
9
10
11
extern "C" {
pub fn secp256k1_context_create(flags: ::std::os::raw::c_uint) -> *mut secp256k1_context;
}

extern "C" {
pub fn secp256k1_ec_pubkey_create(
ctx: *const secp256k1_context,
pubkey: *mut secp256k1_pubkey,
seckey: *const ::std::os::raw::c_uchar,
) -> ::std::os::raw::c_int;
}

使用生成的绑定,测试

我们可以使用 include! 宏将生成的绑定直接转储到 crate 项目的入口中 src/lib.rs

1
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

然后,我们可以编写测试,以验证生成的 Rust FFI 是否可以正常工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#[test]
fn test_create_pubkey() {
// secp256k1返回公钥
let mut pubkey: secp256k1_pubkey = secp256k1_pubkey {
data: [0; 64],
};
let prikey: u8 = 1;

unsafe {
let context = secp256k1_context_create(SECP256K1_CONTEXT_SIGN);
assert!(!context.is_null());
let ret = secp256k1_ec_pubkey_create(& *context, &mut pubkey, &prikey);
assert_eq!(ret, 1);
}
}

完整代码:https://github.com/lesterli/rust-practice/tree/master/ffi/secp256k1-sys

自定义生成的绑定

如果生成的绑定,我们可以通过以下几种方式对结构体,枚举等进行调整:

  • 使用 build.rs 时,通过 bindgen::Builder 的配置方法。
  • 使用 bindgen 命令行时,通过使用其它命令行选项。
  • 也可以直接在 C/C++ 源代码中添加注释。

具体可以参考:https://rust-lang.github.io/rust-bindgen/

与此同时,直接使用 bindgen 生成的 Rust FFI 绑定函数,需要通过 unsafe 的方式访问 C 库中的函数,这不符合人体工程学,实际项目中,我们通常会提供一个安全的包装库。rust-secp256k1 就是这样的一个包装 crate,它为 libsecp256k1 的所有函数提供类型安全的 Rust 绑定,Github 链接:https://github.com/rust-bitcoin/rust-secp256k1。

Cbindgen 工具介绍

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

1
cargo install --force cbindgen

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

1
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 就好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 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 文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 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 对我们之前写的库做一下生成实验

参考链接

  • 说明文档
  • cbindgen.toml
  • announcing-cbindgen

Cbindgen 使用示例

在上一篇中,我们整体介绍了 cbindgen 工具。本文将会示例如何使用 cbindgen 为我们之前写的 Rust 示例库 生成头文件。主要内容包括:

  • 生成的头文件和之前的对比
  • 为 C API 增加枚举类型,并生成头文件

使用 cbindgen.toml

我们知道可以通过 cbindgen.toml 这个配置文件,给 cbindgen 工具配置各种行为参数来生成头文件。示例库提供了 C API,那我们首先在示例库的根目录下创建一个 cbindgen.toml ,并且试试只配置以下一行内容:

1
language = "C"

然后执行以下命令:

1
cbindgen --config cbindgen.toml --crate example_03 --output example_03_header.h

我们可以看到,在根目录下生成了一个 example_03_header.h 的头文件。跟我们之前手动编写的头文件进行对比:

多了以上这几行 #include 文件,这是 cbindgen 工具的默认行为,它会默认导入这些 C/C++ 标准库。我们如果不需要导入这些库时,可以通过增加以下配置内容:

1
no_includes = true

再执行生成命令后,这时我们可以看到这个头文件和我们之前手动编写的基本一样。

这时,如果我们想要在导出的头文件中类型统一加个前缀,比如:capi_,可以在 cbindgen.toml 增加以下配置:

1
[export]prefix = "capi_"

增加枚举类型

为了演示 cbindgen 工具对 enum 枚举类型的支持,我们为示例增加个枚举类型,代码如下:

1
pub enum gender {    BOY,    GIRL,}

同时,相应地修改函数中的一些代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pub extern "C" fn student_alice() -> *mut student {
let mut init_char_array: [c_char; 20] = [0; 20];
for (dest, src) in init_char_array.iter_mut().zip(b"Alice\0".iter()) {
*dest = *src as _;
}
let scores = [92.5, 87.5, 90.0];
let alice = student {
num: 1 as c_int,
total: 280,
name: init_char_array,
scores,
gender: gender::GIRL,
};
Box::into_raw(Box::new(alice))
}

这时,通过 cbindgen 工具生成头文件,我们可以看出新的头文件能正确地包含我们新增的枚举类型:

1
typedef enum {  BOY,  GIRL,} capi_gender;

对于枚举类型中的变体,如果我们希望更符合 C 的风格,可以在 cbindgen.toml 中配置以下内容:

1
[enum]rename_variants = "SnakeCase"

这个规则是针对枚举类型的变体进行重命名,主要的值包括(引用自 cbindgen 的文档):

执行 cbindgen 后,可以看到头文件枚举部分的定义变为:

1
typedef enum {  boy,  girl,} capi_gender;

小结

综上所述,我们演示了对于之前的 Rust 示例库,如何通过配置 cbindgen.toml 使用 cbindgen 生成头文件。

完整示例代码在 Github:https://github.com/lesterli/rust-practice/tree/master/ffi/example\_03

hyper 的 C API 也是通过 cbinggen 来生成头文件的,有兴趣的可以通过此链接围观。

https://github.com/hyperium/hyper/commit/b1cec5c2104e3b2ceb2148c8dcbfecbfcdc91517#diff-06402fdf0de1ac57a0fc84d328a68836f41eedf61764d4d35f11889db149ac42

其它语言调用 Rust 代码 - Python

引言

随着 Rust 生态的发展,一些 Rust 语言实现的优秀工具或基础协议库,受到越来越多的企业或开发者青睐。与此同时,使用 Rust 语言对已有产品和工具进行性能优化或安全性提升,以及开发其它语言的扩展,这样的案例也越来越多。像被大家广泛使用的 curl 工具,其开发者 Daniel Stenberg 已采用 Rust 实现的 HTTP 协议库 hyper 来提供内存安全的 curl

为了不同语言生态中的开发者可以快速地使用 Rust 语言以及 Rust 生态中优秀的工具或库,Rust FFI 编程计划通过编写一系列文章,专门介绍 C 语言之外的其它语言如何调用 Rust 导出库。目前准备介绍的语言列表有 Python,Ruby,Node.js,Go,Java,PHP。

对于每种语言,如果将 Rust 库的公共接口转换为应用程序二进制接口( C ABI),则在其它编程语言中可以相对容易地使用它们,当前列表中的语言都具有某种形式的外部函数接口(C FFI),剩下的就是其它语言和 Rust 类型之间的相互转换。

因此,同之前介绍过的 C 调用 Rust 导出库类似,文章基本上均会先介绍该语言中支持的 FFI 库,然后通过设计一些示例,分别介绍在该语言中调用 Rust 导出库时,如何处理 Rust 中的常见数据类型,包括数值,字符串,数组,结构体等。

Python 中的 FFI 库

目前 Python 中常用来与 FFI 交互的有 ctypescffi。其中,ctypes 已被包含在 Python 标准库中,成为 Python 内建的用于调用动态链接库函数的功能模块。ctypes 的主要问题是,我们必须使用其特定的 API 完全重复 C ABI 的声明。cffi 则是则通过解析实际的 C ABI 声明,自动推断所需的数据类型和函数签名,以避免重写声明。ctypescffi 都使用了 libffi,通过它实现 Python 动态调用其他语言的库。在本文中的示例,我们采用 cffi 库。

安装

最快捷的安装方式是通过 pip

1
pip install cffi

或者通过项目链接 https://pypi.python.org/pypi/cffi,下载源码,编译安装,这里不做介绍,参考链接中有相关的介绍文档。

使用

使用 cffi 的方式有 ABI 模式API 模式 ,前者以二进制级别访问库,而后者使用 C 编译器访问库,所以在运行时,API 模式比 ABI 模式更快。我们的示例中使用 ABI 模式,因为它不需要 C 编译器。

cffi 中,我们可以使用 ffi.cdef(source) 解析给定的 C ABI。在其中注册所有函数,类型,常量和全局变量,这些类型可以在其它函数中立即使用。然后通过 ffi.dlopen(libpath) 使用 ABI 模式加载外部库并返回一个该库的对象,这样我们就可以使用库对象来调用先前由 ffi.cdef() 声明的函数,读取常量以及读取或写入全局变量。这种方式的大致代码框架如下:

1
2
3
4
5
6
7
8
9
10
11
12
# 导入 FFI 类
from cffi import FFI

ffi = FFI()

# 声明数据类型和函数原型
ffi.cdef("""

""")

# 以 ABI 模式加载外部库并返回库对象
lib = ffi.dlopen("")

Python 调用 Rust 代码示例

我们示例代码的目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
example_04
├── Cargo.toml
├── ffi
│ ├── Cargo.toml
│ ├── cbindgen.toml
│ ├── example_04_header.h
│ ├── src
│ │ └── lib.rs
├── .gitignore
├── python
│ └── main.py
├── README.md
├── src
│ └── lib.rs

其中,

  • ffi 目录存放 Rust 代码库暴露给外部的 C ABI 代码;
    • 通过以下命令生成头文件 example_04_header.h:
1
cbindgen --config cbindgen.toml --output example_04_header.h
  • python 目录存放在 Python 调用 Rust 代码库的 Python 代码;
  • src 目录存放 Rust 库的代码,lib.rs 中包含了我们设计并实现的几个示例函数:
    • count_char,计算给定字符串的长度;
    • sum_of_even,计算给定整数数组中所有偶数之和;
    • handle_tuple,处理元组包含整数和布尔类型两个元素,将整数加 1 和布尔取反后返回;

示例 - 整数与字符串

整数在 Rust,C,Python 中都有对应的转换,通常很容易通过 FFI 边界。

字符串则比较复杂,Rust 中的字符串,是一组 u8 组成的 UTF-8 编码的字节序列,字符串内部允许 NUL 字节;但在 C 中,字符串只是指向一个 char 的指针,用一个 NUL 字节作为终止。

我们需要做一些特殊的转换,在 Rust FFI 中使用 std::ffi::CStr,它表示一个 NUL 字节作为终止的字节数组,可以通过 UTF-8 验证转换成 Rust 中的 &str

1
2
3
4
5
6
7
8
9
#[no_mangle]
pub extern "C" fn count_char(s: *const c_char) -> c_uint {
let c_str = unsafe {
assert!(!s.is_null());
CStr::from_ptr(s)
};
let r_str = c_str.to_str().unwrap();
r_str.chars().count() as u32
}

同时,C 的 char 类型对应于 Python 中的单字符字符串,在 Python 中字符串必须编码为 UTF-8,才能通过 FFI 边界。

1
2
3
4
# coding: utf-8

print 'count_char("hello") from Rust: ', lib.count_char("hello")
print 'count_char("你好") from Rust: ', lib.count_char(u"你好".encode('utf-8'))

执行结果为:

1
2
count_char("hello") from Rust:  5
count_char("你好") from Rust: 2

示例 - 数组与切片

在 Rust 和 C 中,数组均表示相同类型元素的集合,但在 C 中,其不会对数组执行边界检查,而 Rust 会在运行时检查数组边界。同时在 Rust 中有切片的概念,它包含一个指针和一组元素的数据。

在 Rust FFI 中使用 from_raw_parts 将指针和长度,转换为一个 Rust 中的切片。

1
2
3
4
5
6
7
8
9
10
11
12
#[no_mangle]
pub extern "C" fn sum_of_even(ptr: *const c_int, len: size_t) -> c_int {
let slice = unsafe {
assert!(!ptr.is_null());
slice::from_raw_parts(ptr, len as usize)
};

let sum = slice.iter()
.filter(|&&num| num % 2 == 0)
.fold(0, |sum, &num| sum + num);
sum as c_int
}

在 Python 中,并没有明显的 C 数组对等物,它们在 CFFI 中对应于的 cdata 类型。可以通过 ffi.new(cdecl,init=None) ,根据指定的 C 类型分配实例,并返回指向它的指针。

1
2
array = ffi.new("int[]", [1, 4, 9, 16, 25])
print 'sum_of_even from Rust: ', lib.sum_of_even(array, len(array))

执行结果为:

1
sum_of_even from Rust:  20

示例 - 元组与结构体

在 C 中没有元组的概念,我们可以做一个特殊的转换,通过在 Rust FFI 中定义与元组相对应的结构体。

1
2
3
4
5
6
7
8
9
10
11
12
#[repr(C)]
pub struct c_tuple {
integer: c_uint,
boolean: bool,
}

#[no_mangle]
pub extern "C" fn handle_tuple(tup: c_tuple) -> c_tuple {
let (integer, boolean) = tup.into();

(integer + 1, !boolean).into()
}

与数组类似,在 Python 中,并没有明显的 C 结构体的对等物,它们在 CFFI 中也对应于的 cdata 类型。

1
2
3
4
5
6
py_cdata = ffi.new('c_tuple *')
py_cdata.integer = 100
py_cdata.boolean = True
print('cdata = {0}, {1}'.format(py_cdata.integer, py_cdata.boolean))
new_py_cdata = lib.handle_tuple(py_cdata[0])
print('change cdata = {0}, {1}'.format(new_py_cdata.integer, new_py_cdata.boolean))

执行结果为:

1
2
cdata = 100, True
change cdata = 101, False

对于结构体,由于无法查看其实例对象内部,所以通常将其视为不透明的指针(opaque pointer)来处理。可以参考之前系列文章中的介绍(https://mp.weixin.qq.com/s/WkOwKPPmmQOjc4IYwvKOfA)。

小结

通过简单的示例,我们可以整理出其它语言调用 Rust 代码的一般模式或步骤。

  1. 针对 Rust 代码中需要公开的 API,为其编写对应的 C API,对应示例中的 ffi 文件夹;
  2. 通过 cbindgen 工具生成 C API 的头文件或手动添加 C API 函数定义;
  3. 在其它语言中,使用其支持调用 C API 的 FFI 模块或库,完成对 Rust 代码的调用。

完整示例代码的 Github 链接:https://github.com/lesterli/rust-practice/tree/master/ffi/example\_04

参考链接

  • 内存安全的 curl:https://www.abetterinternet.org/post/memory-safe-curl/
  • cbindgen 的文档:https://github.com/eqrion/cbindgen/blob/master/docs.md
  • ctypes 的中文文档:https://docs.python.org/zh-cn/3/library/ctypes.html
  • cffi 中文文档:https://cffi-zh-cn.readthedocs.io/zh/latest/overview.html