DEV Community

Gonzalo Ruiz de Villa
Gonzalo Ruiz de Villa

Posted on • Originally published at Medium

AssemblyScript: making WebAssembly more accessible to JavaScript programmers

tl;dr This is an introduction to AssemblyScript: I explain what WebAssembly is, why AssemblyScript maybe an interesting alternative to build WebAssembly for JavaScript developers and, finally, in order to compare JavaScript to AssemblyScript, I comment a small image manipulation project I’ve developed for this purpose.

WebAssembly is one of the biggest revolutions coming to the web, although it is neither Web nor Assembly. WebAssembly, also known as Wasm, is a fast, efficient, safe and low-level bytecode for the Web.

This means that, on one hand, it isn’t an assembly language but bytecode instead. Although both of them are similar in the sense that they are not high-level languages, they are easily understandable, which is something that does not happen with machine code. Thus, they can be classified into an intermediate language category between high-level languages and machine code. The main difference between assembly language and bytecode is that, the first one is created for CPUs while the second is created for virtual machines. That is, one is targeting hardware whereas the other is targeting software.

There is indeed a bytecode textual version, which is named WebAssembly Text Format (or just Wat!).

Additionally, although it’s usually said that Wasm is for the Web, the truth is that it’s not just for the web, because it can be also used for desktop applications, serverless or, even, Crypto and Smart Contracts.

Efficient

WebAssembly was designed to have a binary file format that is easy to download and to compile to machine code. It also allows the code to be compiled at the same time that it is being downloaded. This feature is called Streaming Compilation.

Using a Wasm module from JavaScript is as simple as it follows:

async function run() {
  const {instance} = await WebAssembly.instantiateStreaming(
    fetch("./add.wasm"),
    env: { abort: () => console.log("Abort!") }
  );
  const r = instance.exports.add(1, 2);
  console.log(r);
}
run();

The following way to load a Wasm module suggested by Das Surma will allow you to use Streaming Compilation robustly. It will work even if the Content-Type is not correctly set to application/wasm (Firefox will normally fail, for instance), or even, if you are using Safari (which does not support instantiateStreaming yet).

async function maybeInstantiateStreaming(path, ...opts) {
  // Start the download asap.
  const f = fetch(path);
  try {
    // This will throw either if `instantiateStreaming` is
    // undefined or the `Content-Type` header is wrong.
    return WebAssembly.instantiateStreaming(
      f,
      ...opts
    );
  } catch(_e) {
    // If it fails for any reason, fall back to downloading
    // the entire module as an ArrayBuffer.
    return WebAssembly.instantiate(
      await f.then(f => f.arrayBuffer()),
      ...opts
     );
  }
}

There has been a lot of work on the Web in order to provide a safe environment that protects us from malicious intentions, and Wasm was designed with the same principles. For instance, as JavaScript does too, Wasm is executed in sandboxed environments that keeps it isolated from production environment. As a consequence, for example, it is necessary to use Web File API to access the file system, which is exactly how it needs to be done with JavaScript.

Bytecode

What were the main goals for Wasm design? To be codified in a binary code (very efficient, from the size and loading time point of view), to be executed at native speeds and, also, to take advantage of the common hardware capabilities available in different platforms.

In order to achieve these goals, the authors of Wasm had to build something new (using asm.js as the starting point) instead of using LLVM, Java or .Net bytecode. There fore, they developed a new binary instruction designed to be a portable target for compilation of high level languages like C, C++ or Rust.

“Wat” should I do if I want to program WebAssembly?

One can never know too much, so if you want to learn Wat, go ahead! Nevertheless, if you like JavaScript, look at the following example and correct me if I’m wrong when I say that you would like an alternative to Wat:

(;
  Filename: add.wat
  This is a block comment.
;)
(module
  (func $add (param $p1 i32) (param $p2 i32) (result i32)
    local.get $p1 ;; Push parameter $p1 onto the stack
    local.get $p2 ;; Push parameter $p2 onto the stack
    i32.add ;; Pop two values off the stack and push their sum
    ;; The top of the stack is the return value
  )
  (export "add" (func $add))
)

AssemblyScript

AssemblyScript compiles a strict subset of TypeScript (a typed superset of JavaScript) to WebAssembly. That means that we can take advantage of the JavaScript knowledge to develop Wasm.

