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:
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:
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]//1name="wasm-game-of-life"version="0.1.0"authors=["Nicolas Frankel <nicolas@frankel.ch>"]edition="2018"[lib]//2crate-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:
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 the pkg folder.
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.
Import everything from the hello-wasm-pack package into the wasm 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:
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.
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.
externcrateweb_sys;// 1useweb_sys::console;// 2#[wasm_bindgen]implFoo{pubfnnew()->Foo{utils::set_panic_hook();Universe{}}pubfnlog(&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 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: