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 infinite memory and compute power. Working around this rather annoying limitation usually comes 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 is unbounded and the implementation will get slower and slower and eventually run out of memory.
-
Create a fixed-size universe, where cells on the edges have fewer neighbors than cells in the middle. The downside with this approach is that infinite patterns, like gliders, that reach the end of the universe are snuffed out.
-
Create a fixed-size, periodic universe, where cells on the edges have neighbors that wrap around to the other side of the universe. Because neighbors wrap around the edges of the universe, gliders can keep running forever.
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 Object
s, Array
s, and DOM nodes are allocated — is distinct from WebAssembly's linear memory space, where our Rust values live. WebAssembly currently has no direct access to the garbage-collected heap (as of April 2018, this is expected to change with the "Interface Types" proposal). JavaScript, on the other hand, can read and write to the WebAssembly linear memory space, but only as an ArrayBuffer
of scalar values (u8
, i32
, f64
, etc...). WebAssembly functions also take and return scalar values. These are the building blocks from which all WebAssembly and JavaScript communication is constituted.
wasm_bindgen
defines a common understanding of how to work with compound structures across this boundary. It involves boxing Rust structures, and wrapping the pointer in a JavaScript class for usability, or indexing into a table of JavaScript objects from Rust. wasm_bindgen
is very convenient, but it does not remove the need to consider our data representation, and what values and structures are passed across this boundary. Instead, think of it as a tool for implementing the interface design you choose.
When designing an interface between WebAssembly and JavaScript, we want to optimize 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, serializing and deserializing also imposes overhead, and often imposes copying as well. If we can pass opaque handles to a data structure — instead of serializing it on one side, copying it into some known location in the WebAssembly linear memory, and deserializing on the other side — we can often reduce a lot of overhead.
wasm_bindgen
helps us define and work with opaque handles to JavaScriptObject
s or boxed Rust structures.
As a general rule of thumb, a good JavaScript↔WebAssembly interface design is often one where large, long-lived data structures are implemented as Rust types that live in the WebAssembly linear memory, and are exposed to JavaScript as opaque handles. JavaScript calls exported WebAssembly functions that take these opaque handles, transform their data, perform heavy computations, query the data, and ultimately return a small, copy-able result. By only returning the small result of the computation, we avoid copying and/or serializing everything back and forth between the JavaScript garbage-collected heap and the WebAssembly linear 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 the whole universe into and out of the WebAssembly linear memory on every tick. We do not want to allocate objects for every cell in the universe, nor do we want to 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 that lives in the WebAssembly linear memory, and has a byte for each cell. 0
is a dead cell and 1
is a live cell.
Here is what a 4 by 4 universe looks like in memory:
To find the array index of the cell at a given row and column in the universe, we can use this formula:
1 |
|
We have several ways of exposing the universe's cells to JavaScript. To begin, we will implement [std::fmt::Display
][Display
] for Universe
, which we can use to generate a Rust String
of the cells rendered as text characters. This Rust String is then copied from the WebAssembly linear memory into a JavaScript String in the JavaScript's garbage-collected heap, and is then displayed by setting HTML textContent
. Later in the chapter, we'll evolve this implementation to avoid copying the universe's cells between heaps and to render to <canvas>
.
Another viable design alternative would be for Rust to return a list of every cell that changed states after each tick, instead of exposing the whole universe to JavaScript. This way, JavaScript wouldn't need to iterate over the whole universe when rendering, only the relevant subset. The trade off is that this delta-based design is slightly more difficult to implement.
Rust Implementation¶
In the last chapter, we cloned an initial project template. We will modify that project template now.
Let's begin by removing the alert
import and greet
function from wasm-game-of-life/src/lib.rs
, and replacing them with a type definition for cells:
1 2 3 4 5 6 7 |
|
It is important that we have #[repr(u8)]
, so that each cell is represented as a single byte. It is also important that the Dead
variant is 0
and that the Alive
variant is 1
, so that we can easily count a cell's live neighbors with addition.
Next, let's define the universe. The universe has a width and a height, and a vector of cells of length width * height
.
1 2 3 4 5 6 |
|
To access the cell at a given row and column, we translate the row and column into an index into the cells vector, as described earlier:
1 2 3 4 5 6 7 |
|
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!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
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! Each of the Game's rules follows a straightforward translation into a condition on a match
expression. Additionally, because we want JavaScript to control when ticks happen, we will put this method inside a #[wasm_bindgen]
block, so that it gets exposed to JavaScript.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
So far, 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").
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Finally, we define a constructor that initializes the universe with an interesting pattern of live and dead cells, as well as a render
method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
With that, the Rust half of our Game of Life implementation is complete!
Recompile it to WebAssembly by running wasm-pack build
within the wasm-game-of-life
directory.
Rendering with JavaScript¶
First, let's add a <pre>
element to wasm-game-of-life/www/index.html
to render the universe into, just above the <script>
tag:
1 2 3 4 |
|
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 wasm-game-of-life/www/index.html
's <head>
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
At the top of wasm-game-of-life/www/index.js
, let's fix our import to bring in the Universe
rather than the old greet
function:
1 |
|
Also, let's get that <pre>
element we just added and instantiate a new universe:
1 2 |
|
The JavaScript runs in a requestAnimationFrame
loop. On each iteration, it draws the current universe to the <pre>
, and then calls Universe::tick
.
1 2 3 4 5 6 |
|
To start the rendering process, all we have to do is make the initial call for the first iteration of the rendering loop:
1 |
|
Make sure your development server is still running (run npm run start
inside wasm-game-of-life/www
) and this is what http://localhost:8080/ should look like:
Rendering to Canvas Directly from Memory¶
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. As the JavaScript code already knows the width and height of the universe, and can read WebAssembly's linear memory that make up the cells directly, we'll modify the render
method to return a pointer to the start of the cells array.
Also, instead of rendering Unicode text, we'll switch to using the Canvas API. 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 added earlier with a <canvas>
we will render into (it too should be within the <body>
, before the <script>
that loads our JavaScript):
1 2 3 4 |
|
To get the necessary information from the Rust implementation, we'll need to add some more getter functions for a universe's width, height, and pointer to its cells array. All of these are exposed to JavaScript as well. Make these additions to wasm-game-of-life/src/lib.rs
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Next, in wasm-game-of-life/www/index.js
, let's also import Cell
from wasm-game-of-life
, and define some constants that we will use when rendering to the canvas:
1 2 3 4 5 6 |
|
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
We can directly access WebAssembly's linear memory via memory
, which is defined in the raw wasm module wasm_game_of_life_bg
. To draw the cells, we get a pointer to the universe's cells, 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
To start the rendering process, we'll use the same code as above to start the first iteration of the rendering loop:
1 2 3 |
|
Note that we call drawGrid()
and drawCells()
here before we call requestAnimationFrame()
. The reason we do this is so that the initial state of the universe is drawn before we make modifications. If we instead simply called requestAnimationFrame(renderLoop)
, we'd end up with a situation where the first frame that was drawn would actually be after the first call to universe.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 within the root wasm-game-of-life
directory:
1 |
|
Make sure your development server is still running. If it isn't, start it again from within the wasm-game-of-life/www
directory:
1 |
|
If you refresh http://localhost:8080/, you should be greeted with an exciting display of life!
As an aside, there is also a really neat algorithm for implementing the Game of Life called hashlife. It uses aggressive memoizing and can actually get exponentially faster to compute future generations the longer it runs! Given that, you might be wondering why we didn'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 highly encourage 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 each cell has a fifty-fifty chance of being alive or dead.
Hint: use the js-sys
crate to import the Math.random
JavaScript function.
Answer
*First, add `js-sys` as a dependency in `wasm-game-of-life/Cargo.toml`:*1 2 3 4 |
|
1 2 3 4 5 6 7 8 9 |
|
- 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.
Answer
In Rust, you can use [the `fixedbitset` crate and its `FixedBitSet` type](https://crates.io/crates/fixedbitset) to represent cells instead of `Vec1 2 3 4 5 6 7 8 9 10 11 12 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
1 2 3 4 5 6 7 |
|
1 2 3 4 5 6 7 8 |
|
1 |
|
1 2 3 4 5 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|