Originally I planned to write about WASI (that's why LLVM logo is in background). It is system interface for WebAssembly. Basically a means to run wasm files without browser and give them access to system stuff. But it turned out that it was kind of boring topic to write about. Just use wasmtime (which I could not compile) or wasmer (which I ran successfully with two commands) and execute wasm files if you desire so.

But today we are going to explore the paths of AssemblyScript and WebAssembly. We are going to learn how to compile TypeScript into WebAssembly and also compare performance of compiled JavaScript and WebAssembly by implementing Game of Life.  We will use Ubuntu 18.04 as our OS and webpack as our module bundler.

tl;dr here is codepen with examples:

Run the pen and you will see that WebAssembly gives around 60 FPS, whereas JS gives only 32 FPS. You can also click and drag to add extra chaos to the field.

Alright, back to the tutorial. Let's start with dependencies. You can install latest node using nvm. Then initialize new repo by adding package.json:

{
  "name": "assemblyscript-game-of-life",
  "version": "1.0.0",
  "description": "",
  "main": "src/index.js",
  "scripts": {
    "start": "webpack-dev-server",
    "build": "npm run asbuild && webpack-cli",
    "asbuild": "asc assembly/index.ts -b compiled/gameoflife.wasm --validate --optimize --importMemory --use Math=JSMath"
  },
  "license": "ISC",
  "devDependencies": {
    "assemblyscript": "github:AssemblyScript/assemblyscript",
    "copy-webpack-plugin": "^5.0.4",
    "wasm-loader": "^1.3.0",
    "webpack": "^4.38.0",
    "webpack-cli": "^3.3.6",
    "webpack-dev-server": "^3.7.2"
  },
  "dependencies": {}
}

Here we see start script which is used for developing the application, it opens webpack-dev-server on port 9000. Also we see two build scripts, first is build and it tells webpack to compile it's bundle.js, second is asbuild which tells AssemblyScript to compile ts file into wasm file. Let's stop for a minute on the second script. It has couple of arguments. Interesting ones are importMemory and use Math. Former will allow us to import memory object and use it inside and outside wasm file, latter is needed to import Math object.

Now you can run npm install to install dependencies. Create files assembly/index.ts and src/index.ts. Also create following webpack.config.js:

const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  devtool: 'source-map',
  resolve: {
    extensions: [ '.js' ]
  },
  module: {
    defaultRules: [
      {
        type: "javascript/auto",
        resolve: {}
      },
      {
        test: /\.json$/i,
        type: "json"
      }
    ],
    rules: [
      {
        test: /\.wasm$/,
        use: 'wasm-loader'
      }
    ],
  },
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new CopyWebpackPlugin([{ from: path.resolve(__dirname, 'index.html'), to: path.resolve(__dirname, 'dist') }])
  ],
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    compress: true,
    port: 9000
  }
};

defaultRules is shamelessly copy-pasted from this github issue. I have no idea how that works but that's not the point. The point is: we can now import wasm files!

But let's start simple. Create index.html:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Game of Life</title>
  </head>
  <body>
  <script type="text/javascript" src="bundle.js"></script>
  </body>
</html>

In src/index.js write:

import GameOfLife from '../compiled/gameoflife.wasm'

async function start() {
  const memory = new WebAssembly.Memory({ initial: 256 })
  const importObject = {
    env: {
      abort: () => {},
      memory
    },
    imports: { f }
  }

  const game = await GameOfLife(importObject)
  const { add } = game.instance.exports

  function f(x) {
    console.log('f', x)
  }

  console.log(add(12, 30))
}

start()

To be sure, f is a bad name for a function. But on each step of building this project it helped me so much that it grew on me and I don't want to change it now. Now for assembly/index.ts:

@external('imports', 'f')
declare function f(x: i32): void

f(1)

export function add(x: i32, y: i32): i32 {
  return x + y
}
This is the file structure we will be using for our project

The moment of truth: run npm run asbuild, then npm start and open localhost:9000 in your favorite (not IE) browser. Open developers tools (F12 in chrome) and look at the result.

Glorious

The reason this result is important is that we were able to use core mechanics like import function f from js to wasm and run that function, export and run function add from wasm and also load wasm file using wasm-loader.

Do you like modules? I do! Let's extract index.js as a separate GameOfLifeWebAssembly.js module. Create a new file with:

import GameOfLife from '../compiled/gameoflife.wasm'

