The introductory Guide to AssemblyScript – Danny Guo

WebAssembly, or Wasm, was added to the Web browser standard relatively recently. And it has the potential to expand the capabilities of the Web platform.

Despite WebAssembly’s steep learning curve, AssemblyScript provides a simple gateway. Let’s take a look at why WebAssembly is such a great technology and how we can unlock its potential with AssemblyScript.

WebAssembly

WebAssembly is a relatively low-level language in browsers. It gives developers an alternative compilation target environment in addition to JavaScript, allowing website code to run in a secure sandbox environment at near-native speed.

It was developed by developers of major browsers, including Chrome, Firefox, Safari and Edge, who agreed on a design in 2017. All of these browsers currently support WebAssembly. In addition, overall, WebAssembly is supported by approximately 87% of browsers.

Webassemblies are distributed in binary form. It is superior to JavaScript in speed and file size. It also has a readable text format (similar to Java bytecode).

When WebAssembly first came out, some developers thought it had the potential to replace JavaScript as the primary browser development language. But it’s best to assume that WebAssembly is just a fully integrated tool with existing Web platforms. This is also its upper objective.

Instead of replacing JavaScript, which is already in use, WebAssembly is appealing to more people through the new features it enables. Currently WebAssembly does not have direct access to the DOM, and most existing sites still prefer to use JavaScript, which has been optimized over the years to be fast enough in most cases. Here are some possible WebAssembly usage scenarios:

  • The game
  • Scientific research simulation and computation
  • CAD application
  • Image and video editing

These apps tend to be known as desktop apps. With WebAssembly, the performance of these cpu-heavy applications is closer to that of native applications, and it makes sense to scale to the Web platform.

Existing sites can also benefit from WebAssembly. Figama is a real example. It uses WebAssebly, which significantly reduces load times. If a site has a lot of computation, replacing this source code with WebAssembly can be a good choice.

By now you may have developed some interest in WebAssembly. You can just learn and write the language itself, but it’s really only designed as a compilation target. It is also designed to have good support for C and C++ languages. Go also added support after its 1.11 release. Rust is also exploring it.

But you probably don’t want to learn any of these languages in order to use WebAssembly. Now it’s time to bring AssemblyScript in.

AssemblyScript

AssemblyScript is a TypeScript to WebAssembly compiler. TypeScript from Microsoft adds the concept of type to JavaScript. It has since become a very popular language. AssemblyScript is easy to get started with even if you are not familiar with TS, as it only uses a limited subset of TS.

Because of its similarity to JavaScript, AssemblyScript allows Web developers to easily add WebAssembly to their sites without the need for a completely new language to participate in the process.

Give it a try

Let’s start by writing our first AssemblyScript module (all of the following code can be found in this Github Repo). We need a minimum version 8.0 Nodejs to get WebAssembly support.

Enter an empty folder. Create a package.json file. Then install AssemblyScript. Note that we need to install it directly from the Github repo. The developers didn’t release it to NPM because they didn’t think it was ready for use

Translator’s note: It has been published, and I will revise some of the original text next.

mkdir assemblyscript-demo
cd assemblyscript-demo
npm init
npm install --save-dev assemblyscript
Copy the code

Use the asinit command to generate the scaffold file.

npx asinit .
Copy the code

Our package.json file should include these scripts

{
  "scripts": {
    "asbuild:untouched": "asc assembly/index.ts -b build/untouched.wasm -t build/untouched.wat --sourceMap --validate --debug"."asbuild:optimized": "asc assembly/index.ts -b build/optimized.wasm -t build/optimized.wat --sourceMap --validate --optimize"."asbuild": "npm run asbuild:untouched && npm run asbuild:optimized"}}Copy the code

Index.js in the root directory looks like this:

const fs = require("fs");
const compiled = new WebAssembly.Module(fs.readFileSync(__dirname + "/build/optimized.wasm"));
const imports = {
  env: {
    abort(_msg, _file, line, column) {
       console.error("abort called at index.ts:" + line + ":"+ column); }}};Object.defineProperty(module."exports", {
  get: (a)= > new WebAssembly.Instance(compiled, imports).exports
});
Copy the code

It lets us use our WebAssembly module as if it were a normal JavaScript module by requiring it.

The Assembly directory contains our AssemblyScript source code. Here is a simple addition example.

export function add(a: i32, b: i32) :i32 {
    return a + b;
}
Copy the code

Think of the function’s signature as add(a: number.b: number): number, and it becomes TypeScript. The reason for using I32 is that AssemblyScript uses WebAssembly’s special distinction between integer and floating point types, rather than TypeScript’s unified view of the number type

Let’s build this example

npm run asbuild
Copy the code

These files should appear in the build directory:

optimized.wasm
optimized.wasm.map
optimized.wat
untouched.wasm
untouched.wasm.map
untouched.wat
Copy the code

We can get a normal build and an optimized build. For each version, there is a.wasm binary, a.wasM. map source code map, and a.wat binary in readable text format. This binary file in text format is designed to be readable. But for our ultimate purpose, we don’t need to read or understand it — one of the reasons we used AssemblyScript was to avoid dealing with native WebAssemblies.

Start Node and call the compiled module just like any other normal module

$node Welcome to node.js v12.10.0.type".help" for more information.
> const add = require('./index').add;
undefined
> add(3, 5)
8
Copy the code

Calling WebAssembly from Node is that simple!

Adding a Listening Script

Because AssemblyScript does not currently have a Watch mode, I recommend using a tool like OnChange that automatically rebuilds the corresponding module when the code changes.

