What is AssemblyScript?

What is AssemblyScript?

AssemblyScript is a TypeScript-like language, designed by Daniel Wirtz and Max Graey in 2017, that compiles to WebAssembly. In a nutshell, it’s a language for WebAssembly.

This guide will cover the use cases, best features, and common pitfalls of AssemblyScript, as well as how to use it effectively.

Why AssemblyScript

Writing directly in WebAssembly can be difficult, a little cryptic, and time-consuming, as it’s a low-level language. This is, therefore, not how you want to write or read your code.

Other possible options are to use C, C++, or Rust, then compile to WebAssembly. However, despite being very popular and well established, those languages just aren’t being picked up by new developers, especially web developers who regularly use JavaScript, due to a steep learning curve, even though Rust is one of the most loved programming languages in recent times. C, C++, and Rust show up in niche use cases, and are rarely necessary for daily web development work. This can cause WebAssembly to be seen as inaccessible to web developers.

This was what the AssemblyScript team wanted to change. In contrast to C and C++ that added support for WebAssembly after the fact, AssemblyScript was designed for WebAssembly specifically.

AssemblyScript uses familiar TypeScript syntax, which is nice for those of us accustomed to TypeScript. If you know JavaScript, you can look at TypeScript and pretty much understand it, at least for the most part.

AssemblyScript is definitely not a TypeScript-to-WebAssembly compiler, as stated by the AssemblyScript team. Instead, it reuses the syntax and the language and very closely matches the semantics of TypeScript. Therefore, the code that you read in AssemblyScript looks like TypeScript.

However, you cannot take just any existing TypeScript code, throw it into AssemblyScript, and get it working in WebAssembly. TypeScript has things that just don't work in WebAssembly, like the fact that WebAssembly variables cannot natively hold a string, a number, an object, and a class. AssemblyScript does support these types by managing blocks of WebAssembly memory for you. Because of these differences, the AssemblyScript team had to add some restrictions to the language such as a stricter type system.

AssemblyScript has about 3,000 projects hosted on GitHub, either partially or completely written in AssemblyScript. It’s also got about 5,000 downloads on npm as of March 2021.

Benefits of WebAssembly

Before WebAssembly, JavaScript was the only language that was officially supported by the web. Here are some of the benefits of adding WebAssembly to your web development toolkit:

Using Existing Code

With WebAssembly, we can use existing code written in programming languages like C and C++ and compile to WebAssembly using the Emscripten compiler. Rust also supports WebAssembly out of the box, which allows you to make use of the entire ecosystem of these languages and bring them to the web, even though those libraries might not have been written with the web in mind.

Improved Performance

For the longest time, WebAssembly and JavaScript had relatively near peak performance, as they were optimized by the same engine. The difference, however, is that JavaScript is a dynamically typed language. The engine for JavaScript needs to run the code, observe it, and then start optimizing.

With WebAssembly, that’s already encoded in the WebAssembly file. In WebAssembly, the optimizing compiler kicks in immediately. A compiler called Liftoff generates code quickly at the cost of not generating optimal code. WebAssembly can start running as quickly as possible the second that Liftoff is done, and then an optimizing compiler called TurboFan kicks in and starts optimizing.

As a result, WebAssembly is much more predictable as to how it will behave. While you can reach similar peak performance with JavaScript, it can also regress to being slower. This is known as de-optimization. For example, you can call the same function with a string, with an object, and with an array. If the function gets called with a string all the time, then it generates machine code for handling a string. However, the first time you call it with a different type, it has to fall back to interpreting because the machine code is wrong now if you suddenly call it with an array.

You can expect even more performance from WebAssembly as it increases access to things that JavaScript doesn't have, like:

  • actual shared memory
  • concurrency with threads
  • single instruction, multiple data (SIMD)

Also, because the language decides what type of runtime to ship, things like garbage collection can be in your control. JavaScript’s garbage collector will run whether you want it to or not. In WebAssembly, you can actually change the garbage collector to use something with a bit more flexibility. This can be a big plus for games engines as it can lead to improved performance.

Binary Size

WebAssembly’s binary format is a very compact binary representation of the code that you wrote. Since WebAssembly encodes data using numeric types, you’ll need a bit of JavaScript called glue code whenever you want to pass anything that is not a number (eg, an object, an array, a string) from JavaScript to WebAssembly and back again. Glue code converts what the WebAssembly file understands into what JavaScript understands. Often 80 percent of the file goes away, and you’re left with a more optimized 20 percent.

Getting Started with AssemblyScript

To get started with AssemblyScript, you could either install AssemblyScript with npm or you could use WebAssembly Studio where you can try out AssemblyScript in a little IDE in the browser. It compiles in the browser, so you can just play around without having to install anything.

However, this guide uses npm, so make sure you have the latest version of Node.js installed.

In your terminal, open a new folder.

$ mkdir assemblyscript_project && cd assemblyscript_project

Inside that folder, install AssemblyScript.

$ npm init
$ npm install assemblyscript

Create a new file called main.ts.

$ touch main.ts

Open this assemblyscript_project folder in the text editor of your choice. This guide uses Visual Studio Code.

Inside the main.ts file, add the following code:

export function main(): u32 {
    return 7;
}

This exports a function called main and returns 7. Compile this to WebAssembly by running the following command:

