Skip to content

RFC-002 — Wasm Component Model wrapper

Status: Draft (v0.9 spec-freeze prep). Tracks amendments: A47 (Component Model deferred, core modules ship in v1.0), A97 (mighty:web/dom interface, canonical-ABI return- area bridge OPEN). Target release: v1.1. Owner: unassigned — design owner needed before promotion.

Implementation Status

NOT YET SHIPPED. Forward-looking RFC.

Adjacent v0.13..v0.23 work that does not moot this RFC but narrows its scope:

  • v0.13..v0.19 — WASI Preview 2 + user-supplied WIT ([wit] manifest section; default for wasm32-wasi; every std.* lowering goes through versioned P2 imports). Preview1 adapter is opt-in via AdapterEmbed::new(kind, bytes); the vendored bytes were removed in v0.19.
  • v0.23mty:web/canvas@0.1 + mty:web/input@0.1 WIT pair shipped, with WIT_IMPORT_* / WIT_EXPORT_* drift-guards. wasm32-web embedded-core-module regression harness verified the core module IS embedded at byte offset 189 (the long-standing "header-only component" suspicion was wrong).

The remaining v1.0 OPEN items the RFC must close are unchanged:

  • wit-component end-to-end wrap (currently raw wasm-encoder core modules).
  • Canonical-ABI return bridge for string / option<string> returns from mty:web/dom (get-text, query still return u32 handles in v1.0).
  • preview2 / wasi-cli-compatible host adapter for non-browser execution.

The window opened 2026-05-26 (close 2026-07-25, 60-day track) is substantive: reviewers can still drive the wit-component shape.

Cross-references:

Summary

