WebAssembly



WebAssembly fundamentals

WebAssembly is a technology to write code that runs within the Web, both on the client side (browsers) and the server side (e.g., Node.js) or more generally the cloud. On the client side, browser compatibility may be checked from On the server side, WebAssembly is the highway to serverless computing based on serverless functions within the cloud.

Beyond browsers and Node.js as WebAssembly runtime supports, non-Web WebAssembly runtime supports emerge (comparison ).

As named, WebAssembly is a neutral (assembly) language whose ultimate end is to produce native code by means of a C compiler. In the meantime, source code may be written in any (high-level) programming language, say, AssemblyScript (introduction about WebAssembly with AssemblyScript is ).

A step-by-step approach


Credit: J. Young
Creating a WebAssembly module from scratch

WebAssembly text format (.wat suffix) is the straightforward way of creating a WebAssembly function.

(module
    (func (export "what_year_")
        (result i32)
        i32.const 2023
        return
    )
)

The translation to WebAssembly assembler (.wasm suffix) is also straightforward using tools like, for instance, WebAssembly Explorer (an online translator). Execution next relies on a WebAssembly runtime support like Wasmtime.

wasmtime What_year_.wasm --invoke what_year_
Creating a WebAssembly module with AssemblyScript

AssemblyScript getting started… Once installed, AssemblyScript compiler version may be displayed.

asc -v

Rule(s)

Example (Barcode.ts file) Barcode.as.zip 

// Entry file to generate WebAssembly module, i.e., 'Barcode.wat'/'Barcode.wasm'
export function Barcode_checksum(barcode: string): i32 {
    // You can only export functions that call methods on an instance passed to them:
    return (new EAN(barcode)).correct; // '0', '-1' or '-2'
}

// https://www.assemblyscript.org/concepts.html#special-imports
// Compilation: 'asc assembly/Barcode.ts --target release --use abort=assembly/Barcode/_Abort'
function _Abort(message: usize, fileName: usize, line: u32, column: u32): void {
   // Overriding 'abort' from 'env' (e.g., Node.js)...
}

// AssemblyScript enumerated types can only be backed by 'i32':
enum Result {
    Correct /* = "Correct" */,
    Incorrect = -1 /* = "Incorrect" */,
    Invalid_format = -2 /* = "Invalid format" */
}

// 'Only variables, functions and enums become WebAssembly module exports.':
/* export default */
class EAN {
    // Regular expressions are not native in AssemblyScript:
    // static readonly Format: RegExp = new RegExp("^(?!000)(\\d{13}$)");
    static readonly Thirteen: i32 = 13;
    private readonly _data: Array<i32> = new Array;
    private _correct: i32 = Result.Invalid_format; // Default value...
    get correct(): i32 {
        return this._correct;
    }

    constructor(barcode: string) {
        if (barcode.length === EAN.Thirteen) {
            // barcode.split("").every(function (s: string) { // 'barcode' is divided into individual numbers...
            //     // JavaScript 'parseInt' returns 'f64' format that requires explicit casting in AssemblyScript:
            //     const element: i32 = parseInt(s) as i32; // Conversion for computations...
            //     if (isNaN(element)) return false; // 'every' stops with 'this._correct === Result.Invalid_format'...
            //     // Closures not yet implemented (https://blog.bitsrc.io/typescript-to-webassembly-the-what-the-how-and-the-why-3916a2561d37):
            //     this._data.push(element); // Bug...
            //     return true; // 'every' goes on...
            // });
// Substitute for closure:
            const elements = barcode.split("");
            for (let i = 0; i < elements.length; i++) {
                const element: i32 = parseInt(elements[i]) as i32;
                if (isNaN(element)) break;
                this._data.push(element);
            }
            this._correct = this._checksum() === this._data[EAN.Thirteen - 1] ? Result.Correct : Result.Incorrect;
        }
    }

