WebAssembly - ttulka/programming GitHub Wiki

WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable target for compilation of high-level languages like C/C++/Rust, enabling deployment on the web for client and server applications.

WebAssembly is not primarily intended to be written by hand, rather it is designed to be an effective compilation target for source languages.

WebAssembly modules can be imported into a web (or Node.js) app, exposing WebAssembly functions for use via JavaScript.

  • fast, efficient, and portable,
    • can be executed at near-native speed across different platforms;
  • readable and debuggable,
    • low-level assembly language with a human-readable text format;
  • secure,
    • runs in a safe, sandboxed execution environment;
  • web friendly,
    • plays nicely with other web technologies.

asm.js is a research project at Mozilla that aims to formally define the subset of JavaScript that compilers like Emscripten and Mandreel already generate (typed arrays as memory, etc.)

Right now, there are four main entry points:

  1. Writing or generating WebAssembly directly at the assembly level.
  2. Porting a C/C++ application with Emscripten.
  3. Writing a Rust application and targeting WebAssembly as its output.
  4. Using AssemblyScript which looks similar to TypeScript and compiles to WebAssembly binary.

Wat

https://webassembly.github.io/spec/core/exec

Wat is a textual representation of the wasm binary format to enable WebAssembly to be read and edited by humans.

S-expressions

S-expressions are simple textual format for representing trees.

;; a tree with the root node “module” and two child nodes
;; a "memory" node with the attribute "1" and a "func" node:
(module (memory 1) (func))

Modules

In both the binary and textual formats, the fundamental unit of code in WebAssembly is a module; represented as one big S-expression ( ... ).

(module)
0000000: 0061 736d  ; WASM_BINARY_MAGIC
0000004: 0100 0000  ; WASM_BINARY_VERSION

Functions

All code in a webassembly module is grouped into functions:

( func <signature> <locals> <body> )
  • signature declares what the function takes (parameters) and returns (return values).
  • locals are like vars in JavaScript, but with explicit types declared.
  • body is just a linear list of low-level instructions.

Signature

The signature is a sequence of parameter type declarations followed by a list of return type declarations.

(func (param i32) (param i32) (result f64) ... )
  • i32: 32-bit integer
  • i64: 64-bit integer
  • f32: 32-bit float
  • f64: 64-bit float

Parameters are basically just locals that are initialized with the value of the corresponding argument passed by the caller.

The absence of a (result) means the function doesn’t return anything.

Locals

Locals/parameters can be read and written by the body of the function with the local.get and local.set instructions.

(func (param i32) (local f64)
  local.get 0
  local.get 1)

Text format allows you to name parameters, locals, and most other items by including a name prefixed by a dollar symbol $ just before the type declaration:

(func (param $p1 i32) (param $p2 f32) (local $loc f64)
  local.get $p1
  local.get $p2
  local.get $loc)

Function body

Although the browser compiles it to something more efficient, wasm execution is defined in terms of a stack machine where the basic idea is that every type of instruction pushes and/or pops a certain number of i32/i64/f32/f64 values to/from a stack.

For example, local.get is defined to push the value of the local it read onto the stack, and i32.add pops two i32 values, computes their sum and pushes the resulting i32 value:

(module
  (func (param $p i32) (result i32)
    local.get $p
    local.get $p
    i32.add))

The return value of a function is just the final value left on the stack.

Calling the function from JavaScript

Wasm functions must be explicitly exported by an export statement inside the module:

(export "multiply" (func $multiply))

Like locals, functions are identified by an index by default, but for convenience, they can be named:

(func $multiply … )

After been compiled into Wasm, the fuction can be called by the name:

(module
  (func $multiply (param $p i32) (result i32)
    local.get $p
    local.get $p
    i32.add)
  (export "multiply" (func $multiply))
)
WebAssembly.instantiateStreaming(fetch('multiply.wasm'))
  .then((obj) => {
    console.log(obj.instance.exports.multiply(2));  // 4
  });

Calling the function from the same module

The call instruction calls a single function, given its index or name:

(module
  (func $getAnswer (result i32)
    i32.const 42)
  (func 
      (export "getAnswerPlus1")  ;; shorthand for exporting the function as it is
      (result i32)
    call $getAnswer
    i32.const 1
    i32.add))

Importing functions from JavaScript

WebAssembly has a general way to import functions that can accept either JavaScript or wasm functions:

(module
  ;; import the `log` function from the `console` module:
  (import "console" "log" (func $log (param i32)))
  (func (export "logIt")
    i32.const 13
    call $log))
var importObject = {
  console: {
    log: function(arg) {
      console.log(arg);
    }
  }
};

WebAssembly.instantiateStreaming(fetch('logger.wasm'), importObject)
  .then(obj => obj.instance.exports.logIt());

Declaring globals

WebAssembly has the ability to create global variable instances, accessible from both JavaScript and importable/exportable across one or more WebAssembly.Module instances:

(module
  (global $g (import "js" "global") (mut i32))
  (func (export "getGlobal") (result i32)
    (global.get $g))
  (func (export "incGlobal")
    (global.set $g
      (i32.add (global.get $g) (i32.const 1))))
)
const global = new WebAssembly.Global({value: "i32", mutable: true}, 0);