Promote the v1.0 core-Wasm-module output of mty build --target wasm32-web (and --target wasm32-wasi) to a real Wasm Component Model binary. Use wit-component to wrap the core module against the curated WIT interfaces under mty:caps/* and mty:web/*, implement the canonical ABI's return-area protocol for string and option<string> returns, and ship a preview2 / wasi-cli-compatible host adapter for non-browser execution.

This RFC subsumes the OPEN sub-task in A97 (canonical-ABI bridge for get-text and query).

Motivation

The v1.0 contract emits a core Wasm module with capability imports declared as plain function imports under a mighty module namespace. Hosts must provide ad-hoc bridge code; the WIT contracts under mty:caps/* exist in docs/internals/ but are not enforced at link time. Three consequences:

  1. No type-checked host binding. A host that misspells an import gets a runtime WebAssembly.LinkError rather than a build-time diagnostic.
  2. No language portability. A Mighty Wasm artefact cannot be consumed by other component-model-aware toolchains (e.g. Spin, wasmtime's --component) without a hand-written shim.
  3. Canonical ABI gap for strings. mighty:web/dom.get-text and .query return option<string> in the WIT sketch but emit u32 handles in v1.0 because the core ABI cannot return a variable- length result. Browsers consume handles via a separate read-string-handle(u32) -> string shim.

Adopting the Component Model closes all three.

Detailed design

Pipeline

MtyIR → core .wasm (wasm-encoder, existing)
      → wit-component embed → .wasm with metadata
      → wit-component new   → component .wasm

mty build --target wasm32-web (and --target wasm32-wasi) gains a two-stage post-pass:

  1. Embed WIT metadata. The crate's WIT files (under crates/mty-codegen-wasm/wit/mty/caps/*.wit and mty/web/*.wit) are embedded in the core module via wit_component::embed_component_metadata.
  2. Build component. wit_component::ComponentEncoder::new() .module(&core_bytes)?.encode()? produces the final component.

Behind a --core-only opt-out flag for hosts that still need the v1.0 shape (e.g. existing browser embeds that depend on the mighty.set_text_handle import name).

WIT contract authority

The WIT files in crates/mty-codegen-wasm/wit/ become normative spec artefacts. Promote them to docs/spec/wit/v1.1/ and pin their versions:

package mty:caps@1.1.0;

interface fs {
  variant io-err {
    not-found(string),
    denied(string),
    forbidden(string),
  }
  read: func(path: string) -> result<list<u8>, io-err>;
  write: func(path: string, bytes: list<u8>) -> result<_, io-err>;
  exists: func(path: string) -> bool;
  list-dir: func(path: string) -> result<list<string>, io-err>;
}

interface net { ... }
interface clock { ... }
package mty:web@1.1.0;

interface dom {
  set-text: func(id: string, text: string);
  get-text: func(id: string) -> option<string>;   // RFC-002: real string
  on-click: func(id: string, callback-tag: string);
  query: func(selector: string) -> option<string>; // RFC-002: real string
}

The v0.4 backwards-compatibility imports (get-element-by-id: func(id: string) -> option<u32> etc.) remain in a separate mty:web-compat@1.0.0 package for one major cycle, then drop in v2.0.

Canonical ABI return-area bridge

The Component Model canonical ABI returns variable-length values through a caller-supplied return area: the callee writes the result and the lifted import unpacks it. For Mighty:

  1. The MtyIR Call { func: BuiltinId::DomOp("get_text"), .. } lowers in the wasm32-web backend to:
  2. allocate a 12-byte (ptr + len + discriminant) return area on the stack via the canonical-ABI cabi_realloc entry,
  3. emit a call to the lifted mty:web/dom.get-text import passing (id_ptr, id_len, ret_area),
  4. synthesise an Option[Str] MtyIR value from the return-area contents.
  5. The runtime ABI bridge in sdust_runtime::codegen_abi gains a mighty_alloc_return_area(size, align) -> *mut u8 symbol that wraps the active arena's bump allocator (A50).

The lifted Rust binding is auto-generated by wit-bindgen.

Host adapter

A new crate mty-wasm-host ships:

  • A wasmtime::component::Linker-based host that wires mty:caps/fs, mty:caps/net, mty:caps/clock against the existing FsCap / NetCap / clock surface from the runtime, sharing the same capability-narrowing semantics (A23, A100).
  • A WASI preview2 adapter that maps wasi:cli/run.run onto a Mighty fn main(...) -> Unit!RunErr.
  • A test harness that runs the conformance corpus under wasmtime --component.

Browser embedding

The v1.0 browser embed receives a parallel update:

  1. wasm_bindgen becomes jco transpile (Bytecode Alliance's JavaScript transpiler for components).
  2. The DOM cap implementation moves from hand-written JS into crates/mty-codegen-wasm/host-js/dom.ts and is compiled by jco.
  3. The set_text_handle(u32) -> () v0.4 shim is preserved behind a legacy-handle opt-in for in-flight web apps.

Drawbacks

  • Toolchain weight. wit-component + wasmtime::component add significant build-time dependencies. Behind a wasm-component cargo feature, default on for wasm targets only.
  • Browser-side jco churn. Browser hosts that integrate Mighty must adopt jco's runtime shape. The legacy shim limits the blast radius for the v1.0 → v1.1 transition.
  • Increased artefact size. A component is typically 5–15% larger than the equivalent core module due to canonical-ABI glue. Acceptable for the type-safety win; offset somewhat by enabling cross-tool optimisations.

Alternatives considered

  1. Keep core modules forever. Defeats the interop motivation.
  2. Hand-rolled WIT bindings (no wit-component). Saves the dependency cost but every WIT change becomes a hand-edit chore; regressions from the v0.5 dogfood pass discouraged this.
  3. Component Model 0.3 (preview). More ergonomic resource handles but unstable upstream. Re-evaluate for v2.0.

Unresolved questions

  • Should mighty:web/dom migrate to use Component Model resources (e.g. resource element { get-text: func() -> option<string>; }) instead of pass-by-id strings? Cleaner API, but a bigger break from the v1.0 surface. Punt to RFC-002.b.
  • How do imported components compose with the Mighty capability model? e.g. a third-party component that imports mty:caps/net — should the importer's sandbox block be able to narrow it? Initial answer: yes, via the same cap.host(...) algebra (A23), but the import binding needs to thread the narrowing into the canonical ABI call.
  • Stability of mty:caps/* 1.1 vs 1.x evolution: do we follow WIT's major-version pinning strictly, or allow additive minor bumps?

Adoption plan

  1. v1.1-alpha.1: --target wasm32-component opt-in flag; legacy wasm32-web and wasm32-wasi keep emitting core modules.
  2. v1.1-alpha.2: WIT files promoted to docs/spec/wit/v1.1/; mty-wasm-host crate ships.
  3. v1.1-beta: default flip — wasm32-web / wasm32-wasi emit components by default; --core-only opts back into v1.0 shape.
  4. v1.1.0: A47 reclassified FROZEN; A97 closes (option<string> returns now go through canonical ABI).
  5. v2.0: drop --core-only; drop mty:web-compat@1.0.0 shim.

A 60-day public comment window (longer than RFC-001 because of the toolchain blast radius) opens with v1.1-alpha.1.