Hello WebAssembly

A Look at Webassembly Through a Fantasy Console

Casper Beyer
May 24, 2018 · Unlisted

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

Image for post
Image for post

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

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

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

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

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

// 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

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.

Image for post
Image for post
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

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

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

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

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.

Commit Log

Contains Commits, Duh!

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store