WebAssembly allows you to write performant code in your language of choice that runs in the browser. In this workshop, you will translate JS implementations of some algorithms to Rust, and see how the implementations run in the browser.
The goals of this workshop are to provide an intro to Rust and show how to work with WebAssembly in a web app.
You will need to install Rust and NodeJS tooling for this workshop.
While you can use any editor/IDE you like, I suggest using Visual Studio Code. The Rust extension will give you IntelliSense and compilation errors within the editor.
After cloning this repository and installing the tools above, we are now ready to build and run the project.
All commands will either need to be run in the rust/
or typescript/
directories.
rust/
contains the Rust project with a Cargo.toml
file that specifies the crates we depend on.
typescript/
contains a Vue.js app for running our code.
💡
|
Keep a terminal open in both directories since you will need to switch back and forth every now and then. |
rust/
, run wasm-pack
to create a NodeJS package we can import with npm.wasm-pack build --release
Next, we are ready to start the frontend and see what we have built.
typescript/
, install our npm dependencies and start the Vue development server.npm install
npm run serve
This will start a web server at http://localhost:8080. Open the page in a browser and have a look. Click Run benchmark and see what happens. If you make changes to the Rust code and build it again, the browser should be refreshed automatically. If this does not seem to work, a normal browser refresh should do the trick. Let’s start writing some code!
💡
|
When testing your implementations run the benchmarks a few times. There will be differences between runs and outliers. It’s also good to try different browsers! |
We’ll start by implementing the Fibonacci sequence.
rust/
, open the project and the fibonacci.rs
file in Visual Studio Code.code . code src/fibonacci.rs
ℹ️
|
Visual Studio Code will not properly identify the project as a Rust project if you don’t open the rust/ subdirectory as a project.
|
Let’s have a look at the contents.
use wasm_bindgen::prelude::*; // (1) #[wasm_bindgen] // (2) pub fn fibonacci(n: u32) -> u32 { // (3) n // (4) }
-
Imports the
wasm_bingen
crate, which allows us to export functions to JS and call JS functions. -
Use the
#[wasm_bindgen]
macro to specify that we want to export the function below to JS. -
Definition of the
fibonacci
function.-
pub
means that we want to export the function. -
fn
is the function keyword. -
(n: u32)
specifies that we have one parameter of typeu32
(unsigned 32-bit integer) with the namen
. -
→ u32
is the return type of our function.
-
-
The implementation of the
fibonacci
function that currently returns then
parameter.
Before we start implementing fibonacci
, try changing the function implementation.
pub fn fibonacci(n: &str) -> String { String::from(n) }
rust/
, rebuild the Rust project.wasm-pack build --release
It should compile fine, but have a look at the logs from npm run serve
.
There should be a compilation error complaining about incompatible types.
wasm-pack
automatically generates TypeScript type definitions for us, which will help us catch many errors.
Nice!
Restore the function and start implementing fibonacci
. Have a look in typescript/src/model/fibonacci.ts
for the JS implementations.
Read up on flow of control in Rust and try implementing either the recursive or imperative variant.
Check the console in your browser’s developer tools, to see logs with the input and output of your implementation.
I managed to implement both recursive and imperative variants with similar performance to the JS imperative variant. So in this case, it does not seem like WebAssembly gives you any benefits. Of course, calling back and forth between WebAssembly and JS millions of times has its cost. Let’s try implementing something that will not require as many round trips next!
Selection sort is a simple sorting algorithm to implement. More importantly, its high complexity (O(n2)) means we can spend more time doing number crunching in WebAssembly instead of passing values back to JS.
To implement it, have a look at typescript/src/model/selection-sort.ts
for inspiration. Next, open the Rust file and have a look.
rust/
, open the selection_sort.rs
file.code . code src/fibonacci.rs
There are some new concepts in this signature, the parameter list: &mut [u32]
.
The &
means that we have been passed a borrowed reference.
mut
makes the borrowed reference mutable.
When working with a mutable reference, the strictness of the Rust compiler guarantees that the reference can’t be mutated by something else while we use the value.
While outside the scope for this workshop, "Rust by example" gives a good short intro to the rules of ownership and borrowing in Rust.
This is what makes memory safety possible for Rust without garbage collection.
The other new part of the signature is [u32]
.
The square brackets mean that we have a slice of u32
.
In Rust, a slice is a list of values, similar to an array in many other languages.
Rust also has the concept of array, but unlike slices their size has to be specified at compile time.
Put together, this signature means that we have a mutable reference to a slice of u32
.
This means we can update the values of the slice in place.
Therefore, we don’t need to have a return value in this function.
For implementing selection sort, swap()
will be helpful.
With for loops, we now have everything we need to implement selection_sort()
.
Again, I was only able to achieve similar performance to the JS implementation, which is a bit disappointing. It seems like browsers are great at optimizing JS!
Let’s have a look at how to model data in Rust instead! For this we will use the Universal orbit map puzzle from day 6 of Advent of Code 2019. Read the linked description to get to know the domain a bit.
To demonstrate the overhead of passing data to WebAssembly, this benchmark has a preloaded version where data is passed to Wasm before running the benchmark. Try running the benchmarks before making the implementation to get an idea of the cost.
ℹ️
|
This may not be be the optimal way of passing data between JS and WebAssembly. |
For this puzzle, we will skip the parsing of the string data. Instead, we will make the computation using a JS Object with this structure:
const com = {
orbits: [
{
orbits: []
},
{
orbits: [
{
orbits: [
{
orbits: []
}
]
},
{
orbits: []
}
]
},
{
orbits: []
}
]
}
Let’s look at how this can be parsed in Rust!
use wasm_bindgen::prelude::*;
#[derive(Deserialize)] // (1)
pub struct AstronomicalObject { // (2)
orbits: Vec<AstronomicalObject>, // (3)
}
pub fn parse_astronomical_object(com: &JsValue) -> AstronomicalObject { // (4)
let com: Result<AstronomicalObject, _> = com.into_serde(); // (5)
match com { // (6)
Ok(com) => com,
_ => AstronomicalObject { orbits: Vec::new() },
}
}
pub fn count_orbits(com: &AstronomicalObject) -> u32 { // (7)
com.orbits.len() as u32
}
#[wasm_bindgen]
pub fn parse_and_count_orbits(com: &JsValue) -> u32 { // (8)
let com = parse_astronomical_object(com);
count_orbits(&com)
}
// Omitted the rest of the file
-
This macro tells the Serde library to automatically implement deserialization from JSON for this struct.
-
Definition of a
struct
calledAstronomicalObject
(Apparently, that’s the term to use for this!). -
The
struct
contains a vector of the objects that orbit it. In Rust, a Vector is similar toArrayList
in some other languages. -
JsValue
represents the JS Object we are going to parse. -
The parsing is done with Serde. The
Result
type is Rust’s way of indicating that there may be errors to handle. Also, we can use thecom
variable name again, because Rust supports variable shadowing. -
We handle the potential error with
match
. If we did not have any error (Ok
) we return a dummy implementation. If something went wrong, we return an emptyAstronomicalObject
. -
The function we will implement. Currently, it has a dummy implementation.
-
The function we export to JS that parses the JS and runs the computation.
Try solving the puzzle by adding an impl
block to AstronomicalObject
with a method named count_orbits
.
💡
|
Add a depth: u32 parameter to the method.
|
To test the method you can use the unit test that is also part of rust/src/orbits.rs
.
Run it with cargo run test
or by clicking the run test button in Visual Studio Code.
Some more pointers to one possible solution:
-
Take a look in
typescript/model/orbits.ts
for a JS solution. -
You can call
iter()
on a Vector and then call methods likemap()
andsum()
on it. -
map()
takes a closure as a parameter.
This time, I actually saw a substantial improvement in the WebAssembly performance!
If you like, you can have a look at some of the internals of how the tools work.
-
Have a look in
rust/pkg/rust_wasm_workshop_rust.js
. This is the generated glue code for interoperability with JS. How are we able to send theAstronomicalObject
to Rust? -
Try inspecting the Wasm in the developer tools in the browser. Unfortunately, I haven’t managed to set up source maps. In general, it seems like the developer tools experience with Wasm is not quite there yet.
-
In this project, the Rust compiler (that uses LLVM) is set to optimize for small code size (
opt-level = "s"
). This setting can be modified inrust/cargo.toml
. Try changing to a value of3
for the highest performance. Will this impact any of the benchmarks? What about the size of the wasm-file (check the network tab in the browser developer tools)? You can also tryz
for even smaller code size thans
. Read more about the options here: https://doc.rust-lang.org/cargo/reference/manifest.html.
My conclusion, after all this, is that it is not straight forward to measure the performance difference between JS and WebAssembly. Of course, this may be because we have been using "toy examples" that are not close to real world apps. The Internet is full of more complex examples such as emulators and image processing. One example is WasmBoy, a GameBoy emulator written in AssemblyScript (a subset of TypeScript) and cross compiled to JS and WebAssembly.
Thanks for trying out these exercises!