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 | [build-dependencies] |
在 Cargo.toml
文件的 [build-dependencies]
部分,这样就声明了对 bindgen
的构建时依赖并使用了最新版本 v0.55.1,可随时通过 crates.io bindgen 页面获取最新的版本信息。
其次在 crate 项目的根目录下创建一个 build.rs
文件,用来编译和链接 bindgen
的导出。我们可以通过 C 库的源代码,也可以直接通过链接库,本文选择通过链接库的方式。创建 wrapper.h
文件内容如下:
1 |
创建 build.rs
文件内容如下:
1 | fn main() { |
其中:rustc-link-lib = [KIND =] NAME
用来指定 C 库,传递给 cargo 告知 Rust 编译器 rustc 链接 secp256k1 共享库,可选的 KIND
可以是 static
,dylib
,默认值是动态库 dylib,有关更多详细信息,请参见 rustc --help
。
bindgen::Builder
是 bindgen
的主要入口点,可让为生成的绑定配置各种选项。.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 |
|
由于 Rust 与 C 不同,不允许对结构体进行单独的声明和定义。 我们可以看到 bindgen
用了一个私有的大小为零的类型字段,这是其默认执行的操作。
同时,bindgen
会将 C 中的 const
指针转换为 Rust 中的 const *
,并将没有修饰符的 C 指针转换为 mut *
。如下所示:
1 | extern "C" { |
使用生成的绑定,测试
我们可以使用 include!
宏将生成的绑定直接转储到 crate 项目的入口中 src/lib.rs
:
1 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); |
然后,我们可以编写测试,以验证生成的 Rust FFI 是否可以正常工作:
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 有两种方式:
- 以命令行工具的方式使用;
- 以库的方式在 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
就是生成的头文件。
要注意两点:
- 不是任意 Rust crate 都可以,而是已经做了暴露 C API 开发的库才行。因为 cbindgen 会去扫描整个源代码,把对接的接口抽出来;
- 可以通过 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 | extern crate cbindgen; |
在 build.rs 方式里,也是可以配置 cbindgen.toml 参数的。这里,编译之后,生成的 bindings.h
就是我们要的 C 头文件。
生成的结果看起来是什么样子?
比如,我们有一个 Rust crate:
1 | // trebuchet.rs |
生成后的 C header 文件如下:
1 | // trebuchet.h |
可以看到,cbindgen 完整实现了我们的意图:
- Ammo 有正确的大小和值;
- Target 包含所有字段和正确的布局(字段顺序);
- Trebuchet 被声明为一个 opaque 结构体;
- 所有的函数有了对应的声明。
补充
- cbindgen 不但可以生成 C 头文件,还可以生成 C++ 的,甚至还可以生成 swift 可以用的头文件;
- cbindgen 不是一包到底的,对 C 的支持相对成熟,对 C++ 的差一些。适当的时候,还是需要手动做很多事情的;
- 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 | pub extern "C" fn student_alice() -> *mut student { |
这时,通过 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 交互的有 ctypes 和 cffi。其中,ctypes
已被包含在 Python 标准库中,成为 Python 内建的用于调用动态链接库函数的功能模块。ctypes
的主要问题是,我们必须使用其特定的 API 完全重复 C ABI 的声明。cffi
则是则通过解析实际的 C ABI 声明,自动推断所需的数据类型和函数签名,以避免重写声明。ctypes
和 cffi
都使用了 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 | # 导入 FFI 类 |
Python 调用 Rust 代码示例
我们示例代码的目录结构如下:
1 | example_04 |
其中,
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 |
|
同时,C 的 char
类型对应于 Python 中的单字符字符串,在 Python 中字符串必须编码为 UTF-8,才能通过 FFI 边界。
1 | # coding: utf-8 |
执行结果为:
1 | count_char("hello") from Rust: 5 |
示例 - 数组与切片
在 Rust 和 C 中,数组均表示相同类型元素的集合,但在 C 中,其不会对数组执行边界检查,而 Rust 会在运行时检查数组边界。同时在 Rust 中有切片的概念,它包含一个指针和一组元素的数据。
在 Rust FFI 中使用 from_raw_parts
将指针和长度,转换为一个 Rust 中的切片。
1 |
|
在 Python 中,并没有明显的 C 数组对等物,它们在 CFFI 中对应于的 cdata
类型。可以通过 ffi.new(cdecl,init=None)
,根据指定的 C 类型分配实例,并返回指向它的指针。
1 | array = ffi.new("int[]", [1, 4, 9, 16, 25]) |
执行结果为:
1 | sum_of_even from Rust: 20 |
示例 - 元组与结构体
在 C 中没有元组的概念,我们可以做一个特殊的转换,通过在 Rust FFI 中定义与元组相对应的结构体。
1 |
|
与数组类似,在 Python 中,并没有明显的 C 结构体的对等物,它们在 CFFI 中也对应于的 cdata
类型。
1 | py_cdata = ffi.new('c_tuple *') |
执行结果为:
1 | cdata = 100, True |
对于结构体,由于无法查看其实例对象内部,所以通常将其视为不透明的指针(opaque pointer)来处理。可以参考之前系列文章中的介绍(https://mp.weixin.qq.com/s/WkOwKPPmmQOjc4IYwvKOfA)。
小结
通过简单的示例,我们可以整理出其它语言调用 Rust 代码的一般模式或步骤。
- 针对 Rust 代码中需要公开的 API,为其编写对应的 C API,对应示例中的 ffi 文件夹;
- 通过
cbindgen
工具生成 C API 的头文件或手动添加 C API 函数定义; - 在其它语言中,使用其支持调用 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