Rust-ABI 的前世今生
本文为付费合集 「Rust 生态蜜蜂」8月8号里的选篇,Rust ABI 应该是每个 Rustaceans 都应该知道的。
背景:ABI 概念与 C-ABI
ABI,是 Application Binary Interface 的缩写,应用程序二进制接口。
“维基百科:在计算机软件中,应用二进制接口(ABI)是两个二进制程序模块之间的接口;通常,这些模块之一是库或操作系统工具,而另一个是用户正在运行的程序。
最早的 ABI 是 C-ABI,在阅读这篇文章的大多数人甚至还未出生之前,这份契约就已经缔结完毕了。而看到我这篇文章的大家,也许正在经历另一个高级 ABI 的创建过程: WASI( WebAssembly System Interface)[1] 。
C-ABI 包含两个关键的内核:
数据的内存布局方式 函数如何调用
而 Cpp ABI 和 Rust ABI 则包含更多内容。
#include <limits.h>
extern long long do_stuff(long long value);
int main () {
long long x = do_stuff(-LLONG_MAX);
/* wow cool stuff with x ! */
return 0;
}
对于上面 C 代码,在 x86_64 目标的生成汇编大概如下:
main:
movabs rdi, -9223372036854775807
sub rsp, 8
call do_stuff
xor eax, eax
add rsp, 8
ret
编译器已经在自己和编写函数定义的人之间建立了契约do_stuff
。编译器希望,在 x86_64(64 位)计算机上long long
只使用 1 个寄存器,并且它必须是 rdi
。编译器知道定义了 do_stuff
的人将使用完全相同的约定。这不是源码级别的契约,而是编译器代表开发者和其他编译器“签订”的合约。这就是 ABI。通过此 ABI,应用程序之间可以达到相互调用的目标。
C-ABI 虽然是事实标准,这么多年来行业内都通过 C-ABI 来作为多语言交互的标准 ABI。但实际 C 语言是类型不安全的,如果有未定义行为,C-ABI 会被轻松打破。因为链接器并不会关心代码里的类型,它只看符号。而未定义行为并不会破坏符号,比如 do_stuff
函数。
ABI 的核心问题是,它将最终二进制文件中的符号名称与给定的语义集紧密联系在一起。当针对给定接口编译代码时,这些语义,比如调用约定、寄存器使用、栈空间,等等一些其他行为,都提供了一组单一且最终牢不可破的假设。如果要更改符号的语义,则必须更改符号的名称。
“P.S 目前 Swift 5 已经稳定了 ABI,这句话实际上具体来说是指, Apple 平台上 Swift 的 Stabilized ABI 的现实。因为 ABI 稳定是和系统平台和工具链的属性,而非编程语言的属性。Swift 通过引入一种叫做弹性类型(Resilience Type)[2]的东西,可以实现数据结构变化时保证 ABI 兼容,具体来说,对于动态链接库,只有在运行时才能向 dylib 得知类型的具体大小、对齐、偏移量等ABI信息,而非编译时。
Rust ABI
Rust 目前的 ABI 并未稳定,即,Rust 不保证内存中数据结构的调用约定和内存布局不被改变。
稳定的 ABI 可以支持 Rust crates 之间的动态链接,从而允许 Rust 程序支持动态加载的插件(C/C++ 中常见的功能),也允许 Rust 库可由其他语言(比如 Swift)加载。动态链接将导致项目的编译时间更短,磁盘空间使用更少,因为多个项目可以链接到同一个 dylib。但不稳定的 ABI 在性能优化方面也有它的好处。Google Fuschia OS 没有将 Rust 用于微内核的原因之一就是Rust 没有稳定 ABI。
这里有几个示例来说明什么是不稳定的 ABI:
// 虽然下面的结构体本质是相同的,但是 Rust 编译器不保证给予它们字段相同的内存偏移量
struct A(u32, u64);
struct B(u32, u64);
// Rust 编译器不保证字段的顺序和定义的一样
struct Rect {
x: f32,
y: f32,
w: f32,
h: f32,
}
Rust 编译器会对上面的结构体进行优化,如果内存布局是确定的,就不利于优化了。比如没有办法对结构体字段进行重排以便达到最小化内存占用的优化目标。内存布局不确定性也有利于模糊测试(Fuzzer),因为模糊测试需要将字段随机排序以便更容易地暴露潜在的问题。
#[repr(C)]
struct MyStruct {
x: u32,
y: Vec<u8>,
z: u32,
}
对于该示例来说,虽然使用了#[repr(C)]
让结构体字段的顺序确定了,但是字段的偏移量依然无法确定,因为 Vec<8>
没有任何确定性的排序,从而z
的偏移量是无法确定的。所以这种类型不适合使用 C 的 FFi。而且,Rust 的 C-ABI 也不是标准 C-ABI,存在一些差异。而且 Rust 的 C-ABI 也不支持 trait 对象,之前有 Pre-RFC 提议让 `#[repr(C)]`支持 trait[3],但是也不了了之了。不过目前有第三方库支持在 C-ABI 之间传递 trait 对象:thin_trait_object[4] 和 abi_stable[5] 。
pub struct Foo<T> {
a: T
}
// 单态化 Foo<T> 之后
pub struct Foo_u64{
a: u64
}
对于泛型代码Foo<T>
来说,在编译期会静态分发,即单态化为具体的类型实例Foo_u64
,将其编译为动态库(比如 .so
)并不会包含Foo<T>
的泛型定义,如果对于使用Foo_u32
的库来说,就无法动态链接了。注意,生命周期参数不受影响。需要把库和应用代码共同编译才可以完整链接需要的函数,相对比较麻烦。这一点 Swift 5 稳定 ABI 有不错的应对方法,即,Swift 可以将一个泛型函数编译为一个可以动态处理的替换实现。
更多关于 Rust ABI 和类型布局相关信息可以参考这个博客文章:Notes on Type Layouts and ABIs in Rust[6]。
2020年5月,在 Rust 内部论坛(IRLO)讨论[7]过关于稳定 Rust 的模块化 ABI 的提案。模块化 ABI 就是想把 ABI 实现作为 crate 发布,然后对 crate 进行版本化。目前第三方库 abi_stable 的实现和此想法非常接近。但是想稳定化 ABI 这个过程是非常困难的,目前官方还没有对此提上日程。
Rust 插件系统
正是因为目前 Rust ABI 不稳定,所以在用 Rust 编写插件系统时有很多不便。博客系列 A Plugin System in Rust[8] 对如何用 Rust 实现插件系统做了非常好的总结。这里简单概括一下,感兴趣的可以直接深入学习此系列博客内容。
一个良好的 Rust 插件系统要考虑的几个点:
必须:能够在启动时和运行时加载/卸载插件 必须:支持跨平台 必须:低开销 必须:能用 Rust 开发插件 可选:安全性 可选:向后兼容性 可选:从现有实现移植的工作量不多
实现插件系统的几种方式:
使用脚本语言编写插件在运行时扩展其功能,比如 Python 和 Lua 等。 进程间通信。 基于套接字。(在Rust 不稳定 ABI 的情况下,需要 Rust 插 Rust 来说是一个比较稳妥的方案) 基于管道。(在Rust 不稳定 ABI 的情况下,需要 Rust 插 Rust 来说是一个比较稳妥的方案) 基于内存共享。(会过多使用 unsafe,不推荐) 动态加载。这是最常见的方式。但是 Rust ABI 不稳定,目前需要使用 thin_trait_object[9] 、abi_stable[10] 和 cglue[11] 这样的第三方库支持。 基于 WebAssembly 。不考虑性能的情况下,这也是一种方案。
在这篇博客内容中,作者对这几种方式的优劣都做了对比。他最终得出一个不可能三角:安全性(Safety)、可用性(Unsability )和 性能(Performance) ,即,要选择合适你的插件系统必须在这三个特性之间做权衡,你不可能同时满足这三个特性,只能选择满足其中两个而放弃另外一个。
Rust ABI 不稳定带来的问题比想象严重:
作者在尝试动态加载实现插件,发现 Rust ABI 的不稳定带来的问题比他想象的更加严重。在这之前,他一直认为即使Rust的ABI不稳定,只要库和主二进制文件是用相同的编译器以及std等等版本编译的,就可以安全地动态加载一个库。然而事实证明,ABI 不仅仅是可能在不同的编译器版本之间发生“断裂”,在编译器执行过程中也会发生“断裂”,即,Rust 编译器并不保证同一个类型的布局在每次执行的时候都一致,类型布局可以随着每次编译而改变。所以他的方案是使用 #[repr(C)]
的 C-ABI,以及使用 abi_stable 来获得稳定的 std 库。
他也尝试了 WebAssembly 插件的方式,但是因为性能和 Wasm 没有提供在主机和插件之间有效传递数据的解决方案的原因而放弃。
作者后面尝试了使用 abi_stable 来开发插件系统。abi_stable 插件是按模块来构建的,并且提供了很多 FFI 安全(FFI安全,指FFI 边界提供了稳定的内存布局)的类型,包括 trait 对象的支持,以及提供了处理 FFI 边界恐慌(Panic)的方法。
如果想了解 abi_stable、 cglue、async_ffi 应用以及相关性能测试的更多细节,可以进一步阅读该系列博客。这里就不做过多摘录。
小结
虽然当前 Rust ABI 不稳定带来诸多不便,但是社区目前还是有一些靠谱的解决办法。Rust ABI 到底什么时候稳定,这是个问题,毕竟 C 语言花了几十年才得到一个事实性的稳定标准,而 Swift 在5.0 就拥有了稳定的 ABI 是因为苹果巨大的软件生态必须要求它这么做。而 Rust 目前则没有太大的稳定 ABI 的压力。
延伸阅读
To Save C, We Must Save ABI[12]
Binary Banshees and Digital Demons[13]
Rust ABI Wiki[14]
abi_stable[15] and cglue: Rust ABI 安全代码生成器[16]
Rust Reference: ABI[17]
Series: Plugins in Rust [18]
How Swift Achieved Dynamic Linking Where Rust Couldn't[19]
Rust 稳定模块化 ABI 提案[20]
The Lost Art of Structure Packing[21]
Notes on Type Layouts and ABIs in Rust[22]
RFC 讨论:定义 Rust ABI[23]
So you want to live-reload Rust[24]
[async_ffi](
参考资料
WASI( WebAssembly System Interface): https://wasi.dev/
[2]弹性类型(Resilience Type): https://github.com/apple/swift/blob/main/docs/LibraryEvolution.rst
[3]repr(C)]让结构体字段的顺序确定了,但是字段的偏移量依然无法确定,因为
Vec<8>没有任何确定性的排序,从而
z的偏移量是无法确定的。所以这种类型不适合使用 C 的 FFi。而且,Rust 的 C-ABI 也不是标准 C-ABI,存在一些差异。而且 Rust 的 C-ABI 也不支持 trait 对象,之前有 [Pre-RFC 提议让
#[repr(C)]`支持 trait: https://internals.rust-lang.org/t/pre-rfc-repr-c-for-traits/12598/13
thin_trait_object: https://github.com/kotauskas/thin_trait_object
[5]abi_stable: https://docs.rs/abi_stable/latest/abi_stable/
[6]Notes on Type Layouts and ABIs in Rust: https://gankra.github.io/blah/rust-layouts-and-abis/
[7]讨论: https://internals.rust-lang.org/t/a-stable-modular-abi-for-rust/12347
[8]A Plugin System in Rust: https://nullderef.com/series/rust-plugins/
[9]thin_trait_object: https://github.com/kotauskas/thin_trait_object
[10]abi_stable: https://docs.rs/abi_stable/latest/abi_stable/
[11]cglue: https://github.com/h33p/cglue
[12]To Save C, We Must Save ABI: https://thephd.dev/to-save-c-we-must-save-abi-fixing-c-function-abi
[13]Binary Banshees and Digital Demons: https://thephd.dev/binary-banshees-digital-demons-abi-c-c++-help-me-god-please
[14]Rust ABI Wiki: https://slightknack.github.io/rust-abi-wiki/intro/intro.html
[15]abi_stable: https://docs.rs/abi_stable/latest/abi_stable/
[16]cglue: Rust ABI 安全代码生成器: https://github.com/h33p/cglue
[17]Rust Reference: ABI: https://doc.rust-lang.org/reference/abi.html
[18]Series: Plugins in Rust : https://nullderef.com/blog/plugin-start/
[19]How Swift Achieved Dynamic Linking Where Rust Couldn't: https://gankra.github.io/blah/swift-abi/
[20]Rust 稳定模块化 ABI 提案: https://internals.rust-lang.org/t/a-stable-modular-abi-for-rust/12347
[21]The Lost Art of Structure Packing: http://www.catb.org/esr/structure-packing/
[22]Notes on Type Layouts and ABIs in Rust: https://gankra.github.io/blah/rust-layouts-and-abis/
[23]RFC 讨论:定义 Rust ABI: https://github.com/rust-lang/rfcs/issues/600
[24]So you want to live-reload Rust: https://fasterthanli.me/articles/so-you-want-to-live-reload-rust