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 classVector2D {
  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.

The Art of WebAssembly

The Art of WebAssembly
Author and expert Rick Battagline eases the reader through Wasm's complexities using clear explanations, illustrations, & plenty of examples.
Learn More

Hands-On Game Development with WebAssembly

Hands-On Game Dev with Wasm
Author and expert Rick Battagline teaches 2D game development fundamentals in C++ using the Emscripten WebAssembly toolchain.
Learn More

Classic Solitaire

ClassicSolitaire.com
Are you bored right now? Play Classic Solitaire and be slightly less bored. Also, it's how I earn a living, so it would really help me out if you wasted time on my site. :-)
Play Solitaire