In order to illustrate how similar JavaScript and AssemblyScript are, I’ve prepared this small project where I manipulate a picture with code in vanilla JavaScript and in AssemblyScript compiled to Wasm. You can find it here: [https://github.com/gonzaloruizdevilla/image-manipulation-assemblyscript]

In the project, you will see a picture that it’s loaded inside an html canvas and several buttons that will apply different filters to the pictures when clicked. These buttons will execute the filter either with JavaScript or with the Wasm module generated with AssemblyScript.

App screenshot

Applying the different filters we obtain images like these:
Inverted image

Grayscale image

Emboss

In order to use the project, just clone it from Github, and then install AssemblyScript dependency and compile index.ts AssemblyScript file with the following instructions:

npm install
npm run asbuild

It´s interesting to note that, when a Wasm function is called from JavaScript code, the arguments of the call must be of the following types:

  • i32: 32-bit integer
  • i64: 64-bit integer
  • f32: 32-bit float
  • f64: 64-bit float

Obviously, that means that we can’t pass the image as an argument of the call. In order to be able to use a picture’s information from Wasm, first, it should be stored in a shared area of the memory that it will be created using the WebAssembly.Memory class. This shared memory object is used as an argument of the Wasm instantiating function as you can see in the following code:

//A memory created by JavaScript or in WebAssembly code will be accessible and mutable from both JavaScript and WebAssembly.

const memory = new WebAssembly.Memory({ initial:initial * 2 });

//Instantiating Wasm module

const importObject = { env: { memory, abort: () => console.log("Abort!") }};
const {instance} = await WebAssembly.instantiateStreaming(
    fetch("./build/untouched.wasm"),
    importObject
);

//Creating a typed array reference to write into the memory buffer
const mem = new Uint8Array(memory.buffer);

Before calling a Wasm filter, the picture data in the canvas is retrieved and copied into the shared memory. After that, the Wasm filter is called, then the result is read and stored in imageData. Finally, we send imageData to canvas context, redrawing the images.

JavaScript version

//retrieve image pixels (4 bytes per pixel: RBGA)
const data = imageData.data;
//copy to bytes to shared memory
mem.set(data);

//invoque 'fn'  Wasm filter. We need to inform of the image byte size
const byteSize = data.length;
instance.exports[fn](byteSize, ...args);

//copy the response from the shared memory into the canvas imageData
data.set(mem.slice(byteSize, 2*byteSize))
//update canvas
ctx.putImageData(imageData, 0, 0);

There are four different filter functions implemented in both JavaScript and AssemblyScript: invert, grayscale, sepia and convolve (this one is used to apply the blur, edge detection and emboss filters). As you can see below, they are very similar:

function invert(data) {
    for (var i = 0; i < data.length; i += 4) {
        data[i]     = 255 - data[i];     
        data[i + 1] = 255 - data[i + 1]; 
        data[i + 2] = 255 - data[i + 2]; 
    }
};

function grayscale(data){
    for (var i = 0; i < data.length; i += 4) {
        const avg = 0.3  * data[i] + 0.59 * data[i + 1] + 0.11 * data[i + 2];
        data[i]     = avg;  
        data[i + 1] = avg; 
        data[i + 2] = avg; 
    }
}

function sepia(data){
    for (var i = 0; i < data.length; i += 4) {
        const avg = 0.3  * data[i] + 0.59 * data[i + 1] + 0.11 * data[i + 2];
        data[i]     = avg + 100;  
        data[i + 1] = avg + 50; 
        data[i + 2] = avg; 
    }
}

function addConvolveValue(pos, i, data, length){
    return pos >= 0 && pos < length ? data[pos] : data[i];
}

function convolve(data, w, offset, v00, v01, v02, v10, v11, v12, v20, v21, v22){
    console.log( w, offset, v00, v01, v02, v10, v11, v12, v20, v21, v22)
    const divisor = (v00 + v01 + v02 + v10 + v11 + v12 + v20 + v21 + v22) || 1;
    const length = data.length;
    let res = 0;
    let newData = new Uint8Array(length)
    for(let i = 0; i < length; i++){
        if ((i + 1) % 4 === 0) {
            newData[i] = data[i];
            continue;
        }
        let res = v00 * addConvolveValue(i - w * 4 - 4, i, data, length) +
                    v01 * addConvolveValue(i - w * 4, i, data, length) +
                    v02 * addConvolveValue(i - w * 4 + 4, i, data, length) +
                    v10 * addConvolveValue(i - 4, i, data, length) +
                    v11 * data[i] +
                    v12 * addConvolveValue(i + 4, i, data, length) +
                    v20 * addConvolveValue(i + w * 4 - 4, i, data, length) +
                    v21 * addConvolveValue(i + w * 4 , i, data, length) +
                    v22 * addConvolveValue(i + w * 4 + 4, i, data, length);
        res /= divisor;
        res += offset;
        newData[i] = res;
    }
    data.set(newData)
}

AssemblyScript version

/// <reference path="../node_modules/assemblyscript/dist/assemblyscript.d.ts" />

export function invert(byteSize: i32): i32 {
  for (var i = 0; i < byteSize; i += 4) {
    let pos = i + byteSize; 
    store<u8>(pos, 255 - load<u8>(i));
    store<u8>(pos + 1, 255 - load<u8>(i + 1));
    store<u8>(pos + 2, 255 - load<u8>(i + 2));
    store<u8>(pos + 3, 255);
  }
  return 0;
}


export function grayscale(byteSize: i32): i32 {
  for (var i = 0; i < byteSize; i += 4) {
    let pos = i+byteSize;
    const avg = u8(0.3  *  load<u8>(i) + 0.59 * load<u8>(i + 1) + 0.11 * load<u8>(i + 2));
    store<u8>(pos, avg);
    store<u8>(pos + 1, avg);
    store<u8>(pos + 2, avg);
    store<u8>(pos + 3, 255);
  }
  return 0;
}

export function sepia(byteSize: i32): i32 {
  for (var i = 0; i < byteSize; i += 4) {
    let pos = i+byteSize;
    const avg = 0.3  *  load<u8>(i) + 0.59 * load<u8>(i + 1) + 0.11 * load<u8>(i + 2);
    store<u8>(pos, u8(min(avg + 100, 255)));
    store<u8>(pos + 1, u8(min(avg + 50, 255)));
    store<u8>(pos + 2, u8(avg));
    store<u8>(pos + 3, 255);
  }
  return 0;
}

@inline
function addConvolveValue(pos:i32, oldValue:u8, length:i32): i32 {
  return pos >= 0 && pos < length ? load<u8>(pos) : oldValue;
}

export function convolve(byteSize:i32, w:i32, offset:i32, v00:i32, v01:i32, v02:i32, v10:i32, v11:i32, v12:i32, v20:i32, v21:i32, v22:i32): i32 {
  let divisor = (v00 + v01 + v02 + v10 + v11 + v12 + v20 + v21 + v22) || 0;
  if (divisor === 0) {
    divisor = 1;
  }
  for(let i = 0; i < byteSize; i++){
      if ((i + 1) % 4 === 0) {
        store<u8>(i+byteSize, load<u8>(i));

      } else {
        let oldValue = load<u8>(i);
        let prev = i - w * 4;
        let next = i + w * 4;
        let res = v00 * addConvolveValue(prev - 4, oldValue, byteSize)  +
                  v01 * addConvolveValue(prev, oldValue, byteSize)      +
                  v02 * addConvolveValue(prev + 4, oldValue, byteSize)  +
                  v10 * addConvolveValue(i - 4, oldValue, byteSize)     +
                  v11 * oldValue +
                  v12 * addConvolveValue(i + 4, oldValue, byteSize)     +
                  v20 * addConvolveValue(next - 4, oldValue, byteSize)  +
                  v21 * addConvolveValue(next , oldValue, byteSize)     +
                  v22 * addConvolveValue(next + 4, oldValue, byteSize);
        res /= divisor;
        res += offset;
        store<u8>(i+byteSize, u8(res));
      }
  }
  return 0;
}

As you can see, the AssemblyScript code is extremely similar, but with types and working at a lower level, and this is what allows developers to leverage all the Wasm potential. So, now, it’s your turn to start playing with AssemblyScript and grow your confidence on Wasm technology, which is meant to become more and more important in web development in the coming years!

Top comments (0)