A tutorial on how to compile Rust to WebAssembly and integrate it into a React application
In this post, we're going to show how to compile some Rust code to WebAssembly, and integrate it into a React app.
Why would we want to do this?
It has become very popular in recent years for JavaScript to be used as a compilation target. In other words, developers are writing code in other languages, and compiling that code to JavaScript.
The JavaScript can then be run in a standard web browser. CoffeeScript and TypeScript are both examples of this.
Unfortunately, JavaScript was not designed to be used like this, which presents some difficult challenges. Some smart people recognized this trend, and these challenges, and decided to make WebAssembly (aka WASM).
WebAssembly is a binary format designed from the ground up to be a compile target for the web. This makes it much easier to develop compilers than it is for JavaScript, and also opens up lots of potential performance gains.
As an example, with WASM it's no longer necessary for the browser to parse the code, because it's already in a binary format.
There are lots of ways to get started with WebAssembly, and many examples and tutorials already out there. This post is specifically targeted at React developers who have heard of Rust and/or WebAssembly, and want to experiment with including them in a React app.
I will cover only the basics, and try to keep the tooling and complexity to a minimum.
Source
Complete source code for the final running example is available on GitHub
Prerequisites
You'll first need to have Rust and node installed. They both have excellent installation documentation:
Create the React App
We'll start with a barebones React app. First, create the
directory react_rust_wasm
, and cd into it.
Create the following directories:
src
build
dist
Then, initialize the npm package with default options:
npm init -y
Next, install React, Babel, and Webpack:
npm install --save react react-dom
npm install --save-dev babel-core babel-loader babel-preset-env babel-preset-react webpack webpack-cli
Then, create the following source files:
dist/index.html
:
<!doctype html>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
<title>React, Rust, and WebAssembly Tutorial</title>
</head>
<body>
<div id="root"></div>
<script src="/bundle.js"></script>
</body>
</html>
src/index.js
:
import React from "react";
import ReactDOM from "react-dom";
ReactDOM.render(<h1>Hi there</h1>, document.getElementById("root"));
We will also need a .babelrc
file:
{
"presets": ["react", "env"]
}
And a webpack.config.js
file:
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist")
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}
]
},
mode: "development"
};
You should now be able to test that the React app is working. Run:
npx webpack
This will generate dist/bundle.js
. If you start a
web server in the dist
directory you should be
able to successfully serve the example content.
At this point we have a pretty minimal working React app.
Let's add a button so we have a little interaction.
We'll use the button to activate a dummy function that
represents some expensive computation, which we want to
eventually replace with Rust/wasm for better performance.
Replace src/index.js
with the following:
import React from "react";
import ReactDOM from "react-dom";
function bigComputation() {
alert("Big computation in JavaScript");
}
const App = () => {
return (
<div>
<h1>Hi there</h1>
<button onClick={bigComputation}>Run Computation</button>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
Now if you should get an alert popup when you click the button, with a message indicating that the "computation" is happening in JavaScript.
Adding a Splash of Rusty WASM
Now things get interesting. In order to compile Rust to WebAssembly, we need to configure a few things.
WebAssembly Dependencies
First, we need to use Rust nightly. You can switch your Rust toolchain to nightly using the following command:
rustup default nightly
Next, we need to install the necessary tools for wasm:
rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cli
Create the Rust project
In order to build the Rust code, we need to add a
Cargo.toml
file with the following content:
[package]
name = "react_rust_wasm"
version = "1.0.0"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
You can ignore the lib section for this tutorial. Note that we
have wasm-bindgen
in the dependencies section.
This is the Rust library that provides all the magic that
makes communicating between Rust and JavaScript possible and
almost painless.
Now create the source file src/lib.rs
to contain
our Rust code:
#![feature(proc_macro, wasm_custom_section, wasm_import_module)]
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn big_computation() {
alert("Big computation in Rust");
}
I'll break this down a bit for those who might be new to Rust.
#![feature(proc_macro, wasm_custom_section, wasm_import_module)]
The first line is telling the Rust compiler to enable some special features to allow the WebAssembly stuff to work. These features are only available in the nightly toolchain, which is why we enabled it above.
extern crate wasm_bindgen;
This is how you include code from externally libraries (known as "crates") in Rust.
use wasm_bindgen::prelude::*;
Rust has an excellent module system to keep your code cleanly
separated. This line tells the compiler that we want to be
able to directly access everything in the
wasm_bindgen::prelude
module. Prelude modules are
a convention in the Rust community. If you create a library
for others to use, it's common to include a prelude
module which will automatically import the most important
pieces of your API, to save the user the trouble of
individually importing everything.
#[wasm_bindgen]
extern {
fn alert(s: &str);
}
The extern
keyword declares a section of code
which is defined outside our Rust source. In this case, the
alert
function is defined in JavaScript. The
wasm_bindgen
is invoking a Rust macro which
bridges that block of JS code so it can be used from Rust.
Macros in Rust are very powerful. They're similar to
C/C++ macros in what they can accomplish, but much nicer to
use in my experience. If you've never used C macros, you
can think of a macro as a way for the compiler to transform
your code or generate new code based on parameters provided at
compile time. In this case, the
wasm_bindgen
macro takes care of generating all
the plumbing between Rust and JavaScript, based on the
function names we provide.
#[wasm_bindgen]
pub fn big_computation() {
alert("Big computation in Rust");
}
This is a normal Rust function, except that once again
we're using the wasm_bindgen
macro to
generate the plumbing. In this case, the
big_computation
function is being made available
to be called from JavaScript. When called, this function calls
the alert
function, which as we saw above is
defined in JavaScript. We've set this up to test the
complete loop of calling from JS to Rust and back to JS.
Building
We're now ready to build everything. There are a couple stages to this. We're going to implement these as simple npm scripts. Of course there are lots of fancier ways to do this.
The first stage is to compile the Rust code into wasm. Add the following to your package.json scripts section:
"build-wasm": "cargo build --target wasm32-unknown-unknown"
If you run npm run build-wasm
, you should see
that the file
target/wasm32-unknown-unknown/debug/react_rust_wasm.wasm
has been created.
Next we need to take the wasm file, and convert it into the final form that can be consumed by JavaScript, in addition to generating the proper JS files for wrapping everything. Add the following script to package.json:
"build-bindgen": "wasm-bindgen target/wasm32-unknown-unknown/debug/react_rust_wasm.wasm --out-dir build"
If you run npm run build-bindgen
, you should see
several files created in the build directory.
Note that wasm-bindgen
even creates a
react_rust_wasm.d.ts file for you in case you want to use
TypeScript. Nice!
Ok, now all we need is a build script to do all the steps in order:
"build": "npm run build-wasm && npm run build-bindgen && npx webpack"
Your package.json scripts section should now look something like this:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build-wasm": "cargo build --target wasm32-unknown-unknown",
"build-bindgen": "wasm-bindgen target/wasm32-unknown-unknown/debug/react_rust_wasm.wasm --out-dir build",
"build": "npm run build-wasm && npm run build-bindgen && npx webpack"
},
Running npm run build
should work at this point.
However, we still need to modify our JavaScript code to use
our wasm module instead of the JS function.
Replace src/index.js
with the following:
import React from "react";
import ReactDOM from "react-dom";
const wasm = import("../build/react_rust_wasm");
wasm.then(wasm => {
const App = () => {
return (
<div>
<h1>Hi there</h1>
<button onClick={wasm.big_computation}>Run Computation</button>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
});
There are a couple important changes. First, as of this
writing, you need to use the import
function,
rather than the normal ES6 import syntax. It has something to
do with not being able to load wasm asynchronously yet. In
order to use this function, we need to enable a babel plugin.
Install it with the following:
npm install --save-dev babel-plugin-syntax-dynamic-import
And add it to your .babelrc:
{
"presets": ["react", "env"],
"plugins": ["syntax-dynamic-import"]
}
The import
function returns a promise.
That's why we need to call wasm.then
in
order to kick things off.
You should now be able to successfully run
npm run build
. Reload
dist/index.html
from a web server and you'll
now see a message indicating it's running from Rust. And
just like that, we're done!
Where to go from here
There are a lot of exciting things happening in the world of Rust+WebAssembly. This tutorial was aimed at React developers who just want to get their feet wet. Here are a few other resources you can check out if you want to go deeper.
-
Check out this great post to get an idea of the goals and vision for Rust/WASM.
-
rustwasm/team. This seems to be the central repository for keeping up with the current state of Rust and WebAssembly. It's a fantastic resource.
-
wasm-bindgen I highly recommend reading through their documention and examples. A good chunk of this tutorial is copied almost exactly from there. There are many more advanced features that can be used, such as using other JavaScript APIs, defining structs in Rust and using them in JS, passing those structs between Rust and JS, and many more.
-
stdweb is a bridging library that has some overlap with
wasm-bindgen
.stdweb
has some nice features and macros for letting you write JavaScript inline in your Rust code, rather than just a simple bridge.wasm-bindgen
seems to be more focused on bridging, and is designed to be used with languages other than just Rust in the future. -
Yew is a Rust framework for writing client-side apps. It's heavily inspired by React, but it lets you write your app 100% in Rust.
-
The excellent New Rustacean podcast recently did an episode on Rust/WASM. I highly recommend giving it a listen.