DEV Community

jtenner
jtenner

Posted on • Updated on

An AssemblyScript Primer (for TypeScript developers)

Edit: this guide is accurate up until the end where the topic is about pushing binary data to wasm. This will be updated soon! Please check back again tomorrow!

Leveraging web assembly is quite the challenge, but there are many good ways to do it. Using the emscripten toolchain, or the rust wasm compile target is a great way for developers who sit close to the metal to get their hands dirty and make web applications powered by web assembly.

However, regular developers might find that AssemblyScript, which compiles a dialect of TypeScript to Web Assembly, might be more accessible!

GitHub logo AssemblyScript / assemblyscript

Definitely not a TypeScript to WebAssembly compiler 🚀

AssemblyScript logo

Test status Publish status npm compiler version npm loader version Discord online

AssemblyScript compiles a strict variant of TypeScript (basically JavaScript with types) to WebAssembly using Binaryen. It generates lean and mean WebAssembly modules while being just an npm install away.

About  ·  Introduction  ·  Quick start  ·  Development instructions

Contributors

Contributor logos

Thanks to our sponsors!

Most of the core team members and most contributors do this open source work in their free time. If you use AssemblyScript for a serious task or plan to do so, and you'd like us to invest more time on it, please donate to our OpenCollective. By sponsoring this project, your logo will show up below. Thank you so much for your support!

Sponsor logos

AssemblyScript is currently on github in it's latest and proudest form. On the dev branch is a garbage collected version that uses reference counting to help with memory management. We will be using the dev branch as an example.

AssemblyScript is definitely one of the most interesting languages I have ever used. Using and writing TypeScript to get native performance is like attaching a Rocket Ship to a steam locomotive. If it sounds crazy, you'd be right.

I want to make something perfectly clear. Every programming solution is just another set of problems to pick up and learn. So don't fret! Spin up a test project and get your hands as dirty as you can.

How to use AssemblyScript

So let's hop right into it! We will need to install a new project, so let's run some commands:

npm init
npm install assemblyscript/assemblyscript
npx asinit .

It will ask you a few questions, create a folder structure, touch a few files, and modify your package.json file to contain a few convenience npm scripts to help you get started. Here is a list of the important files it will create and modify:

 Modified: ./package.json
Directory: ./assembly/
  Created: ./assembly/index.js
  Created: ./assembly/tsconfig.json

Notice that AssemblyScript creates a special typescript configuration. That's because AssemblyScript comes with a set of types and rules. These types are maintained to match the functions in the AssemblyScript standard library.

First, let's look at the ./assembly/index.ts file.

// ./assembly/index.ts
/**
 * Exporting a function from the index.ts file will cause AssemblyScript to
 * generate a corresponding function that will be exported to JavaScript.
 **/
export function add(a: i32, b: i32): i32 {
  return a + b;
}

AssemblyScript has some special number types, and they will be explained later in this document. Instead, for now, let's actually build this AssemblyScript module. We do this by running the asbuild npm script that was created for us.

npm run asbuild

This creates a bunch of new web assembly modules a newly created ./build/ folder.

Created: ./build/optimized.wasm
Created: ./build/optimized.wasm.map
Created: ./build/optimized.wat
Created: ./build/untouched.wasm
Created: ./build/untouched.wasm.map
Created: ./build/untouched.wat

When debugging your module it's best to use the untouched version. When you're ready to make a bundle with your module, ship the optimized version. AssemblyScript generates a sourcemap and a .wat file too, which is the text representation of the generated .wasm file. If you are just getting started, you should definitely inspect the .wat file and take a look at what it generates.

Consuming the AssemblyScript Module

AssemblyScript comes with its own loader. It provides a standard way of instantiating the module.

// ./src/index.ts

/**
 * Import the AssemblyScript loader here. If this code runs in the browser,
 * call the `instantiateStreaming` function, otherwise the `instantiateBuffer`
 * method is used in node.js.
 */
import { instantiateStreaming, ASUtil } from "assemblyscript/lib/loader";

/**
 * Defining an interface like this allows you to define the shape of the exported
 * Web Assembly module. Each parameter is a number. Later, when we want to push
 * a string into our module, we will have to generate a pointer to a string.
 * The add function takes two integer parameters and will assume the value is `0`
 * if the parameter is not provided.
 */
interface MyApi {
  add(a: number, b: number): number;
}

/**
 * Imports are used to specify functions that need to be linked. This will be
 * discussed in greater detail later. For now, leave the imports object empty.
 **/
const imports: any = {};

/**
 * Now, let's instantiate our module. Using `fetch()` allows the browser to 
 * download and parse the module at exactly the same time.
 */
async function main(): void {
  var interop: ASUtil & MyApi =
    await instantiateStreaming<MyApi>(fetch("../build/untouched.wasm"), imports);

  // Finally, call the add function we exported
  console.log("The result is:", interop.add(1, 2));
}

The console should output the expected result, which is...

The result is: 3

Next, let's discuss the Language itself.

The AssemblyScript Language

