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> {