Memory

To deal with strings and other more complex data types, WebAssembly provides memory and Reference Types.

Memory is just a large array of bytes that can grow over time. WebAssembly contains instructions like i32.load and i32.store for reading and writing from linear memory.

From JavaScript’s point of view, it’s as though memory is all inside one big (resizable) ArrayBuffer.

We encode a string so that a string’s length in the string itself; we just pass both offset and length as parameters:

(import "console" "log" (func $log (param i32) (param i32)))

On the JavaScript side, we can use the TextDecoder API to easily decode our bytes into a JavaScript string:

function stringFromMemory(memory, offset, length) {
  var bytes = new Uint8Array(memory.buffer, offset, length);
  var str = new TextDecoder('utf8').decode(bytes);
  return str;
}

We can have the WebAssembly module create the memory and export it to JavaScript, or we can either create a Memory object in JavaScript and have the WebAssembly module import the memory:

(import "js" "mem" (memory 1))
(module
  (import "console" "log" (func $log (param i32 i32)))
  (import "js" "mem" (memory 1))
  (data (i32.const 0) "Hi")  ;; write the string contents into global memory
  (func (export "writeHi")
    i32.const 0  ;; pass offset 0 to log
    i32.const 2  ;; pass length 2 to log
    call $log))
var memory = new WebAssembly.Memory({initial:1});

var importObject = { 
  	console: { log: (off, len) => console.log(stringFromMemory(memory, off, len)) }, 
	js: { mem: memory } 
};

WebAssembly.instantiateStreaming(fetch('logger.wasm'), importObject)
  .then(obj => obj.instance.exports.writeHi());

Tables

Tables are basically resizable arrays of references that can be accessed by index from WebAssembly code.

Tables store function references which are of type anyfunc (and not any of i32/i64/f32/f64 that are stored by memory).

anyfunc type couldn’t be stored in linear memory for security reasons: raw function addresses could be corrupted, which is something that cannot be allowed on the web.

(module
  ;; 2 is the initial size,
  ;; `funcref` declares that the element type is function reference:
  (table 2 funcref)  
  ;; `(i32.const 0)` is an offset at what index references start to be populated,
  ;; `$f1 $f2` initializes regions of tables with functions:
  (elem (i32.const 0) $f1 $f2)
  
  (func $f1 (result i32)
    i32.const 42)
  (func $f2 (result i32)
    i32.const 13)
  ...
)

Equivalent calls to create such a table instance in JavaScript:

var tbl = new WebAssembly.Table({initial:2, element:"funcref"});

// function sections:
var f1 = ... /* some imported WebAssembly function */
var f2 = ... /* some imported WebAssembly function */

// elem section
tbl.set(0, f1);
tbl.set(1, f2);

Now, we can call a function dynamically with the call_indirect instruction:

(type $return_i32 (func (result i32)))
(func (export "callByIndex") (param $i i32) (result i32)
  ;; `call_indirect` calls the table implicitely: 
  call_indirect (type $return_i32) (local.get $i))
WebAssembly.instantiateStreaming(fetch('wasm-table.wasm'))
  .then(({instance:{exports:{callByIndex}}}) => {
    console.log(callByIndex(0)); // returns 42
    console.log(callByIndex(1)); // returns 13
    console.log(callByIndex(2)); // error, because there is no index position 2 in the table
  });

Table object can be mutated from JavaScript using the grow(), get() and set() methods to implement sophisticated load-time and run-time dynamic linking schemes.

Shared memories

WebAssembly Memory objects to be shared across multiple WebAssembly instances running in separate Web Workers, in the same fashion as SharedArrayBuffers in JavaScript.

This allows very fast communication between Workers, and significant performance gains in web applications.

(memory 1 2 shared)
var memory = new WebAssembly.Memory({initial:10, maximum:100, shared:true});

memory.buffer // returns SharedArrayBuffer

Control Flow

If-Else
local.get $x
if (result i32)
  ; value of $x > 0
else
  ; value of $x <= 0
end
Loops
i32.const 0
    local.set $i
    
    i32.const 42
    local.set $n

    loop
      local.get $i
      i32.const 1
      i32.add
      local.set $i

      local.get $n
      local.get $i
      i32.gt_s
      br_if 0
    end

    local.get $i  ;; 42
Calling functions
(func $caller
      (export "control2")
      (result i32)
  i32.const 42
  call $callie)

(func $callie
      (param $x i32)
      (result i32)
  local.get $x
  i32.const 1
  i32.add)

Wat to Wasm

https://github.com/webassembly/wabt

# compile wat text into wasm binary:
~/wabt/bin/wat2wasm -v test.wat -o test.wasm

# validate binary:
wasm-validate -v test.wasm

# print info about binary:
wasm-objdump -xs test.wasm

# run it using a stack-based interpreter:
wasm-interp -v --run-all-exports test.wasm

https://github.com/bytecodealliance/wasmtime

wasmtime run test.wasm --invoke main

