引言
随着 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
库。
安装
或者通过项目链接 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()
声明的函数,读取常量以及读取或写入全局变量。这种方式的大致代码框架如下:
# 导入 FFI 类
from cffi
import FFI
ffi = FFI()
# 声明数据类型和函数原型
ffi.cdef(
"""
""")
# 以 ABI 模式加载外部库并返回库对象
lib = ffi.dlopen(
"")
Python 调用 Rust 代码示例
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 代码;
-
python
目录存放在 Python 调用 Rust 代码库的 Python 代码;
-
src
目录存放 Rust 库的代码,
lib.rs
中包含了我们设计并实现的几个示例函数:
-
-
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
。
#[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 边界。
# 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'))
count_char(
"hello")
from Rust:
5
count_char(
"你好")
from Rust:
2
示例 - 数组与切片
在 Rust 和 C 中,数组均表示相同类型元素的集合,但在 C 中,其不会对数组执行边界检查,而 Rust 会在运行时检查数组边界。同时在 Rust 中有切片的概念,它包含一个指针和一组元素的数据。
在 Rust FFI 中使用
from_raw_parts
将指针和长度,转换为一个 Rust 中的切片。
#[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 类型分配实例,并返回指向它的指针。
array = ffi.
new(
"int[]", [
1,
4,
9,
16,
25])
print
'sum_of_even from Rust: ', lib.sum_of_even(
array, len(
array))
sum_of_even
from Rust:
20
示例 - 元组与结构体
在 C 中没有元组的概念,我们可以做一个特殊的转换,通过在 Rust FFI 中定义与元组相对应的结构体。
#[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
类型。
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))
cdata = 100, True
change cdata =
101,
False
对于结构体,由于无法查看其实例对象内部,所以通常将其视为不透明的指针(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