Skip to content

Preview 2 upgrade analysis and recommended approach

lbe edited this page Jun 2, 2026 · 1 revision

I want to upgrade https://github.com/lbe/wasm2go-wasi-host to support wasi preview 2 in addition to it current wasi preview 1 support. I want to leverage the learnings of https://github.com/wazero/wazero. Perform an in-depth analysis of the two projects and produce a recommended approach for adding wasi preview 2

wasm2go-wasi-host: Preview 2 upgrade analysis and recommended approach

Executive summary

wasm2go-wasi-host is already a strong Preview 1 host for wasm2go-generated Go modules. The right way to add WASI Preview 2 is not to mutate the current Preview 1 syscall layer into something Preview 2-shaped. Preview 2 is a component-model system with WIT interfaces, canonical ABI lifting/lowering, and world-based imports/exports. That means it needs a distinct ABI boundary even if much of the underlying resource handling can be shared.

The best architecture is a two-layer host:

  1. Shared resource/runtime core
    Own args, env, clocks, random source, stdio, descriptor table, filesystem mounts, rights, preopens, exit handling, and tracing.

  2. ABI-specific front ends

    • Preview 1 front end: keep your current wasm2go path.
    • Preview 2 front end: add a component-model adapter path that maps a WASI Preview 2 world into the shared resource core.

That lets you preserve existing Preview 1 behavior while adding Preview 2 in a way that is structurally aligned with the standard.


What each project is telling you

wasm2go-wasi-host

Your current host is deliberately small and specialized:

  • it targets WASI snapshot-preview1
  • it is designed for wasm2go-generated Go modules
  • it uses a memory callback on every syscall
  • it models access with preopened directories
  • it uses a single-goroutine owner model
  • it already passes the full wasm32-wasip1 wasi-testsuite inventory

That is a good fit for Preview 1. It is also a sign that the current implementation is close enough to a stable base that it should not be torn apart just to chase Preview 2.

wazero

wazero’s useful lesson is not “copy its whole WASI stack.” The useful lesson is separation of concerns:

  • config is immutable and separate from runtime state
  • runtime state is stored in a context/store object
  • fd management is centralized in a table
  • File and FS are abstractions, not special cases in each syscall
  • filesystem wrappers encode policy (ReadFS, AdaptFS, DirFS) instead of every syscall re-checking rights
  • proc_exit is handled by panic/recover-style termination
  • the start function model should not break older compilers/users when a new WASI generation arrives

The strongest point for your project is that you already adopted several of these ideas in spirit. That means the Preview 2 upgrade should build on the same pattern: move resource handling down, keep ABI handling up.


Why Preview 2 is different enough to require a new front end

Preview 2 is defined in the component model ecosystem:

  • WIT describes interfaces
  • components compose around imports/exports
  • canonical ABI lifts and lowers values across the boundary
  • a “world” bundles the set of interfaces a component expects or provides

That is a different contract from a core wasm module importing wasi_snapshot_preview1 functions directly. So even if the filesystem semantics look familiar, the calling convention and packaging are different.

The practical consequence is:

  • Preview 1 host methods can stay as direct core-wasm import handlers.
  • Preview 2 host methods must sit behind a component-aware boundary.

In other words, Preview 2 is not “more imports”; it is a different embedding model.


What should be shared

These pieces should be shared between Preview 1 and Preview 2:

  • args
  • environ
  • stdin/stdout/stderr
  • clock and sleep hooks
  • random source
  • fd table / descriptor lifecycle
  • rights propagation
  • preopened filesystem configuration
  • filesystem confinement rules
  • exit-status propagation
  • tracing/debugging hooks

If these live in a resource core, both ABIs can use them.

What should not be shared is the ABI shape:

  • Preview 1: direct X... import methods for core wasm
  • Preview 2: component-level imports/exports and canonical ABI shims

The recommended architecture

1) Split the code into three layers

A. Resource core

This is the engine room.

It should own a runtime Context object holding:

  • args
  • env
  • stdio
  • clocks
  • random
  • preopens
  • fd table / open files
  • rights
  • guest path bookkeeping
  • exit status / closed state

This layer should be spec-agnostic where possible. It should not know whether it is serving Preview 1 or Preview 2.

B. Preview 1 ABI adapter

This is your existing wasihost path.

It should translate wasi_snapshot_preview1 imports into resource-core operations. Keep this path stable, because it is already validated and valuable.

C. Preview 2 component adapter

This should translate a component-model WASI world into the same resource core.

It will need:

  • component validation/loading
  • WIT world mapping
  • canonical ABI lift/lower glue
  • a Preview 2-specific start path
  • a way to map Preview 2 filesystem and CLI interfaces onto the shared runtime resources

