Implementing Conway's Game of Life

Design

Before we dive in, we have some design choices to consider.

Infinite Universe

The Game of Life is played in an infinite universe, but we do not have infinitememory and compute power. Working around this rather annoying limitation usuallycomes in one of three flavors:

  • Keep track of which subset of the universe has interesting things happening,and expand this region as needed. In the worst case, this expansion isunbounded and the implementation will get slower and slower and eventuallyrun out of memory.

  • Create a fixed-size universe, where cells on the edges have fewer neighborsthan cells in the middle. The downside with this approach is that infinitepatterns, like gliders, that reach the end of the universe are snuffed out.

  • Create a fixed-size, periodic universe, where cells on the edges haveneighbors that wrap around to the other side of the universe. Becauseneighbors wrap around the edges of the universe, gliders can keep runningforever.

We will implement the third option.

Interfacing Rust and JavaScript

⚡ This is one of the most important concepts to understand and take away from this tutorial!

JavaScript's garbage-collected heap — where Objects, Arrays, and DOM nodesare allocated — is distinct from WebAssembly's linear memory space, where ourRust values live. WebAssembly currently has no direct access to thegarbage-collected heap (as of April 2018, this is expected to change with the"Interface Types" proposal). JavaScript, on the other hand, canread and write to the WebAssembly linear memory space, but only as anArrayBuffer of scalar values (u8, i32, f64,etc…). WebAssembly functions also take and return scalar values. These are thebuilding blocks from which all WebAssembly and JavaScript communication isconstituted.

wasm_bindgen defines a common understanding of how to work with compoundstructures across this boundary. It involves boxing Rust structures, andwrapping the pointer in a JavaScript class for usability, or indexing into atable of JavaScript objects from Rust. wasm_bindgen is very convenient, but itdoes not remove the need to consider our data representation, and what valuesand structures are passed across this boundary. Instead, think of it as a toolfor implementing the interface design you choose.

When designing an interface between WebAssembly and JavaScript, we want tooptimize for the following properties:

  • Minimizing copying into and out of the WebAssembly linear memory.Unnecessary copies impose unnecessary overhead.

  • Minimizing serializing and deserializing. Similar to copies, serializingand deserializing also imposes overhead, and often imposes copying aswell. If we can pass opaque handles to a data structure — instead ofserializing it on one side, copying it into some known location in theWebAssembly linear memory, and deserializing on the other side — we can oftenreduce a lot of overhead. wasm_bindgen helps us define and work with opaquehandles to JavaScript Objects or boxed Rust structures.

As a general rule of thumb, a good JavaScript↔WebAssembly interface design isoften one where large, long-lived data structures are implemented as Rust typesthat live in the WebAssembly linear memory, and are exposed to JavaScript asopaque handles. JavaScript calls exported WebAssembly functions that take theseopaque handles, transform their data, perform heavy computations, query thedata, and ultimately return a small, copy-able result. By only returning thesmall result of the computation, we avoid copying and/or serializing everythingback and forth between the JavaScript garbage-collected heap and the WebAssemblylinear memory.

Interfacing Rust and JavaScript in our Game of Life

Let's start by enumerating some hazards to avoid. We don't want to copy thewhole universe into and out of the WebAssembly linear memory on every tick. Wedo not want to allocate objects for every cell in the universe, nor do we wantto impose a cross-boundary call to read and write each cell.

Where does this leave us? We can represent the universe as a flat array thatlives in the WebAssembly linear memory, and has a byte for each cell. 0 is adead cell and 1 is a live cell.

Here is what a 4 by 4 universe looks like in memory:

Screenshot of a 4 by 4 universe

To find the array index of the cell at a given row and column in the universe,we can use this formula:

  1. index(row, column, universe) = row * width(universe) + column

