When I was looking into how to use WebAssembly (WASM), I kept asking myself: How can I pass my variables back and forth from the WASM module? While you can pass some primitives back and forth quite easily, I was a bit confused about how things like arrays could be passed to and from the WASM.

Then it dawned on me. I’m working with a lower level language here. Assembly doesn’t have arrays. What I need is memory access.

But First, Some Background

There are a number of steps to get WASM ready to be run in the browser.

  1. Write your logic in TypeScript
  2. Use AssemblyScript to compile the TypeScript into WASM
  3. Load the WASM file at run time via JavaScript

Logical flow

This means that some of your code will be compiled before you deploy your code. This means each time there is a change in the TypeScript logic, you must recompile the code into WASM in order to see the changes. This is very different from pure JavaScript, which is interpreted.

A Memory Example with Fibonacci

Take a look at the following example with JavaScript:

1
2
3
4
5
6
7
8
9
10
fibs = [];
targetFib = 10;
for(let n = 0; n <= targetFib; n++) {
if (n === 0 || n === 1) {
fibs[n] = n;
} else {
fibs[n] = fibs[n - 2] + fibs[n - 1];
}
}
console.log(fibs.join(','));

This will print out: 0,1,1,2,3,5,8,13,21,34
It’s a pretty easy process when we can use arrays. Let’s take a look at the same logic but in AssemblyScript:

1
2
3
4
5
6
7
8
9
10
11
const unitSize:i32 = 4;
const targetFib:i32 = 10;
export function findFibWA(): void {
for(let n = 0; n <= targetFib; n++) {
if (n === 0 || n === 1) {
store<i32>(n * unitSize, n);
} else {
store<i32>(n * unitSize, load<i32>((n-1) * unitSize) + load<i32>((n-2) * unitSize));
}
}
}

The two big differences here are the variable types and memory access functions. Let’s break down some of the code.

1
const unitSize:i32 = 4;

The i32 designates this as a 32-bit integer. The number of bytes in a 32-bit integer is 4. Bytes are 8 bits, which means 4 bytes are needed for a 32-bit int (4 x 8 = 32). This unit size will become important when we start accessing memory.

1
store<i32>(n * unitSize, n);

The store function saves values to memory; in this case, we are saving 32-bit ints (i32). store takes two parameters. The first parameter is a pointer, a memory address.

When you save a value to an index of an array (ary[0] = val), JavaScript does all the memory conversions for you. That way you don’t have to worry about it. When working with pointers, we have to know the size of the thing being stored in order to understand where the thing will go. This is why I have made a constant unitSize. I know that each int is 4 bytes. This makes it easy to calculate the correct pointers to store each Fibonacci number.

1
load<i32>((n-1) * elmSize)

This is the exact same idea as store, but in this case, we are accessing memory. This function would access the previously made Fibonacci number.

This, of course, isn’t the entire story. We still need to compile the TypeScript into WASM and load it into the JavaScript.

I’m going to skip the part where we compile the TypeScript into WASM, but if you do want to see the working example, I would suggest looking at the
source code, specifically the build.js.

Accessing Memory from JavaScript

OK, we’re going to look at the JavaScript code (main.assembly.js) first and then break it down line by line.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const setup = async () => {
const arraySize = (4 * targetFib) >>> 0;
const nPages = ((arraySize + 0xffff) & ~0xffff) >>> 16;
const memory = new WebAssembly.Memory({ initial: nPages });
const response = await fetch("../out/main.wasm");
const buffer = await response.arrayBuffer();
const module = await WebAssembly.instantiate(buffer, {
env: { memory: memory }
});
var exports = module.instance.exports;
findFibWASM = function () {
exports.findFibWASM();
let mem = new Uint32Array(memory.buffer);
console.log(Array.from(mem.slice(0, maxFib + 1)));
};
}

You Have to Allocate Memory First

1
2
3
const arraySize = (4 * maxFib) >>> 0;
const nPages = ((arraySize + 0xffff) & ~0xffff) >>> 16;
const memory = new WebAssembly.Memory({ initial: nPages });

WebAssembly always allocates memory by pages. Pages are always 64Kib in size. This logic calculates the number of pages needed to calculate the target Fibonacci number. It then allocates that memory, so that it is ready to be used by a WASM module. You can read more about WebAssembly memory here.

Create the WASM Module

1
2
3
4
5
const response = await fetch("../out/main.wasm");
const buffer = await response.arrayBuffer();
const module = await WebAssembly.instantiate(buffer, {
env: { memory: memory }
});

This code snippet is pretty straight forward. We fetch the .wasm file output and push the resulting buffer into WebAssesbly’s instantiate function. Notice that the second parameter of the instantiate function has the memory that we just allocated.

Using the Exported Function

1
2
3
4
5
6
var exports = module.instance.exports;
findFibWASM = function () {
exports.findFibWASM();
let mem = new Uint32Array(memory.buffer);
console.log(Array.from(mem.slice(0, maxFib + 1)));
};

Once you create the WASM module, all of your exported functions from your TypeScript logic will be on module.instance.exports.

In this case, we can just run the exported function. This should load all of the Fibonacci numbers into memory.

Luckily we can access that memory buffer with memory.buffer. Using this buffer, we can initialize a new Uint32Array. Why 32? That’s because each of our Fibonacci numbers are stored as 32-bit ints.

Finally, we can console.log out our hard work using some array functions to slice out the part of memory that was written.

Demo and Source

Source
Demo

While the source code is working, it might have some differences from what I explained here in the blog post. This was mostly due to the fact that I wanted to test if web assembly could offer a performance boost. At least in this example, it looks like AssemblyScript/WASM is a bit slower, however, not by much.

Sum Up

The TL;DR:
Overview of entire flow:
Memory flow

Some tips:

  1. When storing and loading information with AssemblyScript remember your unit sizes in bytes! You will need to do some pointer arithmetic!
  2. You must allocate memory in the Javascript and this has to be done before the WASM module is initialized.
  3. You can access the memory from Javascript using memory.buffer and loading that buffer into a typed array.
    This is a great reference for working with AssemblyScript: AssemblyScript Cheat Sheet

It feels good to work on some lower level ideas in JavaScript. For me it had been a while since I had to worry about pointers or memory allocation. Hopefully you also learned something and became better for it!