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.
Comes WebAssembly.
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.
- My first cup of Rust
- My second cup of Rust
- The Rustlings exercises - part 1
- The Rustlings exercises - part 2
- Rust on the front-end (this post)
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:
- Either you provide a step-by-step progression path, with solutions along the way, and it becomes just a matter of copy-pasting
- 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 <nicolas@frankel.ch>"]
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
- Meta-information about the package
- Produce a library, not a binary
- Produce both a Rust library as well as a dynamic system library
- 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
- Mirror image of
Cargo.toml
for NPM projects, configured for Wasm - Webpack configuration
- Main entry-point into the "application": call the Wasm code
- 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:
- Compile Rust code to Wasm
- 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 thepkg
folder. - Get the NPM dependencies with
npm install
- 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:
- Call Rust from JavaScript
- Call JavaScript from Rust
- 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
}
- Magic macro!
On the JavaScript side:
import * as wasm from "hello-wasm-pack"; // 1
wasm.foo(); // 2
- Import everything from the
hello-wasm-pack
package into thewasm
namespace - 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
}
- It's not C code, but this is the correct syntax anyway
- Generate the Rust interface so it can compile
- 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.
developer.mozilla.org/en-US/docs/Web/JavaSc..
-- 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
- Add the dependency as optional
- 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
}
- Use
Math
from thejs_sys
crate - 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.
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
}
}
- Require the
web-sys
create. I'm not sure if (or why)extern
is needed. - Use the
console
package console::log_1()
translates intoconsole.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:
To go further:
Originally published at A Java Geek on July 4th, 2021