Emscripten

https://emscripten.org

Emscripten is a complete Open Source compiler toolchain to WebAssembly.

C/C++ ⇒ LLVM ⇒ Emscripten ⇒ Wasm + HTML with JS glue

/* helloworld.c */

#include <stdio.h>

int main() {
  printf("hello, world!\n");
  return 0;
}
## Compile to JS and Wasm
# WebAssembly is emitted by default, without the need for any special flags.

# emcc is installed on the PATH
emcc helloworld.c -o helloworld.js

# via Docker 
docker run --rm -v $(pwd):/src -u $(id -u):$(id -g) \
  emscripten/emsdk emcc helloworld.c -o helloworld.js

# run with Node
node helloworld.js 

## Generate HTML with embedded JS
./emcc tests/helloworld.c -o helloworld.html

# open in the browser
firefox http://localhost/helloworld.html 

## Compile optimized code
# https://emscripten.org/docs/tools_reference/emcc.html#emcc-o1
emcc -O1 helloworld.c -o helloworld.js 	

AssemblyScript

https://assemblyscript.org

TDB

WebAssembly JavaScript API

The JavaScript API provides developers with the ability to create modules, memories, tables, and instances.

Given a WebAssembly instance, JavaScript code can synchronously call its exports, which are exposed as normal JavaScript functions.

Arbitrary JavaScript functions can also be synchronously called by WebAssembly code by passing in those JavaScript functions as the imports to a WebAssembly instance.

JavaScript developers could even think of WebAssembly as just a JavaScript feature for efficiently generating high-performance functions.

The WebAssembly JavaScript object acts as the namespace for all WebAssembly-related functionality:

  • Loading WebAssembly code, using the WebAssembly.instantiate() function.
  • Creating new memory and table instances via the WebAssembly.Memory()/WebAssembly.Table() constructors.
  • Providing facilities to handle errors that occur in WebAssembly.

Exported WebAssembly functions

Exported WebAssembly functions are how WebAssembly functions are represented in JavaScript.

Exported WebAssembly functions are basically just JavaScript wrappers that represent WebAssembly functions in JavaScript.

You can retrieve exported WebAssembly functions by accessing a function exported from a wasm module instance via Instance.exports.

  • Their length property is the number of declared arguments in the wasm function signature.
  • Their name property is the toString() result of the function's index in the wasm module.
(module
  (func $i (import "imports" "imported_func") (param i32))
  (func (export "exported_func")
    i32.const 42
    call $i)) 
var importObject = { imports: { imported_func: arg => console.log(arg) } };

WebAssembly.instantiateStreaming(fetch('simple.wasm'), importObject)
  .then(obj => obj.instance.exports.exported_func())
  .catch(ex => console.error('Wasm Error', ex));

Memory

The memory accessible by a particular WebAssembly Instance is confined to one specific — potentially very small — range contained by a WebAssembly Memory object.

This allows a single web app to use multiple independent libraries — each of which are using WebAssembly internally — to have separate memories that are fully isolated from each other.

The unit is WebAssembly pages — these are fixed to 64KB in size.

// instance has an initial size of 640KB, 
// and a maximum size of 6.4MB
var memory = new WebAssembly.Memory({initial:10, maximum:100});

WebAssembly.instantiateStreaming(fetch('memory.wasm'), {js: {mem: memory}})
...

WebAssembly memory exposes its bytes by providing a buffer getter/setter that returns an ArrayBuffer:

// to write 42 directly into the first word of linear memory
new Uint32Array(memory.buffer)[0] = 42;

// retrieve the same value
new Uint32Array(memory.buffer)[0]

Tables

A WebAssembly Table is a resizable typed array of references that can be accessed by both JavaScript and WebAssembly code.

(module
  (func $thirteen (result i32) (i32.const 13))
  (func $fourtytwo (result i32) (i32.const 42))
  (table (export "tbl") anyfunc (elem $thirteen $fourtytwo))
)
WebAssembly.instantiateStreaming(fetch('table.wasm'))
  .then(obj => {
    // access the data in the tables
    var tbl = obj.instance.exports.tbl;
    console.log(tbl.get(0)());  // 13
    console.log(tbl.get(1)());  // 42
  });

Globals

WebAssembly has the ability to create global variable instances, accessible from both JavaScript and importable/exportable across one or more WebAssembly.Module instances.

(module
   (global $g (import "js" "global") (mut i32))
   (func (export "getGlobal") (result i32)
      (global.get $g))
   (func (export "incGlobal")
      (global.set $g
        (i32.add (global.get $g) (i32.const 1))))
)
const global = new WebAssembly.Global({value:'i32', mutable:true}, 13);

WebAssembly.instantiateStreaming(fetch('global.wasm'), { js: { global } })
.then(({instance}) => {
    instance.exports.getGlobal();  	// 0
    global.value = 42;
    instance.exports.getGlobal()  	// 42
    instance.exports.incGlobal();
    global.value 					// 43
});

Node.js

TBD

WASI

https://wasi.dev

TBD

References

⚠️ **GitHub.com Fallback** ⚠️