Learning WebAssembly #9: AssemblyScript Basics

AssemblyScript is a free and open source TypeScript-like language that gives developers low-level control over Wasm features.


We already met AssemblyScript in the previous part of this series. We created a simple program and got an impression of what programming in AssemblyScript feels like. In this part, we will go deeper into the concepts and basics of the language.

Gettings Started

AssemblyScript compiles a strict variant of TypeScript to WebAssembly using Binaryen compiler and toolchain infrastructure library.

We will use npm to initialize our first AssemblyScript project:

$ npm init -y

AssemblyScript provides a very lightweight and efficient loader of Wasm modules. Besides loading Wasm modules and exposing them via WebAssembly API, the loader is also providing utilities to allocate and read strings, arrays, and classes.

$ npm install --save @assemblyscript/loader

To compile AssemblyScript into Wasm we need to install the asc compiler as a dev dependency:

$ npm install --save-dev assemblyscript

Once installed, the compiler provides a handy scaffolding utility to quickly set up a new project:

$ npx asinit .

We will be modifying two files from the generated project:

index.js
The main file for loading and working with Wasm modules.
assembly/index.ts
AssemblyScript source file to be compiled into Wasm.
tests/index.js
Test suite for the main file.

Exports and Imports

Let us create our first AssemblyScript function in assembly/index.ts:

export function main(): void {
  print(42);
}

Unfortunately, we don’t get the function print for free. Leaving WASI aside, interactions between Wasm modules and operating system must be provided by the glue code, JavaScript in our case. We have to declare the function signature first:

declare function print(n: i32): void;

Then, we can import a custom implementation in the main JavaScript file index.js:

var imports = {
  "index": {
    print: console.log
  }
};

The imports object is used by the initialization of the Wasm module by the loader:

var fs = require("fs");
var loader = require("@assemblyscript/loader");

var imports = {
  "index": {
    print: console.log
  }
};

var wasm = loader.instantiateSync(
  fs.readFileSync("./build/optimized.wasm"), 
  imports);

wasm.exports.main();

Before we can run this code, first, we must compile our Wasm module:

$ npm run asbuild

This convenient script runs the asc compiler and generates untouched debug and optimized release Wasm files into the build directory. Now, we can simply run it:

$ node index.js
42

Eureka, we have just created our first working Wasm program in AssemblyScript!

Strings, Strings, Strings!

Don’t get too excited, but there is one thing I have to share with you: AssemblyScript extends the set of Wasm standard data types for strings.

export function hello(
    name: string): string {

  return "Hello, " + name + "!";
}

We can import and export strings from JavaScript with a little help from the loader:

var { hello, 
  __newString,
  __getString } = wasm.exports;

In order to use exporting operating, the module must be compiled with the --exportRuntime flag:

$ npm run asbuild:optimized -- --exportRuntime

The loaded module contains functions for allocating and retaining strings in Wasm memory:

// allocate string in memory
var pti = __newString("Tomas");

var pto = hello(pti);

// retain string from memory
var str = __getString(pto);

Finally, we have an ordinary JavaScript string object:

console.log(str);

It works as expected:

$ node index.js
Hello, Tomas!

Memory

Wasm uses linear memory stored in a specific memory offset isolated from other programs.

In AssemblyScript, there is static memory for static data known at the compilation time and dynamic memory (heap) managed by the runtime. Programs access chunks of memory via pointers. Dynamic memory is tracked by the runtime’s garbage collector and reused when not needed by the program anymore.

We have already seen allocating memory via __newString loader function. __newString is a convenience variant of the more general function __new for allocating managed objects.

We can achieve the same functionality of __newString with some boiler-plate code:

var { hello, memory,
  __new, __pin, __unpin } = wasm.exports;

var input = "Tomas";
var length = input.length;

// allocate memory (usize, String (id=1))
var pt = __new(length << 1, 1);

// load bytes into memory
var ibytes = new Uint16Array(memory.buffer);
for (let i = 0, p = pt >>> 1; i < length; ++i) 
  ibytes[p + i] = input.charCodeAt(i);

// pin object 
var pti = __pin(pt);

// call wasm
var pto = __pin(hello(pti));

// retrieve string size
var SIZE_OFFSET = -4;
var olength = new Uint32Array(memory.buffer)[pto + SIZE_OFFSET >>> 2];

// load string from memory
var obytes = new Uint8Array(memory.buffer, pto, olength);
var str = new TextDecoder('utf8').decode(obytes);

// unpin objects for GC
__unpin(pti);
__unpin(pto);

console.log(str);

Function __pin pins the object externally so it does not become garbage collected.

As we can see, strings in AssemblyScript are nothing more than arrays of integers stored in memory. The language brings some convenient syntax and utilities, however, internally, it does work with the same old Wasm mechanism.

Arrays

AssemblyScript provides convenient functions not only for strings, but we can also work with all kinds of arrays.

As an example, consider a function that takes an array of 32-bit integers and returns a new array where all elements are multiplied by a scalar. In AssemblyScript, it would look like this:

export function multiply(
    matrix: Int32Array, x: i32): Int32Array {

  var arr = new Int32Array(matrix.length);
  for (var i = 0; i < matrix.length; i++) {
    arr[i] = matrix[i] * x;
  }
  return arr;
}

To work with arrays properly, we need a unique class identifier for Int32Array. We can export it from AssemblyScript:

export const Int32Array_ID =
    idof<Int32Array>();

In JavaScript, we can create an input array and read the result with appropriate utility functions:

var { multiply, Int32Array_ID,
  __newArray, __getArray } = wasm.exports;

// input array
var input = [1, 2, 3];
var ai = __newArray(Int32Array_ID, input));

// call wasm, output array
var ao = __getArray(multiply(arri, 2));

console.log(ao);

Function __getArray copies the array's values so it does not have to be pinned.

The result should be no surprise:

$ node index.js
[ 2, 4, 6 ]

Testing AssemblyScript

After initializing a new AssemblyScript project, you can find a basic test in test/index.js. Basically, we can test everything which we export from the main JavaScript file:

// index.js

// ...

module.exports = wasm.exports;
// test/index.js

var assert = require("assert");
var wasm = require("..");

var pti = wasm.__newString("Test");
var pto = hello(pti);
var str = wasm.__getString(pto);

assert.equal(str, "Hello, Test!");

Testing as such looks pretty tedious. We can hide the JavaScript glue behind a facade function and test that instead of the “naked” Wasm function:

// index.js

// ...

function sayHello(name) {
  var pti = __newString(name);
  var pto = hello(pti);
  var str = __getString(pto);

  return str;
}

module.exports = { sayHello };
// test/index.js

const assert = require("assert");
const index = require("..");

assert.equal(
    index.sayHello("Test"), 
    "Hello, Test!"
);

Run it simply with npm:

$ npm test

Further Steps

We have learned the basics of programming in AssemblyScript, including utilities for working with memory, strings and arrays, and we have written our first unit test.

In the next part of this series we will go deeper into programming in AssemblyScript and learn how to generate some graphics.

Stay tuned!