$ npx asc -b main.wasm main.ts

The AssemblyScript compiler is called asc. You passed your main.ts file with a -b flag, so the binary output file should be main.wasm.

To load in your browser, create a new file called index.html.

$ touch index.html

Inside this file, add the following code:

<!DOCTYPE html>
<html lang="en">
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <head>
        <title>AssemblyScript</title>
    </head>
    <body>
        <script type="module">
            const { instance } = await WebAssembly.instantiateStreaming(
                fetch('main.wasm')
            );

            console.log(instance.exports.main());
        </script>
    </body>
</html>

Let’s break down what just happened:

  1. You fetched the wasm file that was compiled earlier and used the WebAssembly.instantiateStreaming function.
  2. This function will give back to you an instance, and every exported function will be on instance.exports.
  3. Finally, you log the number 7 to your console.

Open the index.html file in your browser. You should see the number 7 logged to your console.

If you got an error in your browser, in your terminal run this script.

$ python -m http.server 8000 --bind 127.0.0.1

You must have python installed on your local machine, we are using it to run a local server in order to serve our .wasm file properly. Next open localhost in your browser, http://localhost:8000/index.html, and all should work perfectly fine.

The compiler can also generate a human-readable file:

$ npx asc -b main.wasm -t main.wat main.ts

It’s not required, but it can be helpful sometimes to see if certain exports are present or just what the function looks like.

Optimization

By default, the AssemblyScript compiler uses no optimizations to improve performance. However, if you're shipping your AssemblyScript/WebAssembly to production, make sure you use the -O flag, which is an optimization pass.

$ npx asc -b main.wasm -O main.ts

Not only does it make your AssemblyScript code faster, but it’s probably also significantly smaller.

Peculiarities with TypeScript

Take a look at the function shown below. You’ll notice that the return type is not a TypeScript type. This is one of the differences between AssemblyScript and TypeScript, as WebAssembly (Wasm) does not support JavaScript types like strings or numbers on its own. Though Wasm doesn’t support these types directly, AssemblyScript does, they are just encoded in WebAssembly (Wasm) differently, so using these types will require a bit of JavaScript called glue code as stated earlier.

export function main(): u32 {
    return 7;
}

You can see the full list of WebAssembly types here.

To use more complex data types in AssemblyScript, an awesome library called AssemblyScript loader can be used to load data types like strings, arrays into WebAssembly memory.

VS Code Configuration

Visual Studio Code, which is the text editor you are following within this guide, by default will put a red error line under it. To fix that, VS Code provides a default tsconfig.json file.

$ touch tsconfig.json

Add the following code inside this file:

{
    "extends": "assemblyscript/std/assembly.json"
}

VS Code will now think this is a kind of valid TypeScript, so no more error line.

Using Imports

You can also import a function from a different module, similarly to TypeScript. The compiler will take care of building it all together, linking, and recognizing which functions you're actually calling.

Update the main.ts file. You’re importing the alert module from sandbox and calling the alert function inside the main function.

import { alert } from './sandbox';
export function main(): void {
    alert(100);
}

Create the sandbox file.

$ touch sandbox.ts

Add the following code to it:

export declare function alert(v: u32): void;

Currently, there is no implementation of alert. The declare keyword, which also exists in TypeScript, assures the validator that this function exists once you run the code; you just didn't implement it yourself.

A common use case is when a browser supports an API that TypeScript doesn't know about yet.

The AssemblyScript compiler will only note that this function takes a u32, however for this to work completely, you need to provide the imports object to the instantiate function. Update index.html like so:

...
const { instance } = await WebAssembly.instantiateStreaming(
    fetch('main.wasm'),
        {
            sandbox: { alert: window.alert },
        }
    );
...

Compile your AssemblyScript code to WebAssembly and refresh your browser to see the alert message of 100:

$ npx asc -b main.wasm main.ts

Not only can you call from JavaScript into WebAssembly, but your main function can call back into JavaScript as well through the import object.

An array can cause the instantiate function to fail, which means it ran out of memory. AssemblyScript actually has memory management, however it needs something to call when things go wrong, called an abort function.

Edit the main.ts file to see an example of this.

export function main(): u32 {
    const arr: u32[] = [4, 5];
    arr.push(6);
    return arr[0] + arr[1] + arr[2];
}

Edit the index.html file:

...
const { instance } = await WebAssembly.instantiateStreaming(
    fetch('main.wasm'),
        {
            env: {
                abort() {
                    throw Error('An error occurred');
                },
            },
        }
    );
...

A more elegant solution is to make use of the AssemblyScript Loader module, which can take care of all this glue code for you.

Another library to check when working with AssemblyScript is as-bind, a library that passes high-level data structures between AssemblyScript and JavaScript.

Conclusion

In this post, you were introduced to AssemblyScript and how it relates to TypeScript. You also learned about WebAssembly and why you might consider it for your next project over JavaScript, and finally you saw how to install AssemblyScript with some working examples.

WebAssembly has been around for several years now, and there are many tools out there that can assist you in writing such applications. One of these is Suborbital, a platform for building powerful web services with WebAssembly.

References

AssemblyScript - HTTP 203 by Jake and Surma featured in Google Chrome Developers AssemblyScript from Wikipedia

Cover photo: Jumbled letters, by Surendran MP