Rust 🤝 WebAssembly with Alex Crichton

Rust 🤝 WebAssembly with Alex Crichton

Last week, we at Suborbital had the joy of sponsoring the revival of the WebAssembly North America meetup.

For our first guest, we hosted a live discussion with Alex Crichton all about his work on bringing WebAssembly support to the Rust programming language.

Alex is a longtime Rust developer, and a former member of Rust's Core, Infrastructure, and Library teams.

These days, he's working at Fastly on the Wasmtime runtime, as well as driving standardization efforts around WASI and the WebAssembly Component Model.

Below is the recording and transcript of that conversation.

Curious about the journey of Rust's wasm32-unknown-unknown and wasm32-unknown-wasi compiler targets? Why they have these names, and how moving from Emscripten to LLVM went? Watch the video above, or check out the abridged transcript below!

Related Links:


Abridged Transcript

Ramón: Alex, tell us a little bit about yourself and what you do.

Alex: Hi, my name is Alex. I live in the Midwest. And I am an engineer who works at Fastly. I work on the WebAssembly runtime aspect of Fastly’s Compute@Edge products, where whenever an HTTP request comes into our CDN, we run some WebAssembly code for it. I work on Wasmtime primarily, which is an open source project run by the Bytecode Alliance. It's a specialized out-of-browser runtime for WebAssembly. So I get to have all the fun of implementing all this.

I've historically worked a large, very large amount on the Rust programming language and libraries and in various ecosystems around that. I did a lot of work to bring WebAssembly into Rust and get WebAssembly some really, really nice targets to use in Rust. And so I don't do quite as much of that nowadays. I'm focusing a lot more on Wasmtime.

Ramón: In 2017, and in 2019, respectively, you added two compilation targets to Rust. One of them is wasm32-unknown-unknown. And the other one is wasm32-unknown-wasi. From what I understand this is adding WebAssembly support to Rust and I want to start with the names, because if you look at a name like wasm32-unknown-unknown it's an interestingly odd name. I did later find out that this is called a "target triple," and I was wondering if you could please maybe give us a little bit of a background on what these mean and why they're named like this?

Alex: It's not to be intentionally mysterious, despite the "unknown" name. The irony is that despite being called target triples, they often come in quartets or in pairs. So this is where you can break apart the wasm32-unknown-unknown into three pieces... but other targets like Linux actually have x86_64-unknown-linux-gnu, which is four different pieces. The idea is that originally these were triples, and then we added pieces on later to get the four pieces... and also figured out how to shorten that down to two. More about that later.

Of the three pieces, we've start with wasm32, which is the target CPU architecture. Like arm32, x86_64.

The second piece is what's called the vendor. Here, it's “unknown”, and most targets have “unknown”, though Windows has pc. I honestly don't know exactly what we use the vendor for, but everyone else seems to use it.

The third one is the OS. So you'll see linux on Linux, or windows on Windows. You see darwin for macOS.

And so those are the three pieces of wasm32-unknown-unknown.

The reason we ended up calling it that was that the goal with that target was to be a very bare bones aspect of WebAssembly. So it's not assuming any vendor, not assuming any operating system. That primarily means it's not going to import anything. So when you use the standard library, no functions are imported into your Wasm module. You can't open a file, you can't make a TCP connection. That's sort of like the general idea of the target.

Ramón: WebAssembly doesn't have a vendor, and doesn't have an OS. At least at this time. But the "32" I found interesting because I never really thought about what the architecture would look like for WebAssembly. Is there a possibility of 64-bit?

Alex: Yes. For 64-bit... the WebAssembly standard gives us the concept of linear memory. And all memory operations, by default, are using a 32 bit address space. So while this target is technically just WebAssembly, we ended up calling it wasm32 to emphasize that the pointers are all this large. But there is a proposal for WebAssembly called Memory 64 which is going to extend WebAssembly to also have 64-bit pointers.

That's going to be such a major difference that we kind of reserved space to eventually have wasm64. It's actually experimentally in the compiler, but it's not really ready just yet because the the actual WebAssembly proposal itself is still in the extension process.

Ramón: This is not the first WebAssembly target for Rust, if I'm not mistaken. The first one was an Emscripten one, is that correct?

