wasm32-unknown-unknown 目标的 extern "C" ABI 自该目标诞生以来就一直使用非标准定义,因为它没有实现 WebAssembly 官方 C ABI,并且它还 泄露了 Rust 编译器和 LLVM 的内部编译器实现细节。这将在未来的 Rust 编译器版本中改变,届时将改为使用 官方 C ABI。
本文详细介绍了此次变更的一些历史背景以及在此宣布的原因,但您也可以直接跳至 “我受影响吗?” 部分。
wasm32-unknown-unknown 的 C ABI 历史
当 wasm32-unknown-unknown 目标于 2017 年首次添加时,当时并未过多关注 extern "C" ABI 的具体定义。2018 年,添加了专用于 wasm 的 ABI 定义,并且该目标至今仍在使用此定义。随着时间的推移,这个定义变得越来越有问题,尽管一些问题已得到修复,但根本原因依然存在。
值得注意的是,这个 ABI 定义与 tool-conventions 中定义的 C API 不符,该文档是当前 WebAssembly 工具链之间应如何交互的标准。最初,除了 Emscripten 之外,所有基于 WebAssembly 的目标都使用了这个非标准定义,但这在 2021 年发生了变化,Rust 的 WASI 目标开始使用修正后的 ABI 定义。然而,wasm32-unknown-unknown 仍然沿用了非标准定义。
现在是时候纠正这个历史遗留问题了,Rust 编译器很快将对 wasm32-unknown-unknown 目标使用正确的 ABI 定义。然而,这意味着生成的 WebAssembly 二进制文件将与之前不同。
什么是 WebAssembly C ABI?
ABI 的定义回答了以下方面的问题:
- 参数在哪些寄存器中传递?
- 结果在哪些寄存器中传递?
- 128 位整数如何作为参数传递?
union如何作为返回值传递?- 参数何时通过内存而不是寄存器传递?
- 类型在内存中的大小和对齐方式是什么?
对于 WebAssembly,这些问题的答案与原生平台略有不同。例如,WebAssembly 没有物理寄存器,并且所有函数都必须用类型进行标注。WebAssembly 确实拥有诸如 i32、i64、f32 和 f64 等类型。这意味着对于 WebAssembly,ABI 需要定义如何用这些类型来表示值。
这就是 tool-conventions 文档的作用所在。该文档定义了如何在 WebAssembly 格式中表示 C 语言的基本类型,以及 C 语言的函数签名如何映射到 WebAssembly 的函数签名。例如,Rust 的 u32 由 WebAssembly 的 i32 表示,并直接作为函数参数传递。如果 Rust 结构 #[repr(C)] struct Pair(f32, f64) 从函数返回,那么将使用一个返回指针,该指针必须具有 8 字节对齐和 16 字节大小。
本质上,WebAssembly C ABI 充当了 C 类型系统和 WebAssembly 类型系统之间的桥梁。这包括内存布局以及 C 函数签名到 WebAssembly 函数签名的转换等细节。
wasm32-unknown-unknown 为何非标准?
尽管当前的 ABI 定义是非标准的,但其许多方面仍与 tool-conventions 指定的相同。例如,类型的大小/对齐方式与 C 中相同。主要区别在于函数签名的计算方式。一个例子(您可以在 godbolt 上跟随查看)是:
pub extern "C"
这将生成以下 WebAssembly 函数:
(func $pair_add (param i32 i32) (result i32)
local.get 1
local.get 0
i32.add
)
值得注意的是,您可以在这里看到结构体 Pair 被“展开”(splatted)成了它的两个组成部分,因此实际的 $pair_add 函数接受两个参数,即 x 和 y 字段。然而,tool-conventions 特别指出,“其他结构体(struct)或联合体(union)”是间接传递的,特别是通过内存。我们可以通过编译以下 C 代码来验证这一点:
;
unsigned
这将生成以下函数:
(func (param i32) (result i32)
local.get 0
i32.load offset=4
local.get 0
i32.load
i32.add
)
在这里,我们可以确信地看到,pair 是在线性内存中传递的,并且这个函数只有一个参数,而不是两个。这个参数是一个指向存储 x 和 y 字段的线性内存的指针。
Diplomat 项目整理了一份比这详细得多的概述,如果您想深入了解,建议查阅。
为何这个问题没有早点修复?
对于 wasm32-unknown-unknown,在 2021 年 WASI 的 ABI 更新时,大家已经很清楚其 ABI 是非标准的。那么,为何不像 WASI 那样修复这个 ABI 呢?最初的主要原因在于 wasm-bindgen 项目。
在 wasm-bindgen 中,目标是让 Rust 轻松集成到使用 WebAssembly 的网页浏览器中。JavaScript 用于与宿主 API 和 Rust 模块本身进行交互。自然地,这种通信涉及到许多 ABI 细节!问题在于 wasm-bindgen 依赖于上面的例子,特别是 Pair 被“展开”(splatted)到参数中,而不是间接传递。如果参数在内存中传递,生成的 JS 将无法正常工作。
当时发现这个问题时,修复 wasm-bindgen 以使其不再依赖这种展开行为被认为相当困难。当时也认为这不是一个普遍问题,而且编译器维护一个非标准 ABI 的成本也不高。然而,多年来压力越来越大。Rust 编译器为了绕过 wasm32-unknown-unknown 上的非标准 C ABI,不得不背负着不断增长的各种“补丁”(hacks)。此外,越来越多的项目开始依赖这种“展开”行为,依赖非标准行为的未知项目数量可能更多,风险也随之增大。
2023 年底,wasm-bindgen 项目修复了绑定生成,使其不再受 `extern "C"` 切换到标准定义的影响。在接下来的几个月里,rustc 添加了一个 future-incompat lint,专门用于引导使用旧版本 wasm-bindgen 的用户迁移到“已修复”的版本。这是为了在经过足够时间后,能够改变 wasm32-unknown-unknown 的 ABI。自 2025 年初以来,使用旧版本 wasm-bindgen 的用户现在将收到一个硬性错误,要求他们进行升级。
然而,尽管贡献者们付出了所有这些巨大的努力,现在发现除了 wasm-bindgen 之外,还有更多项目依赖于这个非标准 ABI 定义。因此,这篇博文旨在向 wasm32-unknown-unknown 的其他用户发出通知,告知 ABI 即将发生破坏性变更,并且项目可能需要进行修改。
我受影响吗?
如果您不使用 wasm32-unknown-unknown 目标,则不受此变更影响。如果您在 wasm32-unknown-unknown 目标上不使用 extern "C",则也不受影响。然而,如果您符合上述情况(使用 wasm32-unknown-unknown 且使用 extern "C"),则可能受影响!
要确定您的项目受到的影响,您可以使用以下几种工具:
- Rust 编译器中添加了一个新的 future-incompat 警告,如果检测到在 ABI 变更时会发生改变的签名,就会发出警告。
- 2023 年,Rust 编译器中添加了
-Zwasm-c-abi=(legacy|spec)标志。该标志默认为-Zwasm-c-abi=legacy,即非标准定义。代码可以使用-Zwasm-c-abi=spec来为 crate 使用 C ABI 的标准定义,以测试变更是否可行。
测试您的 crate 的最佳方法是使用 nightly-2025-03-27 或更高版本进行编译,确保没有警告,然后测试您的项目在使用 -Zwasm-c-abi=spec 时仍能正常工作。如果所有这些都通过,那么您就没问题了,即将到来的 C ABI 变更不会影响您的项目。
我受影响了,怎么办?
那么,您正在使用 wasm32-unknown-unknown,使用 extern "C",并且 Nightly 编译器发出了警告。此外,当使用 -Zwasm-c-abi=spec 编译时,您的项目无法正常工作。现在怎么办?
目前,对您来说不幸的是,这将是一个有些艰难的过渡期。您有几种选择,但它们都有各自的缺点:
-
将您的 Rust 编译器版本固定到当前的稳定版本,直到 ABI 变更后再更新。这意味着您不会收到任何编译器警告(因为旧编译器不会警告),并且在 ABI 变更时也不会受到影响(因为您没有切换编译器)。最终,当您更新到默认使用
-Zwasm-c-abi=spec的稳定编译器时,您将不得不迁移您的 JS 或绑定代码以适应新的 ABI。 -
更新到 Rust nightly 作为您的编译器并传入
-Zwasm-c-abi=spec。这是将 (1) 中所需的工作提前到您的目标上进行。您现在就可以让您的项目兼容-Zwasm-c-abi=spec。这种方法的缺点是您的项目将只能在 nightly 编译器和-Zwasm-c-abi=spec的组合下工作,并且在默认设置切换之前您将无法使用稳定版本。 -
更新您的项目,使其不再依赖
-Zwasm-c-abi=legacy的非标准行为。例如,这包括不在参数中按值传递结构体(structs-by-value)。例如,您可以传递上面的&Pair,而不是Pair。这与上面的 (2) 类似,立即进行更新项目的工作,但好处是可以继续在稳定版 Rust 上工作。然而,这种方法的缺点是,在某些情况下,您可能无法轻易修改或更新您的 C ABI。 -
更新到 Rust nightly 作为您的编译器并传入
-Zwasm-c-abi=legacy。这暂时会消除编译器警告,但请注意,ABI 未来仍会改变,并且-Zwasm-c-abi=legacy选项将完全被移除。当-Zwasm-c-abi=legacy选项被移除后,唯一的选项将是标准 C ABI,也就是当前-Zwasm-c-abi=spec所启用的。
如果您有任何不确定、疑问或困难,请随时通过future-incompat 警告的跟踪 issue 或在 Zulip 上联系。
ABI 变更时间线
目前,还没有关于默认 ABI 如何变更的精确时间表。然而,预计变更将花费 3-6 个月左右的时间,大致会是这样的:
- 2025 年 3 月:(即将到来)- 编译器中将添加一个 future-incompat 警告,以警告项目是否受此 ABI 变更的影响。
- 2025 年 5 月 15 日:此 future-incompat 警告将随 1.87.0 版本到达稳定版 Rust 通道。
- 2025 年夏季:(左右)-
-Zwasm-c-abi标志将从编译器中移除,legacy选项也将完全移除。
-Zwasm-c-abi 具体何时移除将取决于社区的反馈以及 future-incompat 警告触发的频率。然而,希望在 Rust 1.87.0 稳定发布后不久,旧的 legacy ABI 行为就可以被移除。