This is a Rust and WebAssembly workshop! It was originally held on May 2nd, 2018 at the PDXRust meetup.
- Setup
- Hello World
- Conway's Game of Life
- The End
If you don't have rustup
installed yet, run:
curl https://sh.rustup.rs -sSf | sh
Next, use rustup
to install the latest nightly and the
wasm32-unknown-unknown
target:
rustup toolchain add nightly
rustup update nightly
rustup target add wasm32-unknown-unknown --toolchain nightly
Finally, install the wasm-bindgen
CLI tool:
cargo +nightly install -f wasm-bindgen-cli
I recommend using https://github.com/creationix/nvm to manage side-by-side installations of different node versions, but if you already have node >= 8 installed, you can skip this.
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash
source ~/.bashrc
nvm install 9
nvm use 9
Clone the project template:
git clone https://github.com/pdxrust/rust-wasm-workshop.git --branch hello-world
cd rust-wasm-workshop
Take a look at the contents of index.js
and src/lib.rs
. Then build the
project and run the development server!
npm install
npm run build-debug
npm run serve
If you navigate to http://localhost:8080 in your browser*, you should now see the hello-world alert dialog!
You can interrupt the server and get your shell prompt back just by hitting control-C.
Add this test to src/lib.rs
:
#[test]
fn two_plus_two() {
assert_eq!(2 + 2, 4);
}
Now run cargo +nightly test
. You should see this:
Compiling hello_world v0.1.0 (file:///home/fitzgen/rust-wasm-workshop)
Finished dev [unoptimized + debuginfo] target(s) in 0.88 secs
Running target/debug/deps/hello_world-d8a1e575b45635c6
running 1 test
test two_plus_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
It is important that you familiarize yourself with running such tests. We will use unit tests to determine whether your implementation for a section is working or not, and therefore whether it is OK to start the next section or not.
We will be implementing Conway's Game of Life. If you're unfamiliar with it, you can follow that link to Wikipedia, or wait for the group to go through the Game of Life intro slides together.
Before we dive into writing code, we have some design decisions to consider.
The Game of Life is technically played in an infinite universe, but we do not have infinite memory and compute power. Instead, we will create a fixed-size, periodic universe, where cells on the edges have neighbors that wrap around to the other side of the universe.
We can represent the universe as a flat array, and has a byte for each cell. We
will use 0
for dead cells and 1
for alive cells.
Here is what a 4 by 4 universe looks like:
To find the array index of the cell at a given row and column in the universe, we can use this formula:
index(row, column, universe) = row * width(universe) + column
Let's begin by removing the alert
import, greet
function, and two_plus_two
test from src/lib.rs
, and replacing them with a type definition for cells:
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {
Dead = 0,
Alive = 1,
}
Next, let's define the universe. The universe has a width and a height, and a
vector of cells of length width * height
.
#[wasm_bindgen]
pub struct Universe {
width: u32,
height: u32,
cells: Vec<Cell>,
}
Let's define a constructor that initializes the universe with an interesting initial pattern of live and dead cells:
/// Public methods, exported to JavaScript.
#[wasm_bindgen]
impl Universe {
pub fn new() -> Universe {
let width = 64;
let height = 64;
let cells = (0..width * height)
.map(|i| {
if i % 2 == 0 || i % 7 == 0 {
Cell::Alive
} else {
Cell::Dead
}
})
.collect();
Universe {
width,
height,
cells,
}
}
}
The state of the universe is represented as a vector of cells. To make this
human readable, let's implement a basic text renderer. The idea is to write the
universe line by line as text, and for each cell that is alive, print the
unicode character ◼️
("black medium square"). For dead cells, we'll print ◻️
(a "white medium square"). After each row, a newline should be printed.
By implementing the Display
trait from Rust's standard library, we can add a
way to format a structure in a user-facing manner. This will also automatically
give us a to_string
method.
use std::fmt;
impl fmt::Display for Universe {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
unimplemented!("insert code here")
}
}
Next, let's expose the human-readable text to JavaScript, by adding a render
method to the impl Universe
:
/// Public methods, exported to JavaScript.
#[wasm_bindgen]
impl Universe {
// ...
pub fn render(&self) -> String {
self.to_string()
}
}
Add this test to src/lib.rs
:
#[test]
fn universe_displays_correctly() {
let universe = Universe {
width: 4,
height: 4,
cells: vec![
Cell::Dead, Cell::Dead, Cell::Dead, Cell::Dead,
Cell::Dead, Cell::Dead, Cell::Dead, Cell::Alive,
Cell::Dead, Cell::Dead, Cell::Alive, Cell::Alive,
Cell::Dead, Cell::Alive, Cell::Alive, Cell::Alive,
],
};
assert_eq!(
universe.to_string(),
"â—» â—» â—» â—» \n\
â—» â—» â—» â—Ľ \n\
â—» â—» â—Ľ â—Ľ \n\
â—» â—Ľ â—Ľ â—Ľ \n"
);
}
Make sure that running cargo +nightly test
reports this test passing before
continuing!
First, let's add a <pre>
element to our index.html
to render the universe
into. Edit the <body>
element to look like this:
<body>
<pre id="game-of-life-canvas"></pre>
<script src='./bootstrap.js'></script>
</body>
Additionally, we want the <pre>
centered in the middle of the Web page. We
can use CSS flex boxes to accomplish this task. Add the following <style>
tag
inside index.html
's <head>
:
<style>
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
}
</style>
At the top of index.js
, let's fix our import to bring in the Universe
rather
than the old greet
function:
import { Universe } from "./hello_world";
Also, let's get that <pre>
element we just added and instantiate a new
universe:
const pre = document.getElementById("game-of-life-canvas");
const universe = Universe.new();
Now, we can draw the initial universe into the <pre>
!
pre.textContent = universe.render();
If you re-run
npm run build-debug
npm run serve
then you should be able to refresh http://localhost:8080 and see a text representation of your universe!
If not, it is time to troubleshoot and debug. Raise your hand and ask a mentor for help if you are stuck!
You can add logging to help debug like this:
#[wasm_bindgen]
extern {
#[wasm_bindgen(js_namespace = console)]
fn log(msg: &str);
}
// A macro to provide `println!(..)`-style syntax for `console.log` logging.
macro_rules! log {
($($t:tt)*) => (log(&format!($($t)*)))
}
The macro_rules!
definition needs to appear before all uses of the log!
macro it defines, so you should place it towards the top of the file.
You can use it like this:
log!("the value of `x` is {}", x);
If you open your browser's developer tools console, you can view the logs.
Now that we can draw the universe, let's turn our attention to computing the
next generation inside src/lib.rs
.
To access the cell at a given row and column, translate the row and column into an index into the cells vector, as described earlier:
impl Universe {
fn get_index(&self, row: u32, column: u32) -> usize {
unimplemented!("insert code here")
}
}
We could just add this method to to the impl
block we started above, but we
did label that one “Public methods, exported to JavaScript”, and get_index
isn't part of the API we want to expose to JS. It's nice to keep the public API
separate, so we'll start a second impl Universe
block and put get_index
there.
In order to calculate the next state of a cell, we need to get a count of how
many of its neighbors are alive. Let's write a live_neighbor_count
method to
do just that!
impl Universe {
// ...
fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {
let mut count = 0;
for delta_row in [self.height - 1, 0, 1].iter().cloned() {
for delta_col in [self.width - 1, 0, 1].iter().cloned() {
if delta_row == 0 && delta_col == 0 {
continue;
}
let neighbor_row = (row + delta_row) % self.height;
let neighbor_col = (column + delta_col) % self.width;
let idx = self.get_index(neighbor_row, neighbor_col);
count += self.cells[idx] as u8;
}
}
count
}
}
The live_neighbor_count
method uses deltas and modulo to avoid special casing
the edges of the universe with if
s. When applying a delta of -1
, we add
self.height - 1
and let the modulo do its thing, rather than attempting to
subtract 1
. row
and column
can be 0
, and if we attempted to subtract 1
from them, there would be an unsigned integer underflow.
Now we have everything we need to compute the next generation from the current one!
Recall the rules for each cell's state transition:
-
Any live cell with fewer than two live neighbours dies, as if caused by underpopulation.
-
Any live cell with two or three live neighbours lives on to the next generation.
-
Any live cell with more than three live neighbours dies, as if by overpopulation.
-
Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
All other cells remain in the same state.
Translate these rules into Rust code by filling in the tick
method's
definition:
/// Public methods, exported to JavaScript.
#[wasm_bindgen]
impl Universe {
// ...
pub fn tick(&mut self) {
let mut next = self.cells.clone();
for row in 0..self.height {
for col in 0..self.width {
unimplemented!("insert code here")
}
}
self.cells = next;
}
}
Since we want to call this from JavaScript, be sure to put this function in the
impl Universe
block with the #[wasm_bindgen]
attribute above it!
Add the following tests to src/lib.rs
:
use Cell::*;
fn assert_tick(w: u32, h: u32, before: Vec<Cell>, after: Vec<Cell>) {
assert_eq!(before.len(), after.len());
assert_eq!(w as usize * h as usize, before.len());
let mut universe = Universe {
width: w,
height: h,
cells: before,
};
universe.tick();
assert_eq!(
&universe.cells[..],
&after[..]
);
}
#[test]
fn tick_rule_1() {
assert_tick(
5,
5,
vec![
Dead, Dead, Dead, Dead, Dead,
Dead, Dead, Dead, Dead, Dead,
Dead, Dead, Alive, Dead, Dead,
Dead, Dead, Dead, Dead, Dead,
Dead, Dead, Dead, Dead, Dead,
],
vec![
Dead, Dead, Dead, Dead, Dead,
Dead, Dead, Dead, Dead, Dead,
Dead, Dead, Dead, Dead, Dead,
Dead, Dead, Dead, Dead, Dead,
Dead, Dead, Dead, Dead, Dead,
],
);
}
#[test]
fn tick_rule_2() {
assert_tick(
5,
5,
vec![
Dead, Dead, Dead, Dead, Dead,
Dead, Dead, Dead, Dead, Dead,
Dead, Alive, Alive, Dead, Dead,
Dead, Alive, Alive, Dead, Dead,
Dead, Dead, Dead, Dead, Dead,
],
vec![
Dead, Dead, Dead, Dead, Dead,
Dead, Dead, Dead, Dead, Dead,
Dead, Alive, Alive, Dead, Dead,
Dead, Alive, Alive, Dead, Dead,
Dead, Dead, Dead, Dead, Dead,
],
);
}
#[test]
fn tick_rules_3_and_4() {
assert_tick(
5,
5,
vec![
Dead, Dead, Dead, Dead, Dead,
Dead, Dead, Alive, Dead, Dead,
Dead, Alive, Alive, Alive, Dead,
Dead, Dead, Alive, Dead, Dead,
Dead, Dead, Dead, Dead, Dead,
],
vec![
Dead, Dead, Dead, Dead, Dead,
Dead, Alive, Alive, Alive, Dead,
Dead, Alive, Dead, Alive, Dead,
Dead, Alive, Alive, Alive, Dead,
Dead, Dead, Dead, Dead, Dead,
],
);
}
#[test]
fn tick_cells_on_edge() {
assert_tick(
5,
5,
vec![
Dead, Dead, Dead, Dead, Dead,
Dead, Dead, Dead, Dead, Dead,
Alive, Dead, Dead, Alive, Alive,
Dead, Dead, Dead, Dead, Dead,
Dead, Dead, Dead, Dead, Dead,
],
vec![
Dead, Dead, Dead, Dead, Dead,
Dead, Dead, Dead, Dead, Alive,
Dead, Dead, Dead, Dead, Alive,
Dead, Dead, Dead, Dead, Alive,
Dead, Dead, Dead, Dead, Dead,
],
);
}
Make sure that these tests pass when you run cargo +nightly test
before
continuing!
Remove the old pre.textContent = universe.render();
line of code from
index.js
. We will replace it with a recursive requestAnimationFrame
loop. On
each iteration, it computes the next generation of the universe by calling
tick
, and then renders the result into the <pre>
:
const renderLoop = () => {
universe.tick();
pre.textContent = universe.render();
requestAnimationFrame(renderLoop);
};
To kick off the rendering process, all we have to do is schedule the initial animation frame callback:
requestAnimationFrame(renderLoop);
Let's rebuild and restart the server:
npm run build-debug
npm run serve
Refreshing http://localhost:8080 once more, and you should see the Game of Life animated before your eyes!
Generating (and allocating) a String
in Rust and then having wasm-bindgen
convert it to a valid JavaScript string makes unnecessary copies of the
universe's cells. Instead of our current render
method, we can return a
pointer to the start of the cells array. The JavaScript code knows the width and
height of the universe, and can read the bytes that make up the cells directly.
This design does not copy the universe's cells or tax the JavaScript garbage
collector with allocations, but we must directly read the cells' bytes from
WebAssembly's linear memory in JavaScript. Instead of rendering unicode text,
we'll switch to using the Canvas API. We will use this design in the rest of
the tutorial.
First, let's replace the <pre>
we added earlier with a <canvas>
we will
render into (it too should be within the <body>
, before the <script>
that
loads our JavaScript):
<body>
<canvas id="game-of-life-canvas"></canvas>
<script src='./bootstrap.js'></script>
</body>
Return to src/lib.rs
and export (i.e. define them within the #[wasm_bindgen]
block) three new functions to JavaScript:
-
A
width
function that returns the width of the universe as au32
. -
A
height
function that returns the height of the universe as au32
. -
A
cells
function that returns a*const Cell
pointer to the first cell in the universe'scells
array.
Next, let's edit index.js
and define some constants that JavaScript will use
when rendering the canvas:
const CELL_SIZE = 5; // px
const GRID_COLOR = "#CCCCCC";
const DEAD_COLOR = "#FFFFFF";
const ALIVE_COLOR = "#000000";
// These must match `Cell::Alive` and `Cell::Dead` in `src/lib.rs`.
const DEAD = 0;
const ALIVE = 1;
Now, let's rewrite our current JS code (except for the import) to no longer
write to the <pre>
but instead draw to the <canvas>
:
// Construct the universe, and get its width and height.
const universe = Universe.new();
const width = universe.width();
const height = universe.height();
// Give the canvas room for all of our cells and a 1px border
// around each of them.
const canvas = document.getElementById("game-of-life-canvas");
canvas.height = (CELL_SIZE + 1) * height + 1;
canvas.width = (CELL_SIZE + 1) * width + 1;
const ctx = canvas.getContext('2d');
const renderLoop = () => {
universe.tick();
drawGrid();
drawCells();
requestAnimationFrame(renderLoop);
};
To draw the grid between cells, we draw a set of equally-spaced horizontal lines, and a set of equally-spaced vertical lines. These lines criss-cross to form the grid.
const drawGrid = () => {
ctx.beginPath();
ctx.strokeStyle = GRID_COLOR;
// Vertical lines.
for (let i = 0; i <= width; i++) {
ctx.moveTo(i * (CELL_SIZE + 1) + 1, 0);
ctx.lineTo(i * (CELL_SIZE + 1) + 1, (CELL_SIZE + 1) * height + 1);
}
// Horizontal lines.
for (let j = 0; j <= height; j++) {
ctx.moveTo(0, j * (CELL_SIZE + 1) + 1);
ctx.lineTo((CELL_SIZE + 1) * width + 1, j * (CELL_SIZE + 1) + 1);
}
ctx.stroke();
};
To draw the cells, we get the cells pointer into the WebAssembly linear memory
from the universe, construct a Uint8Array
overlaying the cells buffer, iterate
over each cell, and draw a white or black rectangle depending on whether the
cell is dead or alive, respectively. By working with pointers and overlays, we
avoid copying the cells across the boundary on every tick.
// Import the WebAssembly memory at the top of the file.
// The 'hello_world_bg' module is generated by wasm-bindgen,
// hence the '_bg'.
import { memory } from "./hello_world_bg";
// ...
const getIndex = (row, column) => {
return row * width + column;
};
const drawCells = () => {
const cellsPtr = universe.cells();
const cells = new Uint8Array(memory.buffer, cellsPtr, width * height);
ctx.beginPath();
for (let row = 0; row < height; row++) {
for (let col = 0; col < width; col++) {
const idx = getIndex(row, col);
ctx.fillStyle = cells[idx] === DEAD
? DEAD_COLOR
: ALIVE_COLOR;
ctx.fillRect(
col * (CELL_SIZE + 1) + 1,
row * (CELL_SIZE + 1) + 1,
CELL_SIZE,
CELL_SIZE
);
}
}
ctx.stroke();
};
To schedule rendering, we'll use the same code as above to start the first iteration of the rendering loop:
requestAnimationFrame(renderLoop);
Rebuild and restart the development server:
npm run build-debug
npm run serve
If you refresh http://localhost:8080/, you should be
greeted with an exciting display of life in <canvas>
!
-
Instead of hard-coding the initial universe, generate a random one, where each cell has a fifty-fifty chance of being alive or dead.
Hint: use
wasm_bindgen
to import theMath.random
JavaScript function:#[wasm_bindgen] extern { #[wasm_bindgen(js_namespace = Math)] fn random() -> f64; }
-
Representing each cell with a byte makes iterating over cells easy, but it comes at the cost of wasting memory. Each byte is eight bits, but we only require a single bit to represent whether each cell is alive or dead. Refactor the data representation so that each cell uses only a single bit of space.
Thanks for following along!
For more Rust and WebAssembly:
-
Join us on IRC at
#rust-wasm
onirc.mozilla.org
-
Read the Rust and WebAssembly Book. This workshop was derived from the Game of Life tutorial in this book. The book keeps going and discusses profiling for speed and size, as well as debugging in depth!
-
Get involved in making Rust and WebAssembly a great combination by joining the working group!