Using WebAssembly with React
30.12.2018Why Web Assembly?
Web assembly is an emerging technology that allow you to harness the power of pre-compiled code, written in a language that you already know and love, in the browser. This comes with all the usual benefits a compiler brings - such as static analysis, typechecking and high optimization.
Because I also love the react framework, this guide will show you how to set up a simple boilerplate application using the two technologies to calculate fibbonacci numbers.
Prerequisites
I will be writing the code we want to compile to web assembly using C. Make sure you have a working C build chain - such as make and GCC.
Furthermore, you will need to install emscripten. Emscripten is the toolchain that will let you compile C code to the WebAssembly platform. You can follow these instructions to install emscripten for your platform.
Getting started
Start by creating a new node project. Run the following in the location you want your project to be:
npm init -y
This will create a new file package.json
in your current directory. Open the file in your favorite editor (hopefully VSCode). In the json, add a dependencies
section as follows:
[...],
"dependencies": {
"babel-core": "^6.26.3",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
"express": "^4.16.4",
"morgan": "^1.9.1",
"path": "^0.12.7",
"react": "^16.6.0",
"react-dom": "^16.6.0",
"webpack": "^4.23.1",
"webpack-cli": "^3.1.2"
},
[...]
Most of these dependencies are standard react dependencies. We will also be using express as our developemnt server. In your terminal, run:
npm install -y
While you're in here, you might also want to add the following to the scripts
section of the package.json
file. They will come in useful later:
[...],
"scripts": {
"build": "webpack -p",
"dev-build": "webpack -d --watch",
"server": "nodemon src/server/server.js"
},
[...]
As per usual react setup, we need to do some configuring of webpack and babel files before it all works. In your directory, create a file called .babelrc
. Add the following code to it:
{
"presets": ["env", "react"]
}
This will make use of two presets we installed with npm earlier. The first one transpiles your JavaScript code, so you don't have to worry about the compatiblity of any ES6+ features you may use. The second one is the react preset for dealing with JSX syntax.
Now we need to write a quick config file so that webpack knows how to transpile our react app into a siingle file we can load in our HTML. Create the file webpack.config.js
with the following content:
var webpack = require("webpack");
var path = require("path");
var BUILD_DIR = path.resolve(__dirname, "dist");
var APP_DIR = path.resolve(__dirname, "src/client");
var config = {
entry: APP_DIR + '/index.js',
output: {
path: BUILD_DIR,
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.jsx?/,
include: APP_DIR,
loader: 'babel-loader'
}
]
}
};
module.exports = config;
With most of the config done, it's time to write the back-end development server!
Back-End
For our back end in this case, we will be writing a very simple express app in node.js. This will serve our main index.html file that load the react app, as well as any .wasm
files that our app will try to load. So let's set up a nice direcotry structure, and then get coding!
In your base project folder, create a new folder named src
. This would also be a good time to create the output directory, dist
, in your project root folder. Inside src, create a new folder called server
. Inside server
, create a file called server.js
. Your folder structure should look like the following:
-src
-server
server.js
.babelrc
package.json
webpack.config.js
Add the following code to server.js
:
const express = require("express");
const app = express();
const morgan = require("morgan");
app.use(morgan("dev"));
app.get('/', (req, res) => {
res.end(`
<!DOCTYPE html>
<html>
<head>
<title>React and WASM!</title>
</head>
<body>
<h1>Hello react and wasm!</h1>
<div id="reactDiv"/>
</body>
</html>
`);
});
app.listen(3000);
I added just a single route, /
which will serve our react app. I also added the morgan
middleware, for some extra logging. In your project root directory, run:
npm run server
This will start the nodemon
utility. nodemon
watches your server file for changes, and automatically restarts it. You can now navigate to localhost:3000
and should see the following output:
Now we have a basic server working, we can go back and edit it to do something more useful. Start by changing the HTML to be the following:
<!DOCTYPE html>
<html>
<head>
<title>React and WASM!</title>
</head>
<body>
<div id="reactDiv"/>
</body>
<script src="/dist/fib.js"></script>
<script src="/dist/bundle.js"></script>
</html>
This will try to load our (not yet existing) bundle.js
and fib.js
files, which will be generated by react and wasm respectively.
We will also need to serve our javascript files somehow. Add the dist
directory we declared our output directory for in our webpack config file as a static route in express:
app.use('/dist', express.static(path.resolve(__dirname + "../../../dist")));
But there is still a problem: This middleware will only serve files it deems safe, such as .js
and .css
files. Unfortunately, .wasm
files are not a part of this list. So we need to create our own route to serve the WebAssembly files.
Add the following route BEFORE the /
route:
app.get('/:filename.wasm', (req, res) => {
fs.readFile(path.resolve(__dirname, "../../../dist", req.params.filename), (err, data) => {
if (err) {
console.log(err);
res.status(404).send(errorMessage);
return;
}
res.set("Content-Type", "wasm")
res.send(data);
});
});
This route will take all requests to wasm files and try to serve them, setting the content type header appropriately. You will also need to import another two modules. Add this code to the top of your file:
const fs = require("fs");
const path = require("path");
With all of that done, we can move on the interesting part: The front end!
Front-End
Let's start with the most boring thing: We need an entry point for our react app, as defined in the webpack.config.js
file. So create a new directory under PROJECT_ROOT/src/client
. In this directory, make a file called index.js
and add the following content:
import React from 'react';
import {render} from 'react-dom';
import App from './App.jsx';
render(<App />, document.getElementById('reactDiv'));
This requires a App.jsx
file, so let's create that next. This file will hold our main UI, a text box and a button. On clicking the button, we want to read the value in the textbox, and calculate and output the fibonacci value of the read number:
import React from 'react';
export default class App extends React.Component {
constructor() {
super();
this.buttonClicked = this.buttonClicked.bind(this);
this.numberTyped = this.numberTyped.bind(this);
this.state = {
x: 0,
fib: 1
}
}
buttonClicked() {
let fib = _fib(this.state.x);
this.setState({
fib: fib
});
}
numberTyped(e) {
console.log(e.target.value);
this.setState({
x: e.target.value
});
}
render() {
return (
<div>
<input type="text" onChange={this.numberTyped}></input>
<button onClick={this.buttonClicked}>Calculate Fib!</button>
<div>{this.state.fib}</div>
</div>
);
}
}
It's a bit of a mouthfull, but if you know react, it should be no problem to understand. The only strange thing here is the _fib
function. This is how we call our WebAssembly functions. When the JavaScript "glue" code is generated by the compiler and loaded in our html (this is the fib.js
file), it will place all C functions preceeded by an underscore into the gloabal scope. So that finally brings us to writing and compiling our C to WebAssembly!
In the same client directory, create a main.c
file. Add the following basic fibonnacci implementation into it:
#include <stdio.h>
#include <stdint.h>
#include <emscripten.h>
int main() {
printf("Wasm loaded!\n");
}
EMSCRIPTEN_KEEPALIVE
int64_t fib(int16_t n) {
int i, t, a = 0, b = 1;
for (i = 0; i < n; i++) {
t = a + b;
a = b;
b = t;
}
return b;
}
The main function is not strictly needed, but will be executed as soon as the browser has finished loading the wasm file. So it is a good indication that everything went smoothly. Note the EMSCRIPTEN_KEEPALIVE
: This stops the compiler from removing the function when compiling, as it would seem unused and therefore unnecessary to it otherwise.
So how do we compile it? With make ofcourse - we're writing C code after all! Create a new Makefile
in the client
directory. Add the following content:
all:
emcc main.c -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' -o ../../dist/fib.js
Let's dissect this: emcc
is our compiler. main.c
is our C file to compile. With the -s
flag, we can pass various options. The -s WASM=1
tells the compiler that we want to target WASM. The next flag tells the compiler to use cwrap
. cwrap
is what lets us call the compiled functions in our javascript code by prefixing an underscore, like we did earlier. All that's left is our output file. The -o
flag will create the JavaScript "glue" code to load our Wasm module in the PROJECT_ROOT/dist
folder.
So run make
- and if all goes well, that's it! You have just compiled a C file into Wasm, ready to be loaded by our react app!
In the project root directory, run the following:
npm run build
npm run server
Navigate to localhost:3000
, and you should find your React and Wasm app running!