AssemblyScript Class Deep Dive
October 4, 2020
This is a tutorial on the basics of AssemblyScript OOP. Much of the focus is on the interaction between JavaScript and the WebAssembly module compiled from AssemblyScript using the asc compiler. I’m going to assume that the reader has some basic familiarity with HTML/CSS/JavaScript and Node.js. OOP in AssemblyScript uses class structures similar to TypeScript, so in many cases, you can consult the TypeScript documentation. However, AssemblyScript is not TypeScript. Many of the differences between the two languages arise from AssemblyScript having a compile target of WebAssembly and TypeScript compiling to JavaScript. I’m going to focus on how the interface between the JavaScript embedding environment and AssemblyScript works.
In this tutorial, we will discuss how to create and export classes from AssemblyScript and compile those classes into both WebAssembly Text and a binary WASM module. We will instantiate a compiled WASM module from a JavaScript Node app and called the methods of our AssemblyScript class. We will also learn how to do this using the AssemblyScript loader module. We will then extend our original AssemblyScript class creating a second class. Finally, we will compare the performance of the AssemblyScript loader with direct calls to the WebAssembly module from JavaScript.
I am using VisualStudio code, and I’ve installed the asc compiler using npm. If you haven’t installed the asc compiler, you will want to consult the AssemblyScript quick start: AssemblyScript Quick-Start
Classes in AssemblyScript
Let’s begin by creating a new AssemblyScript file called vector.ts. Right now, AssemblyScript is
piggybacking on top of TypeScript file formatting, which works in most cases. In VS Code, this does result in some
annoying auto-completion, but it’s what all the cool kids are doing right now, so go with it. Once you have created
your AssemblyScript file, add the following class called Vector2D
:
export class Vector2D {
x: f32;
y: f32;
constructor(x: f32, y: f32) {
this.x = x;
this.y = y;
}
Magnitude(): f32 {
return Mathf.sqrt(this.x * this.x + this.y * this.y);
}
}
The code above exports a class called Vector2D
. The class has two attributes, x
and
y
. It also has a constructor
that takes as parameters x
and y
and assigns those values to this.x
and this.y
. Unfortunately, the TypeScript shorthand
that allows you to leave your constructor empty if your input parameters have names that match the names of your
attributes is not yet available in AssemblyScript. Finally, there is a Magnitude
method that calculates
the magnitude of the vector by summing the squares of x
and y
and taking the square root
of that sum.
If you are familiar with TypeScript, you will notice this code looks just like the class
structures
TypeScript uses. The primary difference is that instead of using number types
as TypeScript would, we are using f32
types for 32-bit floating-point numbers. You can use the number
type in your AssemblyScript, but this is the same as using f64
64-bit
floating-point numbers. The f64
type is the most precise data type you can use. It is also the poorest
performing in most circumstances, so if you are going to use the number type,
you should keep that in mind. Let’s compile vector.ts with the following asc command on the
command line:
asc vector.ts --runtime none -o vector.wat
The command above will not create a WASM module. We are creating a WAT file so we can look through what the asc compiler does with our AssemblyScript code. The first parameter passed to asc is the name of the AssemblyScript file we want to compile. The --runtime none flag tells the compiler not to include the AssemblyScript runtime. The AssemblyScript runtime gives you some great features like garbage collection. These features can be helpful, but they add a lot of additional code to your module, so we will steer clear of the runtime for this tutorial. After the runtime flag, we pass in the -o vector.wat flag to tell the asc compiler the name of the output file. The compiler uses the extension to determine if it is going to output WAT (WebAssembly Text) or the WASM binary file. In this case, we will take a look at the output of our compile to see what asc is giving us. If you open up vector.wat and scroll down a little, you will notice the following exports:
(export "memory" (memory $0))
(export "Vector2D" (global $vector/Vector2D))
(export "Vector2D#get:x" (func $vector/Vector2D#get:x))
(export "Vector2D#set:x" (func $vector/Vector2D#set:x))
(export "Vector2D#get:y" (func $vector/Vector2D#get:y))
(export "Vector2D#set:y" (func $vector/Vector2D#set:y))
(export "Vector2D#constructor" (func $vector/Vector2D#constructor))
(export "Vector2D#Magnitude" (func $vector/Vector2D#Magnitude))
Notice how the compiler generated get
and set
accessor functions for the x
and y
attributes and exported them so that you can access them from the embedding environment. We can
also see that the WASM module exported the constructor
and
Magnitude
functions. You will notice that the methods are all prefixed with the name of the class and a
#
character (Vector2D#
). Also, the set and get methods have a suffix,
which tells you what attribute they are setting and getting such as :x
or :y
. If we would
like to access these functions and attributes from our JavaScript without using the
AssemblyScript loader, we will need to remember this naming convention.
What if you don’t want to export all of the attributes to the embedding environment? Add the private keyword to the beginning of the x and y attributes in your AssemblyScript and recompile with the asc command to see what happens. Below is a new version of the AssemblyScript with two private attributes:
export class Vector2D {
private x: f32;
private y: f32;
constructor(x: f32, y: f32) {
this.x = x;
this.y = y;
}
Magnitude(): f32 {
return Mathf.sqrt(this.x * this.x + this.y * this.y);
}
}
The private
modifier in front of the x
and y
attributes tells the
AssemblyScript compiler that you do not want these attributes to be publicly accessible. If you recompile with the
same asc command we used earlier; the WASM module no longer exports the accessor methods that set and get
the x
and y
variables to the embedding environment:
(export "memory" (memory $0))
(export "Vector2D" (global $vector/Vector2D))
(export "Vector2D#constructor" (func $vector/Vector2D#constructor))
(export "Vector2D#Magnitude" (func $vector/Vector2D#Magnitude))
CAUTION: IN TYPESCRIPT, THREE MODIFIERS ARE USED TO DEFINE HOW ATTRIBUTES CAN BE ACCESSED. THESE MODIFIERS ARE PUBLIC, PRIVATE, AND PROTECTED AND BEHAVE A LITTLE DIFFERENTLY IN ASSEMBLYSCRIPT THAN THEY DO IN TYPESCRIPT. THE PROTECTED METHOD IN ASSEMBLYSCRIPT BEHAVES THE SAME AS THE PUBLIC MODIFIER, SO YOU SHOULD AVOID USING IT TO PREVENT CONFUSION.
The private
modifier prevents AssemblyScript from exporting the get
and set
methods when it compiles the module. Unlike in other OO languages such as TypeScript, the private
modifier in AssemblyScript does not prevent classes that extend the original from accessing that attribute. Please
keep that in mind when you are designing your classes in AssemblyScript. Let’s compile our AssemblyScript into a
WASM module so that we can call it from our JavaScript:
asc vector.ts --runtime none -o vector.wasm
When we change the -o vector.wat to -o vector.wasm, we tell the asc compiler that we would like our output to be a WebAssembly binary file. That will allow us to load and run the module from a JavaScript embedding environment. Now that we have the vector.wasm file, let’s take a look at how we can load and call that function using Node.js.
JavaScript embedding environment
We will be using Node.js to load and execute our WebAssembly module. If you were to use a browser, the primary
difference would be the use of WebAssembly.instantiateStreaming
instead of using Node to load the WASM
module from the filesystem and calling WebAssembly.instantiate
. Create a file called vector.js and add
the following JavaScript code to it:
const fs = require('fs');
(async () => {
let wasm = fs.readFileSync('vector.wasm');
let obj = await WebAssembly.instantiate(wasm);
let Vector2D = {
init: function (x, y) {
return obj.instance.exports["Vector2D#constructor"](0, x, y)
},
Magnitude: obj.instance.exports["Vector2D#Magnitude"],
}
let vec1_id = Vector2D.init(3, 4);
let vec2_id = Vector2D.init(4, 5);
console.log(`
vec1.magnitude=${Vector2D.Magnitude(vec1_id)}
vec2.magnitude=${Vector2D.Magnitude(vec2_id)}
`);
})();
In this app, I’m using the fs
Node.js module to load
the binary WASM data from a file
inside of an asynchronous IIFE. Once we have the binary data, we can pass it to
WebAssembly.instantiate
, which returns a WebAssembly module object. We will then create a JavaScript
object we call Vector2D
, which mirrors the functions inside of the WebAssembly module. We create an
init
function that calls the WebAssembly module’s Vector2D
constructor passing in 0 as the
first parameter. The constructor
function allows you to create a Vector2D
object anywhere
in linear memory you like. That can be helpful when you are managing your memory. In this case, we will make the
constructor create a new object at the next available memory location, so we pass in 0
for this first
parameter. The function will then return the location in linear memory where it
created this object. The Magnitude
attribute in Vector2D
takes its value from
obj.instance.exports["Vector2D#Magnitude"]
, which is a function in our WebAssembly module.
After defining the JavaScript Vector2D object, we call the Vector2D.init
function twice to create two
Vector2D
WebAssembly objects in linear memory. The Vector2D.init
function returns the
linear memory address of the Vector2D
WebAssembly objects, so we need to track those in variables to
use for method calls. We then call Vector2D.Magnitude
twice
inside of a console.log template string. We pass in the vector ids we saved earlier, which the
magnitude
function uses in the WebAssembly module to know which object it is using. The
Magnitude
function passes back the magnitude
of the given vector, which the app logs to
the console. We can then run this app using node with the following command on the command line:
node vector.js
Running it results in the following output to the command line:
vec1.magnitude=5
vec2.magnitude=6.4031243324279785
The two values are the magnitude of our first vector where x = 3 and y = 4, and the magnitude of the second vector where x = 4 and y = 5.
Now that we have seen how to make calls into our AssemblyScript app directly, let’s look at how we can use the AssemblyScript loader to make things a little easier.
AssemblyScript Loader
The AssemblyScript loader can be installed using npm and makes the interface between an AssemblyScript module and the JavaScript embedding environment a lot simpler to use. It does come with a bit of a performance hit, which we can explore a little later in this tutorial. Before we install the AssemblyScript loader and make changes to our JavaScript, we will begin by making a few modifications to our AssemblyScript code. Open up vector.ts and change the code to the following:
export class Vector2D {
x: f32;
y: f32;
constructor(x: f32, y: f32) {
this.x = x;
this.y = y;
}
Magnitude(): f32 {
return Mathf.sqrt(this.x * this.x + this.y * this.y);
}
add(vec2: Vector2D): Vector2D {
this.x += vec2.x;
this.y += vec2.y;
return this;
}
}
There are two changes to the previous version of vector.ts. First, we remove the private
modifiers from the x
and y
attributes. The second change is the addition of an
add
function that adds a second vector to this vector. Now that we have a new version of vector.ts, we need to
recompile it with asc:
asc vector.ts --runtime none -o vector.wasm
Now that we have a new version of the WebAssembly module, let’s use npm to install the AssemblyScript loader:
npm i @assemblyscript/loader
Create a new JavaScript file called vector_loader.js and add the following code to it:
const fs = require('fs');
const loader = require('@assemblyscript/loader');
(async () => {
let wasm = fs.readFileSync('vector.wasm');
let module = await loader.instantiate(wasm);
let Vector2D = await loader.demangle(module).Vector2D;
let vector1 = Vector2D(3, 4);
let vector2 = Vector2D(4, 5);
vector2.y += 10;
vector2.add(vector1);
console.log(`
vector1=(${vector1.x}, ${vector1.y})
vector2=(${vector2.x}, ${vector2.y})
vector1.magnitude=${vector1.Magnitude()}
vector2.magnitude=${vector2.Magnitude()}
`);
})();
The first difference between the vector_loader.js and the vector.js file is that we must require
the AssemblyScript loader
. Rather than using the WebAssembly.instantiate
function from the
IIFE, we must call the loader.instantiate
function. That returns a loader
module that
works a little differently than the WebAssembly module object returned by the WebAssembly.instantiate
call. Immediately after instantiating the loader
module, we call loader.demangle
passing
it the module returned by loader.instantiate
. The demangle function returns an object structure that
provides us with functions we can use to instantiate objects from our WebAssembly module. We pull the
Vector2D
function out of the object structure so that we can use it as a constructor
function for creating Vector2D
objects in JavaScript. We use that function to create a
vector1
and vector2
object, passing in the x
and y
values for
those vectors. We can now use these objects as regular JavaScript objects. The loader wires everything up for us.
For example, we are calling vector2.y += 10
to increase the value of vector2.y
by ten and
calling vector2.add( vector1 )
calls the add function on the vector2
object passing in
vector1
. Finally, in our console.log
call, we can use values like vector1.x
and vector1.y
. Once you have your JavaScript file ready, run it with the following node command:
node vector_loader.js
You should see the following output:
vector1=(3, 4)
vector2=(7, 19)
vector1.magnitude=5
vector2.magnitude=20.248456954956055
In the next section we will learn how to extend our AssemblyScript class through inheritance.
Extending classes in AssemblyScript
The syntax for extending classes in AssemblyScript is the same as it is in TypeScript. You do have to keep in mind that you will be able to access private values from your extended class, which you can not do in TypeScript. If you are not using the AssemblyScript loader, you can access an object that implements an extended class as if it were the base class. That can be both useful and potentially hazardous (if you forget the type of object you are working with). Open up the vector.ts file and add the following class after the Vector2D definition:
...
export class Vector3D extends Vector2D {
z: f32;
constructor(x: f32, y: f32, z: f32) {
super(x, y);
this.z = z;
}
Magnitude(): f32 {
return Mathf.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
}
add(vec2: Vector3D): Vector3D {
super.add(vec2);
this.z += vec2.z;
return this;
}
}
The new Vector3D
class extends
the Vector2D
class. It keeps the original
x
and y
attributes and adds a third z
attribute for the third dimension. Its
constructor calls
super
, which runs the constructor from the Vector2D
class it extends. It then sets the
value of this.z
to the z
parameter passed into the constructor. The Magnitude
method
is overridden (replacing Magnitude
from Vector2D
) to take the third dimension into account
when calculating the magnitude of the vector. Finally, the add
function calls the Vector2D
add
function using super.add
and then increases the value of this.z
using the
z
attribute value of the vec2
parameter.
Now that we’ve added our Vector3D
class to the AssemblyScript file, we can recompile our WebAssembly
module using asc:
asc vector.ts --runtime none -o vector.wasm
Now that we have a new WASM module, we can modify the vectorloader.js file to pull in the
Vector3D class
. Here is the updated version of vectorloader.js:
const fs = require('fs');
const loader = require("@assemblyscript/loader");
(async () => {
let wasm = fs.readFileSync('vector.wasm');
let module = await loader.instantiate(wasm);
let { Vector2D, Vector3D } = await loader.demangle(module);
let vector1 = Vector2D(3, 4);
let vector2 = Vector2D(4, 5);
let vector3 = Vector3D(5, 6, 7);
vector2.y += 10;
vector2.add(vector1);
vector3.z++;
console.log(`
vector1=(${vector1.x}, ${vector1.y})
vector2=(${vector2.x}, ${vector2.y})
vector3=(${vector3.x}, ${vector3.y}, ${vector3.z})
vector1.magnitude=${vector1.Magnitude()}
vector2.magnitude=${vector2.Magnitude()}
vector3.magnitude=${vector3.Magnitude()}
`);
})();
We changed the line that took the Vector2D
function from the call to demangle
, and
changed it to destructure the result, creating both a Vector2D
and Vector3D
function
variable. We create an object vector3
, using the function Vector3D
, which is passed
x
, y
, and z
values. We increment vector3.z
for no particular
reason other than to show that you can do it. Inside of the template string passed to console.log
, We
added a line that displays the x
, y
, and z
values in vector3
, as
well as the magnitude of vector3
.
When you run this JavaScript from the command line using node, you get the following output:
vector1=(3, 4)
vector2=(7, 19)
vector3=(5, 6, 8)
vector1.magnitude=5
vector2.magnitude=20.248456954956055
vector3.magnitude=11.180339813232422
Now let’s look at how the performance of the loader compares to naked calls into the WASM module.
Performance of loader vs naked WASM
The AssemblyScript loader gives us a much more intuitive structure for interaction between the WebAssembly API and our JavaScript. The final section of this tutorial will make a quick performance comparison of the loader with direct calls into the WASM modules. To run this test, we don’t need to write any additional AssemblyScript. Create a new JavaScript file called vector_perform.js and add the following code:
const fs = require('fs');
const loader = require("@assemblyscript/loader");
(async () => {
let wasm = fs.readFileSync('vector.wasm');
let module = await loader.instantiate(wasm);
let obj = await WebAssembly.instantiate(wasm);
let nVector2D = {
init: function (x, y) {
return obj.instance.exports["Vector2D#constructor"](0, x, y)
},
getX: obj.instance.exports["Vector2D#get:x"],
setX: obj.instance.exports["Vector2D#set:x"],
getY: obj.instance.exports["Vector2D#get:y"],
setY: obj.instance.exports["Vector2D#set:y"],
Magnitude: obj.instance.exports["Vector2D#Magnitude"],
add: obj.instance.exports["Vector2D#add"],
}
let nVector3D = {
init: function (x, y, z) {
return obj.instance.exports["Vector3D#constructor"](0, x, y, z)
},
getX: obj.instance.exports["Vector3D#get:x"],
setX: obj.instance.exports["Vector3D#set:x"],
getY: obj.instance.exports["Vector3D#get:y"],
setY: obj.instance.exports["Vector3D#set:y"],
getZ: obj.instance.exports["Vector3D#get:z"],
setZ: obj.instance.exports["Vector3D#set:z"],
Magnitude: obj.instance.exports["Vector3D#Magnitude"],
add: obj.instance.exports["Vector3D#add"],
}
let start_time_naked = (new Date()).getTime();
let vec1_id = nVector2D.init(1, 2);
let vec2_id = nVector2D.init(3, 4);
let vec3_id = nVector3D.init(5, 6, 7);
for (let i = 0; i < 1_000_000; i++) {
nVector2D.add(vec1_id, vec2_id);
nVector3D.setX(vec3_id, nVector3D.getX(vec3_id) + 10);
nVector2D.setY(vec2_id, nVector2D.getY(vec2_id) + 1);
nVector2D.Magnitude(vec2_id);
}
console.log("naked time=" + (new Date().getTime() - start_time_naked));
let { Vector2D, Vector3D } = await loader.demangle(module);
let start_time_loader = (new Date()).getTime();
let vector1 = Vector2D(1, 2);
let vector2 = Vector2D(3, 4);
let vector3 = Vector3D(5, 6, 7);
for (i = 0; i < 1_000_000; i++) {
vector1.add(vector2);
vector3.x += 10;
vector2.y++;
vector2.Magnitude();
}
console.log("loader time=" + (new Date().getTime() - start_time_loader));
})();
Now we get to see what it costs for us to have that pretty AssemblyScript loader syntax. This JavaScript creates an
object to hold the naked calls to the Vector2D
WebAssembly class called nVector2D
, and one
for the Vector3D
class called nVector3D
. We then set the variable
start_naked_time
to the current time and initialize three vector objects. Two of the vector objects are
Vector2D
objects, and one is a Vector3D
object. After initializing the vectors, We loop
one million times, making calls to those objects I chose somewhat arbitrarily. I didn’t put a whole lot of thought
into which calls to make, so this isn’t a perfect performance test. The goal is to get some numbers and see how they
compare. I figure as long as we make the same calls to the naked and loader versions, we should be able to get a
reasonable comparison. We then use console.log
to log out the amount of time it took to initialize the
vectors and run through the loop. After logging the time it took to make the naked calls, we use the
loader.demangle
function to create the Vector2D
and Vector3D
constructor
functions. We then initialize start_time_loader
to the current time and call the Vector2D
and Vector3D
functions to create three objects mirroring what we did earlier with the naked
initialization calls. We loop one million times executing the same functions as earlier, except through the loader.
Finally, we log
out the amount of time it took to do everything through the loader. Now that we have
created the vector_perform.js function, let’s run it from the command
line by executing:
node vector_perform.js
This is the output I got when I ran it:
naked time=65
loader time=111
As you can see, the loader took almost twice as long to execute. The difference was even starker when I included the initialization calls in a loop. If you are going to use the AssemblyScript loader, it would probably be a good idea to structure your code to make as few calls as possible between the JavaScript and AssemblyScript.
Summary
In this tutorial, we learned how to create and export classes from AssemblyScript and compile those classes into
both WebAssembly Text and a binary WASM module. We looked at the effect that adding the private
modifier to an attribute had on our compiled WebAssembly module. We instantiated a compiled WASM module from a
JavaScript Node app and called the methods of our AssemblyScript class. We then learned how to do this using the
AssemblyScript loader module. We extended our Vector2D
class creating a Vector3D
class,
exporting and calling its methods. Finally, we compared the performance of the AssemblyScript loader with direct
calls to the WebAssembly module from JavaScript.
I hope you enjoyed this tutorial. If you have any questions, please feel free to message me on Twitter: @battagline
I will be posting more tutorials and walkthroughs on my wasm book playground.