In this codelab, you'll learn about Web Assembly - a new cross-browser, portable assembly and binary format for the web. You'll learn how to take native code—in C—build it to the Web Assembly format, and then call it directly from JavaScript on any webpage.

What you'll need

What you'll build

Keep in mind that you don't need Emscripten to build Web Assembly, but it greatly simplifies the process—just like regular hardware assembly, Web Assembly can be tough to work with by hand.

If you'd like to learn more about Web Assembly, be sure to check out Alex Danilo's Web Assembly talk (recorded on the 1st day of I/O 2017).

First, download the code for this codelab here- https://github.com/googlecodelabs/web-assembly-introduction/archive/master.zip

Next, you'll need to set up the Emscripten SDK. This tool leverages clang to take C/C++ code, convert it to LLVM bitcode, and finally release in the Web Assembly format. (Emscripten can also output pure JavaScript, but Web Assembly represents a step up for speed and size).

Emscripten at Google I/O 2017

If you're onsite at Google I/O 2017, then Emscripten is already available on your machine—open a Terminal window, and type emcc. You should see something like:

codelab@kiosk ~ $ emcc
WARNING:root:no input files

That's great! You haven't told emcc to compile anything, but the tool exists. If you see this, then please skip to the next page.

Download and install the Emscripten SDK

Run the following commands inside a console to fetch and configure the Emscripten SDK. The SDK differs from Emscripten in that it embeds its own versions of Clang, Node, Python etc—to avoid compatibility problems with the system.

Your development environment of choice will need its standard developer tools installed already.

git clone https://github.com/juj/emsdk.git
cd emsdk
./emsdk install sdk-incoming-64bit binaryen-master-64bit
./emsdk activate sdk-incoming-64bit binaryen-master-64bit

Warning: Emscripten won't compile if it's in a path with spaces. This is probably a bug, but avoid it for now.

This will take a couple of minutes to fetch and build. Once it's done, you can run this command to set up your environment for the current console:

source ./emsdk_env.sh

You should now be able to run the emcc command, where it will report no files specified. Hooray!

For your first trick, you're going to build a simplified version of "Conway's Game of Life" and interact with it via HTML and JavaScript.

We'll use Emscripten—it is a compiler, but also provides a replacement standard library. The Game of Life, however, is purely computational work and doesn't use any standard library methods (e.g. malloc or free).

As we don't need to include the standard library, the build and run process is simplified, and it's a conceptually easier place to start.

Examine the source

First, open up the lyff/lyff.c file—in the code you downloaded earlier—and take a look. It should look a bit like this:

