Rust on the front-end

Up until now, JavaScript has been the only ubiquitous language available in browsers. It has made JavaScript much more popular than its design (and its associated flaws) would have allowed. Consequently:

  • The number of JavaScript developers has grown tremendously and steadily
  • The ecosystem around front-end JavaScript has become more extensive and much more complex
  • The pace of changes has increased so that developers complain about JavaScript fatigue
  • Interestingly enough, JavaScript sneaked on the back-end via Node.js
  • etc.

I don't want to start a holy war about the merits of JavaScript, but IMHO, it only survived this far because of its role in browsers. In particular, current architectures move the responsibility of executing the code from the server to the client. It puts a lot of strain on the latter. There are not many ways to improve performance: either buy more powerful (and expensive!) client machines or make the JavaScript engines better.

WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications.

Wasm is not designed to replace JavaScript in the browser (yet?) entirely but to improve the overall performance.
Though Rust is intended for system programming, it does offer compilation to WebAssembly.

Rust and WebAssembly

Learning Rust is a long process; learning Rust and WebAssembly even more so. Fortunately, people of goodwill already made the journey a bit easier. They wrote an entirely free and online tutorial book dedicated solely to this subject, available under a friendly Open Source license.

As both a learner and a trainer, I know how hard it is to create a good tutorial:

  1. Either you provide a step-by-step progression path, with solutions along the way, and it becomes just a matter of copy-pasting
  2. Or you provide something less detailed, but you run the risk that some learners get blocked and cannot finish.

The book avoids both pitfalls as it follows the first pattern but provides optional problems at the end of each chapter. To avoid blocking the learner, each problem provides a general hint to lead learners to the solution. If you cannot (or do not want to) solve a specific problem, you can continue onto the next chapter anyway. Note that in the associated Git repository, each commit references either a standard copy-paste step or a problem to solve.

Moreover, the book provides two sections, the proper tutorial, and a reference part. Thus, you can check for the relevant documentation bits during the tutorial and deepen your understanding after it.

A Rust project

The first tutorial step focuses on the setup. It's short and is the most "copy-pastey" of all. The reason for that is that it leverages cargo-generate, a Cargo plugin that allows creating a new project by using an existing Git repository as a template. In our case, the template is a Rust project ready to compile to Wasm. The project's structure is:

wasm-game-of-life/
├── Cargo.toml
└── src
    ├── lib.rs
    └── utils.rs

It's the structure of "standard" Rust projects. Now is an excellent time to look at the Cargo.toml file. It plays the pom.xml role, listing meta-information about the package, dependencies, compilation hints, etc.

[package]                                              // 1
name = "wasm-game-of-life"
version = "0.1.0"
authors = ["Nicolas Frankel <[email protected]>"]
edition = "2018"

[lib]                                                  // 2
crate-type = ["cdylib", "rlib"]                        // 3

[features]
default = ["console_error_panic_hook"]

[dependencies]
wasm-bindgen = "0.2.63"                                // 4

# Rest of the file omitted for clarity purposes
  1. Meta-information about the package
  2. Produce a library, not a binary
  3. Produce both a Rust library as well as a dynamic system library
  4. The Wasm-producing dependency

Integrating the front-end

The project as it stands is not very interesting: you cannot see the magic happening. The next step in the tutorial is to add a web interface to interact with the Rust code compiled to Wasm.

As for the previous step, a command allows copying code from GitHub. Here, the command is npm, and the template is create-wasm-app. Let's run the command:

npm init wasm-app www

The previous command outputs the following structure:

wasm-game-of-life/
└── www/
    ├── package.json                   // 1
    ├── webpack.config.js              // 2
    ├── index.js                       // 3
    ├── bootstrap.js                   // 4
    └── index.html
  1. Mirror image of Cargo.toml for NPM projects, configured for Wasm
  2. Webpack configuration
  3. Main entry-point into the "application": call the Wasm code
  4. Asynchronous loader wrapper for index.js

At this point, it's possible to execute the whole code chain, provided we go through the required build steps:

  1. Compile Rust code to Wasm
  2. Generate the JavaScript adapter code. You can run this step and the previous one with a single call to wasm-pack. Check the generated files in the pkg folder.
  3. Get the NPM dependencies with npm install
  4. Run a local webserver with npm run start.

Browsing to http://localhost:8080 should display a simple alert() message.

At the end of this section, the exercise mandates you to change the alert() to prompt() to provide parameterization. You should change the Rust code accordingly and re-compile it. The web server should reload the new code on the fly so that refreshing the page should display the updated code.

My idea behind this post is not to redo the whole tutorial but to focus on the juicy parts. With Rust on the front-end, it boils down to:

  1. Call Rust from JavaScript
  2. Call JavaScript from Rust
  3. Call browser APIs from Rust

