WebAssembly Modules

Compile a reusable module with SCRIPTC_TARGET=wasm32-wasi and scriptc build --lib --profile. The output is a WASI Preview 1 reactor: a .wasm file with callable exports, named host imports, and its own memory. Application code runs as compiled Wasm, without an embedded JavaScript engine.

Your web application owns loading, browser events, animation scheduling, and rendering. It supplies the module's imports and calls its exports. scriptc does not generate a web application or a graphics adapter.

Define the interface

The library profile names the entry module and its public functions. A reachable callback call becomes a synchronous import from the scriptc namespace.

counter.ts
declare function changed(value: number): void;
let count = 0;
export function step(delta: number): number {
  count += delta;
  changed(count);
  return count;
}
counter.json
{
  "profile_format": 1,
  "name": "counter",
  "entry": "counter.ts",
  "emission": "llvm",
  "abi": {
    "prefix": "counter_",
    "init_symbol": "counter_init",
    "sink_register_symbol": "counter_sink",
    "callback_register_symbol": "counter_callback"
  },
  "callbacks": [{ "name": "changed", "params": ["f64"], "returns": "void" }],
  "exports": [{ "export": "step", "symbol": "counter_step", "params": ["f64"], "returns": "f64" }]
}

Build with the wasm32-wasi target and select the output with -o counter.wasm. Without -o, the output is .scriptc/counter.wasm beside the profile. The compiler API's compileLibrary() returns this path in archivePath, matching the existing native library API.

The profile uses the native library schema. sink_register_symbol and callback_register_symbol name native registration functions; a Wasm reactor does not export them. Wasm binds callbacks during instantiation and delivers panics through scriptc.panic.

Set "optimization": "dev" in the profile for faster builds. Eligible npm dependencies are attempted automatically. A profile may add "npm_static": ["three"] to explicitly attempt compilation from a package's source, including packages without bundled declarations. Unsupported reachable code still produces a compiler diagnostic.

Instantiate and initialize

A browser host needs a WASI Preview 1 adapter as well as the application's callbacks. Browsers do not supply WASI automatically. Inspect WebAssembly.Module.imports(module) to see the exact imports required by your compiled program. Supply real implementations or explicit failures for unsupported operations.

loader.js
export async function loadCounter(url, wasi) {
  const module = await WebAssembly.compileStreaming(fetch(url));
  let api;
  const instance = await WebAssembly.instantiate(module, {
    ...wasi.getImportObject(),
    scriptc: {
      changed(value) {
        console.log(value);
      },
      panic(pointer, length) {
        const bytes = new Uint8Array(api.memory.buffer, pointer, length);
        throw new Error(new TextDecoder().decode(bytes));
      },
    },
  });
  api = instance.exports;
  wasi.initialize(instance);
  api.counter_init();
  return api;
}

This loader assumes an adapter with getImportObject() and initialize(instance) methods, as provided by Node's node:wasi. Browser adapters may use different method names. Initialization must first connect the adapter to the instance's exported memory and invoke _initialize once, then call the profile's init_symbol. Do not call a command-style start(); reactors have no _start export.

After initialization, counter_step(2) returns 2, and another call returns 4. Calling counter_init() again resets application globals and reevaluates module initialization. Instantiate the module again for independent state and memory.

Values and memory ownership

Profile classWasm boundary
f64JavaScript number.
bool, u8, u32, i32Wasm i32; booleans use 0 or 1. u32 values may appear as signed JavaScript numbers; use value >>> 0 to read them unsigned.
i64, u64JavaScript BigInt. The existing library integer checks and proofs restrict values to the exact JavaScript number range.
string, bytes parametersTwo i32 arguments: byte offset and byte length. Strings use UTF-8. Entry parameters are copied into runtime-owned values.
string, bytes resultsTwo extra i32 arguments identify writable four-byte output slots. The function writes the result pointer and byte length there, in little-endian order.
voidNo result.

The native profile's direction restrictions still apply: u8/u32/i32 are export parameters and callback scalars; i64/u64 are export-only. Callback results must be scalars or void. Callback string/byte parameters are borrowed only for the duration of that callback; copy them before retaining them in the host.

Allocate host input buffers and output slots with scriptc_alloc(size); a zero result indicates allocation failure. Release each allocation exactly once with scriptc_free(pointer). Only free pointers returned by scriptc_alloc. These allocations remain live across application calls, initialization, and collection until freed.

Result buffers belong to the module. If the profile declares abi.result_reset_symbol, results accumulate until that function is called. Otherwise, the next application export resets the result arena. Initialization and the optional abi.collect_symbol also invalidate results. Allocation and free do not reset the arena. Copy a result into host-owned storage before invalidating it; never pass its pointer to scriptc_free.

Recreate typed arrays and DataViews from memory.buffer after every Wasm call that may allocate. Memory growth replaces the backing buffer and invalidates older views. Pointers remain offsets into the instance's memory; never use a pointer with another instance.

Errors and scheduling

A compiled trap or escaped exception calls scriptc.panic(pointer, length) with the native library's structured diagnostic bytes, then traps if the callback returns. Discard the instance after a panic, a Wasm trap, or an exception escaping a host import. Recovery uses a new instance; calling the initialization export does not repair a failed instance.

Host callbacks run synchronously. They must return before calling any application export, initialization, collection, allocation, or free on the same instance. Reentry is rejected. Copy callback data and schedule subsequent work after the outer call returns. The web app can call a frame export from requestAnimationFrame and render its results through Canvas, WebGL, or WebGPU.

See Limitations for browser graphics and library-mode boundaries.