First introduction to AssemblyScript

Support for WebAssembly (Wasm) technology has appeared in browsers relatively recently. But this technology can well expand the capabilities of the web, making it a platform capable of supporting such applications that are usually perceived as desktop.



Mastering WebAssembly can be a daunting task for web developers. However, the AssemblyScript compiler can improve the situation.









The author of the article, which we are publishing today’s translation, offers to first talk about why WebAssembly is a very promising technology, and then take a look at how AssemblyScript can help unlock Wasm’s potential.



Webassssembly



WebAssembly can be called a low-level language for browsers. It gives developers the ability to create code that compiles into something other than JavaScript. This allows the programs included in the web pages to work almost as fast as native applications for various platforms. Such programs run in a limited, secure environment.



Representatives of the teams responsible for the development of all leading browsers (Chrome, Firefox, Safari and Edge) were involved in the creation of the WebAssembly standard. They agreed on a system architecture in early 2017. Now all of the above browsers support WebAssembly. In fact, this technology can be used in approximately 87% of browsers.



WebAssembly code exists in binary format. This means that such code is smaller than similar JavaScript code and loads faster. Wasm-code, in addition, can be presented in text format , so that people can read and edit it.



When the WebAssembly standard first appeared, some developers thought that it could well take the place of JavaScript and become the main language of the web. But WebAssembly is best seen as a new tool that integrates well with the existing web platform. This is one of his priority goals .



Instead of replacing JavaScript where this language has been used for a long time and successfully, WebAssembly provides new interesting opportunities for web developers. True, the Wasm code does not have direct access to the DOM, so most existing web projects will continue to use JavaScript. This language, over the years of development and optimization, is already quite fast. And WebAssembly has its own applications :





All of these uses for Wasm are united by what their respective applications are usually considered desktop. But due to the fact that WebAssembly allows you to reach a level of performance close to native, many similar applications can now be implemented using the web platform.



WebAssembly can take advantage of existing web projects. An example is the Figma project. Thanks to the Wasm application, the loading time of this project was significantly improved. If a website uses code that performs heavy calculations, then, in order to improve the performance of this website, it makes sense to replace only such code with a WebAssembly analog.



You might want to try using WebAssembly in your own projects. This language can well be studied and written immediately on it . But, nevertheless, WebAssembly was originally developed as a compilation target for other languages. It was designed with good support for C and C ++. Wasm experimental support appeared in Go 1.11. A lot of effort is also being put into writing Wasm applications in Rust .



But it’s entirely possible that web developers will not want to learn C, C ++, Go, or Rust just to use WebAssembly. What do they do? The answer to this question can give AssemblyScript.



AssemblyScript



AssemblyScript is a compiler that converts TypeScript code to WebAssembly code. TypeScript is a language developed by Microsoft. This is a superset of JavaScript, featuring improved type support and some other features. TypeScript has become a fairly popular language. It should be noted that AssemblyScript is able to convert to Wasm only a limited set of TypeScript constructs. This means that even someone who is not familiar with TypeScript can quickly learn this language at a level sufficient to write code that AssemblyScript understands.



At the same time, considering the fact that TypeScript is very similar to JavaScript, we can say that AssemblyScript technology allows web developers to easily integrate Wasm-modules into their projects without the need to learn a completely new language.



Example



Let's write our first AssemblyScript module. All the code that we are going to discuss now can be found on GitHub . To support WebAssembly, we need at least Node.js 8.



Create a new directory, initialize the npm project and install AssemblyScript:



mkdir assemblyscript-demo cd assemblyscript-demo npm init npm install --save-dev github:AssemblyScript/assemblyscript
      
      





Note that AssemblyScript must be installed directly from the project’s GitHub repository . AssemblyScript has not yet been published in npm, as developers do not yet consider it ready for widespread use.



We will create auxiliary files using the asinit



command included in AssemblyScript:



 npx asinit .
      
      





Now the scripts



section of our package.json



should take the following form:



 {  "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"  } }
      
      





The index.js



file located in the root folder of the project will look 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: () => new WebAssembly.Instance(compiled, imports).exports });
      
      





This allows you to include WebAssembly modules in your code using the require command. That is - the same way as regular JavaScript modules are connected.



The assembly



