AssemblyScript - Passing Data to and From Your WebAssembly Program

AssemblyScript takes a strict subset of TypeScript and allows it to be compiled to WebAssembly. This is a very persuasive selling point for developers familiar with JavaScript and TypeScript as it immediately allows them to transfer their skills to be able to write WebAssembly programs. This is exciting as WebAssembly has proved useful from things like game engines such as Unity to design tools such as Figma.

In this short blog post, we will look at how you can pass data from JavaScript to WebAssembly programs created using AssemblyScript. This blog will assume you've followed the quick start guide and that you are familiar with npm and JavaScript.

As it currently stands the only types WebAssembly supports are integers and floats, which are stored in linear memory (a single contiguous address space of bytes) for the WebAssembly program. As a language, AssemblyScript works with us in abstracting away some of the complexity of managing more complex types like strings and arrays. A common use-case for AssemblyScript might be to write a WebAssembly module and then make use of that within a JavaScript runtime. At some point, it will probably be necessary to pass some data from the JavaScript code to the WebAssembly module and/or back again. Let's take a look at how we go about passing various data types to AssemblyScript.

Numbers #

Passing numbers to our program is straightforward and doesn't require any special treatment. We can achieve this by just passing them number values directly to our WebAssembly module function. You can use i32 your AssemblyScript code for intergers and f32 for float types, like so:

// AssemblyScript
export function add(a: i32, b: i32): i32 {
return a + b;
}

export function addFloats(a: f32, b: f32): f32 {
return a + b;
}

// JavaScript
{
add,
addFloats
} = wasmModule.exports;

const result = add(1, 2);
// result will be 3

const floatResult = addFloats(1.5, 2.5);
// result will be 4

This is great, especially if we don't need to deal with any other types. However if our program gets more complex we may need to start dealing with other types.

Introducing the loader #

As mentioned previously, WebAssembly as it stands only deals with number types. So how can we go about passing JavaScript data types like strings and arrays to our WebAssembly programs? One solution is to to use the AssemblyScript loader which simplifies the process of loading more complex data types into the WebAssembly memory. The module provides a set of convenience functions to allow loading in types like strings and arrays into memory, returning their pointers. It also allows for the managing of their lifecycle via retaining and releasing them. To get started using the AssemblyScript loader lets install it into our project using npm:

npm install @assemblyscript/loader

Once we've compiled our AssemblyScript program to a wasm file, we will want to use this in our web application.

Let's start with how we go about instantiating our program (here we will be in a Node environment):

const loader = require("@assemblyscript/loader");
const buf = fs.readFileSync("./build/optimized.wasm");
const wasm = new WebAssembly.Module(new Uint8Array(buf));
loader
.instantiate(wasm, {
env: {
abort: (err) => {
console.error(err);
},
},
})
.then((wasmModule) => {
console.log(wasmModule.exports);
// Code to use the instantiated wasm module
});

Strings #

For more complex types like strings, we can leverage the loader. Strings in AssemblyScript are immutable, and hence we can't change a string once we've passed its pointer to the AssemblyScript function. We could, however, return a pointer to a newly constructed string value. In this case, we'll replace 'hello' in a string with 'hi' in the string and return a new string pointer, and then read it with the __getString method:

// AssemblyScript
export function replaceHelloWithHi(a: string): string {
return a.replace("hello", "hi");
}

// JavaScript
{
__retain,
__allocString,
__release,
replaceHelloWithHi
} = wasmModule.exports;
const originalStr = "hello world";
const ptr = __retain(__allocString(originalStr));
const newPtr = replaceHelloWithHi(ptr);
const newStr = __getString(newPtr);
__release(ptr);
__release(newStr);
console.log(newStr);
// logs out 'hi world'

Arrays #

If you have a regular untyped array in our JavaScript side, we'll still need to allocate a typed array on the WebAssembly side, we can use the AssemblyScript i32 type for this. We can get its id using the idof function to get the ID. For Typed Arrays we can use the same approach use the appropriate Typed Array type, in this case, Int32Array. We use idof like so:


// AssemblyScript
export const i32ArrayId = idof<i32[]>()
export const Int32ArrayId = idof<Int32Array>()

Now we have the IDs we can use them using the appropriate functions from the AssemblyScript loader. We will need to allocate an array in the module's memory and retain it to make sure it doesn't get collected prematurely. Let's work through this for the example of summing an array:

// AssemblyScript
export function sumArray(arr: i32[]): i32 {
let sum: i32 = 0;
for (let i: i32 = 0; i < arr.length; i++) {
sum = sum + arr[i];
}
return sum;
}
// JavaScript
const {
__retain,
__allocArray,
__release,
i32ArrayId,
Int32ArrayId,
sumArray,
} = wasmModule.exports;

// Untyped arrays

const arrayPtr = __retain(__allocArray(i32ArrayId, [1, 2, 3]));
const sum = sumArray(arrayPtr);

__release(arrayPtr);

// Now for TypedArrays

const typedArrayPtr = __retain(
__allocArray(Int32ArrayId, new Int32Array([1, 2, 3]))
);
const typedSum = sumArray(typedArrayPtr);

__release(typedArrayPtr);

Are there any other approaches? #

The loader could is deliberately quite minimalist and not as abstracted as they potentially could be. If you are looking for something simpler, I would definitely recommend taking a look at Aaron Turner's asbind library which steamlines the process. For example, we can reduce the string example to the following code:

// JavaScript
import { AsBind } from "as-bind";
const wasm = fs.readFileSync("./build/optimized.wasm");

(async () => {
const asBindInstance = await AsBind.instantiate(wasm);

const response = asBindInstance.exports.replaceHelloWithHi("Hello World!");
console.log(response); // Hi World!
})();

Published