Alex: That's right. All of this, all of WebAssembly, is predated by Emscripten. Emscripten was sort of the birth of asm.js, which is where you can compile a bunch of C and C++ code to JavaScript—very carefully crafted JavaScript—so runs just right in browsers. And it's super fast.

WebAssembly was the evolution of that.

Originally, we had asmjs-unknown-emscripten. The target was considered JavaScript, vendor was unknown, and the platform or the OS at the time was considered Emscripten and the toolchain around that. So prior to the wasm32-unknown-unknown target, there was a wasm32-unknown-emscripten.

So that all existed in the compiler at the time, just using very different backend things internally.

Ramón: So originally you were using Emscripten, but when creating this wasm32-unknown-unknown target, your pull request mentioned that you wanted to forego using Emscripten in favor of LLVM. What was the reasoning there?

Alex: There's a lot of history here, but Emscripten was historically built upon a LLVM back end called fastcomp. This was a fork of LLVM. I don't actually know the original history of this, but fastcomp was tasked with taking LLVM IR as input, and then generating JavaScript code itself. But since this was a fork and never actually made its way back upstream, it sort of always diverged and was based on older versions of LLVM.

It was just a whole different compilation model. It wasn't centered around what you would expect from native tools. Things weren't exactly object files in the same way. But anyway... there's a lot of history around how Emscripten was working, but it was not really where the WebAssembly target was going.

Official WebAssembly support was added to LLVM upstream, around this time period. So after fastcomp, but before we had the wasm32-unknown-unknown target. At the time, Emscripten had not migrated to use the wasm backend in LLVM, but that was considered the future. Even Emscripten wanted to switch to that, but it was going take some work to actually get it done.

So the goal that I had was to sort of get us working on the LLVM WebAssembly backend. Start getting bugfixes there, bug reports, real user experience... kind of flushing all that out.

There's a lot of various trade offs, but I think most of them were in favor of the LLVM backend. So for example, this has official object files, it's more integrated with the LLVM toolchain, and it's all upstream. We could matched the LLVM updates, and we don't have to worry about keeping fastcomp, Rust's LLVM, and upstream LLVM, all in sync at the same time.

Ramón: When you decided to forego Emscripten in favor of LLVM, and some of that other tooling. How difficult was that? Did you encounter any obstacles doing so?

Alex: Oh, yes, very much. So the LLVM back end for WebAssembly was very new at the time. It was sort of in development for Emscripten—a lot of Emscripten developers were working on it at the time—but it was not done. It was not fully integrated, and did not have all the bugs flushed out.

So we had a whole slew of linking issues. We had compiler bugs. We had mis-compilations. We had LLVM panics. The whole gamut of whenever you write a new compiler backend.

What can go wrong? Everything went wrong.

So there was a lot of fun debugging, all the narrowing things down, sending bug reports upstream, getting it fixed and everything.

Ramón: I imagine that that that made things a little bit slower to get up to speed then.

Alex: Very much so.

Ramón: I noticed in the pull requests that... a lot of components that are usually built into compilers or runtimes, you ported to Rust. Such as dlmalloc and binaryen.

Could you tell us about these?

Alex: So this is where WebAssembly is a very low level target. It kind of only gives you math. You can add numbers, you can multiply numbers, you can move things from memory, but that's kind of it. It doesn't really give you any high level operations there. And additionally, this unknown-unknown target was supposed to not import anything, which meant it couldn't assume the host gave it anything or had any inputs as a result of that.

So what this all amounts to is... it's extremely common in Rust programs, or pretty much any program in the world, that you want to allocate memory. It's a relatively low level operation, and a lot of garbage collected languages like JavaScript or Python don't give you access to it.

Rust abstracts memory allocation away so you don't have to worry about it too much, but under the hood, it’s very common for it to say "I want 10 bytes of memory." And then some time to say "I'm done with these 10 bytes of memory, please let someone else have these 10 bytes of memory." And that's typically handled by what's called a memory allocator.

WebAssembly doesn't have a memory allocator. It just gives you the ability to say "give me 65 kilobytes of memory." But that's massive... and it's also a more permanent kind of resource usage. It's like WebAssembly saying "just give my process more memory."

So anyway, dlmalloc was a generic memory allocation implementation, written in C originally. It was a good trade off between code size—not too big, not too small—with speed. Not too fast, but not too slow, either.

