wasm

Recipes

Hand-written WAT, rustc-built WASI binaries, native CLI builds.

Hand-written WAT, no WASI

The committed examples/hello.wasm is a hand-written WAT module that calls env.putchar 13 times. No WASI imports, no preopens needed:

sbt 'cliJVM/run examples/hello.wasm'
# Hello, world!

The matching .wat source is at examples/hello.wat. After editing, regenerate the binary with wat2wasm:

wat2wasm examples/hello.wat -o examples/hello.wasm

Freestanding C, no libc

examples/c/hello.wasm is a freestanding C program — no libc, no wasi-sdk. The source declares fd_write directly as a wasi import and defines _start as the entry point; the build is just clang --target=wasm32 -nostdlib plus wasm-ld. Useful for understanding what a wasi binary actually is once you strip the libc convenience layer off.

sbt 'cliJVM/run examples/c/hello.wasm'
# Hello from freestanding C!

The matching examples/c/hello.c source and Makefile are committed alongside; see examples/c/README.md for the build invocation.

C with wasi-libc (<stdio.h>, argv, exit codes)

examples/c-libc/hexdump.c is the smallest “real C program” target: it uses <stdio.h> (fopen/fread/printf/fprintf(stderr, ...)), reads its path argument from argv[1], and returns an exit code from main. The build requires wasi-sdk — the freestanding example above doesn’t, but anything that touches libc does.

# Install wasi-sdk first, then:# Install wasi-sdk first, then:
make -C examples/c-libc
sbt 'cliJVM/run --preopen /sandbox:./fixtures -- \
                examples/c-libc/hexdump.wasm /sandbox/data.bin'
# 00000000: 48 65 6c 6c 6f 0a                              |Hello.|

The .wasm is not committed (wasi-sdk isn’t free to install everywhere); see examples/c-libc/README.md for the build invocation.

Real WASI binary with a host-backed preopen

examples/rust/word_count.wasm is a rustc-built wasm32-wasip1 binary that reads a path passed as argv[1] from a wasi preopen and prints wc -lwc-style counts:

mkdir -p ./data
echo "The quick brown fox jumps over the lazy dog." > ./data/input.txt
echo "Pack my box with five dozen liquor jugs."    >> ./data/input.txt

sbt 'cliJVM/run --preopen ./data:/data examples/rust/word_count.wasm /data/input.txt'
#        2       17       86 /data/input.txt

The --preopen flag points the wasi-libc startup walk at the host’s ./data directory and tells the guest the visible name is /data. The trailing /data/input.txt is forwarded to the program as argv[1] — wasm-cli treats any positional args after the wasm file as WASI argv. From the rust binary’s perspective, /data/input.txt resolves; absolute paths outside /data don’t.

The matching examples/rust/src/main.rs source and Cargo.toml are committed; rebuild with cargo build --release --target=wasm32-wasip1 --manifest-path=examples/rust/Cargo.toml.

Real WASI binary with file writes

The test-suite fixture real_rust_filewrite.wasm writes a small file under its preopen — useful for confirming write capability against a host directory:

rm -rf /tmp/wasm-write
mkdir -p /tmp/wasm-write
sbt 'cliJVM/run --preopen /tmp/wasm-write:/sandbox \
                wasi/shared/src/test/resources/fixtures/real_rust_filewrite.wasm'
cat /tmp/wasm-write/output.txt

The path-sandbox rules apply: the rust binary asking for /sandbox/output.txt writes to /tmp/wasm-write/output.txt; the same binary asking for ../escape returns ENOTCAPABLE.

Native CLI binary

cliNative builds a standalone Scala Native binary — no JVM startup overhead, useful when you want to run wasm in a shell pipeline:

sbt cliNative/nativeLink
./cli/native/target/scala-3.8.3/wasm-cli-out \
    --preopen /tmp/sandbox:/sandbox \
    real_rust_fileread.wasm

The Scala Native build uses the same HostPreopen.fromDir shape as the JVM build but backs it with java.nio.file (via Scala Native’s JVM-compatibility layer).

Scala.js CLI (Node.js)

sbt’s command-line parser doesn’t forward positional args to scalajs‘s run task, so the JS CLI runs through Node directly:

sbt cliJS/fastLinkJS
node cli/js/target/scala-3.8.3/wasm-cli-fastopt/main.js \
     --preopen /tmp/sandbox:/sandbox \
     real_rust_fileread.wasm

The Scala.js build backs HostPreopen.fromDir with Node’s fs.*Sync APIs. Same trait surface, different syscalls underneath.

Inspecting a module without running it

--list-exports prints every exported function without instantiating:

sbt 'cliJVM/run --list-exports my-module.wasm'

Useful when picking an --invoke target, or when sanity-checking that a build pipeline produced the symbols you expect.

Search

Esc
to navigate to open Esc to close