folder contains the index.ts



file. It has source code written according to AssemblyScript rules. Automatically generated boilerplate code is a simple function to add two numbers:



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





Perhaps you expected the signature of a similar function to look like add(a: number, b: number): number



. So it would look if it were written in ordinary TypeScript. But here, instead of type number



, type i32



. This happens because AssemblyScript code uses specific WebAssembly types for integers and floating point numbers, rather than the generic number type from TypeScript.



Let's assemble the project:



 npm run asbuild
      
      





The following files should appear in the build



folder:



 optimized.wasm optimized.wasm.map optimized.wat untouched.wasm untouched.wasm.map untouched.wat
      
      





There are optimized and regular versions of the assembly. Each version of the assembly gives us a binary .wasm file, a map map .wasm.map, and a textual representation of the binary code in a .wat file. The test presentation of the Wasm code is intended for the programmer, but we won’t even look at this file. Actually, one of the reasons for using AssemblyScript is that it eliminates the need to work with Wasm code.



Now let's run Node.js in REPL mode and make sure that the compiled Wasm-module can be used in the same way as any regular JS-module:



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





In general, this is all that is needed to use WebAssembly technology in Node.js.



Equipping a project with an observer script



In order to automatically rebuild the module during development when making changes to it, I recommend using the onchange package. The fact is that AssemblyScript does not yet have its own system for monitoring file changes. Install the onchange package:



 npm install --save-dev onchange
      
      





Add asbuild:watch



script to package.json



. The -i



flag is included in the command so that the build process starts once when the script is called, before any events occur.



 {  "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"  } }
      
      





Now, instead of constantly running the asbuild



command, just run asbuild:watch



once.



Performance



We’ll write a simple test that will evaluate the performance level of the Wasm code. The main scope of WebAssembly is to solve tasks that use the processor intensively. For example, these are some kind of “heavy” calculations. Let's create a function that finds out if a certain number is prime.



The basic JS implementation of a similar function is shown below. It is arranged very simply, checks the number by brute force, but for our purposes it is suitable, since it performs large amounts of calculations.



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





A similar function, written for the AssemblyScript compiler, looks almost the same. The main difference is the presence of type annotations in the code:



 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; }
      
      





To analyze the performance of the code, we will use the Benchmark.js package. Install it:



 npm install --save-dev benchmark
      
      





Create the 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();
      
      





Here is what I managed to get after running the node benchmark



command on my computer:



 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.
      
      





As you can see, the AssemblyScript implementation of the algorithm was 20% faster than the JS implementation. However, note that this test is a microbenchmark . Do not rely too much on its results.



In order to find more reliable results of the performance research of AssemblyScript-projects, I recommend taking a look at this and this benchmark.



Using a Wasm Module on a Web Page



Let's use our Wasm module on a web page. We start by creating an index.html



file with the following contents:



 <!DOCTYPE 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>
      
      





Now create the demo.js



file, the code of which is shown below. There are many ways to load WebAssembly modules. The most efficient is compiling and initializing them in streaming mode using the WebAssembly.instantiateStreaming function. Please note that we will need to redefine the abort function, which is called if some statement is not executed.



 (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.`;    }); })();
      
      





Next, install the static-server package. We need a server because in order to use the WebAssembly.instantiateStreaming



function, the module must be serviced using the application/wasm



MIME type.



 npm install --save-dev static-server
      
      





Add the appropriate script to package.json



:



 {  "scripts": {    "serve-demo": "static-server"  } }
      
      





Now npm run serve-demo



command and open the local host URL in the browser. If you enter a certain number in the form, you can find out whether it is simple or not. Now, in developing AssemblyScript, we have come a long way from writing code to using it in Node.js and on a web page.



Summary



WebAssembly and, therefore, AssemblyScript, are unable to somehow speed up any site magically. But Wasm never was tasked with accelerating absolutely everything. This technology is remarkable in that it opens the way to the web for new kinds of applications.



Something similar can be said about AssemblyScript. This technology simplifies access to WebAssembly for a large number of developers. It allows, when creating code in a language close to JavaScript, to use the capabilities of WebAssembly to solve complex computational problems.



Dear readers! How do you assess the prospects for using AssemblyScript in your projects?








All Articles