We have several ways of exposing the universe's cells to JavaScript. To begin,we will implement std::fmt::Display for Universe, which we canuse to generate a Rust String of the cells rendered as text characters. ThisRust String is then copied from the WebAssembly linear memory into a JavaScriptString in the JavaScript's garbage-collected heap, and is then displayed bysetting HTML textContent. Later in the chapter, we'll evolve thisimplementation to avoid copying the universe's cells between heaps and to renderto <canvas>.

Another viable design alternative would be for Rust to return a list of everycell that changed states after each tick, instead of exposing the whole universeto JavaScript. This way, JavaScript wouldn't need to iterate over the wholeuniverse when rendering, only the relevant subset. The trade off is that thisdelta-based design is slightly more difficult to implement.

Rust Implementation

In the last chapter, we cloned an initial project template. We will modify thatproject template now.

Let's begin by removing the alert import and greet function fromwasm-game-of-life/src/lib.rs, and replacing them with a type definition forcells:

  1. # #![allow(unused_variables)]
  2. #fn main() {
  3. #[wasm_bindgen]
  4. #[repr(u8)]
  5. #[derive(Clone, Copy, Debug, PartialEq, Eq)]
  6. pub enum Cell {
  7. Dead = 0,
  8. Alive = 1,
  9. }
  10. #}

It is important that we have #[repr(u8)], so that each cell is represented asa single byte. It is also important that the Dead variant is 0 and that theAlive variant is 1, so that we can easily count a cell's live neighbors withaddition.

Next, let's define the universe. The universe has a width and a height, and avector of cells of length width * height.

  1. # #![allow(unused_variables)]
  2. #fn main() {
  3. #[wasm_bindgen]
  4. pub struct Universe {
  5. width: u32,
  6. height: u32,
  7. cells: Vec<Cell>,
  8. }
  9. #}

To access the cell at a given row and column, we translate the row and columninto an index into the cells vector, as described earlier:

  1. # #![allow(unused_variables)]
  2. #fn main() {
  3. impl Universe {
  4. fn get_index(&self, row: u32, column: u32) -> usize {
  5. (row * self.width + column) as usize
  6. }
  7. // ...
  8. }
  9. #}

In order to calculate the next state of a cell, we need to get a count of howmany of its neighbors are alive. Let's write a live_neighbor_count method todo just that!

  1. # #![allow(unused_variables)]
  2. #fn main() {
  3. impl Universe {
  4. // ...
  5. fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {
  6. let mut count = 0;
  7. for delta_row in [self.height - 1, 0, 1].iter().cloned() {
  8. for delta_col in [self.width - 1, 0, 1].iter().cloned() {
  9. if delta_row == 0 && delta_col == 0 {
  10. continue;
  11. }
  12. let neighbor_row = (row + delta_row) % self.height;
  13. let neighbor_col = (column + delta_col) % self.width;
  14. let idx = self.get_index(neighbor_row, neighbor_col);
  15. count += self.cells[idx] as u8;
  16. }
  17. }
  18. count
  19. }
  20. }
  21. #}

The liveneighbor_count method uses deltas and modulo to avoid special casingthe edges of the universe with ifs. When applying a delta of -1, we _addself.height - 1 and let the modulo do its thing, rather than attempting tosubtract 1. row and column can be 0, and if we attempted to subtract 1from them, there would be an unsigned integer underflow.

