DEV Community

Cover image for Introducción a AssemblyScript: cómo hacer WebAssembly más fácil para programadores de JavaScript
Gonzalo Ruiz de Villa
Gonzalo Ruiz de Villa

Posted on • Updated on • Originally published at Medium

Introducción a AssemblyScript: cómo hacer WebAssembly más fácil para programadores de JavaScript

tl;dr Introducción a AssemblyScript: explico qué es WebAssembly, por qué AssemblyScript es una alternativa de construcción de WebAssebly interesante para programadores de JavaScript y comento un sencillo proyecto de manipulación de imágenes que permite comparar las implementaciones de JavaScript y AssemblyScript.

WebAssembly es una de las grandes revoluciones que llegan a la web, aunque no es Web ni es Assembly. WebAssembly, también conocido como Wasm, es un bytecode eficiente, seguro y de bajo nivel para la Web.

Es decir, por un lado no es un lenguaje ensamblador sino bytecode. Aunque ambos son similares en el sentido de que aunque no son lenguajes de alto nivel, se pueden entender razonablemente, cosa que no pasa con el código máquina. Esto los coloca en una categoría de lenguajes intermedios entre los lenguajes de alto nivel y el código máquina. La principal diferencia entre el lenguaje ensamblador y el bytecode es que el primero se crea para las CPUs y el segundo para máquinas virtuales, es decir, uno para hardware y otro para software.

Por cierto, existe una versión textual del bytecode llamada WebAssembly Text Format o Wat para abreviar.

Y además, aunque se dice que es para la Web, no es sólo para la Web ya que se puede usar para aplicaciones de escritorio, serverless o, incluso, para Crypto y Smart contracts.

Efficiente

WebAssembly fue diseñado para tener un formato de fichero binario muy compacto, rápido de descargar y de compilar a código máquina. Tanto es así que, además, permite incluso compilar el código a la vez que se está descargando. Esta característica se llama Streaming Compilation.

Usar un módulo de Wasm desde JavaScript es tan sencillo como:

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

La siguiente forma de cargar módulos Wasm propuesta por Das Surma https://dassur.ma/things/raw-wasm/ te permitirá usar de forma robusta el Streaming Compilation. Funcionando aunque el Content-Type no esté correctamente establecido a application/wasm (que hace que falle en Firefox, por ejemplo) o si usas Safari (que todavía no soporta instantiateStreaming)

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

Seguro

Se ha trabajado mucho en la Web para proporcionar un entorno seguro que nos proteja de intenciones maliciosas, y Wasm continúa en la misma línea. Por ejemplo, al igual que JavaScript se ejecuta en un entorno Sandbox que lo aísla del entorno de producción. Como consecuencia de esto, para acceder al file system se tiene usar la Web File Api al igual que se haría en JavaScript.

Bytecode

Los principales objetivos en el diseño de Wasm fueron que pudiera ser codificado en un formato binario muy eficiente desde el punto de vista del tamaño y del tiempo de carga a la par que pudiese ejecutarse a velocidades nativas y pudiese, además, aprovecharse de las capacidades de hardware comunes de un amplio espectro de plataformas.

Estos objetivos fueron los que obligaron a construir algo nuevo (usando asm.js como punto de partida) en lugar de usar LLVM, el bytecode de Java o .Net. De esta manera, se diseñó un nuevo formato de instrucciones binario que es un objetivo de compilación para lenguajes de alto nivel como C, C++ o Rust.

Wat should I do if I want to program WebAssembly?

El saber no ocupa lugar, así que si te apetece aprender Wat ¡adelante!, aunque inspeccionando el siguiente código, probablemente si dominas JavaScript te gustaría alguna alternativa más sencilla:

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

Si lo tuyo es JavaScript, entonces C, C++, Rust y lenguajes similares probablemente tampoco te resultarán atractivas, aunque probablemente será cuestión de tiempo que esa situación cambie. Afortunadamente, mientras tanto, existe una alternativa que sí te encajará: AssemblyScript

