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
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_
AssemblyScript getting started… Once installed, AssemblyScript compiler version may be displayed.
asc -v
Rule(s)
- AssemblyScript has, compared to TypeScript, its own types like, for instance,
i32
oru32
(unsigned 32-bit integer). In constrast, TypeScript core types, namelyany
andunknown
, are not part of AssemblyScript.- Although AssemblyScript is inspired by TypeScript, there are significant differences (see also ☛).
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)
- The AssemblyScript compiler generates two files from
Barcode.as
:Barcode.wat
(WebAssembly text format based on symbolic expressions found in programming languages like List Processing -LisP-) andBarcode.wasm
(assembler).- WebAssembly Binary Toolkit (WABT) allows the move from
.wat
to.wasm
(and vice-versa). As an alternative, WebAssembly Explorer is an online translator.- Visualization of WebAssembly assembler in human-readable way is possible as well.
Credit: Mozilla Developer Network (MDN) Web Docs
.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.
Rule(s)
- Common JavaScript programs may enable WebAssembly modules by using the WebAssembly JavaScript API.
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)
- In WebAssembly, non-numerical types (i.e., types different from integers like
i64
, reals likef64
…), strings typically, must be handled through memory pages (1 page: 640 kB).WebAssembly.Memory
embodies such a mechanism within the WebAssembly JavaScript API.WebAssembly modules may own memory pages and export them. The WebAssembly JavaScript API may then access them through, if exported,
executable.exports.memory
under thewindow.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"...
Rule(s)
- The key issue behind WebAssembly is the fact that, by nature, a module has no access (and therefore consumption possibility) of “host” resources. For example, access to the file system in Node.js (as “host”) is based on the
fs
object. A WebAssembly module cannot deal with files in general throughfs
in Node.js apart from appropriate settings at AssemblyScript compilation time.
exportRuntime
compiler option (default istrue
) 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)
- Simply speaking, exported (custom) functions in WebAssembly modules (e.g.,
Barcode_checksum
) compute output data that aim at being pushed as input data in other functions.- AssemblyScript provides a standard library including commonalities in JavaScript engines, say,
console
,process
… with local functions, say,info
forconsole
,exit
forprocess
… These are (facilitation) host functions.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
andBarcode.wasm
) leading to probable cumbersome execution in other worlds than JavaScript engines.
Rule(s)
- The lack of resource access and consumption facilities in WebAssembly modules leads to WebAssembly System Interface -WASI-: original paper (March 2019).
- WASI is promoted by the Bytecode Alliance, which includes Amazon, Google, Microsoft, Intel…
WASI: the case of Node.js (see also tutorial ☛)
Rule(s)
*Node.js WASI API imposes that the “start” function must be explicitly named
- Node.js provides a builtin API for WASI* ☛.
- Principle: Node.js “pushes” its execution context to WASI, namely
env
,process
… The executed WebAssembly module having Node.js as host transparently accesses and consumes resources from this execution context using a Portable Operating System Interface X -POSIX-, which is a compliant entry point namedwasi_snapshot_preview1
☛._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)
- as-wasi is the AssemblyScript support for WASI.
- as-wasi exposes resources and possible usages (e.g.,
WASI.Console.log
below) in a standardized way (i.e., WASI) avoiding the use of convenient AssemblyScript host functions specific to JavaScript engines (console.log
).- WASI aims at creating higher neutrality within AssemblyScript code. WebAssembly modules written in AssemblyScript or competitor programming languages like Rust, have agnostic mechanisms in
.wat
and.wasm
files in the way, the host execution context is handled.Scenario
Barcode_WASI.ts
imports functionality fromBarcode.ts
. Code inBarcode_WASI.ts
is transformed by means of AssemblyScript intoBarcode.wat
andBarcode.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.zipimport * 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'
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 compilerexportStart
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
- Installation ☛ and next test…
wasmedge -v
WasmEdge: dealing with host functions
https://thenewstack.io/rust-and-webassembly-serverless-functions-in-vercel
- Installation ☛ and next test…
wash -V