// Steps through one iteration of Conway's Game of Life. Returns the number of now alive cells, or
// -1 if no cells changed this iteration: i.e., stable game.
int board_step() {
  int total_alive = 0;
  int change = 0;

  // place output in A/B board
  byte *next = (byte *) &boardA;
  if (board == next) {
    next = (byte *) &boardB;
  }
  clear_board_ref(next);
...

It provides a few methods, including board_init and board_step, to configure the game board and run a single iteration. It's an extremely simple implementation. It doesn't explicitly 'export' any methods to JavaScript, but you can do that as part of the build step.

Build the code

To build our code, run the following inside the console from the lyff directory:

emcc \
  -s WASM=1 -s ONLY_MY_CODE=1 -s EXPORTED_FUNCTIONS="['_board_init','_board_ref','_board_step']" \
  -o output.js *.c

(If your console can't find emcc, be sure you've run source ./emsdk_env.sh as described on the previous page)

What does this build step do? The first line tells emcc what to compile and where to output (although it will technically write the Web Assembly code to output.wasm).

The second line provides a number of required flags:

  1. WASM tells Emscripten to generate Web Assembly—this doesn't happen by default
  2. ONLY_MY_CODE only compiles your methods, and stops Emscripten including parts of its standard library
  3. Lastly, EXPORTED_FUNCTIONS lists the methods we'd like to access from JavaScript. Note that these names are from the source, with a prefixed underscore.

(You can also try emcc --help to find out more).

The build step will generate the output.wasm file, which is the binary Web Assembly file. In the next step, we'll interact with the exported methods via JavaScript.

Create a new file, index.html, as a sibling of lyff.c and output.wasm inside the lyff folder. We'll be using this to load the Web Assembly file.

Start by creating a basic HTML outline—copy the code below:

<!DOCTYPE html>
<html>
<head>
<script>

// Add code inside the script tag.

</script>
</head>
<body>

<canvas id="canvas" style="image-rendering: pixelated; border: 2px solid blue;"></canvas>

</body>
</html>

Web Assembly loader

Next, let's insert a standard Web Assembly loader inside the script tag. Web Assembly is only supported by modern, evergreen browsers—so you can use ES6 features such as async, await and Promises:

  async function createWebAssembly(path, importObject) {
    const bytes = await window.fetch(path).then(x => x.arrayBuffer());
    return WebAssembly.instantiate(bytes, importObject);
  }

This method accepts a path, which we fetch, and then pass its contents to WebAssembly.instantiate.

The Environment

We also need to specify an importObject: this provides the environment Web Assembly runs in as well as any other parameters to instantiation. At a minimum, you need to provide an object like this—add it at the end your script tag:

  const memory = new WebAssembly.Memory({initial: 256, maximum: 256});
  const env = {
    'abortStackOverflow': _ => { throw new Error('overflow'); },
    'table': new WebAssembly.Table({initial: 0, maximum: 0, element: 'anyfunc'}),
    'tableBase': 0,
    'memory': memory,
    'memoryBase': 1024,
    'STACKTOP': 0,
    'STACK_MAX': memory.buffer.byteLength,
  };
  const importObject = {env};

This environment mostly configures the memory available to Web Assembly. You create a WebAssembly.Memory object that provides 256 pages of memory—each page is 65k, so we're giving the code 16mb of total RAM.

There's also an abortStackOverflow method to be called if Web Assembly runs out of stack space. While you have to provide this, you probably never want it to be called.

Putting it together

Now, we can pass the importObject to the createWebAssembly method, and do something with the output. Add this code at the end of the script tag:

  createWebAssembly('output.wasm', importObject).then(wa => {
    const exports = wa.instance.exports;
    console.info('got exports', exports);
    exports._board_init();  // setup lyff board

    // TODO: interact with lyff board

  }).catch(err => console.warn('err loading wasm', err));

You can load the .wasm file built earlier, and grab the exported methods—the ones you specified in EXPORTED_FUNCTIONS. Let's also call board_init, to set up the board.

Serve over HTTP

At this point, you should save the HTML file and load it up in a real browser! You'll need to run a HTTP server from the lyff directory, because Web Assembly scripts are loaded with CORS restrictions. Use serve or python -m SimpleHTTPServer.

Load up the page, and open the developer console—you should see a log of the exported methods. 🎆

This is all well and good, but let's now show the game's current state and perform 'steps'.

First, let's add a method—inside your HTML/JavaScript—that can render the current state of the board. To do this, we'll access the memory we previously gave to the Web Assembly Code.

But wait—where is the board inside the memory? The second exported method, board_ref, returns the pointer to the current board. The board is a 100x100 grid encoded as a 'bitset'—one bit per cell. Web Assembly can't natively return strings, or arrays to JavaScript client code, as they are real objects. You can add a JavaScript helper method to provide easier access.

At the TODO: interact you added on the previous page (inside the createWebAssembly callback), add this method and code:

  createWebAssembly('output.wasm', importObject).then(wa => {
    const exports = wa.instance.exports;
    console.info('got exports', exports);
    exports._board_init();  // setup lyff board

    function getBoardBuffer() {
      return new Uint8Array(memory.buffer, exports._board_ref());
    }
    function draw() {
      const buffer = getBoardBuffer();
      // TODO: render buffer
    }

    draw();

    // TODO: step through board

  }).catch(err => console.warn('err loading wasm', err));

Drawing the buffer

Replace the draw method above with this code:

    function draw() {
      const buffer = getBoardBuffer();

      const dim = 100;  // nb. fixed size
      canvas.width = canvas.height = dim + 2;
      canvas.style.width = canvas.style.height = `${dim*5}px`;
      const data = new ImageData(canvas.width, canvas.height);

      for (var x = 1; x <= dim; ++x) {
        for (var y = 1; y <= dim; ++y) {
          var pos = (y * (dim + 2)) + x;
          var i = (pos / 8) << 0;
          var off = 1 << (pos % 8);

          var alive = (buffer[i] & off);
          if (!alive) { continue; }

          const doff = (y * canvas.width + x) * 4;
          data.data[doff+0] = 255;
          data.data[doff+3] = 255;
        }
      }

      canvas.getContext('2d').putImageData(data, 0, 0)
    }

This reads the format encoded inside the buffer and renders it to a canvas. This format is outside the scope of the codelab, but you should feel free to read the C code or HTML in depth to understand the bit twiddling work it's doing!

If you save and load the index page now, you should see some red lines on the screen. This is the initial state of the Game of Life! Good work—the hardest part is over, and we're nearly there.

Step through the game

Finally, we need to provide a way for the user to step through the simulation. Let's make it so that every click on the canvas steps through one frame.

After the draw method is called for the first time, replace the TODO: step through board line with the following code:

    canvas.onclick = ev => {
      exports._board_step();
      draw();
    };

Save and reload. Every time you click on the canvas, you'll call board_step, and then draw a new frame. You've finished the first ½ of this codelab!

(If you'd like to see the solution, check out solution inside the lyff folder).

Extension tasks

In this codelab, you've skipped over some of the parts of the Web Assembly integration.

  1. The game state is fixed and is created by setting a number of bytes to 255 at the start of board_init. Instead, you could modify the HTML to allow configuration of the board itself—potentially by accessing its memory directly through board_ref.
  2. In an earlier version of this codelab, the Game of Life played forever by using requestAnimationFrame—rather than clicking a million times. Could you restore this behavior?

Going back to the 90s

One of the great thing about growing up as a geek in the early 90s was that we made pretty pictures using lots of CPU cycles. The most famous of these were the Mandelbrot and Julia Sets. You can see pictures of these wonders of iterative systems on Lode Vandevenne's page dedicated to all things Julia and Mandelbrot.

In the previous section we saw how we can build a small wasm file by writing code in pure C, and making sure not to utilise anything from the C standard library. On the flipside, if we do use the full power of Emscripten, you can:

In this step we will take Lode's Mandelbrot visualiser code, add in some code from StackOverflow to convert from HSV to RGB, then use Embind to make a chunk of memory we malloc'd visible in JavaScript as a typed array. We can then use JavaScript to take this typed array, and convert it into an ImageData object that we can render directly into a canvas element.

The first step is to decide on what sort of API we want to expose from our C++ code into JavaScript. In this step we are going to go simple. We will expose a single function call that takes the width and height of the viewport, the zoom level, and the real and imaginary location at the center of the viewport. The function will return Emscripten's val object, which represents a JavaScript value, in this case an array of bytes appropriately structured for ImageData.

mandelbrot.cpp

#include <emscripten/bind.h>
#include <cstddef>
#include <cstdlib>

// CSV to RGB conversion from http://stackoverflow.com/questions/3018313/
typedef struct
{
        double r; // a fraction between 0 and 1
        double g; // a fraction between 0 and 1
        double b; // a fraction between 0 and 1
} rgb;

typedef struct
{
        double h; // angle in degrees
        double s; // a fraction between 0 and 1
        double v; // a fraction between 0 and 1
} hsv;

static rgb hsv2rgb(hsv in);

rgb hsv2rgb(hsv in)
{
        double hh, p, q, t, ff;
        long i;
        rgb out;

        if (in.s <= 0.0)
        { // < is bogus, just shuts up warnings
                out.r = in.v;
                out.g = in.v;
                out.b = in.v;
                return out;
        }
        hh = in.h;
        if (hh >= 360.0)
                hh = 0.0;
        hh /= 60.0;
        i = (long)hh;
        ff = hh - i;
        p = in.v * (1.0 - in.s);
        q = in.v * (1.0 - (in.s * ff));
        t = in.v * (1.0 - (in.s * (1.0 - ff)));

        switch (i)
        {
        case 0:
                out.r = in.v;
                out.g = t;
                out.b = p;
                break;
        case 1:
                out.r = q;
                out.g = in.v;
                out.b = p;
                break;
        case 2:
                out.r = p;
                out.g = in.v;
                out.b = t;
                break;

        case 3:
                out.r = p;
                out.g = q;
                out.b = in.v;
                break;
        case 4:
                out.r = t;
                out.g = p;
                out.b = in.v;
                break;
        case 5:
        default:
                out.r = in.v;
                out.g = p;
                out.b = q;
                break;
        }
        return out;
}

uint8_t *buffer = nullptr;
size_t bufferSize = 0;

// Mandlebrot definition from http://lodev.org/cgtutor/juliamandelbrot.html

emscripten::val mandelbrot(int w, int h, double zoom, double moveX, double moveY)
{
        if (buffer != nullptr)
        {
                free(buffer);
        }

        // The image format that imageData expects is four unsigned bytes: red, green, blue, alpha
        bufferSize = w * h * 4;
        buffer = (uint8_t *)malloc(bufferSize);
        if (buffer == nullptr)
        {
                // Following the JavaScript idiom that undefined is error
                return emscripten::val::undefined();
        }

        // each iteration, it calculates: newz = oldz*oldz + p, where p is the current pixel, and oldz stars at the origin
        double pr, pi;                                                                                 // real and imaginary part of the pixel p
        double newRe, newIm, oldRe, oldIm; // real and imaginary parts of new and old z
        rgb color;                                                                                                 // the RGB color value for the pixel
        int maxIterations = 360;                                         // after how much iterations the function should stop. Chosen to make take up full HSV hue range.

        // loop through every pixel
        for (int y = 0; y < h; y++)
                for (int x = 0; x < w; x++)
                {
                        // calculate the initial real and imaginary part of z, based on the pixel location and zoom and position values
                        pr = 1.5 * (x - w / 2) / (0.5 * zoom * w) + moveX;
                        pi = (y - h / 2) / (0.5 * zoom * h) + moveY;
                        newRe = newIm = oldRe = oldIm = 0; //these should start at 0,0
                        // "i" will represent the number of iterations
                        int i;
                        // start the iteration process
                        for (i = 0; i < maxIterations; i++)
                        {
                                // remember value of previous iteration
                                oldRe = newRe;
                                oldIm = newIm;
                                // the actual iteration, the real and imaginary part are calculated
                                newRe = oldRe * oldRe - oldIm * oldIm + pr;
                                newIm = 2 * oldRe * oldIm + pi;
                                // if the point is outside the circle with radius 2: stop
                                if ((newRe * newRe + newIm * newIm) > 4)
                                        break;
                        }
                        // use color model conversion to get rainbow palette, make brightness black if maxIterations reached
                        hsv hsvColor;
                        hsvColor.h = i;
                        hsvColor.s = 1; // fully saturated.
                        hsvColor.v = i < maxIterations;
                        color = hsv2rgb(hsvColor);
                        //draw the pixel
                        size_t bufferOffset = (x + y * w) * 4;
                        buffer[bufferOffset + 0] = color.r * 255;
                        buffer[bufferOffset + 1] = color.g * 255;
                        buffer[bufferOffset + 2] = color.b * 255;
                        buffer[bufferOffset + 3] = 255;
                }

        return emscripten::val(emscripten::typed_memory_view(bufferSize, buffer));
}

EMSCRIPTEN_BINDINGS(hello)
{
        emscripten::function("mandelbrot", &mandelbrot);
}

We are utilising code from both StackOverflow, and from Lode's Mandelbrot viewer tutorial dating back to 2004. If you compare the code on the linked pages and the code above, the number of changes required to bring this code to the web is really quite minimal, which talks directly to WebAssembly's ability to enable repurposing a lot of native code.

There is one touch of macro magic in this file, right at the end. The EMSCRIPTEN_BINDINGS macro adds code into our generated wasm file that registers our mandelbrot function with the Emscripten run time, and thus makes it easily visible to JavaScript. The Embind function macro uses template magic to introspect at compile time the arguments and return type of the mandelbrot function we defined.

Next, we need to get the array of bytes generated in C++ onto the screen. In this sample we do this the most expedient way possible, by putting JavaScript directly into a HTML file. We don't recommend this approach for production code, but this is a tutorial focused on exploring new capabilities, and encouraging exploration. Onwards and upwards!

index.html

<!DOCTYPE html>
<html>

<head>
    <style>
        html,
        body {
            height: 100%;
            margin: 0;
            padding: 0;
        }

        .canvas {
            width: 100vw;
            height: 100vh;
        }
    </style>
</head>

<body>
    <canvas id="canvas" class="canvas"></canvas>
    <script src="mandelbrot.js"></script>
    <script>
        Module.addOnPostRun(() => {
            const canvas = document.getElementById('canvas');

            // Canvas resizing from http://stackoverflow.com/a/43364730/2142626
            const width = canvas.clientWidth;
            const height = canvas.clientHeight;
            if (canvas.width !== width || canvas.height !== height) {
                canvas.width = width;
                canvas.height = height;
            }

            console.time('mandelbrot');
            const mandelbrot = Module.mandelbrot(width, height, 1, -0.5, 0);
            console.timeEnd('mandelbrot');
            
            console.time('canvas put image data');
            const imageData = new ImageData(new Uint8ClampedArray(mandelbrot), width, height);
            const context = canvas.getContext('2d');
            context.putImageData(imageData, 0, 0);
            console.timeEnd('canvas put image data');
        });
    </script>
</body>

</html>

Given what we have seen so far, the above JavaScript might be a tad perplexing. There is no loading of wasm files, but there is a JavaScript file named suspiciously like the C++ file we created earlier. What sort of magic is this?

What we are seeing is the Emscripten generated runtime wrapper that is responsible for loading the wasm file, and supplying the supporting functionality to make the C standard library work. We generate it with a Makefile.

Makefile

export EMCC_DEBUG=1

mandelbrot.js: mandelbrot.cpp
        em++ --bind --std=c++11 mandelbrot.cpp -s WASM=1 -s ALLOW_MEMORY_GROWTH=1 -o mandelbrot.js

clean:
        rm *.js *.wasm

There are a couple of interesting points here. First up, we've exported an environment variable for EMCC_DEBUG, which forces the Emscripten compiler to log all of it's build steps. This is useful if you see random failures, and you want some help in figuring out what is breaking. The next is that we've added an additional flag to the the Emscripten C++ compiler defining that we want to have the JavaScript runtime support code to allow resizing of the runtime stack. We've done this here to allow really large surfaces to be malloc'd in C++ land, without breaking the page. Running make will generate both mandelbrot.wasm and mandelbrot.js.

make

If you now create a web server in this directory, you will see the fruits of your labour.

python -m SimpleHTTPServer

If inspect the resulting page in Chrome's performance analyzer you will see that we do a fair amount of work in a single hit, stalling the UI while we calculate the whole image. Wouldn't it be better if we spread the calculations out into smaller chunks, thus preventing the locking up of the UI. Let's do tiled Mandlebrot!

This time around we are going to render the full mandelbrot in small tiles. The tiles will be small enough so that each tile is rendered in under the magic 16ms required to maintain 60 frames per second. Admittedly, this is probably overkill for this sample, but it allows us to explore a different way of integrating C++ and JavaScript, this time with a much finer grained API.

mandelbrot.cpp

#include <cstddef>
#include <cstdlib>
#include <emscripten/bind.h>

// CSV to RGB conversion from http://stackoverflow.com/questions/3018313/
typedef struct {
  double r; // a fraction between 0 and 1
  double g; // a fraction between 0 and 1
  double b; // a fraction between 0 and 1
} rgb;

typedef struct {
  double h; // angle in degrees
  double s; // a fraction between 0 and 1
  double v; // a fraction between 0 and 1
} hsv;

static rgb hsv2rgb(hsv in);

rgb hsv2rgb(hsv in) {
  double hh, p, q, t, ff;
  long i;
  rgb out;

  if (in.s <= 0.0) { // < is bogus, just shuts up warnings
    out.r = in.v;
    out.g = in.v;
    out.b = in.v;
    return out;
  }
  hh = in.h;
  if (hh >= 360.0)
    hh = 0.0;
  hh /= 60.0;
  i = (long)hh;
  ff = hh - i;
  p = in.v * (1.0 - in.s);
  q = in.v * (1.0 - (in.s * ff));
  t = in.v * (1.0 - (in.s * (1.0 - ff)));

  switch (i) {
  case 0:
    out.r = in.v;
    out.g = t;
    out.b = p;
    break;
  case 1:
    out.r = q;
    out.g = in.v;
    out.b = p;
    break;
  case 2:
    out.r = p;
    out.g = in.v;
    out.b = t;
    break;

  case 3:
    out.r = p;
    out.g = q;
    out.b = in.v;
    break;
  case 4:
    out.r = t;
    out.g = p;
    out.b = in.v;
    break;
  case 5:
  default:
    out.r = in.v;
    out.g = p;
    out.b = q;
    break;
  }
  return out;
}

const int TILE_SIZE = 64;

class Mandelbrot {
private:
  int width;
  int height;
  double zoom;
  double moveX;
  double moveY;

  // generate mandelbrot image in tiles
  int currentTileX = 0;
  int currentTileY = 0;

  // The image buffer handed back to JS for rendering into canvas
  // The image format that imageData expects is four unsigned bytes: red,
  // green, blue, alpha
  uint8_t buffer[TILE_SIZE * TILE_SIZE * 4];

public:
  Mandelbrot(int width, int height, double zoom, double moveX, double moveY)
      : width(width), height(height), zoom(zoom), moveX(moveX), moveY(moveY) {}

  // Mandlebrot definition adapted from
  // http://lodev.org/cgtutor/juliamandelbrot.html

  emscripten::val nextTile() {
    if (this->currentTileY * TILE_SIZE > this->height) {
      // If we have generated all of the tiles, return undefined
      // so that the JS will stop calling us.
      return emscripten::val::undefined();
    }

    // each iteration, it calculates: newz = oldz*oldz + p, where p is the
    // current pixel, and oldz stars at the origin

    // real and imaginary part of the pixel p
    double pr, pi;
    // real and imaginary parts of new and old z
    double newRe, newIm, oldRe, oldIm;
    // the RGB color value for the pixel
    rgb color;
    // after how much iterations the function should stop.
    int maxIterations = 180;

    // Generate a TILE_SIZE x TILE_SIZE array of pixels
    for (int y = this->currentTileY * TILE_SIZE;
         y < (this->currentTileY + 1) * TILE_SIZE; y++) {
      for (int x = this->currentTileX * TILE_SIZE;
           x < (this->currentTileX + 1) * TILE_SIZE; x++) {
        // calculate the initial real and imaginary part of z, based on the
        // pixel location and zoom and position values
        pr = 1.5 * (x - this->width / 2) / (0.5 * this->zoom * this->width) +
             this->moveX;
        pi = (y - this->height / 2) / (0.5 * this->zoom * this->height) +
             this->moveY;
        newRe = newIm = oldRe = oldIm = 0; // these should start at 0,0
        // "i" will represent the number of iterations
        int i;
        // start the iteration process
        for (i = 0; i < maxIterations; i++) {
          // remember value of previous iteration
          oldRe = newRe;
          oldIm = newIm;
          // the actual iteration, the real and imaginary part are calculated
          newRe = oldRe * oldRe - oldIm * oldIm + pr;
          newIm = 2 * oldRe * oldIm + pi;
          // if the point is outside the circle with radius 2: stop
          if ((newRe * newRe + newIm * newIm) > 4)
            break;
        }
        // use color model conversion to get rainbow palette, make brightness
        // black if maxIterations reached
        hsv hsvColor;
        hsvColor.h = i * 2;
        hsvColor.s = 1; // fully saturated.
        hsvColor.v = i < maxIterations;
        color = hsv2rgb(hsvColor);
        // draw the pixel
        size_t bufferOffset =
            ((x - this->currentTileX * TILE_SIZE) +
             (y - this->currentTileY * TILE_SIZE) * TILE_SIZE) *
            4;
        this->buffer[bufferOffset + 0] = color.r * 255;
        this->buffer[bufferOffset + 1] = color.g * 255;
        this->buffer[bufferOffset + 2] = color.b * 255;
        this->buffer[bufferOffset + 3] = 255;
      }
    }

    emscripten::val returnVal = emscripten::val::object();
    returnVal.set("data", emscripten::val(emscripten::typed_memory_view(
                              TILE_SIZE * TILE_SIZE * 4, this->buffer)));
    returnVal.set("width", emscripten::val(TILE_SIZE));
    returnVal.set("height", emscripten::val(TILE_SIZE));
    returnVal.set("x", emscripten::val(this->currentTileX * TILE_SIZE));
    returnVal.set("y", emscripten::val(this->currentTileY * TILE_SIZE));

    // Increment to the next tile
    this->currentTileX++;
    if (this->currentTileX * TILE_SIZE > this->width) {
      this->currentTileX = 0;
      this->currentTileY++;
    }

    return returnVal;
  }
};

EMSCRIPTEN_BINDINGS(hello) {
  emscripten::class_<Mandelbrot>("Mandelbrot")
      .constructor<int, int, double, double, double>()
      .function("nextTile", &Mandelbrot::nextTile);
}

The C++ code has changed. This time we have wrapped up the Mandelbrot generation code as a C++ class, instead of a single function. This allows us to expose a member function on the class that returns individual tiles. Due to the fact that the tiles are a fixed size, we have eliminated the need to malloc a runtime dynamically sized array, which simplifies the generated JavaScript support code. We can thus drop the ALLOW_MEMORY_GROWTH from our Makefile.

Makefile

export EMCC_DEBUG=1

mandelbrot.js: mandelbrot.cpp
        em++ --bind --std=c++11 mandelbrot.cpp -s WASM=1 -o mandelbrot.js

clean:
        rm *.js *.wasm

The tradeoff is that our JavaScript is now a touch more complex. Instead of having a singular typed array to push into canvas, we are now painting each tile individually. This explains why the val we return from the C++ code is more complicated than in the previous step.

index.html

<!doctype html>
<html>

<head>
    <style>
        html,
        body {
            height: 100%;
            margin: 0;
            padding: 0;
        }

        .canvas {
            width: 100vw;
            height: 100vh;
        }
    </style>
</head>

<body>
    <canvas id="canvas" class="canvas"></canvas>
    <script src="mandelbrot.js"></script>
    <script>
        Module.addOnPostRun(() => {
            const canvas = document.getElementById('canvas');
            const context = canvas.getContext('2d');

            // Canvas resizing from http://stackoverflow.com/a/43364730/2142626
            const width = canvas.clientWidth;
            const height = canvas.clientHeight;
            if (canvas.width !== width || canvas.height !== height) {
                canvas.width = width;
                canvas.height = height;
            }

            const mandelbrot = new Module.Mandelbrot(width, height, 1, -0.5, 0);

            function drawTile() {
                const tile = mandelbrot.nextTile();
                if (tile) {
                    const imageData = new ImageData(new Uint8ClampedArray(tile.data), tile.width, tile.height);
                    context.putImageData(imageData, tile.x, tile.y);
                    window.requestAnimationFrame(drawTile);
                }
            }
            window.requestAnimationFrame(drawTile);

        });
    </script>
</body>

</html>

Due to the fact that we have sliced up generating the whole image into small tiles, we now have a requestAnimationFrame callback loop in the JavaScript to pull in one new tile for each frame. This leads to an interesting loading animation as each tile is rendered on screen.

If you'd like to access either of the Mandelbrot examples, they're also located in the source code you downloaded to start off the codelab.

Congratulations! We hope you've been successfully introduced to Web Assembly, and are eager to try more. Check out WebAssembly.org for more information.

Reward yourself with a donut 🍩 and, if you're at Google I/O 2017, be sure to high-five a codelab helper and ask for a sticker. 🙏