    private _checksum(): i32 {
        // Méthode : https://fr.wikipedia.org/wiki/EAN_13#Calcul_de_la_cl%C3%A9_de_contr%C3%B4le_EAN_13
        // '471-9-5120-0288-x' with 'x' as checksum (i.e., 'x === 9')
        // '7', '9', '1', '0', '2', '8'
        const remainder = (this._data.filter((element: i32, index: i32) => index % 2 !== 0)
                .reduce((result, element) => result + 3 * element, 0) +
            // '4', '1', '5', '2', '0', '8'
            this._data.filter((element: i32, index: i32) => index % 2 === 0 && index !== EAN.Thirteen - 1)
                .reduce((result, element) => result + element, 0)) % 10;
        return remainder === 0 ? 0 : 10 - remainder;
    }
}

Rule(s)


Credit: Mozilla Developer Network (MDN) Web Docs
Inner workings of WebAssembly (memory)

.wat format (imported memory from host)

Example (Memory.wat file) Memory.wat.zip 

(module
  (import "memory_from_JavaScript" "memory_within_module" (memory 0))
  (data (i32.const 0) "Franck Barbier") ;; String is stored to the memory object from position 0...
  (func (export "get_length")
        (result i32)
    i32.const 14 ;; Data length
    return
    )
 )

Memory management (see also )

Example (Memory.wat file) Memory.wat.zip 

// JavaScript creates a WebAssembly-based memory object:
const memory_from_JavaScript = new WebAssembly.Memory({initial: 1}); // 1 page - 640 kB
// Translation using WABT: 'wat2wasm Memory.wat -o Memory.wasm'
WebAssembly.compileStreaming(window.fetch("./Memory.wasm"))
    // "importObject" is *REQUIRED* when the instantiated module actually imports something:
    .then((module) => WebAssembly.instantiate(module, {
        memory_from_JavaScript: {memory_within_module: memory_from_JavaScript}
    }))
    .then((executable) => {
        window.console.assert(executable instanceof WebAssembly.Instance);
        const length = executable.exports.get_length(); // '(func (export "get_length")'
        const bytes = new Uint8Array(memory_from_JavaScript.buffer, 0, length);
        window.alert(new TextDecoder('UTF8').decode(bytes));  // "Franck Barbier"
    });

General approach

