---
title: WebAssembly Modules
url: "https://scriptc.dev/docs/wasm"
docs_index: /llms.txt
lastUpdated: 2026-10-18
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

A WebAssembly module lets a web page or another application call functions written in TypeScript. Use it when you want to run compiled logic, such as a simulation, a parser, or geometry processing, inside a host that owns the user interface. To build a program that runs on its own under a WASI runtime instead, see [WebAssembly (WASI)](/docs/platforms#webassembly-wasi-preview-1).

You build a module in library mode, with `SCRIPTC_TARGET=wasm32-wasi` and `scriptc build --lib --profile`. The result is a WASI Preview 1 reactor: a `.wasm` file with callable exports, named imports for host callbacks, and its own memory. Your application loads the module, supplies its imports, calls its exports, and handles events, scheduling, and rendering.

Module builds need [zig](https://ziglang.org) and the `@scriptc/runtime-wasm32-wasi` runtime pack in your project, at the same version as `scriptc --version`. See [Cross-compile](/docs/platforms#cross-compilation-via-zig).

```console
$ npm install --save-dev @scriptc/runtime-wasm32-wasi@0.3.5
```

## Define the interface

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

```ts title="counter.ts"
declare function changed(value: number): void;
let count = 0;
export function step(delta: number): number {
  count += delta;
  changed(count);
  return count;
}
```

```json title="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 the module:

```console
$ SCRIPTC_TARGET=wasm32-wasi scriptc build --lib --profile counter.json -o counter.wasm
```

Without `-o`, the output is `.scriptc/counter.wasm` beside the profile. The compiler API's `compileLibrary()` returns this path in `archivePath`, as it does for native libraries.

The profile uses the same schema as native library builds. <code>sink\_register\_symbol</code> and <code>callback\_register\_symbol</code> name native registration functions; a Wasm reactor does not export them. Wasm binds callbacks during instantiation and delivers panics through <code>scriptc.panic</code>.

Set `"optimization": "dev"` in the profile to turn off release optimization. npm dependencies compile automatically, as in executables. Add `"npm_static": ["three"]` to name packages explicitly, like the [`--npm-static`](/docs/dependencies#explicit-package-selection) option. Unsupported code that the module reaches 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 <code>WebAssembly.Module.imports(module)</code> to see the exact imports required by your compiled program. Supply real implementations or explicit failures for unsupported operations.

```js title="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 <code>getImportObject()</code> and <code>initialize(instance)</code> methods, as provided by Node's <code>node:wasi</code>. Browser adapters may use different method names. Initialization must first connect the adapter to the instance's exported memory and invoke <code>\_initialize</code> once, then call the profile's <code>init\_symbol</code>. Do not call a command-style <code>start()</code>; reactors have no <code>\_start</code> export.

After initialization, <code>counter\_step(2)</code> returns <code>2</code>, and another call returns <code>4</code>. Calling <code>counter\_init()</code> again resets application globals and reevaluates module initialization. Instantiate the module again for independent state and memory.

## Values and memory ownership

<table>
  <thead>
    <tr><th>Profile class</th><th>Wasm boundary</th></tr>
  </thead>

  <tbody>
    <tr>
      <td><code>f64</code></td>

      <td>JavaScript number.</td>
    </tr>

    <tr>
      <td><code>bool</code>, <code>u8</code>, <code>u32</code>, <code>i32</code></td>

      <td>Wasm i32; booleans use 0 or 1. u32 values may appear as signed JavaScript numbers; use <code>value >>> 0</code> to read them unsigned.</td>
    </tr>

    <tr>
      <td><code>i64</code>, <code>u64</code></td>

      <td>JavaScript BigInt. The existing library integer checks and proofs restrict values to the exact JavaScript number range.</td>
    </tr>

    <tr>
      <td><code>string</code>, <code>bytes</code> parameters</td>

      <td>Two i32 arguments: byte offset and byte length. Strings use UTF-8. Entry parameters are copied into runtime-owned values.</td>
    </tr>

    <tr>
      <td><code>string</code>, <code>bytes</code> results</td>

      <td>Two extra i32 arguments identify writable four-byte output slots. The function writes the result pointer and byte length there, in little-endian order.</td>
    </tr>

    <tr>
      <td><code>cstring</code> parameters</td>

      <td>One i32 argument: the byte offset of NUL-terminated UTF-8 text. The text is copied into a runtime-owned string; malformed UTF-8 becomes U+FFFD. A zero pointer traps with <code>SC4012</code>.</td>
    </tr>

    <tr>
      <td><code>cstring</code> results</td>

      <td>An i32 result: the byte offset of NUL-terminated UTF-8 text in the result arena. A result containing U+0000 traps with <code>SC4015</code> instead of being truncated.</td>
    </tr>

    <tr>
      <td><code>void</code></td>

      <td>No result.</td>
    </tr>
  </tbody>
</table>

The native library rules about directions 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. A <code>cstring</code> callback parameter is one i32 pointer to NUL-terminated text, borrowed the same way; a value containing U+0000 traps with <code>SC4015</code>. The <code>cstring</code> class requires <code>"profile\_format": 2</code>. In native libraries it is a <code>const char \*</code> with the same rules.

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

Result buffers belong to the module. If the profile declares <code>abi.result\_reset\_symbol</code>, results accumulate until that function is called. Otherwise, the next application export resets the result arena. Initialization and the optional <code>abi.collect\_symbol</code> 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 <code>scriptc\_free</code>.

Recreate typed arrays and DataViews from <code>memory.buffer</code> 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 <code>scriptc.panic(pointer, length)</code> 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 <code>requestAnimationFrame</code> and render its results through Canvas, WebGL, or WebGPU.

See [Limitations](/docs/limitations#browser-embedding) for browser graphics and library-mode restrictions.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)