Skip to content

sebastianljunggren/rust-wasm-workshop

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

57 Commits
 
 
 
 
 
 

Repository files navigation

WebAssembly with Rust

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.

Setup

You will need to install Rust and NodeJS tooling for this workshop.

  • rustup will help you install rustc (compiler) and cargo (package manager).

  • wasm-pack lets you target WebAssembly with Rust.

  • NodeJS with npm is needed for building the frontend.

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.
In 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.

In 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!

Fibonacci sequence

We’ll start by implementing the Fibonacci sequence.

In 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.

rust/src/fibonacci.rs
use wasm_bindgen::prelude::*; // (1)

#[wasm_bindgen] // (2)
pub fn fibonacci(n: u32) -> u32 { // (3)
    n // (4)
}
  1. Imports the wasm_bingen crate, which allows us to export functions to JS and call JS functions.

  2. Use the #[wasm_bindgen] macro to specify that we want to export the function below to JS.

  3. 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 type u32(unsigned 32-bit integer) with the name n.

    • → u32 is the return type of our function.

  4. The implementation of the fibonacci function that currently returns the n parameter.

Before we start implementing fibonacci, try changing the function implementation.

pub fn fibonacci(n: &str) -> String {
    String::from(n)
}
In 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

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.

In 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!

Count orbits

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!

rust/src/orbits.rs
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
  1. This macro tells the Serde library to automatically implement deserialization from JSON for this struct.

  2. Definition of a struct called AstronomicalObject (Apparently, that’s the term to use for this!).

  3. The struct contains a vector of the objects that orbit it. In Rust, a Vector is similar to ArrayList in some other languages.

  4. JsValue represents the JS Object we are going to parse.

  5. 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 the com variable name again, because Rust supports variable shadowing.

  6. 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 empty AstronomicalObject.

  7. The function we will implement. Currently, it has a dummy implementation.

  8. 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 like map() and sum() on it.

  • map() takes a closure as a parameter.

This time, I actually saw a substantial improvement in the WebAssembly performance!

Extra credits

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 the AstronomicalObject 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 in rust/cargo.toml. Try changing to a value of 3 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 try z for even smaller code size than s. Read more about the options here: https://doc.rust-lang.org/cargo/reference/manifest.html.

Conclusion

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!

About

Try Rust and WebAssembly by completing exercises

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •