Hello WebAssembly

A Look at Webassembly Through a Fantasy Console

Casper Beyer
Commit Log

--

Author’s Note: Oh god this is a terrible article, refresher coming soon.

The WebAssembly Community Group reached a consensus on the initial specification for WebAssembly in early 2017. It’s now available and enabled by default in most browsers so lets take a look at it and implement a fantasy console with pure WebAssembly.

What is WebAsssembly

The name WebAsssembly is actually a bit of a misnomer because it’s a statically typed instruction format targeting stack based virtual machines which is much higher level representation in constrast with your typical register based assembly.

However, naming things is hard so ignoring that, what is exactly is WebAssembly?

  • WebAssembly is an instruction format that runs on a stack based virtual machine.
  • WebAssembly is statically typed, value types are limited to i32, i64, f32 and f64.
  • WebAssembly can only read and write from it’s own linear memory which is an array buffer. It has no direct access to external JavaScript variables values unless they are copied into memory or passed through the call stack.
  • WebAssembly can not call functions which are not explicitly forwarded to it as an import. Functions can only have value types as parameters and return value.

Semantically it’s actually more or less identical to it’s predecessor Asm.js, a statically typed subset of JavaScript. The major difference between the two, is that WebAssembly is a binary format designed to be more size and load time efficient than what the equivalent JavaScript source with Asm.js type annotations would be.

The WebAssembly binary format is meant to be a compile target and there’s already a quite a few languages that compile to WebAssembly at different levels of completion including but not limited to C, C++, C#, Go and Rust.

However to get a better feel of how WebAssembly actually works we’re not going to go that high level, in the interest of exploration we’ll use the WebAssembly text format.

Getting To Hello With WebAssembly

We need to be able to walk before we can run, so before get to the fun parts lets get a minimal example working, in this case writing a string to the document.

Because WebAssembly lives in a host with no knowledge of the host it’s slightly more involved than just calling a function.

Assembling our Module

To assemble our module from the WebAssembly s-expression text format we’ll use the WebAssembly Binary Toolkit. Unfortunately there are no binary releases available, there is a recipie for brew but it’s not up to date so building from source is the way to go.

With that done, we can assemble our program from the following source

;; hello.wat
(module
;; Import our trace function so that we can call it in main
(import "env" "trace" (func $trace (param i32)))
;; Define our initial memory with a single page (64KiB).
(memory $0 1)
;; Store a null terminated string at byte offset 0.
(data (i32.const 0) "Hello world!\00")
;; Export the memory so it can be read in the host environment.
(export "memory" (memory $0))
;; Define the main function with no parameters.
(func $main
;; Call the trace function with the constant value 0.
(call $trace (i32.const 0))
)
;; Export the main function so that the host can call it.
(export "main" (func $main))
)

We’ll assemble with wat2wasm

For a quick and dirty assemble it’s also possible to use an online service like Web Assembly Studio which uses the same toolchain to produce the results for you, it’s actually pretty neat I just find it inconvinient to be dependent on online browser tools.

Calling Into The Browser from the module

Without calling into the browser our module can’t cause any side effects, so lets define the trace function and write to the document.

We’ll have to keep a global variable which will hold the module’s memory later on, we’ll read the bytes from this until we hit the null terminator and write it to the document.

function trace(byteOffset) {
var s = '';
var a = new Uint8Array(memory.buffer);
for (var i = byteOffset; a[i]; i++) {
s += String.fromCharCode(a[i]);
}
document.write(s);
}

This is the basically how all interoperability between WebAssembly and it’s host work, communication by memory and the call stack.

As a side note, it’s true that WebAssembly can’t call into the browser by itself but that doesn’t mean you can’t write glue that does it.

To give WebAssembly access to JavaScript objects you would basically need store them in a JavaScript array or object and then give WebAssembly the index as a faux pointer. This faux pointer can then be stored as usual in WebAssembly, passed around in functions and then used to reference the correct JavaScript object in other glue functions.