AssemblyScript

AssemblyScript (AS) es un subconjunto de TypeScript que es a su vez JavaScript con tipos. Este subconjunto de TypeScript se puede compilar a Wasm fácilmente, con lo que podemos aprovechar los conocimientos de JavaScript para desarrollar Wasm.

Para ilustrar cuánto se parecen JavaScript y TypeScript, he preparado este pequeño proyecto en el que manipulo una imagen tanto con JavaScript como con TypeScript. Puedes encontrarlo aquí: https://github.com/gonzaloruizdevilla/image-manipulation-assemblyscript

En el proyecto se muestra una imagen que se carga dentro de un canvas y varios botones que aplicarán distintos filtros a la imagen. Los botones ejecutarán el filtro con JavaScript o con Wasm generado con AssemblyScript:

Projecto AssemblyScript

Aplicando los filtros obtendremos imágenes como estas:

Inverted image

Grayscale image

Emboss filter

Para usar el proyecto, tras clonarlo de Github, puedes instalar la dependencia de AssemblyScript y compilar el fichero de AssemblyScript index.ts con las siguientes instrucciones:

npm install
npm run asbuild

Es interesante hacer notar que cuando se invocan funciones de Wasm desde JavaScript, los argumentos de la llamada solo pueden ser de los siguientes tipos:

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

Evidentemente, no podemos pasar la imagen a través de un argumento de la llamada a Wasm. Por lo tanto, para poder enviar la información de la imagen a Wasm, primero hay que dejarla en una zona compartida de memoria entre el contexto de JavaScript y Wasm que se crea instanciando la clase WebAssembly.Memory. Después dicha instancia se usa al instanciar el módulo Wasm, como puedes ver a continuación:

//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);

Antes de invocar a Wasm, copiamos los datos de la imagen del canvas en la memoria compartida. Después invocamos al filtro de Wasm, leemos la respuesta y la guardamos en imageData y, por último, enviamos imageData al contexto del canvas para que la imagen se pinte de nuevo.

//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);

En el proyecto , hay cuatro funciones de manipulación tanto en JavaScript como en AssemblyScript: invert, grayscale, sepia y convolve (esta última para aplicar los filtros de blur, edge detection y emboss). Como podemos ver son muy similares:

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

Y ahora la versión de AssemblyScript:

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

Como podéis ver, el código es extremadamente parecido, pero con tipos y trabajando a un nivel un poco más bajo, que es lo que nos permite usar todo el potencial de Wasm. Así que, ¡ahora solo toca animarse, empezar a jugar con AssemblyScript y coger confianza en la tecnología Wasm, que va a ser una parte cada vez más importante de la web en los próximos años.

Referencias

WebAssembly https://webassembly.org/

WebAssembly: Neither Web, Nor Assembly, but Revolutionary https://www.javascriptjanuary.com/blog/webassembly-neither-web-nor-assembly-but-revolutionary

Raw WebAssembly https://dassur.ma/things/raw-wasm/
Understanding Text Format https://developer.mozilla.org/en-US/docs/WebAssembly/Understanding_the_text_format

Writing WebAssembly by Hand https://blog.scottlogic.com/2018/04/26/webassembly-by-hand.html

WebAssembly Text Format https://webassembly.github.io/spec/core/text/index.html

Making WebAssembly even faster: Firefox’s new streaming and tiering compiler https://hacks.mozilla.org/2018/01/making-webassembly-even-faster-firefoxs-new-streaming-and-tiering-compiler/

WebAssembly, bytecode for browser https://www.scriptol.com/programming/wasm.php
asm.js Spec Working Draft http://asmjs.org/spec/latest/

WebAssembly.Memory() https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Memory

Pixel manipulation in canvas http://www.phpied.com/pixel-manipulation-in-canvas/

Canvas pixels #2: convolution matrix https://www.phpied.com/canvas-pixels-2-convolution-matrix/

Oldest comments (0)