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:
- Writing or generating WebAssembly directly at the assembly level.
- Porting a C/C++ application with Emscripten.
- Writing a Rust application and targeting WebAssembly as its output.
- Using AssemblyScript which looks similar to TypeScript and compiles to WebAssembly binary.
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 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))
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
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.
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/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)
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.
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
});
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))
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());
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);
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 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.
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
local.get $x
if (result i32)
; value of $x > 0
else
; value of $x <= 0
end
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
(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)
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 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
TDB
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 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 thetoString()
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));
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]
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
});
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
});
TBD
TBD
- https://webassembly.org
- https://bytecodealliance.org
- https://developer.mozilla.org/en-US/docs/WebAssembly
- WebAssembly W3C Core Specification
- WebAssembly specification
- WebAssembly instruction set
- WebAssembly JavaScript API
- WebAssembly CNCF Cloud Native Interactive Landscape
- WebAssembly Binary Toolkit
- https://github.com/mdn/webassembly-examples
- wasmtime: A standalone runtime for WebAssembly
- WASI: A system interface to run WebAssembly outside the web
- WAVM - WebAssembly Virtual Machine
- http://asmjs.org
- https://emscripten.org
- https://www.assemblyscript.org
- https://www.assemblyscript.org/editor.html
- https://github.com/wasm3/wasm3
- https://wascc.dev
- https://innative.dev
- https://www.npmjs.com/package/inline-webassembly
- WasmFiddle
- WebAssembly Explorer
- WebAssembly Code Explorer
- A practical guide to WebAssembly memory
- Binary Security of WebAssembly
- WebAssembly, Unicode and the Web Platform
- WebAssembly for the rest of us