In a previous post we looked at how to compile the popular programming language Go to WebAssembly. WebAssembly is a new programming language which provides a compact binary format for the web. In this post we’ll explore another WebAssembly target language called AssemblyScript. AssemblyScript allows developers to write strictly typed TypeScript (a typed superset of JavaScript) code and compile it to WebAssembly.
Here we introduce how to get up and running with AssemblyScript. If you are not familiar with TypeScript and its syntax, please review the TypeScript Definitive Guide.
Starting an AssemblyScript Project
To get started with AssemblyScript we need to install AssemblyScript with our project. On the assumption that we have initialised with npm (npm init
) we can continue to install AssemblyScript in the following manner:
npm install --save-dev AssemblyScript/assemblyscript
Next we will want to scaffold our AssemblyScript project. AssemblyScript provides a simple command line tool, asinit
, which we can call within our target project folder using npx
:
npx asinit .
You will see an interactive prompt in the following manner:
./assembly
Directory holding the AssemblyScript sources being compiled to WebAssembly.
./assembly/tsconfig.json
TypeScript configuration inheriting recommended AssemblyScript settings.
./assembly/index.ts
Exemplary entry file being compiled to WebAssembly to get you started.
./build
Build artifact directory where compiled WebAssembly files are stored.
./build/.gitignore
Git configuration that excludes compiled binaries from source control.
./index.js
Main file loading the WebAssembly module and exporting its exports.
./package.json
Package info containing the necessary commands to compile to WebAssembly.
This gives the overview of what command line tool provides and which files get created or updated. By pressing y
we can continue to scaffold our project. As implied, this will update our project to have an assembly
folder where our entry point lives for building our AssemblyScript program. By default it will contain the following code:
export function add(a: i32, b: i32): i32 {
return a + b;
}
Since this gives us a basic program (adding two 32 bit integers) out the box, we can actually compile the program using the provided asc
tool. This tool has been added with appropriately targeted paths, to our package.json
. We can then run the aliased command:
npm run asbuild
This command will produce files in 6 files in your build folder, an optimised and untouched wasm file, along with a source map
and text format wat
file for both.
Running a WebAssembly Program
WebAssembly can run in Node.js or in the browser. The approach for using a compiled WebAssembly (wasm file) in both environments is similar. The main difference is that with Node.js we can read files, where as in the browser we have to fetch
a remote file instead.
There are a few ways to instantiate a module in WebAssembly:
- WebAssembly.Instance – Synchronous instantiation
- WebAssembly.instantiate – Asynchronous instantiation
- WebAssembly.instantiateStreaming – Asynchronous streaming instantiation
instantiateStreaming
is currently supported in modern environments except Node.js and Safari. Here is a simple polyfill that falls back to instantiate
for Safari and Node.js:
if (!WebAssembly.instantiateStreaming) {
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer();
return await WebAssembly.instantiate(source, importObject);
};
}
Now, how do we do we pick and leverage these functions? For Node.js, it might make sense to run the wasm code synchronously:
const wasmModule = new WebAssembly.Module(fs.readFileSync(__dirname + "/build/optimized.wasm"));
const instance = new WebAssembly.Instance(wasmModule, {});
const memory = new Uint8Array(instance.exports.memory.buffer);
In the browser things are a bit different, and we can make use of the more efficient instantiateStreaming
:
WebAssembly.instantiateStreaming(fetch('/build/optimized.wasm'), {})
.then(wasmModule => {
const exports = wasmModule.instance.exports;
const mem = new Uint32Array(exports.memory.buffer);
});
Now that we have seen how to load our wasm files, let’s see how we can write more in depth AssemblyScript code.
Overview of Writing AssemblyScript
AssemblyScript allows you to write TypeScript code and compile it to WebAssembly. There are some limitations to this process. For example, because of the necessity of variables and arguments to be statically typed, it is not possible to write union types, use any
or undefined
as everything must be typed. We also must explicitly include return types.
Another thing we’ll need to note is the types in AssemblyScript. For example, as we’ve noted in the brief auto-generated example, we cannot just use number
like we would in TypeScript. Instead we have to specify types that WebAssembly can understand, like i32
or f32
. We can use bool
and string
.
One key thing to understand about AssemblyScript is that it uses linear memory. This means data in an AssemblyScript program is located at a specific offset. This memory comes in two parts; static memory and heap memory. Here the heap memory requires a memory allocator. Thankfully there are three allocators available, namely arena
, tlsf
and buddy
. You can read more about the AssemblyScript memory layout and management options, but for the sake of simplicity we will use tlsf
.
Managing the memory can be tricky, especially if you’re from a TypeScript or JavaScript background. As such, here we will use the loader
provided by AssemblyScript, which provides utility functions for setting and getting data.
Simple Programs with AssemblyScript
We’re also going to need some setup. First we’re going to add a memory allocator and also export the memory
object so we can work with strings and arrays. This looks a little something like this at the top of our AssemblyScript file:
// AssemblyScript
import "allocator/tlsf";
export { memory };
If you’re using a linter, it might complain about the exporting memory
statement but this can be ignored. For the purpose of this post (and to reduce cognitive load) we will assume that we are working in a browser environment, and that we are writing source code in TypeScript. For the sake of brevity the following examples do not feature loading of the WebAssembly file (which is covered above).
Now that we’ve setup the initial AssemblyScript file, let’s look at writing some basic functions. Here we take some basic data types and pass them back to JavaScript from AssemblyScript with the help of the loader
.
Let’s start with a basic greeting function. This will take a string from our JavaScript (TypeScript) code, run it in WebAssembly, and return it back.
// AssemblyScript
export function getGreeting(name: string): string {
const hello = "hello "
return hello + name;
}
// TypeScript - Browser
const name = wasmModule.newString("James");
const greeting = wasmModule.getGreeting(name);
const greetingStr = wasmModule.getString(greeting);
Here newString
and getString
are functions that come from the loader
(there has been work towards potentially improving namespace support). This works fine but does not showcase the benefit of WebAssembly. What about something more resource intensive? Let’s generate some Fibonacci numbers with AssemblyScript:
// AssemblyScript
export function fibonacci(n: i32): i32 {
let i: i32 = 1;
let j: i32 = 0
let k: i32;
let t: i32;
for (k = 1; k <= Math.abs(n); k++) {
t = i + j;
i = j;
j = t;
}
if (n < 0 && n % 2 === 0) {
j = -j;
}
return j;
}
// TypeScript - Browser
const n = 10000;
const wasmFibResult = wasmModule.fibonacci(n);
Here we have generated the fibonnaci number n
using an iterative approach. This works but the return data type is a simple 32 bit integer. What if we want to return an Array? Let’s write a Prime Sieve in AssemblyScript. Prime Sieves return an array where one denotes a prime and zero denotes non-primes. In this case we will write the popular Sieve of Eratosthenes:
// AssemblyScript
export function sieveOfEratosthenes(n: i32): Int8Array {
// Input: an integer n > 1.
// Let A be an array of Boolean values, indexed by integers 0 to n,
// initially all set to true.
let sieve = new Int8Array(n);
for (let i = 0; i < n; i++) {
sieve[i] = 1;
}
// // 0 and 1 are not considered primes
sieve[0] = 0;
sieve[1] = 0;
// for i = 2, 3, 4, ..., not exceeding ?n:
// if A[i] is true:
// for j = i2, i2+i, i2+2i, i2+3i, ..., not exceeding n:
// A[j] := false.
for (let i = 2; i < Math.sqrt(n); i++) {
if (sieve[i]) {
for (let j = i*i, k = 1; j < n; j = i*i+k*i, k = k+1) {
sieve[j] = 0;
}
}
}
// Output: all i such that A[i] is true.
return sieve;
}
// TypeScript - Browser
const n = 10000;
wasmModule.sieveOfEratosthenes(size)
The loader does not currently support support ordinal Array
s so we need to specify the output as Int8Array
. Hopefully here you can see some examples of increasing complexity and see how AssemblyScript might help, especially in writing CPU intensive code.
Conclusion
AssemblyScript is very powerful, allowing developers who already know TypeScript to write code that is consistently performant for the web. However, it is still relatively early in its development. Many features are still on the roadmap, including better support for Classes and standard library improvements, among a host of other things.
A final point of note is that in theory as long as you follow AssemblyScripts guidance for portable code you could write code in AssemblyScript and compile it to JavaScript and WebAssembly, conditionally loading either depending on target environment support. This is arguably a very powerful and appealing feature, and is promising for the future of AssemblyScript’s development.
Getting help
If you’d like to know more about WebAssembly, AssemblyScript, TypeScript, and performance, or if you need help improving performance within your application, feel free to contact us to discuss how we can help!