AssemblyScript Numbers

The AssemblyScript programming language is a slightly modified subset of TypeScript. It comes with a standard library that should be familiar to JavaScript developers, because their goal was to stick as close to the ECMAScript specification as possible. In fact, most of the functions you'll need have already been implemented, or are limited in a special way that is relevant to being run from within web assembly.

AssemblyScript takes direct advantage of the specialized number types that are implemented in the Web Assembly specification. As a result, the number types that Web Assembly uses all have a type alias that resolves to number when using existing TypeScript tooling.

var int8: i8 = <i8>0; // 8-bit signed integer [-128 to 127]
var uint8: u8 = <u8>0; // 8-bit unsigned integer [0 to 255]
var int16: i16 = <i16>0; // 16-bit signed integer [-32,768 to 32,767]
var uint16: u16 = <u16>0; // 16-bit unsigned integer [0 to 65,535]
var int32: i32 = <i32>0; // 32-bit signed integer [-2,147,483,648 to 2,147,483,647]
var uint32: u32 = <u32>0; // 32-bit unsigned integer [0 to 4,294,967,295]
var int64: i64 = <i64>; // 64-bit signed integer [-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807]
var uint64: i64 = <u64>0; // 64-bit unsigned integer [0 to 18,446,744,073,709,551,615]
var float32: f32 = <f32>0.0; // 32-bit float [32 bit float range]
var float64: f64 = <f64>0.0; // 64-bit float [64 bit float range]
var pointer: usize = <usize>0; // a 32/64-bit pointer to a location in memory

All the number types are stored in i32, i64, f32, and f64 values, just like regular web assembly numbers.

Some math operations require explicit conversions to different number types. If you mess up and accidentally add a float and an integer together, the compiler will remind you to perform a proper number cast.

AssemblyScript Functions

Most functions must be annotated with return types.

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

var myVerboseSumFunction: (a: i32, b: i32) => i32 =
  (a: i32, b: i32): i32 => a + b;

This becomes awfully verbose, especially when you just want to add two numbers together. However, it's a great habit to be in when writing your functions to annotate them explicitly. When performing array maps and reduces, your arrow functions might look a bit ugly. Since there is no closure supported yet. It may be necessary to hoist these sort of functions to the module's global scope instead.

/**
 * Use every function parameter here. It's quite verbose.
 **/
function add(left: i32, right: i32, index: i32, self: Int32Array): i32 {
  return left + right;
}

var myArray: Int32Array = new Int32Array(100);

var sum: i32 = myArray.reduce(add);

Now that AssemblyScript has reference counting, Closure will eventually be supported.

AssemblyScript Classes

Classes are easy to use in AssemblyScript. For example, let's take a vector class:

/**
 * Exporting a class in an AssemblyScript module only exports its functions.
 * Emscripten generates glue code for classes, and AssemblyScript does not come
 * with this feature. Instead, exporting a class to JavaScript will add all the
 * prototype functions, property gets/sets, and constructor functions to the
 * exports object.
 */
export class Vec_3 {

  /**
   * Constructors work exactly like TypeScript.
   */ 
  constructor(
    public x: f64 = 0.0,
    public y: f64 = 0.0,
    public z: f64 = 0.0,
  ) {}

  /**
   * Operator overloading is supported in AssemblyScript using the `@operator`
   * decorator. We can even ask this computation to happen inline with the 
   * `@inline` function decorator to cause this computation to happen in an 
   * "inline" fashion. It's limited to operations that can be performed on the 
   * *same* object type. Calling a "+" operator on a `Matrix` with a `Vector` is 
   * not valid in AssemblyScript.
   */
  @inline @operator("+")
  protected add_vector(value: Vec_3): Vec_3 {
    return new Vec_3(this.x + value.x, this.y + value.y, this.z + value.z);
  }

  /**
   * To make a computed property, follow the ECMAScript syntax for computed
   * properties.
   **/
  public get length(): f64 {
    return Math.sqrt(this.x * this.x + this.y * this.y + this.z + this.z);
  }
}

If a type needs to be nullable, then we use the type union syntax.

/**
 * Nullable types are only valid on *reference* types, or strings.
 **/
var my_vector: Vec_3 | null = null;

/**
 * Nullable numbers, when set to null will equal `0`. Currently, this is a
 * limitation of the Web Assembly specification. AssemblyScript cannot discern
 * between the number `0` and `null`. Therefore, all the number types must be
 * represented as valid numeric values.
 **/
var my_number: i32 | null = null;
assert(my_number == 0); // true

The AssemblyScript compiler will display a warning if a nullable number type is used. This is expected to change when multi-value return types become supported. Multi-value return types will enable returning multiple numbers on the stack instead of just a single value, thus allowing AssemblyScript to discern between null and 0.

You can check out the overview on multi-value returns here:

GitHub logo WebAssembly / multi-value

Proposal to add multi-values to WebAssembly

Build Status

Multi-value Proposal for WebAssembly

Note: This proposal has been merged into the WebAssembly standard on 2020/04/09.

