Skip to content

Commit 03422d3

Browse files
committed
C ABI Changes for wasm32-unknown-unknown
This adds a post for changes discussed in rust-lang/rust#122532 and rust-lang/rust#138601, notably as a result of this decision: rust-lang/rust#122532 (comment)
1 parent cf30bb5 commit 03422d3

File tree

1 file changed

+258
-0
lines changed

1 file changed

+258
-0
lines changed

Diff for: content/c-abi-changes-for-wasm32-unknown-unknown.md

+258
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
+++
2+
layout = "post"
3+
date = 2024-03-20
4+
title = "C ABI Changes for `wasm32-unknown-unknown`"
5+
author = "Alex Crichton"
6+
+++
7+
8+
The `extern "C"` ABI for the `wasm32-unknown-unknown` target has been using a
9+
non-standard definition since the inception of the target in that it does not
10+
implement the [official C ABI of WebAssembly][tool-conventions] and it
11+
additionally [leaks internal compiler implementation details][leak-details] of
12+
both the Rust compiler and LLVM. This will change in a future version of the
13+
Rust compiler and the [official C ABI][tool-conventions] will be used instead.
14+
15+
[tool-conventions]: https://github.com/WebAssembly/tool-conventions/blob/main/BasicCABI.md
16+
[leak-details]: https://github.com/rust-lang/rust/issues/115666
17+
18+
## History of `wasm32-unknown-unknown`'s C ABI
19+
20+
When the `wasm32-unknown-unknown` target [was originally added][inception] in
21+
2017 not much care was given to the exact definition of the `extern "C"` ABI at
22+
the time. In 2018 [an ABI definition wasm added just for wasm][orig-abi] and the
23+
target is still using this definition [to this day][current-abi]. This
24+
definitions has become more and more problematic over time and while some issues
25+
have been fixed, the root cause still remains.
26+
27+
Notably this ABI definition does not match the [tool-conventions] definition of
28+
the C API, which is the current standard for how WebAssembly toolchains should
29+
talk to one another. Originally this non-standard definition was used for all
30+
WebAssembly based target except Emscripten, but [this changed in 2021][fix-wasi]
31+
where the WASI targets for Rust use a corrected ABI definition. Still, however,
32+
the non-standard definition remained in use for `wasm32-unknown-unknown`.
33+
34+
The time has now come to correct this historical mistake and the Rust compiler
35+
will soon be using a correct ABI definition for the `wasm32-unknown-unknown`
36+
target. This means, however, that generated WebAssembly binaries will be
37+
different than before?
38+
39+
## What is a WebAssembly C ABI?
40+
41+
The definition of an ABI answers questions along the lines of:
42+
43+
* What registers are arguments passed in?
44+
* What registers are results passed in?
45+
* How is a 128-bit integers passed as an argument?
46+
* How is a `union` passed as a return value?
47+
* When are parameters passed through memory instead of registers?
48+
* What is the size and alignment of a type in memory?
49+
50+
For WebAssembly these answers are a little different than native platforms.
51+
WebAssembly does not have physical registers for example and functions must all
52+
be annotated with a type. What WebAssembly does have is types such as `i32`,
53+
`i64`, `f32`, and `f64`. This means that for WebAssembly an ABI needs to define
54+
how to represent values in these types.
55+
56+
This is where the [tool-conventions] document comes in. That document provides a
57+
definition for how to represent primitives in C in the WebAssembly format, and
58+
additionally how function signatures in C are mapped to function signatures in
59+
WebAssembly. For example a Rust `u32` is represented by a WebAssembly `i32` and
60+
is passed directly as a parameter as a function argument. If the Rust structure
61+
`#[repr(C)] struct Pair(f32, f64)` is returned from a function then a return
62+
pointer is used which must have alignment 8 and size of 16 bytes.
63+
64+
In essence the WebAssembly C ABI is acting as a bridge between C's type system
65+
and the WebAssembly type system. This includes details such as in-memory layouts
66+
and translations of a C function signature to a WebAssembly function signature.
67+
68+
## How is `wasm32-unknown-unknown` non-standard?
69+
70+
Despite the ABI definition today being non-standard many aspects of it are
71+
still the same as what [tool-conventions] specifies. For example size/alignment
72+
of types is the same as it is in C. The main difference is how function
73+
signatures are calculated. An example (where you can follow along on [godbolt])
74+
is:
75+
76+
```rust
77+
#[repr(C)]
78+
pub struct Pair {
79+
x: u32,
80+
y: u32,
81+
}
82+
83+
#[unsafe(no_mangle)]
84+
pub extern "C" fn pair_add(pair: Pair) -> u32 {
85+
pair.x + pair.y
86+
}
87+
```
88+
89+
This will generate the following WebAssembly function:
90+
91+
```wasm
92+
(func $pair_add (param i32 i32) (result i32)
93+
local.get 1
94+
local.get 0
95+
i32.add
96+
)
97+
```
98+
99+
Notably you can see here that the struct `Pair` was "splatted" into its two
100+
components so the actual `$pair_add` function takes two arguments, the `x` and
101+
`y` fields. The [tool-conventions], however [specifically says] that "other
102+
struct[s] or union[s]" are passed indirectly, notably through memory. We can see
103+
this by compiling this C code:
104+
105+
```c
106+
struct Pair {
107+
unsigned x;
108+
unsigned y;
109+
};
110+
111+
unsigned pair_add(struct Pair pair) {
112+
return pair.x + pair.y;
113+
}
114+
```
115+
116+
which yields the generated function:
117+
118+
```wasm
119+
(func (param i32) (result i32)
120+
local.get 0
121+
i32.load offset=4
122+
local.get 0
123+
i32.load
124+
i32.add
125+
)
126+
```
127+
128+
Here we can see, sure enough, that `pair` is passed in linear memory and this
129+
function only has a single argument, not two. This argument is a pointer into
130+
linear memory which stores the `x` and `y` fields.
131+
132+
## What hasn't this been fixed?
133+
134+
For `wasm32-unknown-unknown` it was well-known at the time in 2021 when WASI's
135+
ABI was updated that the ABI was non-standard. Why then, in the 4 years within
136+
that duration and 4 years afterward has the ABI not been fixed like with WASI?
137+
The main reason originally for this was the [wasm-bindgen
138+
project][wasm-bindgen].
139+
140+
In `wasm-bindgen` the goal is to make it easy to integrate Rust into a web
141+
browser with WebAssembly. JavaScript is used to interact with host APIs and the
142+
Rust module itself. Naturally this communication touches on a lot of ABI
143+
details! The problem was that `wasm-bindgen` relied on the above example,
144+
specifically having `Pair` "splatted" across arguments instead of passed
145+
indirectly. The generated JS wouldn't work correctly if the argument was passed
146+
in-memory.
147+
148+
At the time this was discovered it was found to be significantly difficult to
149+
fix `wasm-bindgen` to not rely on this splatting behavior. At the time it also
150+
wasn't though to be a widespread issue nor was it costly for the compiler to
151+
have a non-standard ABI. Over the years though the pressure has mounted. The
152+
Rust compiler is carrying an [ever-growing list of hacks][leak-details] to work
153+
around the non-standard C ABI on `wasm32-unknown-unknown`. Additionally more
154+
projects have started to rely on this "splatting" behavior and the risk has
155+
gotten greater that there are more unknown projects relying on the non-standard
156+
behavior.
157+
158+
In late 2023 [the wasm-bindgen project fixed bindings generation][wbgfix] to be
159+
compatible with the standard definition of `extern "C"`. In the following months
160+
a [future-incompat lint was added to rustc][fcw1] to specifically migrate users
161+
of old `wasm-bindgen` versions to a "fixed" version. This was in anticipation of
162+
changing the ABI of `wasm32-unknown-unknown` once enough time had passed. Since
163+
early 2025 users of old `wasm-bindgen` versions [will now receive a hard
164+
error][hard-error] asking them to upgrade.
165+
166+
Despite all this heroic effort done by contributors, however, it has now come to
167+
light that there are more projects than `wasm-bindgen` relying on this
168+
non-standard ABI definition. Consequently this blog post is intended to serve as
169+
a notice to other users on `wasm32-unknown-unknown` that the ABI break is
170+
upcoming and projects may need to be changed.
171+
172+
## Am I affected?
173+
174+
If you don't use the `wasm32-unknown-unknown` target, you are not affected by
175+
this change. If you don't use `extern "C"` on the `wasm32-unknown-unknown`
176+
target, you are also not affected. If you fall into this bucket, however, you
177+
may be affected!
178+
179+
To determine the impact to your project there are a few tools at your disposal:
180+
181+
* A new [future-incompat warning][fcw2] has been added to the Rust compiler
182+
which will issue a warning if it detects a signature that will change when the
183+
ABI is changed.
184+
* In 2023 a [`-Zwasm-c-abi=(legacy|spec)` flag was added][specflag] to the Rust
185+
compiler. This defaults to `-Zwasm-c-abi=legacy`, the non-standard definition.
186+
Code can use `-Zwasm-c-abi=spec` to use the standard definition of the C ABI
187+
for a crate to test out if changes work.
188+
189+
The best way to test your crate is to compile with the most recent Nightly
190+
compiler, ensure there are no warnings, and then test your project still works
191+
with `-Zwasm-c-abi=spec`. If all that passes then you're good to go and the
192+
upcoming change to the C ABI will not affect your project.
193+
194+
## I'm affected, now what?
195+
196+
So you're using `wasm32-unknown-unknown`, you're using `extern "C"`, and the
197+
nightly compiler is giving you warnings. Additionally your project is broken
198+
when compiled with` -Zwasm-c-abi=spec`. What now?
199+
200+
At this time this will unfortunately be a somewhat rough transition period for
201+
you. There are a few options at your disposal but they all have their downsides:
202+
203+
1. Pin your Rust compiler version to the current stable, don't update until the
204+
ABI has changed. This means that you won't get any compiler warnings (as old
205+
compilers don't warn) and additionally you won't get broken when the ABI
206+
changes (as you're not changing compilers). Eventually when you update to a
207+
stable compiler with `-Zwasm-c-abi=spec` as the default you'll have to port
208+
your JS or bindings to work with the new ABI.
209+
210+
2. Update to Rust nightly as your compiler and pass `-Zwasm-c-abi=spec`. This is
211+
front-loading the work required in (1) for your target. You can get your
212+
project compatible with `-Zwasm-c-abi=spec` today. The downside of this
213+
approach is that your project will only work with a nightly compiler and
214+
`-Zwasm-c-abi=spec` and you won't be able to use stable until the default is
215+
switched.
216+
217+
3. Update your project to not rely on the non-standard behavior of
218+
`-Zwasm-c-abi=legacy`. This involves, for example, not passing
219+
structs-by-value in parameters. You can pass `&Pair` above, for example,
220+
instead of `Pair`. This is similar to (2) above where the work is done
221+
immediately to update a project but has the benefit of continuing to work on
222+
stable Rust. The downside of this, however, is that you may not be able to
223+
easily change or update your C ABI in some situations.
224+
225+
If you have uncertainties, questions, or difficulties, feel free to reach out on
226+
[the tracking issue for the future-incompat warning][tracking] or on Zulip.
227+
228+
## Timeline of ABI changes
229+
230+
At this time there is not an exact timeline of how the default ABI is going to
231+
change. It's expected to take on the order of 3-6 months, however, and will look
232+
something roughly like this:
233+
234+
* 2025 March: (soon) - a [future-incompat warning][fcw2] will be added to the
235+
compiler to warn projects if they're affected by this ABI change.
236+
* 2025-05-15: this future-incompat warning will reach the stable Rust channel as
237+
1.87.0.
238+
* 2025 Summer: (ish) - the `-Zwasm-c-abi` flag will be removed from the compiler
239+
and the `legacy` option will be entirely removed.
240+
241+
Exactly when `-Zwasm-c-abi` is removed will depend on feedback from the
242+
community and whether the future-incompat warning triggers much. It's hoped that
243+
soon after the Rust 1.87.0 is stable, though, that the old legacy ABI behavior
244+
can be removed.
245+
246+
[wbgfix]: https://github.com/rustwasm/wasm-bindgen/pull/3595
247+
[specflag]: https://github.com/rust-lang/rust/pull/117919
248+
[fcw1]: https://github.com/rust-lang/rust/pull/117918
249+
[fcw2]: https://github.com/rust-lang/rust/pull/138601
250+
[hard-error]: https://github.com/rust-lang/rust/pull/133951
251+
[inception]: https://github.com/rust-lang/rust/pull/45905
252+
[orig-abi]: https://github.com/rust-lang/rust/pull/48959
253+
[current-abi]: https://github.com/rust-lang/rust/blob/78948ac259253ce89effca1e8bb64d16f4684aa4/compiler/rustc_target/src/callconv/wasm.rs#L76-L114
254+
[fix-wasi]: https://github.com/rust-lang/rust/pull/79998
255+
[godbolt]: https://godbolt.org/z/fExj4M4no
256+
[conventions-struct]: https://github.com/WebAssembly/tool-conventions/blob/main/BasicCABI.md#function-arguments-and-return-values
257+
[wasm-bindgen]: https://github.com/rustwasm/wasm-bindgen
258+
[tracking]: https://github.com/rust-lang/rust/issues/138762

0 commit comments

Comments
 (0)