Now we have everything we need to compute the next generation from the currentone! Each of the Game's rules follows a straightforward translation into acondition on a match expression. Additionally, because we want JavaScript tocontrol when ticks happen, we will put this method inside a #[wasm_bindgen]block, so that it gets exposed to JavaScript.

  1. # #![allow(unused_variables)]
  2. #fn main() {
  3. /// Public methods, exported to JavaScript.
  4. #[wasm_bindgen]
  5. impl Universe {
  6. pub fn tick(&mut self) {
  7. let mut next = self.cells.clone();
  8. for row in 0..self.height {
  9. for col in 0..self.width {
  10. let idx = self.get_index(row, col);
  11. let cell = self.cells[idx];
  12. let live_neighbors = self.live_neighbor_count(row, col);
  13. let next_cell = match (cell, live_neighbors) {
  14. // Rule 1: Any live cell with fewer than two live neighbours
  15. // dies, as if caused by underpopulation.
  16. (Cell::Alive, x) if x < 2 => Cell::Dead,
  17. // Rule 2: Any live cell with two or three live neighbours
  18. // lives on to the next generation.
  19. (Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
  20. // Rule 3: Any live cell with more than three live
  21. // neighbours dies, as if by overpopulation.
  22. (Cell::Alive, x) if x > 3 => Cell::Dead,
  23. // Rule 4: Any dead cell with exactly three live neighbours
  24. // becomes a live cell, as if by reproduction.
  25. (Cell::Dead, 3) => Cell::Alive,
  26. // All other cells remain in the same state.
  27. (otherwise, _) => otherwise,
  28. };
  29. next[idx] = next_cell;
  30. }
  31. }
  32. self.cells = next;
  33. }
  34. // ...
  35. }
  36. #}

So far, the state of the universe is represented as a vector of cells. To makethis human readable, let's implement a basic text renderer. The idea is to writethe universe line by line as text, and for each cell that is alive, print theUnicode character ("black medium square"). For dead cells, we'll print (a "white medium square").

By implementing the Display trait from Rust's standard library, we can add away to format a structure in a user-facing manner. This will also automaticallygive us a to_string method.

  1. # #![allow(unused_variables)]
  2. #fn main() {
  3. use std::fmt;
  4. impl fmt::Display for Universe {
  5. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  6. for line in self.cells.as_slice().chunks(self.width as usize) {
  7. for &cell in line {
  8. let symbol = if cell == Cell::Dead { '◻' } else { '◼' };
  9. write!(f, "{}", symbol)?;
  10. }
  11. write!(f, "\n")?;
  12. }
  13. Ok(())
  14. }
  15. }
  16. #}

Finally, we define a constructor that initializes the universe with aninteresting pattern of live and dead cells, as well as a render method:

  1. # #![allow(unused_variables)]
  2. #fn main() {
  3. /// Public methods, exported to JavaScript.
  4. #[wasm_bindgen]
  5. impl Universe {
  6. // ...
  7. pub fn new() -> Universe {
  8. let width = 64;
  9. let height = 64;
  10. let cells = (0..width * height)
  11. .map(|i| {
  12. if i % 2 == 0 || i % 7 == 0 {
  13. Cell::Alive
  14. } else {
  15. Cell::Dead
  16. }
  17. })
  18. .collect();
  19. Universe {
  20. width,
  21. height,
  22. cells,
  23. }
  24. }
  25. pub fn render(&self) -> String {
  26. self.to_string()
  27. }
  28. }
  29. #}

With that, the Rust half of our Game of Life implementation is complete!

Recompile it to WebAssembly by running wasm-pack build within thewasm-game-of-life directory.

Rendering with JavaScript

First, let's add a <pre> element to wasm-game-of-life/www/index.html torender the universe into, just above the <script> tag:

  1. <body>
  2. <pre id="game-of-life-canvas"></pre>
  3. <script src="./bootstrap.js"></script>
  4. </body>

Additionally, we want the <pre> centered in the middle of the Web page. We canuse CSS flex boxes to accomplish this task. Add the following <style> taginside wasm-game-of-life/www/index.html's <head>:

  1. <style>
  2. body {
  3. position: absolute;
  4. top: 0;
  5. left: 0;
  6. width: 100%;
  7. height: 100%;
  8. display: flex;
  9. flex-direction: column;
  10. align-items: center;
  11. justify-content: center;
  12. }
  13. </style>

At the top of wasm-game-of-life/www/index.js, let's fix our import to bring inthe Universe rather than the old greet function:

  1. import { Universe } from "wasm-game-of-life";