However trampolining between JavaScript and WebAssembly does come at a cost, this is why things like vector math libraries compiled to WebAssembly for consumption by JavaScript is a very bad idea. As a general rule of thumb for pure computation WebAssembly is no faster than JavaScript using typed arrays.

Loading and Running the Module

Moving on, we need to actually load, compile and instansiate the module. There are actually a few ways to go about this including streaming compilation but those functions are not implemented everywhere.

// hello.js
const response = await fetch('hello.wasm');
const buffer = await response.arrayBuffer();
const module = await WebAssembly.compile(buffer);
const instance = await WebAssembly.instantiate(module, {
env: {
trace: function trace(byteOffset) {
var s = '';
var a = new Uint8Array(memory.buffer);
for (var i = byteOffset; a[i]; i++) {
s += String.fromCharCode(a[i]);
}
document.write(s);
},
}
);
let exports = instance.exports;
var memory = exports.memory;

With that all done, we’ve got our trivial Hello world example. It takes a little bit of setup to get there but as far as language embedding goes it’s a fairly minimal when compared to writing bindings for other languages.

A More Interesting Example: A Fantasy Console

A literal Hello world example is pretty dull so lets scratch the surface of a more interesting example, a fantasy console.

The premise of fantasy consoles like PICO-8 and TIC-80 is two fold, one is to bring back the limitations for aesthetic reasons, the second more interesting one is to bring back the programming model.

Most old school computers and consoles as-well use memory mapping to deal with all kinds of input and output, reading a certain address in memory would give you the state of the gamepad, writing to a region of memory would put pixels on the screen and so on.

Memory Layout of a hypothetical fantasy console

So we will use WebAssembly’s memory model to do just that and define a very simple fantasy console with basic support for device input and video output.

Ticks and Interrupts

WebAssembly doesn’t have any intrinsic support for co-routines or thread yielding so emulating interrupts and interrupt requests would be fairly tedious, not impossible but very tedious.

So instead trying to emulate that, our entire process will be tick based. We’ll export a single function from our module named tick which is called at around 60 times per second via requestAnimationFrame in JavaScript.

;; cartridge.wat
(module
;; Conceptually it makes a more sense to import the memory
;; since the host has a predefined memory layout that is
;; outside of our control.
;;
;; As a rule of thumb: shared libraries import host memory
;; and executables export their own memory.
;;
;; As an added bonus importing the memory means that we can
;; essentially get hot-swapping of modules for free because
;; we are keeping the memory intact.
(import "env" "memory" (memory $0 1))
;; Define the tick function, again with no parameters
(func $tick
;; The game logic would go here.
)
;; Export the tick function so that the host can call it.
(export "tick" (func $tick))
)

Mapping Memory to the Display

Our video buffer is 240x136 pixels in size, where each pixel is a nibble (4 bits). A nibble can store a value up to 16 which means it can hold an index va value in a 16 color palette.

To get this onto the actual display, we will use a canvas of the same size and copy the pixels after each tick.

// Uint8 view of the memory buffer, it's worth noting that the 
// memory buffer
will change when grown so caching any views needs
// to be done with care.
var bytes = new Uint8Array(memory.buffer);
// Get the image data of the display context so that we can do
// direct pixel manipulation.
var image = context.getImageData(0, 0, 240, 136);
// Iterate over each line in sequence as normal but skip
// over odd columns since each byte contains two nibbles
for (var y = 0; y < 136; y++) {
for (var x = 0; x < 240; x += 2) {
var b = bytes[(y * 120) + (x / 2)];
// Get the lower bits by masking the higher bits.
var lo = (b & 0x0F);
image.data[((y * 240 + x) * 4) + 0] = palette[lo][0];
image.data[((y * 240 + x) * 4) + 1] = palette[lo][1];
image.data[((y * 240 + x) * 4) + 2] = palette[lo][2];
image.data[((y * 240 + x) * 4) + 3] = palette[lo][3];
// Get the higher bits by shifting.
var hi = (b >> 4);
image.data[((y * 240 + x) * 4) + 4] = palette[hi][0];
image.data[((y * 240 + x) * 4) + 5] = palette[hi][1];
image.data[((y * 240 + x) * 4) + 6] = palette[hi][2];
image.data[((y * 240 + x) * 4) + 7] = palette[hi][3];
}
}
// Write the image data back to the graphics context.
context.putImageData(image, 0, 0);

With that done we can write a little module in the WebAssembly text format to display some classic loading bands. The crux of it is writing via the store opcode.

;; cartridge.wat
(module
(import "env" "memory" (memory $0 1))
(func $tick
;; We'll use this local integer for temporary storage.
(local $value i32)
;; We'll use this local integer as a loop counter.
(local $index i32)
;; Initialize our loop counter to 0.
(set_local $index
(i32.const 0)
)
;; Declare our loop block
(loop $loop
;; Store a byte into our memory buffer.
(i32.store8
;; Store at the byte offset given by our counter.
(get_local $index)
;; The value we store is OR'ed together with itself to pack
;; both the low and high bits.
(i32.or
;; Shift the value 4 bits left
(i32.shl
;; Store and return the nibble
(tee_local $value
;; Get the palette index by getting the remainder
;; This is effectivly the modulus operator.
(i32.rem_s
;; Divide to get the same color index for N rows
(i32.div_s
(get_local $index)
(i32.const 1080)
)
(i32.const 16)
)
)
(i32.const 4)
)
;; Since tee_local stored the color we want already
;; we can just get it for the upper bits.
(get_local $value)
)
)
;; Branch into the loop block if the condition is not met
(br_if $loop
;; Not equal comparison
(i32.ne
;; Increment the index counter by one and return the value
(tee_local $index
(i32.add
(get_local $index)
(i32.const 1)
)
)
(i32.const 16320)
)
)
)
)
(export "tick" (func $tick))
)

After all that we have a color band, it’s not much to look at but it is something to look at and is somewhat reminiscent of waiting for Commadore 64 games that would never load from cassette tapes.

Collecting Input

Capturing input is the same idea just the other way around, we write to the memory from JavaScript from event handlers and read from those addresses in the WebAssembly module.

window.addEventListener('mousemove', function(event) {
var bytes = new Uint8Array(memory.buffer);
bytes[0x3FC4] = event.clientX;
bytes[0x3FC5] = event.clientY;
});
window.addEventListener('wheel', function(event) {
var bytes = new Uint8Array(memory.buffer);
bytes[0x3FC6] += event.deltaY;
});
window.addEventListener('mousedown', function(event) {
var bytes = new Uint8Array(memory.buffer);
bytes[0x3FC7] = event.buttons;
});
window.addEventListener('mouseup', function(event) {
var bytes = new Uint8Array(memory.buffer);
bytes[0x3FC7] = event.buttons;
});

And then we can read from in WebAssembly with the load instructions, i32.load8_u in this case to load an unsigned byte.

Conclusion

That concludes this brief view at WebAssembly and the WebAssembly Text format. Diving into tons of examples did not make much sense here because it would basically read like the reference manual. Also at the end of the day it’s more preferable to just write in C than the WebAssembly Text format because it’s fairly verbose and tedious but it helps to at-least be able grok it because it’s what browsers will show when you do view source.

So to summarize

  • WebAssembly is pretty damn amazing for computationally heavy things that live in isolation like encoders, decoders and games.
  • WebAssembly is great for compiling from static languages, C essentially maps 1:1 onto WebAssembly.
  • WebAssembly CAN call into the DOM, it’s just requires work similar to providing bindings for scripting languages.
  • WebAssembly isn’t magic and is not going to make your web pages faster, actually it will most likely slower because of trampolining overhead.
  • WebAssembly is not a standard, so please provide fallbacks. Compiling from WebAssembly to Asm.js can be a viable method.

That’s it for now, stay tuned for the next installment.

--

--

Casper Beyer
Commit Log

Indie Game Developer, Professional Software Developer and Expert Jak Shaver. Working on Deno.