Create a new project:
$ cargo new --bin hello
$ cd hello
$ rustup target add wasm32-wasip1Build and run:
$ cargo build --release --target wasm32-wasip1
$ wasm3 ./target/wasm32-wasip1/release/hello.wasm
Hello, world!Create hello.ts:
import "wasi"
import {Console} from "as-wasi"
Console.log('Hello World!');Create package.json:
{
"scripts": {
"asbuild:debug": "asc hello.ts -b hello.wasm --debug",
"asbuild:optimized": "asc hello.ts -b hello.wasm -O3s --noAssert",
"asbuild:tiny": "asc hello.ts -b hello.wasm -O3z --noAssert --runtime stub --use abort=",
"build": "npm run asbuild:optimized"
},
"devDependencies": {
"assemblyscript": "*",
"as-wasi": "*"
}
}Build and run:
$ npm install
$ npm run build
$ wasm3 hello.wasm
Hello World!Create hello.go:
package main
import "fmt"
func main() {
fmt.Printf("Hello, %s!\n", "world")
}Build and run:
$ tinygo build -o hello.wasm -target wasi -no-debug hello.go
$ wasm3 hello.wasm
Hello, world!Create hello.go:
package main
import "fmt"
func main() {
fmt.Printf("Hello, %s!\n", "world")
}Build and run:
$ GOOS=wasip1 GOARCH=wasm go build -o hello.wasm hello.go
$ wasm3 hello.wasm
Hello, world!The Go runtime is linked into every module, so even this one lands at about 2.5 MB.
What that buys is the whole runtime: goroutines, the scheduler,
the garbage collector, time and os all work under Wasm3.
TinyGo targets the same interface at a fraction of the size.
Create hello.zig:
const std = @import("std");
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("Hello, {s}!\n", .{"world"});
}Build and run:
$ zig build-exe -O ReleaseSmall -target wasm32-wasi hello.zig
$ wasm3 hello.wasm
Hello, world!Create hello.c:
#include <stdio.h>
int main() {
printf("Hello, %s!\n", "world");
return 0;
}Build and run:
$ zig build-exe -O ReleaseSmall -target wasm32-wasi hello.c -lc
$ wasm3 hello.wasm
Hello, world!Create add.zig:
export fn add(a: i32, b: i32) i64 {
return a + b;
}Build and run:
$ zig build-lib add.zig -O ReleaseSmall -target wasm32-freestanding
$ wasm3 --repl add.wasm
wasm3> add 1 2
Result: 3The easiest way to start is to use the WASI SDK, which is a Clang that already knows where the sysroot is:
export WASI_SDK_PATH=/opt/wasi-sdkCreate hello.cpp:
#include <iostream>
int main() {
std::cout << "Hello, world!" << std::endl;
return 0;
}Or hello.c:
#include <stdio.h>
int main() {
printf("Hello, %s!\n", "world");
return 0;
}Build and run:
$ $WASI_SDK_PATH/bin/clang++ -mcpu=lime1 -O3 hello.cpp -o hello.wasm
$ $WASI_SDK_PATH/bin/clang -mcpu=lime1 -O3 hello.c -o hello.wasm
$ wasm3 hello.wasm
Hello World!-mcpu=lime1 selects the Lime1
feature set - bulk-memory-opt, call-indirect-overlong, extended-const, multivalue,
mutable-globals, nontrapping-fptoint and sign-ext. It is what the modules under
test/wasi are built with: everything in it is cheap for an interpreter to support, and
Wasm3 implements all of it. Without -mcpu, Clang targets the 2017 MVP and gives up
those instructions; with a richer -mcpu, it will emit SIMD, which Wasm3 does not have.
Clang runs wasm-opt on the result whenever it can find one on PATH, so a Binaryen
older than the features being compiled will fail the link with validation errors. Either
install a current one or pass --no-wasm-opt.
Useful clang options:
-
-nostdlib Do not use the standard system startup files or libraries when linking
-
-Wl,--no-entry Do not output any entry point
-
-Wl,--export=<value> Force a symbol to be exported, e.g. -Wl,--export=foo to export foo function
-
-Wl,--export-all Export all symbols (normally combined with --no-gc-sections)
-
-Wl,--initial-memory=<value> Initial size of the linear memory, which must be a multiple of 65536
-
-Wl,--max-memory=<value> Maximum size of the linear memory, which must be a multiple of 65536
-
-z stack-size=<value> The auxiliary stack size, which is an area of linear memory, and must be smaller than initial memory size.
-
-Wl,--strip-all Strip all symbols
-
-Wl,--shared-memory Use shared linear memory
-
-Wl,--allow-undefined Allow undefined symbols in linked binary
-
-Wl,--allow-undefined-file=<value> Allow symbols listed in <file> to be undefined in linked binary
-
-pthread Support POSIX threads in generated code
Limitations:
setjmp/longjmpandC++ exceptionsare not available- no support for
threadsandatomics - no support for
dynamic libraries
Create hello.gr:
print("Hello, world!")
Build and run:
$ grain compile hello.gr -o hello.wasm
$ wasm3 hello.wasm
Hello, world!Create hello.wat:
(module
;; wasi_snapshot_preview1!fd_write(file_descriptor, *iovs, iovs_len, nwritten) -> status_code
(import "wasi_snapshot_preview1" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))
(memory 1)
(export "memory" (memory 0))
;; Put a message to linear memory at offset 32
(data (i32.const 32) "Hello, world!\n")
(func $main (export "_start")
;; Create a new io vector at address 0x4
(i32.store (i32.const 4) (i32.const 32)) ;; iov.iov_base - pointer to the start of the message
(i32.store (i32.const 8) (i32.const 14)) ;; iov.iov_len - length of the message
(call $fd_write
(i32.const 1) ;; file_descriptor - 1 for stdout
(i32.const 4) ;; *iovs - pointer to the io vector
(i32.const 1) ;; iovs_len - count of items in io vector
(i32.const 20) ;; nwritten - where to store the number of bytes written
)
drop ;; discard the WASI status code
)
)Build and run:
$ wat2wasm hello.wat -o hello.wasm
$ wasm3 hello.wasm
Hello, world!Create swap.wat:
(module
(func (export "swap") (param i32 i32) (result i32 i32)
(local.get 1)
(local.get 0)
)
)Build and run:
$ wat2wasm swap.wat -o swap.wasm
$ wasm3 --repl swap.wasm
wasm3> :invoke swap 123 456
Result: 456:i32, 123:i32Drag'n'drop any of the WASI apps to WebAssembly.sh and run:
$ wasm3-strace /tmp/hello.wasmThe execution trace will be produced:
_start () {
__wasilibc_init_preopen () {
malloc (i32: 16) {
dlmalloc (i32: 16) {
sbrk (i32: 0) {
} = 131072
sbrk (i32: 65536) {Click to expand!
} = 131072
} = 131088
} = 131088
calloc (i32: 24, i32: 0) {
dlmalloc (i32: 96) {
} = 131120
memset (i32: 131120, i32: 65504, i32: 0) {
} = 131120
} = 131120
po_map_assertvalid (i32: 131088) {
}
po_map_assertvalid (i32: 131088) {
}
}
wasi_unstable!fd_prestat_get(3, 65528) { <native> } = 0
malloc (i32: 2) {
dlmalloc (i32: 2) {
} = 131232
} = 131232
wasi_unstable!fd_prestat_dir_name(3, 131232, 1) { <native> } = 0
__wasilibc_register_preopened_fd (i32: 3, i32: 131120) {
po_map_assertvalid (i32: 131088) {
}
po_map_assertvalid (i32: 131088) {
}
strdup (i32: 131232) {
strlen (i32: 131232) {
} = 1
malloc (i32: 2) {
dlmalloc (i32: 2) {
} = 131248
} = 131248
memcpy (i32: 131248, i32: 131233, i32: 131232) {
} = 131248
} = 131248
wasi_unstable!fd_fdstat_get(3, 65496) { <native> } = 0
po_map_assertvalid (i32: 131088) {
}
po_map_assertvalid (i32: 131088) {
}
} = 0
free (i32: 131232) {
dlfree (i32: 131232) {
}
}
wasi_unstable!fd_prestat_get(4, 65528) { <native> } = 0
malloc (i32: 2) {
dlmalloc (i32: 2) {
} = 131232
} = 131232
wasi_unstable!fd_prestat_dir_name(4, 131232, 1) { <native> } = 0
__wasilibc_register_preopened_fd (i32: 4, i32: 131120) {
po_map_assertvalid (i32: 131088) {
}
po_map_assertvalid (i32: 131088) {
}
strdup (i32: 131232) {
strlen (i32: 131232) {
} = 1
malloc (i32: 2) {
dlmalloc (i32: 2) {
} = 131264
} = 131264
memcpy (i32: 131264, i32: 131233, i32: 131232) {
} = 131264
} = 131264
wasi_unstable!fd_fdstat_get(4, 65496) { <native> } = 0
po_map_assertvalid (i32: 131088) {
}
po_map_assertvalid (i32: 131088) {
}
} = 0
free (i32: 131232) {
dlfree (i32: 131232) {
}
}
wasi_unstable!fd_prestat_get(5, 65528) { <native> } = 8
__wasm_call_ctors () {
}
__original_main () {
printf (i32: 65536, i32: 0) {
vfprintf (i32: 69512, i32: 0, i32: 65536) {
printf_core (i32: 0, i32: -1, i32: 65536, i32: -16, i32: 65480) {
} = 0
__towrite (i32: 69512) {
} = 0
printf_core (i32: 69512, i32: 8, i32: 65536, i32: -1, i32: 65480) {
__fwritex (i32: 65536, i32: 0, i32: 7) {
memcpy (i32: 68472, i32: 0, i32: 65536) {
} = 68472
} = 7
__fwritex (i32: 65543, i32: 0, i32: 0) {
memcpy (i32: 68479, i32: 0, i32: 65543) {
} = 68479
} = 0
pop_arg (i32: 64456, i32: 0, i32: 9) {
}
strnlen (i32: 65548, i32: 0) {
memchr (i32: 65548, i32: 4, i32: 0) {
} = 65553
} = 5
__fwritex (i32: 67222, i32: 65553, i32: 0) {
memcpy (i32: 68479, i32: 0, i32: 67222) {
} = 68479
} = 0
__fwritex (i32: 65548, i32: 65553, i32: 5) {
memcpy (i32: 68479, i32: 0, i32: 65548) {
} = 68479
} = 5
__fwritex (i32: 65545, i32: 0, i32: 2) {
__stdout_write (i32: 69512, i32: 0, i32: 65545) {
__isatty (i32: 1) {
wasi_unstable!fd_fdstat_get(1, 64376) { <native> } = 0
} = 1
__stdio_write (i32: 69512, i32: 64368, i32: 65545) {
writev (i32: 1, i32: -16, i32: 64384) {
Hello, world!
wasi_unstable!fd_write(1, 64384, 2, 64380) { <native> } = 0
} = 14
} = 2
} = 2
memcpy (i32: 68472, i32: -1, i32: 65547) {
} = 68472
} = 2
} = 14
} = 14
} = 14
} = 0
__prepare_for_exit () {
dummy () {
}
__stdio_exit () {
__ofl_lock () {
} = 69504
}
}
}Any .wasm module can be run under a fixed gas budget.
--gas-meter reports what a run costs, and --gas-limit sets the budget
it is allowed to spend:
$ wasm3 --gas-meter --func fib test/lang/fib32.wasm 24
Gas used: 8935.3961
Result: 46368
$ wasm3 --gas-limit 100 --func fib test/lang/fib32.wasm 24
Gas used: 100.0183
Error: [trap] out of gasNote
Enabling Gas Metering costs about 10% of throughput.
Across the test/wasi benchmarks a metered run measures 0.91x an unmetered one.
Instruction costs come from the table ewasm's metering design is written against, whose unit is a ten-thousandth of a gas - hence the four decimal places.
d_m3DeterministicProfile builds an interpreter on which the same module, fed the
same input, produces the same run wherever and whenever it is run.
It is a build option rather than a runtime flag because that is what a profile is.
The profiles proposal defines one as a
subset of the language that an ecosystem picks once, for all of its modules - not
something a module or a call turns on - and names the deterministic profile as the
marker set {N,T}: the constructs whose behaviour is nondeterministic, and threading.
cmake -DCMAKE_C_FLAGS=-Dd_m3DeterministicProfile=1 ..A build that has it says so in its --version banner.
N. The one rule the proposal spells out is that a NaN result is normalized to the
canonical NaN, which is d_m3CanonicalNaN and is turned on by this. Without it, the
sign and payload of a NaN are the host's to choose and different architectures choose
differently, so the same module produces different bits on x86, on ARM and on MIPS.
abs, neg and copysign still carry their operand's payload through, as the spec
defines them to. The other N-marked constructs are relaxed SIMD, which Wasm3 does not
implement, so there is nothing to exclude.
T. Threads and atomics, which Wasm3 does not implement either.
The proposal is about the language and stops at the module's edge. What a module reads through its imports is nondeterministic in exactly the way that matters, so this option covers it too:
- every WASI clock starts at a fixed instant and moves forward by a fixed tick on each reading, and by exactly what a wait asked for;
random_getreturns a stream from a fixed seed;poll_oneoffstops waiting on anything real - a descriptor is ready at once, and a sleep is the clock moving rather than time passing.
Each runtime carries its own clock and its own entropy, both starting where the last one did, so a run is replayed by making a fresh runtime.
Two limits belong alongside, and are separate because each is useful on its own:
--gas-limit, so that a runaway module stops after the same amount of work
everywhere, and a memory limit, so that memory.grow fails at the same point rather
than wherever the host allocator gives out. What the guest reads through the rest of
WASI - files, arguments, the environment - is input, and reproducing a run means
supplying it again.
- WebAssembly by examples by Aaron Turner
- Understanding the text format on MDN