关于 Rust 的基本介绍,我在之前的文章有做过一些总结。
本篇文章我们关注如何在 python 中调用 Rust 开发的模块。
Rust FFI 的一般思路
Rust 可以编译出兼容 C ABI 的动态库或者静态库,Rust 调用其他语言,以及 Rust 被其他语言调用,基本都是通过 C ABI 来进行 FFI 调用。
所以我们可以看出,实际上 C++ 调用 Rust 并不是特别方便,需要使用 Rust 提供的 C 接口,也因此没有办法使用 C++ 提供的类型,而 Rust 在导出接口的时候,也没有办法使用 Rust 的类型系统,需要转换成 C 类型。
大多数时候我们都会在这种场景下写一层 wrapper 和 converter,用来自动生成 FFI 层的一些胶水代码。
对于 Python 这类高级语言调用 Rust,基本也是类似的思路,我们可以简单总结为下图:
值得庆幸的是,对于 Python 调用 Rust,社区已经有非常多现成的成熟工具可以使用,基于这些工具,我们可以比较方便地专注于 Rust 实现逻辑本身,无需关注太多 FFI 和转换细节。
入门
一个比较方便的方法是使用 PyO3,PyO3 不仅仅提供了 rust binding,也提供了创建 python 包的开箱即用的脚手架工具 maturin,使用 maturin 我们可以很方便地创建一个基于 rust 开发的 python 扩展模块。
我们这里整理一下官方文档中提供的最简单的方式,读者可以直接依次执行下面的 shell 脚本:
1 | mkdir string_sum |
这个时候,我们可以得到一个简单的 Rust 项目,并且包含了一个示例调用,我们无需修改任何代码,可以直接执行下面的命令测试:
1 | maturin develop 会自动打包出一个 wheel 包,并且安装到当前的 venv 中 |
进阶工具
接下来,我们介绍几个方便我们使用 Rust 开发 python 包的进阶工具或引导。
setuptools-rust
setuptools-rust 是一个 setuptools 的插件,让我们可以比较方便地编写使用 pyo3 开发的 rust python 包。
我们可以 clone 它的源代码,直接使用它提供的示例,参考如下命令测试:
1 | cd examples/rust_with_cffi |
dict-derive
这个 rust 库提供了 FromPyObject 和 IntoPyObject 两个宏,使用这两个宏,我们可以很方便地进行 python dict 结构和 Rust 结构体的转换。
例如我们声明这样一个结构体:
1 |
|
我们就直接可以在导出函数中这样使用了:
1 | // Requires FromPyObject to receive a struct as an argument |
我们通过宏展开可以发现,这两个宏所做的事情就是分别将 pyo3::types::PyDict
转换成 Rust 结构体和将 Rust 结构体转换成 pyo3::types::PyDict
。
整体宏展开的代码不多,还是比较方便阅读的。
rust-numpy
rust-numpy 是一个 rust 版本的 numpy C ABI 封装,使用这个库我们可以在 Rust 中调用 numpy
接下来我们运行该库的示例代码。
我们需要先安装 nox,nox 是一个 python 自动化任务工具。
1 | python3 -m pip install nox |
之后我们进入到命令行直接执行即可:
1 | $ cd examples/simple |
顺利的情况下,我们可以看到它会输出测试成功:
1 | tests/test_ext.py ..... [100%] |
pandas
在 Rust 中并没有直接和 python 中的 pandas 包对标的诸如 pandas-rs 包。
不过 Rust 标准库本身也提供了非常多的数据处理函数如筛选、过滤等,我们可以自己手写代码完成大部分 pandas 的工作。
在这篇文章中,作者使用了大约 160,000行/ 130列,总大小为 150Mb 的数据, 分别使用 Rust 和 Pandas 处理并测试,我们可以看到提升还是比较显著的:
Time(s) | Mem Usage(Gb) | |
---|---|---|
Pandas | 3.0s | 2.5Gb |
Rust | 1.6s 🔥 -50% | 1.7Gb 🔥 -32% |
其他
pyo3 的 README 里面还列举了一些其他的工具库,使用起来相对比较简单,这里就不做单独介绍了。
- pyo3-log:在 Rust 中使用 python 的 logging 库。
- pyo3-built:可以在编译 rust 的 python 模块的时候写入一些构建信息,如 rust 版本等。
- pyo3-asyncio:python asyio 的 Rust binding,可以将 python 的 async 转换成 Rust 的 features。
- rustimport:可以在 python 中直接引入 rust 代码,但因为引入的时候需要编译,笔者不是很建议在生产环境中直接使用。
虽然上面介绍了这么多工具,但是笔者认为,在实际使用中,还是远远不够的,我们应该还会结合业务,寻找和造出更多轮子,这部分工作就有待我们进一步开拓了。