As for portability, dlmalloc had been ported to dozens of Unix platforms at the time, and was basically a perfect target for WebAssembly.

So Rust needed a memory allocator, and there wasn't exactly anything off the shelf that was suitable for the standard library's use case. So I chose dlmalloc. I ported it all to Rust... I just kind of stared at the C code, wrote some Rust code, and kind of went one-to-one for everything. And that was how dlmalloc was born. It was basically just a translation from the C code to the Rust code.

That means that in the standard library, we now have a memory allocator. And that means that when you compile to WebAssembly, you don't actually have to pull it anything else, because it's all baked in there.

I can cover Binaryen next.

Ramón: Yes, please.

Alex: Binaryen was actually not ported to Rust. This was a bit a little confusing from the pull request, but it was more on the toolchain side of things.

This is where we're getting into history. Binaryen is a project that was spearheaded by the Emscripten developers as an effort to get all the asm.js and Emscripten output over to WebAssembly. So... Binaryen is a nice toolkit for doing lots of different stuff with WebAssembly. Nowadays, one of its flagship features is a command line executable called wasmopt, which will take a WebAssembly module and give you a WebAssembly module... But the idea is that it optimizes in between there. It has its own inlining, has its own WebAssembly specific optimizations, all that good stuff.

At the time, Binaryen was the only way to get output from LLVM and then produce the WebAssembly executable. Remember, early on, there were a lot of bugs in the WebAssembly backend.

Nowadays, you have a linker to officially get this, you get object files, you can link it all together. But none of that existed at the time that I added this target.

I was very antsy at the time, I wanted to get this landed. So I wanted to get anything, anything working whatsoever. And this was where I took the one thing that worked, which was Emscripten at the time, and I think what it did was it was actually parse the textual assembly output of LLVM.

LLVM textual assembly output is sort of a misnomer, because it's not really the text format for WebAssembly. It's a sort of LLVM specific thing.

But we took and parsed all that and actually generated a WebAssembly module. So this was a piece of compiler tooling, in which we we took the C++ code—or Binaryen, which was all written in C++—we compiled all that, linked it into the Rust compiler. Very similar to what we do with LLVM: we just compile all of it and link it all straight in. And then we took all that and that was what actually produced the WebAssembly executables at the time.

Now, all of that has since been removed, since it was just a temporary stopgap until all the features of LLVM came into place. It was basically a temporary transitional period.

Ramón: You mentioned that the additions to LLVM have made this significantly easier, such as the addition of lld. Could you tell us a little bit about that, and maybe what it stands for?

Alex: Well, I can tell you all about it, but I actually have no idea what it stands for. The naming... so all LLVM related things, they've all got to be called "LL... Something". So you've got lldb for debugging, you've got LLVM... LLVM technically stands for "Low Level Virtual Machine," but it's not actually a virtual machine. It's more of a compiler toolkit. We can ignore that.

Anyway, ld is the system linker on Unix. So it's what you typically use to take object files and generate an executable file you can actually run. And lld is the LLVM version of ld. They just put on an "L" on front. It's a linker written in C++ for the LLVM toolchain, and it's got a whole bunch of targets. It can generate ELF for Linux, Mach-O for macOS, PE for Windows... then support was added for WebAssembly.

An object file format was defined for WebAssembly, but it turns out that WebAssembly object files are just .wasm files themselves. And lld has a driver called wasm-ld used to link all of these object files into one final WebAssembly binary. This performs what you would typically expect of a linker, which is it takes a bunch of compiled artifacts and creates one final artifact with everything shoved in there.

This is what powers the separate compilation model of Rust: when you download Rust, you get the standard library. But it's a precompiled version of the standard library. Then you write your code and compile it separately from the standard library. Then lld is the thing that comes together, slurps it all up, and generates one final Wasm binary file to run.

Listen to the full meetup to hear more about linking, Link Time Optimization (LTO), wasm-bindgen, the WebAssembly Component Model, and the wasm32-unknown-wasi target.


Want to catch the next meetup? Sign up at meetup.com.

To learn more about how Suborbital is using Rust and WebAssembly to bring user-defined plug-ins to SaaS applications, visit our home page.