Upgrading Rust’s Emscripten Support

Hi everyone! We wanted to let you know about some changes to the Rust compiler’s Emscripten support.

Background

Rust has been able to emit asm.js using Emscripten and has been able to emit WebAssembly using either Emscripten or the unknown triple (wasm32-unknown-unknown). The unknown triple is the preferred approach in general, especially when writing some new Rust code especially for the Web. But the Emscripten path is very useful when porting Rust programs that were originally written to run natively - for example, a Rust game engine using SDL2 and OpenGL can be compiled to the Web using Emscripten’s port of SDL2 and its OpenGL compatibility layer. And in general a mixed project of both Rust and C/C++ may be easier to port using Emscripten due to the C/C++ support there. Emscripten may also be useful if you want built-in integration for features like Asyncify.

What’s Changing

Emscripten has supported two backends for a while now, the “fastcomp” asm.js backend, and the LLVM wasm backend. The wasm backend and its integration in Emscripten has gotten very good and we currently recommend people use it, as it has several advantages like emitting smaller and faster code, and much faster linking. In particular, the wasm backend fixes several bugs with Rust+Emscripten (like IR compatibility issues, as fastcomp was very tailored to the LLVM IR from Clang).

We are migrating Emscripten in general from fastcomp to the wasm backend, and we’d like to do that in Rust as well. Aside from the benefits already mentioned, a major benefit for Rust here is that this change will allow removal of the fastcomp LLVM build. Besides that internal change, you shouldn’t see much change when using the new backend (except some speedups!) but there are a few known bugs.

One thing that does change significantly is the asm.js support, which was only present in fastcomp. Emscripten’s wasm backend support has an option to emit non-asm.js JavaScript, which uses Binaryen’s wasm2js to convert the wasm to JS. This is smaller than the old asm.js in most cases, and generally preferable, but it doesn’t benefit from asm.js optimizations, so if you benchmark it in a browser with asm.js optimizations (most of them these days) it may appear slower. Because this isn’t asm.js, we are planning to remove the separate asmjs-unknown-emscripten target. To get the JS output, you can instead use the emscripten target normally and add -s WASM=0 to the link-time flags. Alternatively, if the community prefers, we can rename the asmjs-unknown-emscripten target to js-unknown-emscripten but keep it as a separate target.

As a result, after this change you will have the following targets:

  • wasm32-unknown-unknown (self-contained binaries)

  • wasm32-unknown-emscripten (full applications with dependencies)

  • wasm32-wasi (experimental wasm-native platform)

  • js-unknown-emscripten (If the community prefers this over linker flags)

- Thomas and Alon

10 Likes

:partying_face:

2 Likes

I am worried about the polyfill story for Rust-as-WASM. Currently at $workplace we have a Rust codebase compiled to asm.js and linked into a larger JS application. This is fast on newer browsers and works as-is on older browsers.

Looks like after this change the "fast AND compatible" choice will no longer be available, and we'll need to compile and test two builds: one in WASM that's actually fast, and one that is backwards-compatible. We'll also need to write and maintain two different versions of library loading (to make matters worse, WASM compilation is async) and two different versions of JS<->Rust FFI glue, which is memory-unsafe by design (and yes, we've tried binding generators, they're way too slow for getting large float arrays in and out).

This problem is not specific to Rust - anything compiled with Emscripten is affected. I understand the WASM backend is the future, but I'm wondering if wasm2js could be improved to produce output in the asm.js ballpark before the new backend is flipped on by default?

3 Likes

I understand the concern, yeah. I think this is hard to avoid, though. The core issue is that JS is great for dynamic optimization and wasm is great for static optimization, and asm.js is something kind of weird in the middle. It's awkward for both browsers and the toolchain to support asm.js in addition to the other two.

On the toolchain side, asm.js is not enough for even the full wasm MVP - for example, imported Tables can't work in asm.js. For a subset of the wasm MVP it can work, but as wasm expands the fraction that is asm.js-compatible will shrink, and the effort to maintain an asm.js path increases, and seems impractical given the current number of contributors.

And on the browser side, I don't expect them to keep asm.js optimizations around forever. As they are just an optimization, browsers can remove them, and doing so will reduce complexity and remove security risks (and the replacement - wasm - has been available for a while).

As JS optimizations improve, hopefully the (non-asm.js) JS polyfill will be reasonably fast. I doubt it can get to full asm.js speed, but I'd hope it can be within a factor of 2 or so for asm.js-compatible code. (In that case, some users that really don't want two builds may be ok with a single JS one.) On the emscripten benchmark suite most fall in that range (some are faster than wasm, in fact! likely because of runtime inlining), but a few are worse. I'm optimistic those can be improved because while JS is hard to optimize in general, this subset is much simpler (almost as simple as asm.js, but not quite).

How large is your project? If it's small enough then you can use sync compilation with wasm (in emscripten, set WASM_ASYNC_COMPILATION=0). A problem with larger files is some browsers will refuse to compile them synchronously. But maybe we can convince browsers to change that, especially since they have fast baseline compilers everywhere now.

Another option for avoiding separate library loading code is to make your project load as async even in the JS case (which is pretty easy with the emscripten JS, just use the onRuntimeInitialized callback). In that case it should not be easily observable to the outside whether the JS or wasm build is being used.

4 Likes