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