The Rust language is one of the earliest adopters of WebAssembly, and it has more than one way to compile to it:
wasm32-unknown-unknownwhich uses the LLVM WebAssembly backend directly to compile dynamic libraries.
wasm32-unknown-emscriptenwhich uses Emscripten to compile whole programs.
wasm32-unknown-emscripten makes sense if you have a complete program that uses APIs like libc, SDL, OpenGL, etc. For example, maybe you’re writing a game in Rust and you want to compile that same codebase to both a native build and to the Web.
Both are valid use cases, but it’s possible the Rust community is more interested in one than the other, which seems to be the case given all the excitement about
wasm32-unknown-unknown. Part of that excitement is about code size: really small wasm binaries are possible that way. It makes sense those binaries would be smaller given that they are dynamic libraries, as opposed to complete programs that link in system support etc., so I didn’t think much about this until I saw this tweet which mentioned
.js sizes far, far larger than I’d expect! That led to some investigation that I’ll summarize in this post, a lot of which surprised me. The good news, summarized at the end, is that it is possible to generate small wasm binaries using Rust + Emscripten, but as we’ll see, it needs to be done carefully.
Consider this hello world program in C:
We can build it by getting rustup and doing this:
Rust and Code Size
The Rust FAQ has an entry on code size, which states:
There are several factors that contribute to Rust programs having, by default, larger binary sizes than functionally-equivalent C programs. In general, Rust’s preference is to optimize for the performance of real-world programs, not the size of small programs.
And indeed, compiling that same hello world program natively (using
rustc hello.rs -C opt-level=s and
strip -g hello) leads to 532K, which is in the same ballpark as the Web build of the Rust code. Compiling the C code natively (using
gcc -Os) leads to 8K, which again, is in the same ballpark as the Web build of the C code. So in a sense the issue here is not the Web nor Emscripten, rather it’s more a general difference between Rust and C.
Perhaps, then, we shouldn’t expect to compile Rust programs using C APIs like SDL or OpenAL and get small code sizes, on the Web or otherwise? But let’s keep going and see how much we can improve on the numbers we saw before.
Making the Rust More Like C
The Rust FAQ mentions that we can write Rust that uses C APIs like C does, avoiding
println! and just directly calling
printf. We can also use the
start feature in order to implement the C
main() ourselves, avoiding the extra runtime support Rust would otherwise generate. Here is what that looks like:
Also, we can compile with
-C panic=abort to avoid Rust trying to handle panics with nice stack traces, which would bring in a bunch of code. With these changes, things improve a lot! Rust’s
.js is basically the same size as C’s
.js, and Rust’s
.wasm is 13K, which is almost 10x smaller than before. It’s still 5x larger than C’s
.wasm, though, so let’s keep going.
C API Optimizations
It turns out that the remaining difference is because
clang will optimize a call to
puts when possible. But due to a current issue on Rust nightly builds that isn’t happening. We can call
puts ourselves, though, by changing the 2 relevant lines:
Compiling this, Rust’s
.wasm file is 2,416 bytes - basically the same size as C’s
It is possible to use Emscripten to compile Rust programs using C APIs to small WebAssembly binaries. They can be just as small as corresponding C programs, in fact, and those can be quite compact. But while that’s a nice result, we did have to make some changes to our Rust to get there:
- Call the
putsC API directly, to avoid Rust’s
println!()overhead (for more info, see the Rust-wasm notes on string operations causing bloat).
- Use the
startfeature, to avoid Rust’s
- Compile with
-C panic=abort, to avoid Rust’s stack trace overhead.
Overall, the main price we had to pay to get a small
.wasm file was to avoid idiomatic Rust code like
println!(). It’s disappointing we can’t use Rust in the most natural way and expect tiny code sizes. However, it wouldn’t be fair to criticize Rust on this. For one thing, idiomatic C++ using
malloc. Overall, I expect things to improve, but also we’ll keep seeing issues like these as WebAssembly adoption increases.
Thanks to acrichto, fitzgen, and sunfish for help with figuring this stuff out!