2) Make the component adapter narrow at first

Do not start by trying to implement the entire component model surface.

Start with the smallest useful WASI Preview 2 scope for your workload, probably the CLI world and the filesystem/stdio pieces you actually need. Then expand only after it is working end to end.

That keeps the project tractable.

3) Preserve Preview 1 as a first-class path

Your current users and test corpus are Preview 1-shaped. Keep that path intact while you add Preview 2.

A dual-path design also gives you a fallback if a Preview 2 binary is not yet available from wasm2go or if a specific component feature is not implemented yet.


What to borrow from wazero’s design discipline

Immutable configuration

Use a builder/config object for module instantiation, but keep it separate from runtime state. The config should be cloned or otherwise protected from accidental mutation.

Why this matters:

  • easier reasoning
  • fewer hidden side effects
  • safer concurrent use
  • easier ABI migration later

Centralized fd table

Do not let Preview 2-specific code poke at ad hoc fd maps.

The fd table should be a first-class runtime structure:

  • lookup
  • open
  • close
  • renumber
  • invalidate
  • bulk close on teardown

That is the right place to preserve open-file-description semantics and rights inheritance.

File and FS abstractions

Make filesystems policy objects.

This is one of the best ideas in wazero:

  • AdaptFS for read-only embedded files
  • DirFS for writable host directories
  • ReadFS for enforcement
  • File interface for open handles

This keeps right checks and mutation policy out of each syscall body.

Exit as a structured termination path

The panic/recover ExitError approach is already a good fit for your wasm2go embedding model. Keep it.

Keep the start function logic additive

wazero explicitly warns that Preview 2 will not necessarily use the same _start story as Preview 1. The lesson is to add new startup behavior, not replace the old one.

That is the right mental model for your project too.


Recommended implementation plan

Phase 1: harden the shared resource core

Before touching Preview 2, make sure the runtime core is cleanly isolated.

Goals:

  • all descriptor state lives in one runtime object
  • all filesystem policy is behind interfaces
  • all mounts are declared once
  • Preview 1 handlers consume the runtime core, not embedder fields directly

Phase 2: add a Preview 2-facing API surface

Introduce a new package or subpackage for component-aware loading and host binding.

This layer should:

  • detect whether an input is a core module or a component
  • choose the correct execution path
  • map the world/interface set to the shared resource core
  • provide a start entrypoint appropriate for the component model

Phase 3: support a minimal WASI Preview 2 world

Start with the specific Preview 2 world you need for wasm2go use cases.

Most likely:

  • CLI-oriented command flow
  • stdio
  • env/args
  • preopened filesystem
  • clocks/random

Defer networking, async, and other packages until you genuinely need them.

Phase 4: add compatibility adapters

If your ecosystem still produces Preview 1 modules, keep the bridge story simple.

Possible options:

  • keep running Preview 1 modules directly
  • or package them through a Preview 1-to-Preview 2 adapter when you need a component-shaped artifact

This mirrors the pattern used in the Wasmtime ecosystem, where a Preview 1 module can be bridged into Preview 2 via an adapter component.

Phase 5: grow the Preview 2 surface incrementally

Add packages only when a real module requires them.

Do not front-load:

  • full async support
  • arbitrary component composition
  • all WASI packages
  • networking unless needed

Concrete design recommendation for this repo

I would structure it like this:

  • internal/wasi/
    Shared runtime types, fd table, filesystem interfaces, clock interfaces, errno mappings, and resource handling.

  • wasihost
    Preview 1 ABI adapter for wasm2go-generated modules.

  • componenthost or similar
    Preview 2 component adapter and world binding layer.

  • wasm2go-run
    Auto-detect whether the input is a core module or a component and route accordingly.

This gives you a stable migration path:

  • Preview 1 keeps working
  • Preview 2 arrives as a new execution mode
  • shared resource code stays reusable across both

What not to do

Do not:

  • try to encode Preview 2 as just more Preview 1 imports
  • merge component ABI concerns into the current syscall methods
  • replace the current runtime model with a component model all at once
  • lose Preview 1 compatibility
  • implement all of Preview 2 before supporting any real workload

My recommendation in one sentence

Keep the current Preview 1 host intact, extract and strengthen the shared resource/runtime core, and add WASI Preview 2 as a separate component-model adapter layer that reuses that core instead of rewriting it.


Practical next milestone

The best first milestone is a design spike that answers three questions:

  1. What is the minimal shared runtime core API?
  2. What exact Preview 2 world do you need first?
  3. How will wasm2go-run choose Preview 1 vs Preview 2 at runtime?

Once those are answered, implementation becomes mostly mechanical.

Clone this wiki locally