Fast browser plugins with Rust and WebAssembly
WebAssembly is a secure way of building binary code that runs inside a browser, using a protected sandbox. JavaScript can talk to the plugin, but only through explicitly exposed methods, and the same is true of WebAssembly methods calling JavaScript functions from inside the plugin.
Using Rust to create WebAssembly plugins that work in a browser is not new, but I wanted to share the build chain I have been using, as it is simpler than the standard process documented by Mozilla, especially if you are not using Node.js to build your site. We’ll create a simple plugin to show the process, and in the next post we will try something more ambitious.
What and why of WebAssembly
So the “what” is a piece of binary code, with methods (functions), that can be executed by the browser using JavaScript.
The “why” is of course speed – Rust is much faster than JavaScript, especially for processing intensive tasks such as validating text or interpreting and executing code.
WebAssembly has been around for a few years, and was first developed by Mozilla, as a way to extend browser functionality without the limitations of JavaScript. Since being released, it has gained traction for performance critical tasks, but remains rather niche, mainly because JavaScript is easy and adequate for most tasks, but also because of its complexity, and the complexity of Rust.
The setup
What follows is (I think) a relatively simple setup that allows you to create WebAssembly modules that can be loaded and used in the browser. It is simpler than the usual pipeline, because it does not require Node.js or any other non-Rust dependencies. Parts of this are documented, but not easily in one place, thus the motivation for this post.
First of all, you need to have Rust installed.
Then, you just need to install wasm-bindgen-cli, the command-line utility for wasm-bindgen (note that the linked documentation seems to be no longer maintained). This installs the package globally, and is required for the steps that follow:
cargo install wasm-bindgen-cli
Creating your project
Create a project with cargo new, just like any other Rust project, but include
the --lib flag to make it a library. Then change into the new directory, and
add the dependency wasm-bindgen (not to be confused with the -cli global
package installed above):
cargo new --lib hello-wasm
cd hello-wasm
cargo add wasm-bindgen
This creates the new project, with code in src/lib.rs, the contents of which we will shortly replace.
Note that the project name is important, and fiddly to change later, so choose wisely.
Create the code for your plugin
Edit the Rust program src/lib.rs, for example:
use wasm_bindgen::prelude::*;
// Format a string to say hello
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {name}!")
}
// Convert string by replacing instances of "aaa" with "XXX"
#[wasm_bindgen]
pub fn convert(text: &str) -> String {
text.replace("aaa", "XXX")
}
This short program defines two functions which are exposed to JavaScript. One of them takes a name and return a string with “Hello,” in front of it, the other replaces all instances of the substring “aaa” with “XXX”, and returns the changed string. Note that incoming strings are str references (&str), and return values are Strings. Parameters and return values can be numbers and other values as well.
Note that #[wasm_bindgen] makes Rust function callable from JavaScript, an
attribute that serves a similar purpose to a @decorator in Python.
Build the project
Step 1: build the project in release mode using cargo build but with
a –target flag for WebAssembly:
cargo build --release --target wasm32-unknown-unknown
Next convert the binary to a WebAssembly module (filetype .wasm), including additional files that help with mapping the module’s functions for JavaScript access:
wasm-bindgen --target web --out-dir pkg target/wasm32-unknown-unknown/release/hello_wasm.wasm
This creates the output in the ./pkg folder, but you can use anything you want.
If you have problems, make sure that Cargo.toml contains the following:
[lib]
crate-type = ["cdylib"]
Embedding the plugin in a web page
Loading and using the module is as simple as embedding some JavaScript in your
page, using a <script type="module"> tag. It has to be of type module,
since the JavaScript inside it imports a module. That import looks like this:
import init, { greet, convert } from "./pkg/hello_wasm.js";
Note that init is imported first, outside braces, and that the methods you
expose in your Rust code need to be include inside braces.
The imported module then needs to initialized, and this happens asynchronously, so you need to await it, using your favorite approach, e.g.,
await init();
Then, you can call the module’s functions as if they were plain JavaScript, e.g.,
alert(greet("Hello from Wasm"));
or
var changedText = convert("This is aaa string");
Here is a complete, self-contained HTML file that loads and demonstrates the module created above:
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>WASM example</title>
</head>
<body>
<p>Click me button to say hello:
<button id="button1">Button</button>
</p>
<p>Enter text, aaa will be converted:</p>
<textarea id="text1" style="width: 100%; height: 200px"></textarea>
<script type="module">
// Import from the WASM module and its JavaScript glue
import init, { greet, convert } from "./pkg/hello_wasm.js";
await init();
function change_text() {
var t = document.getElementById("text1");
t.value = convert(t.value);
}
function hello() {
alert(greet("Hello from Wasm"));
}
// Attach event handlers to elements (cannot use in-line handlers here)
document.getElementById("button1").addEventListener("click", hello);
document.getElementById("text1").addEventListener("input", change_text);
</script>
</body>
</html>
Note that you need to serve the HTML from a web server, it will not work as file:// output. So to try it out, put this HTML file in the root folder of your project, and run a simple Python web server:
python -m http.server 8000
and browse to http://localhost:8000
Or use nginx, or another web server. Just make sure that the path to the module load is correct, relative to the root of the web site (e.g., “./pkg/hello_wasm.js” in the example above).
Sources
The official Mozilla documentation is helpful for understanding the concepts, but requires the Node.js build chain, which is bloated if you are not using Node for anything else in your site.
I found this source helpful for explaining simpler alternative approaches, but it took a while to get everything working, so I hope this post is useful to anyone coming to it for the first time.