Programming using Web Assembly

Drawing pixel graphics on an HTML canvas

As I described in a previous post, Web Assembly is intended as a target language for compilers, not as a general-purpose language for programmers. In other words, you’re not supposed to write programs in Web Assembly itself but instead write in a higher-level language then compile it into Web Assembly.

I first met assembly languages on 8-bit microcomputers where they were used to write software that ran as fast as possible on what was by modern standards very slow hardware.

Even though Web Assembly is not intended as an application programming language, I wanted to have a go at programming with it, so I coded a simple module that calculates the Mandelbrot Set to be rendered on an HTML canvas.

In this post I take you through the steps I followed:

  • how to write a web assembly program by hand using WAT
  • how to call a Web Assembly function from Javascript in an HTML page
  • how to share memory between Javascript and Web Assembly
  • how to draw bitmap graphics onto an HTML canvas using Web Assembly and putImageData

Note: The following assumes you already have some experience of writing Javascript applications for a web browser.

Getting Started

Web Assembly programs are distributed as a sequence of byte codes in a binary file format called WASM. These files can’t easily be edited in a text editor, so the Web Assembly standard defines an alternative textual representation, called WAT.

To start the project I created a simple WAT file that contains a single function that does nothing more than return the number 1234. It’s called main.wat.

main.wat

There is only one actual Web Assembly instruction in the above code : i32.const 1234. It puts the number 1234 onto a stack; whatever is on the stack when the function ends becomes the function’s return value.

The rest of the file is “pseudo-code” declaring the module, declaring the function and what it returns, naming the function “run” and exporting it so it can be called from elsewhere.

The WAT text file needs to be converted to the binary WASM format. The Web Assembly Binary Toolkit project includes a tool to do this, named wat2wasm.

The tool is available with a web interface here, but I chose to build and install it myself, then run it locally as

wat2wasm main.wat     # This outputs a file named main.wasm

Running the WASM module in a web page

Once I had a compiled WASM module, some boilerplate was necessary to load the module and run it in a browser. This is done using HTML and Javascript as follows:

index.html
main.js

This code uses fetch in Javascript to load the module, so these files need to be hosted on a web server. I used the simple web server that comes with Python, which can serve files locally:

python -m SimpleHTTPServer

If you are following along, you can navigate to http://localhost:8000/ and open Developer Tools, and you should see the result 1234 logged to the console.

Drawing a pixel on a canvas

With the WASM module up and running, I could go ahead and add an HTML canvas. Web Assembly doesn’t have direct access to the DOM API, including the Canvas API, so my WASM code draws on the canvas by setting pixel colour values in an array, which is then mapped onto the canvas using putImageData.

A Web Assembly module has access to a linear memory space that can be shared with Javascript. The memory is defined outside the module and imported, and it’s this memory that stores the pixel data.

Here’s the previous WAT file, modified to import the memory into the module and set a single pixel to a sort of dark plum colour. This has an RGBA value of#880088ff, which in WASM’s little-endian format is the number 0xff880088. (I like to think of it as an “ABGR” value!)

main.wat

There are three instructions in the above code. The first puts the address of the memory location on to the stack. The second puts the pixel data value on the stack, and the third reads the previous two values off the stack and stores the value in memory.

To render the memory onto the canvas it has to be wrapped as a Javascript Uint8ClampedArray and passed to an ImageData constructor, as in the following example:

main.js
index.html

Filling the whole canvas

Now that I could draw coloured pixels onto a canvas, I was almost ready to draw something more interesting. Before that, though, I needed a way to loop through all the pixels in the canvas.

Loops in Web Assembly work in a nested block structure, much like languages such as Javascript and C, but unlike other assembly languages and machine code, where loops are handled by conditionally branching off to different parts of the program. In Web Assembly, control can only jump to well-defined blocks of code. In regular machine code, control can jump to any address in the program. This has the potential to crash the program, or worse, it can enable hackers to take control of the program and/or expose sensitive data.

A typical loop in WAT looks like this:

(loop
.. do some stuff ..
   ... shall I continue with loop ? ...
b_if 0 ;; The 0 says where to branch to, not when to branch
)

loop defines the start of a loop block, and b_if 0 means “branch if the value on the stack is truthy, to the 0th block above this one”.

Here is main.wat after I modified it to loop over all 512 × 512 pixels in the canvas and set them to purple:

main.wat

This code adds a local variable $addr declared with the local pseudo-instruction. In binary WASM files, the variables are referenced as numbers, starting from 0, but WAT lets us write meaningful aliases, like $addr above. The following would also work:

(local i32)    ;; (local $addr i32)
get_local 0 ;; $addr is an alias for 0

The Mandelbrot Set

The foundations were now in place, so I could code something more interesting to look at. I chose the Mandelbrot Set because it uses a simple formula yet gives a spectacular result.

For more information on how the Mandelbrot set is calculated, check out the Wikipedia page, but essentially the code goes as follows:

  • Iterate through every memory location.
  • Calculate a corresponding real and imaginary (x and y) coordinate for the pixel.
  • Repeatedly apply the Mandelbrot formula at that coordinate until either its value grows too large, or after a fixed number of iterations have occurred.
  • Choose a colour from a predefined palette of 8 colours, depending on the number of iterations that occurred.

The colours for the image are chosen from a hard-coded palette of 8 colours, selected by taking the least significant 3 bits of the iteration count. The palette is defined using data expressions and stored in memory after the pixel data. A minor change is needed to main.js to add an extra page of memory for it:

Start of main.js

Here is the WAT file in all its glory. Rather than explain each instruction here, I have commented the code, so hopefully, you can see what it’s doing. The algorithm is the escape-time algorithm from the Wikipedia page.

main.wat

It’s a lot of code compared to the equivalent in a high-level language! But that is the nature of assembly language.

The WAT text format allows a more compact “s-expression” representation, which groups instructions together on a line using parentheses. I deliberately chose not to use it for this experiment, preferring to keep to a more traditional assembly style, but next time I think I will use the other style to improve readability.

Although there is a lot of code here, the essentials are the same as the previous examples. The only new things are some arithmetic and logic operations (sub, mul, div, and, le, lt_u), converting between integer and floating point types (convert_u), and using datato define the colour palette. I encourage you to consult the Web Assembly documentation to find out more about these instructions if you are interested.

The end result

The result

So there you have it — a Mandelbrot set calculated using Web Assembly and drawn onto an HTML canvas. There’s a lot of scope for improvement, but I found it to be a fun and interesting exercise to introduce myself to Web Assembly. I hope you do too!