Also, let's get that <pre> element we just added and instantiate a newuniverse:

  1. const pre = document.getElementById("game-of-life-canvas");
  2. const universe = Universe.new();

The JavaScript runs in a requestAnimationFrameloop. On each iteration, it draws the current universeto the <pre>, and then calls Universe::tick.

  1. const renderLoop = () => {
  2. pre.textContent = universe.render();
  3. universe.tick();
  4. requestAnimationFrame(renderLoop);
  5. };

To start the rendering process, all we have to do is make the initial call forthe first iteration of the rendering loop:

  1. requestAnimationFrame(renderLoop);

Make sure your development server is still running (run npm run start insidewasm-game-of-life/www) and this is whathttp://localhost:8080/ should look like:

Screenshot of the Game of Life implementation with text rendering

Rendering to Canvas Directly from Memory

Generating (and allocating) a String in Rust and then having wasm-bindgenconvert it to a valid JavaScript string makes unnecessary copies of theuniverse's cells. As the JavaScript code already knows the width andheight of the universe, and can read WebAssembly's linear memory that make upthe cells directly, we'll modify the render method to return a pointer to thestart of the cells array.

Also, instead of rendering Unicode text, we'll switch to using the CanvasAPI. We will use this design in the rest of the tutorial.

Inside wasm-game-of-life/www/index.html, let's replace the <pre> we addedearlier with a <canvas> we will render into (it too should be within the<body>, before the <script> that loads our JavaScript):

  1. <body>
  2. <canvas id="game-of-life-canvas"></canvas>
  3. <script src='./bootstrap.js'></script>
  4. </body>

To get the necessary information from the Rust implementation, we'll need to addsome more getter functions for a universe's width, height, and pointer to itscells array. All of these are exposed to JavaScript as well. Make theseadditions to wasm-game-of-life/src/lib.rs:

  1. # #![allow(unused_variables)]
  2. #fn main() {
  3. /// Public methods, exported to JavaScript.
  4. #[wasm_bindgen]
  5. impl Universe {
  6. // ...
  7. pub fn width(&self) -> u32 {
  8. self.width
  9. }
  10. pub fn height(&self) -> u32 {
  11. self.height
  12. }
  13. pub fn cells(&self) -> *const Cell {
  14. self.cells.as_ptr()
  15. }
  16. }
  17. #}

Next, in wasm-game-of-life/www/index.js, let's also import Cell fromwasm-game-of-life, and define some constants that we will use when renderingto the canvas:

  1. import { Universe, Cell } from "wasm-game-of-life";
  2. const CELL_SIZE = 5; // px
  3. const GRID_COLOR = "#CCCCCC";
  4. const DEAD_COLOR = "#FFFFFF";
  5. const ALIVE_COLOR = "#000000";

Now, let's rewrite the rest of this JavaScript code to no longer write to the<pre>'s textContent but instead draw to the <canvas>:

  1. // Construct the universe, and get its width and height.
  2. const universe = Universe.new();
  3. const width = universe.width();
  4. const height = universe.height();
  5. // Give the canvas room for all of our cells and a 1px border
  6. // around each of them.
  7. const canvas = document.getElementById("game-of-life-canvas");
  8. canvas.height = (CELL_SIZE + 1) * height + 1;
  9. canvas.width = (CELL_SIZE + 1) * width + 1;
  10. const ctx = canvas.getContext('2d');
  11. const renderLoop = () => {
  12. universe.tick();
  13. drawGrid();
  14. drawCells();
  15. requestAnimationFrame(renderLoop);
  16. };

