WebAssembly and Rust: There and Back Again
I’m convinced WebAssembly is the next big thing, and it stretches beyond the Web. Here’s my experience learning the basics with the help of Rust.
“If you want to understand something, write about it”. So I decided to follow the famous advice and tell a short story about my foray into the WebAssembly land.
What is WebAssembly?
WebAssembly (a.k.a. Wasm) is an open standard that includes specifications for bytecode, its text representation and a secure host environment that would execute the code. The initial aim was to run C code on the web, but eventually, a range of compilers and runtimes was developed. So now we can run WebAssembly without a web browser or JavaScript.
It’s not the first attempt to create a cross-platform runtime. How is WebAssembly different from the previous “write once, run anywhere” technologies?
- It’s light and simple: no heavy virtual machines, no platforms with complex APIs
- It’s an open standard: nothing is proprietary, nothing is for sale
- There’s no single “primary” language as it is the case with Java or dotNet
- No specialisation: as WebAssembly isn’t a platform, it can be used for anything
Now that I wrote this it looks strange that it is the four “have nots” rather than “haves”. I think it’s an important thing about WebAssembly: it’s not Java. It’s so simple, open and universal that it can run anywhere.
I can’t count how many times I saw the phrase that WebAssembly isn’t designed to replace JavaScript, but rather to complement it. Well, let’s be honest: I did my time as a web developer and I’m not a fan of JS. When I’m looking at WebAssembly, what I actually think is “Can it replace JavaScript?”. Many people focus on performance, and it is important, but what if I just don’t want to write JavaScript anymore? When will its unnatural monopoly come to an end?
WebAssembly may be the answer to that question.
What is Rust?
I wanted to start from a relatively low level — no complex frameworks, no tooling. I like to know how things work before I use a high-level framework. At the same time, I wasn’t determined enough to write WebAssembly by hand. And didn’t want to write any C code either.
Fortunately, we have Rust programming language. Rust is characterized by two main things: it doesn’t let you mismanage the memory (by applying its novel ownership system), and it doesn’t use garbage collection — which means there’s almost no runtime. These traits make it an ideal language to be compiled to bytecode and run on a light virtual machine.
Such as WebAssembly.
Prerequisites
For our little experiment we’ll need to install a few bits and pieces:
- Install Rust: https://www.rust-lang.org/tools/install
- Install
wasm-gc
optimiser:cargo install wasm-gc
- Wasm-pack bundler:
cargo install wasm-pack
Miniserve
, a simple web server:cargo install miniserve
- Wasmer, the runtime that will let us run WebAssembly outside the browser: https://wasmer.io/
First Rust project
First, let’s write some Rust code. We’ll create a simple project using Cargo, the awesome Rust CLI:
cargo init --lib
We’re creating a dynamic library, so will need a correct crate-type
in Cargo.toml
— the project’s main metadata file:
Now, let’s add two small functions: one that calculates a Fibonacci number, and another that quite simply returns a string:
I assume this code should be obvious to anyone, even if you don’t have any experience with Rust. Two things to note: the first is the #[no_mangle]
tags — we need them to make the functions available in a dynamically linked module, and the second is that the first function has a return type String
. String
is a dynamic type, a heap-allocated sequence of UTF-8 characters — this will require special attention when we get to bundling WebAssembly.
We can build it to native code using cargo build, but our goal is WebAssembly, so we need to install the compilation target:
rustup target add wasm32-unknown-unknown
Now we can do:
cargo build --target wasm32-unknown-unknown --release
After the compilation is done, the WebAssembly file can be found in target/wasm32-unknown-unknown/release/wasm_example.wasm
. This file we can already load on a web page or run on any Wasm runtime.
But if you look at its size, it’s quite big — for web especially, we would like a smaller file. No problem, let’s use wasm-gc
to optimise it:
wasm-gc ./target/wasm32-unknown-unknown/release/wasm_example.wasm ./wasm_example_rust.wasm
The optimised file is just 17Kb (down from 1.4M).
Running with Wasmer
What can we do with this file now? A good start would be to test it locally. We can use Wasmer to call functions from the wasm file:
wasmer wasm_example_rust.wasm -i fibonacci 10
89
Works! What about the function that returns a string?
wasmer wasm_example_rust.wasm -i will_return_string
error: failed to run `wasm_example_rust.wasm`╰─> 1: Function expected 1 arguments, but received 0: “”
Wait, our function doesn’t require any parameters! What does this mean? Let’s build some suspense — we’ll get to that later.
Running on the Web
By definition, the WebAssembly code can be executed in any supporting web browser — meaning almost every browser these days. Unfortunately, the file still needs to be loaded by JavaScript. The code is straightforward though.
We simply fetch the file, instantiate the object and then run both functions. To serve it, we can use miniserve
or any other web server.
miniserve . --index index.html
Let’s open a browser, navigate to localhost:8080 and take a look in the console:
Hmm, this again — fibonacci
works as expected, but we still can’t return a string.
A look inside
Wasm file is the bytecode in its binary format. But WebAssembly also provides a text representation called WAT. We can restore it from our binary, using either wasm2wat console tool or its online demo: https://webassembly.github.io/wabt/demo/wasm2wat/. There’s also a nice VSCode extension.
Once uploaded, our file turns into a surprisingly large bulk of code, somewhat akin to Lisp — but, with some effort, it is understandable. For example, we can find the definition of the fibonacci
function:
(func $fibonacci (export “fibonacci”) (type $t5) (param $p0 i32) (result i32)
It’s clear that it accepts an integer parameter and returns also an integer. What about our other function, that is acting weirdly?
(func $will_return_string (export “will_return_string”) (type $t4) (param $p0 i32)
As you can see, it’s been rewritten to accept a parameter while it now returns nothing. Why is that? Digging deeper, I found that $p0
is effectively a memory address — a pointer — where the function can place the result. Apparently, this is the only way we can return a dynamic entity like a String in WebAssebly.
The reason for that is that WebAssemby operates a very limited number of primitive types (remember the four “have nots”?) — the concept of strings simply doesn’t exist in this dimension.
Curiously, if we leave out the will_return_string
function from our Rust code, compile to wasm and convert to WAT, the result becomes short and clear:
We can conclude that the rest was just boilerplate for moving the memory around.
The glue
To overcome these difficulties, we must come back again and start anew. This time around we’re going to use a wrapper, a tool that will take care of all the boilerplate, both on the Rust and the JavaScript side. The Rust code doesn’t change, just the scaffolding.
First, we add a dependency to Cargo.toml
. The wasm-opt part is a workaround I had to add because of a bug in this release of wasm-bindgen
(don’t ask).
Then, slightly modify the code:
Here we inform Rust that we want to use wasm-bindgen
and replace #[no_mangle]
tags with #[wasm_bindgen]
tags.
Now, as the result will be a javascript package, we must use wasm-pack to build it:
wasm-pack build --target web
This time we skip wasm-gc
, wasm-pack
does the size optimisation for us.
Unlike the artefacts that Cargo produces, the result is generated in the pkg directory. It is a proper npm package with all the javascript boilerplate. For simplicity though, we’ll just create another small HTML file to run it:
Here everything is taken care for us by wasm-pack. Basically, the only thing we need to do is to import the JS module — it loads our WebAssembly code underneath and publishes both functions in its namespace.
So what will we see if we serve this to Web? Let’s find out.
Both functions work now! Even the one that returns a string.
In conclusion
WebAssembly is limited but that’s a good thing, its power is in its simplicity.
Rust isn’t so simple, but a powerful language that at the same time remains relatively low-level.
One can’t just simply walking into WebAssembly from Rust, but one most certainly can with a little help from these tools:
wasm-bindgen
, that provides the glue between Rust and Javascript. It works both ways: we can use JS API from Rust and Rust code from JS.wasm-pack
, the bundler that packs our code so it can be executed in a web browser.
What about Rust coupled with WebAssembly replacing Javascript? Well, this remains a dream for now. Some day, I believe we move on from HTML and the browser will simply execute Wasm applications. Unfortunately, we’re not quite there yet.
Still, WebAssembly is a powerful technology and it’s becoming increasingly popular. As it makes way to production in big corporations, I’m sure a bright future awaits WebAssembly.
Stay tuned for more WebAssembly articles — Go and Python are next in line!
Links
- All the code can be found in my GitHub repo: https://github.com/moor84/wasm-example
- Yew — possibly the future of Web development: https://yew.rs/docs/en/
- The bug in
wasm-bindgen
, for which I had to apply a workaround: https://github.com/rustwasm/wasm-pack/issues/886