(import "a" "f" (func ...))
(import "a" "g" (func ...))
(import "b" "m" (memory 0))
const importObject = {
    a: {f: function (...) {...}, g: function (...) {...}},
    b: {m: new WebAssembly.Memory(...)}
};
WebAssembly.instantiate(module, importObject).then(executable => { // Etc.
Using a WebAssembly module

Rule(s)

Client side

Example Barcode.as.zip 

<script type="module">
    import {Barcode_checksum} from "./build/Barcode.js"; // JavaScript file is generated by AssemblyScript!
    window.document.body.innerText = Barcode_checksum("4719512002889");
</script>

Example (WebAssembly JavaScript API)

window.fetch("./build/Barcode.wasm").then(bytes => bytes.arrayBuffer()).then(async buffer => {
    window.console.assert(buffer instanceof ArrayBuffer);
    const module = await WebAssembly.compile(buffer);
    window.console.assert(module instanceof WebAssembly.Module);
    // Etc.
});

Example (WebAssembly JavaScript API)

const module = await WebAssembly.compileStreaming(window.fetch("./build/Barcode.wasm"));
window.console.assert(module instanceof window.WebAssembly.Module);
const _imports = WebAssembly.Module.imports(module); // Array of declared imports...
    // for (const _import of _imports)
    //     window.console.log(Object.getOwnPropertyNames(_import).join("-"));
    /** Compilation is such that 'abort' is bypassed:
     * 'asc assembly/Barcode.ts --target release --use abort=assembly/Barcode/_Abort'
     * As a result, AssemblyScript compiler no longer imports 'abort'...
     * (see also https://www.assemblyscript.org/concepts.html#special-imports)
     */
    // Caution, be sure to evict any import:
window.console.assert(_imports.length === 0);

WebAssembly.instantiate(module).then(executable => {
    window.console.assert(executable instanceof WebAssembly.Instance);
        // Memory provided by WebAssembly module:
    const memory = executable.exports.memory;
    window.console.assert(memory instanceof WebAssembly.Memory);
    const buffer = memory.buffer;
    window.console.assert(buffer instanceof ArrayBuffer);
        // Alter WebAssembly module memory in JavaScript:
    const barcode = new Uint16Array(buffer); // 'ArrayBuffer' objects cannot be directly handled...
    window.console.assert(typeof executable.exports.__new === 'function');
    const pointer = executable.exports.__new("4719512002889".length << 1, 2) >>> 0;
    for (let i = 0; i < "4719512002889".length; i++)
        barcode[(pointer >>> 1) + i] = "4719512002889".charCodeAt(i);
    window.console.assert("Barcode_checksum" in executable.exports);
    window.alert(executable.exports.Barcode_checksum(pointer)); // Display is '0' meaning "OK"...
}, error => window.console.error(error));

Rule(s)

WebAssembly modules may own memory pages and export them. The WebAssembly JavaScript API may then access them through, if exported, executable.exports.memory under the window.console.assert(executable instanceof WebAssembly.Instance) condition.

Example (Barcode.wat)

(memory $0 1)
…                
(export "memory" (memory $0))

Server side (e.g., Node.js )

Example Barcode.as.zip 

import path from 'path';
import {fileURLToPath} from 'url';

import {Barcode_checksum} from "./build/Barcode.js"; // JavaScript file is generated by AssemblyScript!

// Display the fact that executable file extension is '.mjs' so that Node.js uses ES module technology:
// Note: 'ReferenceError: __dirname is not defined in ES module scope'
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
console.info(__filename + " WITHIN " + __dirname);

console.assert(typeof Barcode_checksum === 'function');
console.log(Barcode_checksum("4719512002889")); // Display is '0' meaning "OK"...
Resource access and consumption

Rule(s)

exportRuntime compiler option (default is true) provides C-like primitives (e.g., __new) to deal with pointers within JavaScript. As expected, host memory is the first resource a WebAssembly module aims at dealing with.

"options": {
    "bindings": "esm",
    "exportRuntime": true,
    "exportStart": "_start"
},
…

Host functions: the case of JavaScript engines

Rule(s)

Example

export function Barcode_checksum(barcode: string): i32 {
    // You can only export functions that call methods on an instance passed to them:
    const checksum = (new EAN(barcode)).correct;
    console.info(checksum.toString()); // Host function from AssemblyScript standard library...
    return checksum; // '0', '-1' or '-2'
}

Unfortunately, host functions are exported in WebAssembly modules (Barcode.wat and Barcode.wasm) leading to probable cumbersome execution in other worlds than JavaScript engines.

WebAssembly System Interface -WASI-

Rule(s)

WASI: the case of Node.js (see also tutorial )

Rule(s)

*Node.js WASI API imposes that the “start” function must be explicitly named _start.

Example (Using_Barcode.wasm_server_side_WASI.mjs file as Node.js host program)

import {readFile} from 'node:fs/promises';
import {env} from 'node:process';
import {WASI} from 'wasi';

process.removeAllListeners('warning'); // '(node:18916) ExperimentalWarning: WASI is an experimental feature. This feature could change at any time'
console.info("\t(Node.js host) Current directory: " + process.cwd());
console.info("\t(Node.js host) Barcode: " + process.argv[2]);

const wasi = new WASI({
    args: process.argv,
    env, // Caution here: 'process.env' is *ACTUALLY* passed to WASI...
    preopens: {
        '/sandbox': process.cwd() // Access to resource(s) in current directory...
    },
    returnOnExit: true // 'wasi.start()' returns the exit code to Node.js...
});
const wasm = await WebAssembly.compile(await readFile(new URL('./build/Barcode.wasm', import.meta.url)));
// Imported object is required, i.e., the API itself to access resource(s):
const instance = await WebAssembly.instantiate(wasm, {wasi_snapshot_preview1: wasi.wasiImport});
// Node.js WASI API imposes that the “start” function must be explicitly named '_start':
const checksum = wasi.start(instance); // '0', '-1' or '-2'
console.info("\t(Node.js host) Checksum: " + checksum);
process.exit(checksum); // Node.js returns the exit code to Windows ('Write-Output $?' -> 'True' or 'False')...

Rule(s)

Scenario

Barcode_WASI.ts imports functionality from Barcode.ts. Code in Barcode_WASI.ts is transformed by means of AssemblyScript into Barcode.wat and Barcode.wasm. Node.js execution involves the --experimental-wasi-unstable-preview1 option.

"asbuild:Barcode_WASI": "asc assembly/Barcode_WASI.ts --target release --use abort=wasi_abort",
"Node.js:WASI": "npm run asbuild:Barcode_WASI && node --experimental-wasi-unstable-preview1 Using_Barcode.wasm_server_side_WASI.mjs 4719512002889",
npm run Node.js:WASI

Example (Barcode_WASI.ts file) Barcode.as.zip 

import * as WASI from "as-wasi/assembly";

import {Barcode_checksum} from "./Barcode";
// Access and consume command line arguments from host:
const barcode: string = WASI.CommandLine.all[0].includes("node")
    ? WASI.CommandLine.all[2]
    : WASI.CommandLine.all[1]; // Wasmtime
WASI.Console.log("\t\t(WebAssembly module hosted) Barcode: " + barcode);
let checksum: i32 = Barcode_checksum(barcode);
WASI.Console.log("\t\t(WebAssembly module hosted) Checksum: " + checksum.toString());
checksum = WASI.CommandLine.all[0].includes("node")
    ? checksum
    : -checksum; // Wasmtime requires [0..126) interval...
WASI.Process.exit(checksum); // Node.js: '0', '-1' or '-2' versus Wasmtime: '0', '1' or '2'
Wasmtime from Bytecode Alliance

WASI: the case of Wasmtime (see also tutorial )

Rule(s)

Once installed Wasmtime, which comes with a Command Line Interface (CLI), runtime version may be displayed.

wasmtime -V

Execution encompasses native code (e.g., Windows) compilation, WebAssembly module instantiation (as done with WebAssembly JavaScript API) and execution.

wasmtime run Barcode.wasm

Equivalent ways of executing omit run and/or use .wat file.

wasmtime Barcode.wasm 4719512002889
wasmtime run Barcode.wat 4719512002889
wasmtime Barcode.wat 4719512002889

Such executions rely on a _start implicit function, which is actually called. Instead, an explicit “start” function may be set up, e.g., my_start, including other customizations, e.g., memory page allocation. AssemblyScript compiler exportStart option allows the explicit naming of the “start” function.

wasmtime Barcode.wasm --invoke my_start 4719512002889

Keeping _start, WebAssembly module (build directory) issued from AssemblyScript is run.

"Wasmtime:WASI": "npm run asbuild:Barcode_WASI && wasmtime build/Barcode.wasm 4719512002889",
npm run Wasmtime:WASI

Get result at Operating System -OS- level (Windows PowerShell).

Write-Output $? // 'True' ('0') or 'False' ('1' or '2')

Alternative (Windows PowerShell).

$LASTEXITCODE
WasmEdge from Cloud Native Computing Foundation -CNCF-
wasmedge -v

WasmEdge: dealing with host functions

https://thenewstack.io/rust-and-webassembly-serverless-functions-in-vercel
wasmCloud from Cloud Native Computing Foundation (CNCF)
wash -V
WebAssembly on the server side and the cloud