To draw the grid between cells, we draw a set of equally-spaced horizontallines, and a set of equally-spaced vertical lines. These lines criss-cross toform the grid.

  1. const drawGrid = () => {
  2. ctx.beginPath();
  3. ctx.strokeStyle = GRID_COLOR;
  4. // Vertical lines.
  5. for (let i = 0; i <= width; i++) {
  6. ctx.moveTo(i * (CELL_SIZE + 1) + 1, 0);
  7. ctx.lineTo(i * (CELL_SIZE + 1) + 1, (CELL_SIZE + 1) * height + 1);
  8. }
  9. // Horizontal lines.
  10. for (let j = 0; j <= height; j++) {
  11. ctx.moveTo(0, j * (CELL_SIZE + 1) + 1);
  12. ctx.lineTo((CELL_SIZE + 1) * width + 1, j * (CELL_SIZE + 1) + 1);
  13. }
  14. ctx.stroke();
  15. };

We can directly access WebAssembly's linear memory via memory, which isdefined in the raw wasm module wasm_game_of_life_bg. To draw the cells, weget a pointer to the universe's cells, construct a Uint8Array overlaying thecells buffer, iterate over each cell, and draw a white or black rectangledepending on whether the cell is dead or alive, respectively. By working withpointers and overlays, we avoid copying the cells across the boundary on everytick.

  1. // Import the WebAssembly memory at the top of the file.
  2. import { memory } from "wasm-game-of-life/wasm_game_of_life_bg";
  3. // ...
  4. const getIndex = (row, column) => {
  5. return row * width + column;
  6. };
  7. const drawCells = () => {
  8. const cellsPtr = universe.cells();
  9. const cells = new Uint8Array(memory.buffer, cellsPtr, width * height);
  10. ctx.beginPath();
  11. for (let row = 0; row < height; row++) {
  12. for (let col = 0; col < width; col++) {
  13. const idx = getIndex(row, col);
  14. ctx.fillStyle = cells[idx] === Cell.Dead
  15. ? DEAD_COLOR
  16. : ALIVE_COLOR;
  17. ctx.fillRect(
  18. col * (CELL_SIZE + 1) + 1,
  19. row * (CELL_SIZE + 1) + 1,
  20. CELL_SIZE,
  21. CELL_SIZE
  22. );
  23. }
  24. }
  25. ctx.stroke();
  26. };

To start the rendering process, we'll use the same code as above to start thefirst iteration of the rendering loop:

  1. drawGrid();
  2. drawCells();
  3. requestAnimationFrame(renderLoop);

Note that we call drawGrid() and drawCells() here before we callrequestAnimationFrame(). The reason we do this is so that the initial stateof the universe is drawn before we make modifications. If we instead simplycalled requestAnimationFrame(renderLoop), we'd end up with a situation wherethe first frame that was drawn would actually be after the first call touniverse.tick(), which is the second "tick" of the life of these cells.

It Works!

Rebuild the WebAssembly and bindings glue by running this command from withinthe root wasm-game-of-life directory:

  1. wasm-pack build

Make sure your development server is still running. If it isn't, start it againfrom within the wasm-game-of-life/www directory:

  1. npm run start

If you refresh http://localhost:8080/, you should begreeted with an exciting display of life!

Screenshot of the Game of Life implementation

As an aside, there is also a really neat algorithm for implementing the Game ofLife called hashlife. It usesaggressive memoizing and can actually get exponentially faster to computefuture generations the longer it runs! Given that, you might be wondering why wedidn't implement hashlife in this tutorial. It is out of scope for this text,where we are focusing on Rust and WebAssembly integration, but we highlyencourage you to go learn about hashlife on your own!

Exercises

  • Initialize the universe with a single space ship.

  • Instead of hard-coding the initial universe, generate a random one, where eachcell has a fifty-fifty chance of being alive or dead.

Hint: use the js-sys crate to importthe Math.random JavaScriptfunction.

Answer First, add js-sys as a dependency in wasm-game-of-life/Cargo.toml:

  1. # ...
  2. [dependencies]
  3. js-sys = "0.3"
  4. # ...