export default async () => {
  const memory = new WebAssembly.Memory({ initial: 256 })
  const importObject = {
    env: {
      abort: () => {},
      memory
    },
    imports: { f }
  }

  const game = await GameOfLife(importObject)
  const { add } = game.instance.exports

  function f(x) {
    console.log('f', x)
  }

  return { add }
}

And also introduce Main module in index.js:

import GameOfLifeWebAssembly from './GameOfLifeWebAssembly'

async function Main() {
  const game = await GameOfLifeWebAssembly()
  console.log(game.add(12, 30))
}

window.addEventListener('load', Main)

You can run the application again to make sure it produces the same output as before the refactoring. Now add a canvas to your index.html <body>:

<canvas id="canvas"></canvas>

We will need this canvas to display alive and dead cells. 1 cell = 1 pixel on the canvas. Some js code will be needed to display Uint32Array on the canvas. In your index.js:

  const width = 100
  const height = 100

  const canvas = document.getElementById('canvas')
  canvas.width  = width
  canvas.height = height

  const context = canvas.getContext('2d')
  const imageData = context.createImageData(width, height)
  const imageDataView = new Uint32Array(imageData.data.buffer)

  imageDataView[0] = 0xFF000000

  context.putImageData(imageData, 0, 0)

We create new ImageData and set first value to 0xFF000000. uint32 stands for 32 bit unsigned integer. Unsigned means that first bit does not indicate its sign, so every uint32 ranges from 0 to 2^32 - 1 = 4294967295. Each uint32 in array represents a pixel on the canvas and is in format ABGR which stands for Alpha, Blue, Green, Red. Alpha channel is the transparency of the color, where 0xFF means opaque. To sum it up we get an opaque black pixel. Open localhost:9000 and look at the black pixel in the top left of your screen.

Calling external js functions from wasm is slower than writing and reading shared memory. From js side we will need a memory view in GameOfLifeWebAssembly.js:

const memory = new WebAssembly.Memory({ initial: 256 })
const memoryView = new Uint32Array(memory.buffer)
...
return { memoryView }

From wasm side we will need functions store<u32> and load<u32> in index.ts:

@external('imports', 'width')
declare const WIDTH: i32

@external('imports', 'height')
declare const HEIGHT: i32

@external('imports', 'f')
declare function f(x: i32): void

const BLACK = 0xFF000000
const WHITE = 0

function set(x: i32, y: i32): void {
  store<u32>(toPointer(x, y), BLACK)
}

function toPointer(x: i32, y: i32): i32 {
  return (y * WIDTH + x) * 4
}

set(0, 0)
set(0, 1)

Don't forget to import width and height. Also in index.js we will need to copy data from shared memory to canvas. To do this try:

  const imageDataView = new Uint32Array(imageData.data.buffer)

  imageDataView.set(game.memoryView.slice(0, width * height))

  context.putImageData(imageData, 0, 0)

Now open localhost:9000 and look carefully. You should be able to spot 2 black pixels in the top left corner.

Now grab index.ts from my github repo, modify GameOfLifeWebAssembly.js like that:

import GameOfLife from '../compiled/gameoflife.wasm'

export default async (width, height) => {
  const memory = new WebAssembly.Memory({ initial: 256 })
  const memoryView = new Uint32Array(memory.buffer)
  const importObject = {
    env: {
      abort: () => {},
      memory
    },
    imports: { f, width, height },
    Math
  }

  const game = await GameOfLife(importObject)

  console.log(game)
  const { randomize, step } = game.instance.exports

  function f(x) {
    console.log('f', x)
  }

  return { randomize, step, memoryView }
}

To add game loop we will need requestAnimationFrame function. In index.js:

  game.randomize()

  function cycle() {
    game.step()
    imageDataView.set(game.memoryView.slice(0, width * height))
    context.putImageData(imageData, 0, 0)
    requestAnimationFrame(cycle)
  }

  cycle()

Run npm run build. Voila! You have 100x100 working Game of Life on localhost:9000.

Also to further address performance questions, I added a simple benchmark in my repo. I make game's step couple of times on randomized board and measure milliseconds it takes to complete. Results are:

JS takes twice as much time as WebAssembly to calculate. This benchmark is representative because rendering on canvas is identical for JS and WebAssembly code.

Final version with JS/WebAssembly comparison and also painting feature is available on my github.

Fun fact: before @inline-ing some functions in wasm its FPS was comparatively lower than FPS of JS code.

Important note: official AssemblyScript repository has Game of Life already implemented. Here is their source code. I learned some mechanics by reading this code. There are other tutorials that helped.

P.S. Look at this cool demo with simulation of the wave equation.