npm install --save-dev onchange
Copy the code

Add the script asbuild:watch in pakcage. Json. Add the -i flag to make it perform a build the first time it runs

{
  "scripts": {
    "asbuild:untouched": "asc assembly/index.ts -b build/untouched.wasm -t build/untouched.wat --sourceMap --validate --debug"."asbuild:optimized": "asc assembly/index.ts -b build/optimized.wasm -t build/optimized.wat --sourceMap --validate --optimize"."asbuild": "npm run asbuild:untouched && npm run asbuild:optimized"."asbuild:watch": "onchange -i 'assembly/**/*' -- npm run asbuild"}}Copy the code

Instead of repeatedly re-running asbuild, you can now run AsBuild: Watch once

performance

Next, we’ll run a basic benchmark test to see how much performance has improved. WebAssembly specializes in CPU heavy tasks, such as algebra. So here’s how to start: Determine if a number is prime.

The reference implementation is the following, which is a very primitive, very crude algorithm, after all, the goal is to test such high-intensity algebraic calculations

function isPrime(x) {
  if (x < 2) {
    return false;
  }

  for (let i = 2; i < x; i ++) {
    if (x % i === 0) {
      return false; }}return true;
}
Copy the code

An equivalent AssemblyScript implementation would simply add some type annotations:

function isPrime(x: u32) :bool {
  if (x < 2) {
    return false;
  }

  for (let i: u32 = 2; i < x; i++) {
    if (x % i === 0) {
      return false; }}return true;
}
Copy the code

We’ll also use benchmark.js

npm install --save-dev benchmark
Copy the code

Create benchmark.js file:

const Benchmark = require('benchmark');

const assemblyScriptIsPrime = require('./index').isPrime;

function isPrime(x) {
  for (let i = 2; i < x; i++) {
    if (x % i === 0) {
      return false; }}return true;
}

const suite = new Benchmark.Suite;
const startNumber = 2;
const stopNumber = 10000;

suite.add('AssemblyScript isPrime'.function () {
    for (let i = startNumber; i < stopNumber; i++) {
        assemblyScriptIsPrime(i);
    }
}).add('JavaScript isPrime'.function () {
    for (let i = startNumber; i < stopNumber; i++) {
        isPrime(i);
    }
}).on('cycle'.function (event) {
    console.log(String(event.target));
}).on('complete'.function () {
    const fastest = this.filter('fastest');
    const slowest = this.filter('slowest');
    const difference = (fastest.map('hz') - slowest.map('hz')) / slowest.map('hz') * 100;
    console.log(`${fastest.map('name')} is ~${difference.toFixed(1)}% faster.`);
}).run();
Copy the code

Running Node Benchmark on my machine yields these results:

AssemblyScript isPrime x 74.00 ops/sec ±0.43% (76 runs sampled)
JavaScript isPrime x 61.56 ops/sec ±0.30% (64 runs sampled)
AssemblyScript isPrime is ~20.2% faster.
Copy the code

It’s important to note that this is a small, unscaled benchmark, so be careful before applying the results to serious situations.

For more information about AssemblyScript benchmarks, check out WasmBoy Benchmark and Wave Qeuation Benchmark

Load module

Next let’s load our module into the web page. Create an index.html


      
<html>
    <head>
        <meta charset="utf-8" />
        <title>AssemblyScript isPrime demo</title>
    </head>
    <body>
        <form id="prime-checker">
            <label for="number">Enter a number to check if it is prime:</label>
            <input name="number" type="number" />
            <button type="submit">Submit</button>
        </form>

        <p id="result"></p>

        <script src="demo.js"></script>
    </body>
</html>
Copy the code

Create a demo.js.

In theory there is more than one way to load the WebAssembly module, but the most efficient or compile them, and combining the WebAssembly. InstantiateStreaming methods to instantiate them to a Stream. Note that we need to provide an ABORT method that is called when the assertion is invalid

(async() = > {const importObject = {
        env: {
            abort(_msg, _file, line, column) {
                console.error("abort called at index.ts:" + line + ":"+ column); }}};const module = await WebAssembly.instantiateStreaming(
        fetch("build/optimized.wasm"),
        importObject
    );
    const isPrime = module.instance.exports.isPrime;

    const result = document.querySelector("#result");
    document.querySelector("#prime-checker").addEventListener("submit", event => {
        event.preventDefault();
        result.innerText = "";
        const number = event.target.elements.number.value;
        result.innerText = `${number} is ${isPrime(number) ? ' ' : 'not '}prime.`; }); }) ();Copy the code

We need a server to use WebAssembly. InstantiateStreaming, these modules need to be provided according to the MIME type application/wasm. Static – server can provide the function (translator note: I can’t find where VSCode’s live server configures MIME Type anyway.

npm install --save-dev static-server
Copy the code

Add a line of script to package.json:

{
  "scripts": {
    "serve-demo": "static-server"}}Copy the code

Run NPM run serve-demo and open localhost in your browser. If you submit a number in the table, you should see a prompt telling you whether the number is prime. Now we’ve taken care of the steps from writing AssemblyScript to using it in a web page.

conclusion

WebAssembly, even with the expansion of AssemblyScript, won’t magically make all websites faster, but that doesn’t matter. WebAssembly exists because it enables a wide range of Web capabilities.

AssemblyScript, on the other hand, makes WebAssembly more accessible to developers, making it easier to use WebAssembly in places that require a lot of algebra while still using JavaScript.