This repository is a clone of github.com/WebAssembly/spec/ It is meant for discussion, prototype specification and implementation of a proposal to add support for returning multiple values to WebAssembly.

Original README from upstream repository follows...

spec

This repository holds a prototypical reference implementation for WebAssembly which is currently serving as the official specification. Eventually, we expect to produce a specification either written in human-readable prose or in a formal specification language.

It also holds the WebAssembly testsuite, which tests numerous aspects of conformance to the spec.

View the work-in-progress spec at webassembly.github.io/spec.

At this time, the contents of this repository are under development and known to be "incomplet and inkorrect".

Participation is welcome. Discussions about new features, significant…

Another really great supported feature of AssemblyScript classes is generics!

/**
 * This is a class that pushes `T` values to the protected `data` array.
 **/
class Writer<T> {
  protected data: T[] = new Array<T>(0); 
  constructor() { }

  @inline
  protected write(value: T): void {
    this.data.push(value);
  }
}

Class extension also works as expected.

class MyWriter extends Writer<f64> {
  constructor() {
    super(); // always call super
  }

  /**
   * Access parent class functions on `super`!
   **/
  public write_value(value: f64): void {
    super.write(value);
  }
}

Linking To Javascript

AssemblyScript reuses the declare keyword to designate a linked function. Usually declare is used to describe an existing environment function that is defined elsewhere, but here it makes sense to use this syntax to generate a Web Assembly import.

Let's take the following imported function.

var imports: any = {
  customMath: {
    add(a: number, b: number): number {
      return a + b;
    }
  }
};

var wasm: ASUtil = instantiateStreaming<MyAPI>(fetch("./module.wasm"), imports);

Next, tell the module where to find the add function within your module.

// @ts-ignore: import a custom_add function from the customMath.add namespace
@external("customMath", "add")
export declare function custom_add(a: f64, b: f64): f64;

Each parameter, regardless of type, will become a 64-bit float number when Web Assembly calls into JavaScript. This is the same for returning object references and strings. For example, passing an Image or a string to JavaScript might look something like this:

class Image {
  width: i32;
  height: i32;
}

@external("image", "load")
export declare function image_load(image: Image, source: string): void;

Then we have to ask the AssemblyScript loader to unpack the string values and access the memory ourselves manually.

// ./src/index.ts

var wasm: ASUtil<T>;

var imports = {
  image: {
    // image references and string references are pointers
    load(imgPointer: number, sourcePointer: number): void {
      // Do something with source now
      var source: string = wasm.__getString(sourcePointer);
      fetch(source).then(e => e.blob())
        .then(e => createImageBitmap(e))
        .then(e => {
          var imageIndex: number = imgPointer / 4; // the pointer is properly aligned
          wasm.I32[imageIndex] = e.width; // width property is first i32
          wasm.I32[imageIndex + 1] = e.height; // height is second property
        });
    }
  }
};

wasm = await instantiateStreaming<T>(fetch("my/assemblyscript.wasm"), imports);

Writing and reading memory from web assembly is quite manageable.

Conclusions

There are limits and pitfalls in every language. AssemblyScript is not an exception, but it is a fascinating and amazing tool to speed up your code that you can easily put on your toolbelt. Please feel free to provide feedback on this article, or ask any questions in the comment section.

Don't forget to use a testing framework. 😉

GitHub logo jtenner / as-pect

🔥Blazing🔥 fast testing with AssemblyScript

jtenner/as-pect

This package is a monorepo that contains the cli and the core for the @as-pect packages.

Greenkeeper badge Build Status Coverage Status lerna

Write your module in AssemblyScript and get blazing fast bootstrapped tests with WebAssembly speeds!

Documentation

To view the documentation, it's located here on the gitbook. If there are any issues with the docs, please feel free to file an issue!

Contributors

To contribute please see CONTRIBUTING.md.

Thanks to @willemneal and @MaxGraey for all their support in making as-pect the best software it can be.

Other Contributors:

Special Thanks

Special thanks to the AssemblyScript team for creating AssemblyScript itself.

Happy Coding!
-Josh

Top comments (4)

Collapse
 
ryougi1 profile image
James Long Gu

Hi Josh, thanks for the interesting read!

Inside index.ts, I'm getting the error 'Type "ASUtil" is not generic'. It's the same code you have, any idea why?

Collapse
 
jtenner profile image
jtenner

Yep! This AssemblyScript primer is outdated in very little ways. Instead, use ASUtil & T. The generic part used to be the exported wasm API that you expect the module to return. Now it's just a plain interface.

const wasm: ASUtil & T = instantiateBuffer<T>(buffer, imports);
Collapse
 
ryougi1 profile image
James Long Gu

Cheers Josh! Managed to import the AS loader and run some basic tests with __allocString and __getString. Works like a charm!

Thread Thread
 
jtenner profile image
jtenner

That's great! Make sure to speak up when you have problems. Assemblyscript is a burgeoning platform and things don't always work! You can also check out docs.assemblyscript.org/ for lots of documentation.

Good luck!