-
Notifications
You must be signed in to change notification settings - Fork 0
Preview 2 upgrade analysis and recommended approach
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 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:
-
Shared resource/runtime core
Own args, env, clocks, random source, stdio, descriptor table, filesystem mounts, rights, preopens, exit handling, and tracing. -
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.
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-wasip1wasi-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’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
-
FileandFSare abstractions, not special cases in each syscall - filesystem wrappers encode policy (
ReadFS,AdaptFS,DirFS) instead of every syscall re-checking rights -
proc_exitis 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.
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.
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
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.
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.
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
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.
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.
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
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.
Make filesystems policy objects.
This is one of the best ideas in wazero:
-
AdaptFSfor read-only embedded files -
DirFSfor writable host directories -
ReadFSfor enforcement -
Fileinterface for open handles
This keeps right checks and mutation policy out of each syscall body.
The panic/recover ExitError approach is already a good fit for your wasm2go embedding model. Keep it.
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.
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
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
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.
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.
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
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. -
componenthostor 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
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
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.
The best first milestone is a design spike that answers three questions:
- What is the minimal shared runtime core API?
- What exact Preview 2 world do you need first?
- How will
wasm2go-runchoose Preview 1 vs Preview 2 at runtime?
Once those are answered, implementation becomes mostly mechanical.