Then, use the js_sys::Math::random function to flip a coin:

  1. # #![allow(unused_variables)]
  2. #fn main() {
  3. extern crate js_sys;
  4. // ...
  5. if js_sys::Math::random() < 0.5 {
  6. // Alive...
  7. } else {
  8. // Dead...
  9. }
  10. #}
  • Representing each cell with a byte makes iterating over cells easy, but itcomes at the cost of wasting memory. Each byte is eight bits, but we onlyrequire a single bit to represent whether each cell is alive or dead. Refactorthe data representation so that each cell uses only a single bit of space.

Answer

In Rust, you can use the fixedbitset crate and its FixedBitSettype to represent cells instead ofVec<Cell>:

  1. # #![allow(unused_variables)]
  2. #fn main() {
  3. // Make sure you also added the dependency to Cargo.toml!
  4. extern crate fixedbitset;
  5. use fixedbitset::FixedBitSet;
  6. // ...
  7. #[wasm_bindgen]
  8. pub struct Universe {
  9. width: u32,
  10. height: u32,
  11. cells: FixedBitSet,
  12. }
  13. #}

The Universe constructor can be adjusted the following way:

  1. # #![allow(unused_variables)]
  2. #fn main() {
  3. pub fn new() -> Universe {
  4. let width = 64;
  5. let height = 64;
  6. let size = (width * height) as usize;
  7. let mut cells = FixedBitSet::with_capacity(size);
  8. for i in 0..size {
  9. cells.set(i, i % 2 == 0 || i % 7 == 0);
  10. }
  11. Universe {
  12. width,
  13. height,
  14. cells,
  15. }
  16. }
  17. #}

To update a cell in the next tick of the universe, we use the set methodof FixedBitSet:

  1. # #![allow(unused_variables)]
  2. #fn main() {
  3. next.set(idx, match (cell, live_neighbors) {
  4. (true, x) if x < 2 => false,
  5. (true, 2) | (true, 3) => true,
  6. (true, x) if x > 3 => false,
  7. (false, 3) => true,
  8. (otherwise, _) => otherwise
  9. });
  10. #}

To pass a pointer to the start of the bits to JavaScript, you can convertthe FixedBitSet to a slice and then convert the slice to a pointer:

  1. # #![allow(unused_variables)]
  2. #fn main() {
  3. #[wasm_bindgen]
  4. impl Universe {
  5. // ...
  6. pub fn cells(&self) -> *const u32 {
  7. self.cells.as_slice().as_ptr()
  8. }
  9. }
  10. #}

In JavaScript, constructing a Uint8Array from Wasm memory is the same asbefore, except that the length of the array is not width height anymore,but width height / 8 since we have a cell per bit rather than per byte:

  1. const cells = new Uint8Array(memory.buffer, cellsPtr, width * height / 8);

Given an index and Uint8Array, you can determine whether thenth bit is set with the following function:

  1. const bitIsSet = (n, arr) => {
  2. const byte = Math.floor(n / 8);
  3. const mask = 1 << (n % 8);
  4. return (arr[byte] & mask) === mask;
  5. };

Given all that, the new version of drawCells looks like this:

  1. const drawCells = () => {
  2. const cellsPtr = universe.cells();
  3. // This is updated!
  4. const cells = new Uint8Array(memory.buffer, cellsPtr, width * height / 8);
  5. ctx.beginPath();
  6. for (let row = 0; row < height; row++) {
  7. for (let col = 0; col < width; col++) {
  8. const idx = getIndex(row, col);
  9. // This is updated!
  10. ctx.fillStyle = bitIsSet(idx, cells)
  11. ? ALIVE_COLOR
  12. : DEAD_COLOR;
  13. ctx.fillRect(
  14. col * (CELL_SIZE + 1) + 1,
  15. row * (CELL_SIZE + 1) + 1,
  16. CELL_SIZE,
  17. CELL_SIZE
  18. );
  19. }
  20. }
  21. ctx.stroke();
  22. };