Call Rust from JavaScript

To call Rust from JavaScript, you need to compile the Rust code to Wasm and provide the thin JavaScript wrapper. The template project already has it configured. You only need to use the wasm-bindgen macro on the Rust functions you want to make available.

#[wasm_bindgen]           // 1
pub fn foo() {
    // do something
}
  1. Magic macro!

On the JavaScript side:

import * as wasm from "hello-wasm-pack";       // 1

wasm.foo();                                    // 2
  1. Import everything from the hello-wasm-pack package into the wasm namespace
  2. You can now call foo()

Call JavaScript from Rust

The guiding principle behind the tutorial is Conway's Game of Life. One way to initialize the board is to set each cell to either dead or alive randomly. Because the randomization should occur at runtime, we need to use JavaScript's Math.random(). Hence, we also need to call JavaScript functions from Rust.

Basic set up uses Foreign Function Interface via the extern keyword:

#[wasm_bindgen]
extern "C" {                                 // 1
    #[wasm_bindgen(js_namespace = Math)]     // 2
    fn random() -> f64;
}

#[wasm_bindgen]
fn random_boolean() -> bool {
    random() < 0.5                           // 3
}
  1. It's not C code, but this is the correct syntax anyway
  2. Generate the Rust interface so it can compile
  3. Use it

While this works, it's highly error-prone. Alternatively, the js-sys crate provides all available bindings out-of-the-box:

Bindings to JavaScript’s standard, built-in objects, including their methods and properties.

This does not include any Web, Node, or any other JS environment APIs. Only the things that are guaranteed to exist in the global scope by the ECMAScript standard.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects

-- Crate js_sys

To set up the crate, you only need to add it to the relevant section in the manifest:

[dependencies]
js-sys = { version = "0.3.50", optional = true }  # 1

[features]
default = ["js-sys"]                              # 2
  1. Add the dependency as optional
  2. Activate the optional feature

I must admit that I don't understand why to set the dependency optional and activate it on another line. I'll leave it at that for now.

The previous configuration allows the following code:

use js_sys::Math;               // 1

#[wasm_bindgen]
fn random_boolean() -> bool {
    Math::random() < 0.5        // 2
}
  1. Use Math from the js_sys crate
  2. Compile fine with the Rust compiler and call JavaScript's Math.random() at runtime

Call browser APIs from Rust

The js-sys crate allows us to call JavaScript APIs inside of Rust code. However, to call client-side APIs, for example, console.log(), the web_sys crate is necessary.

Raw API bindings for Web APIs

This is a procedurally generated crate from browser WebIDL which provides a binding to all APIs that browsers provide on the web.

This crate by default contains very little when compiled as almost all of its exposed APIs are gated by Cargo features. The exhaustive list of features can be found in crates/web-sys/Cargo.toml, but the rule of thumb for web-sys is that each type has its own cargo feature (named after the type). Using an API requires enabling the features for all types used in the API, and APIs should mention in the documentation what features they require.

-- Crate web_sys

Here's how to configure it:

[dependencies]
web-sys = { version = "0.3", features = ["console"] }

We can use the crate like this:

extern crate web_sys;                                 // 1

use web_sys::console;                                 // 2

#[wasm_bindgen]
impl Foo {
    pub fn new() -> Foo {
        utils::set_panic_hook();
        Universe {}
    }

    pub fn log(&self) {
        console::log_1("Hello from console".into());  // 3
    }
}
  1. Require the web-sys create. I'm not sure if (or why) extern is needed.
  2. Use the console package
  3. console::log_1() translates into console.log() with one parameter at runtime

Conclusion

In this post, we have detailed the three main points of using Rust in the browser: calling Rust from JavaScript, calling JavaScript from Rust, and calling browser APIs from Rust.

The following video gives you a taste of the final result; I can only encourage you to try the tutorial out by yourself.

The complete source code for this post can be found on Github:

wasm-pack-template

A template for kick starting a Rust and WebAssembly project using wasm-pack.

Build Status

Tutorial | Chat

Built with 🦀🕸 by The Rust and WebAssembly Working Group

About

📚 Read this template tutorial! 📚

This template is designed for compiling Rust libraries into WebAssembly and publishing the resulting package to NPM.

Be sure to check out other wasm-pack tutorials online for other templates and usages of wasm-pack.

🚴 Usage

🐑 Use cargo generate to Clone this Template

Learn more about cargo generate here.

cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name my-project
cd my-project

🛠️ Build with wasm-pack build

wasm-pack build

🔬 Test in Headless Browsers with wasm-pack test

wasm-pack test --headless --firefox

🎁 Publish to NPM with wasm-pack publish

wasm-pack publish

🔋 Batteries Included



To go further:

Originally published at A Java Geek on July 4th, 2021

22