WASI Preview 2 changes the foundations of WASI Preview 1 (where, for instance, a function can only exchange numbers). It rests on the Component Model, whose contribution fits in one sentence: the contract becomes a file, and the tooling generates all the conversion code from that file (yes, I know, that's a very crude shortcut).
Rust is still the language furthest ahead in supporting the various WASI versions, especially on the toolchain side. But my heart belongs to Golang, and TinyGo offers WASI Preview 2 support too — a little less "integrated" from a toolchain point of view, so it takes a small effort to get going, but it works perfectly.
Today we'll see how to build a "wasip2" component in Go with TinyGo, and how to use it from Node.js.
Prerequisites
| Tool | Version | Role |
|---|---|---|
| TinyGo | 0.41.1 | compiles Go to a WebAssembly component |
| Go | 1.26.5 | required by TinyGo |
| Node.js | 22.22.1 | the host |
wasm-tools | 1.256.0 | component packaging, invoked by TinyGo |
jco | 1.28.1 | transpiles a component into a JavaScript module |
wit-bindgen-go | v0.7.0 | generates the Go types from the WIT contract |
wasm-tools is not optional: TinyGo invokes it for the wasip2 target. Grab the binary from the wasm-tools releases and put it in your PATH.
The other two install like this:
npm install -g @bytecodealliance/jco
go install go.bytecodealliance.org/cmd/wit-bindgen-go@latest
Check that
$(go env GOPATH)/binis in yourPATH, otherwisewit-bindgen-gowill stay out of reach.
The "basics"
Three notions are enough to get started.
A component is not an ordinary WebAssembly module: it's a module wrapped together with the description of its interfaces. Where a Preview 1 module exposes functions built out of integers, a component exposes typed functions.
WIT (WebAssembly Interface Types) is the language that description is written in. A .wit file is a contract, independent of any programming language.
A world is that contract seen from a component: the list of what it imports (what it needs) and what it exports (what it offers).
Step 1: describe the component (write the interface/the contract in WIT)
To describe the component, we need a .wit file. Create the following tree:
.
├── greetings-component
│ └── wit
│ └── greetings.wit
And here is our component's WIT "contract":
package demo:greetings@0.1.0;
/// Data types shared between the host and the component.
interface types {
/// A record is a structure with named fields: a struct in Go, an object in JS.
record dog {
name: string,
breed: string,
}
/// Types compose: a human holds a record
record human {
name: string,
age: u32,
dog: dog,
}
}
/// An *exported* interface: the component provides it, the host calls it.
interface greetings {
use types.{human, dog};
/// A record as parameter, a string as the result.
greet: func(h: human) -> string;
/// Lists travel as they are, in both directions.
greet-all: func(people: list<human>) -> list<string>;
}
/// A world describes everything a component needs and everything it offers.
world guest {
/// Gives the component access to the base WASI interfaces (clock, I/O, randomness, ...) that the TinyGo runtime needs.
include wasi:cli/imports@0.2.0;
/// `greetings` uses `types`, so `human` and `dog` need an identity in the
/// component. Exporting `types` gives them one *inside* the component.
export types;
export greetings;
}
Step 2: copy the WASI 0.2 WIT tree out of the TinyGo install
The TinyGo world has to declare (include wasi:cli/imports@0.2.0) the WASI imports its runtime emits, otherwise componentization fails — and since WIT only resolves its dependencies locally, in wit/deps/, we need the WASI .wit files on disk.
We copy them from the TinyGo install because it's the only source guaranteed to be at the same version as the runtime.
Rust doesn't need this: the WASI imports of the Rust std are added by the linker, which encodes the component type itself from the WASI its std embeds.
You can check the path of the TinyGo install that holds the WASI 0.2 WITs:
ls "$(tinygo env TINYGOROOT)/lib/wasi-cli/wit"
# Copy the WASI 0.2 WIT tree out of the TinyGo install (needed by the include).
TINYGO_WIT="$(tinygo env TINYGOROOT)/lib/wasi-cli/wit"
cd greetings-component
mkdir -p wit/deps/cli
cp $TINYGO_WIT/*.wit wit/deps/cli/
cp -r $TINYGO_WIT/deps/* wit/deps/
Step 3: generate the bindings
cd greetings-component
go mod init demo
wit-bindgen-go generate --world guest --out internal/ ./wit
You should end up with the following tree:
.
├── go.mod
├── internal
│ ├── demo
│ │ └── greetings
│ │ ├── greetings
│ │ ├── guest
│ │ └── types
│ └── wasi
│
Next, we need the go.bytecodealliance.org/cm package:
go get go.bytecodealliance.org/cm@v0.3.0
For strings, the bindings depend on the
go.bytecodealliance.org/cmmodule. Sogo.modmust require it (go get go.bytecodealliance.org/cm@v0.3.0).
Step 4: write the implementation
In greetings-component, next to the go.mod file, create a main.go file:
package main
import (
greetings "demo/internal/demo/greetings/greetings"
"fmt"
"go.bytecodealliance.org/cm"
)
func init() {
greetings.Exports.Greet = func(h greetings.Human) (result string) {
return "👋 Hello " + h.Name + ", your dog's name is " + h.Dog.Name
}
greetings.Exports.GreetAll = func(people cm.List[greetings.Human]) (result cm.List[string]) {
messages := make([]string, 0, len(people.Slice()))
for _, p := range people.Slice() {
messages = append(messages, fmt.Sprintf("🤓 Hello %s and %s!", p.Name, p.Dog.Name))
}
return cm.ToList(messages)
}
}
func main() {} // required by TinyGo, even if it's empty
Step 5: compile the component
## component: compile the WebAssembly component from the TinyGo code.
# --wit-package points at the WIT directory, --wit-world at the world to honour.
# TinyGo delegates the component packaging to wasm-tools, which must be in PATH.
cd greetings-component
tinygo build -target=wasip2 -wit-package ./wit -wit-world guest -o ../nodejs-host/greetings.wasm
the command will create a
./nodejs-hostfolder next to./greetings-component.
Step 6: use our component from Node.js (the WASI host)
Node cannot run a component directly — there is no node:wasi equivalent for Preview 2. We go through jco, which translates the component into a JavaScript module Node can use:
cd nodejs-host
npm init -y
npm pkg set type=module
npm install --save-dev @bytecodealliance/jco
npm install @bytecodealliance/preview2-shim
preview2-shimis a runtime dependency, not a dev one: the generated JS imports it directly (it's the one implementing wasi:cli, wasi:clocks, wasi:io… that theguestworld includes).
Transpiling:
npx jco transpile greetings.wasm -o ./greetings
You should get something like this:
.
├── greetings
├── greetings.wasm
├── node_modules
├── package-lock.json
└── package.json
Using it from Node
The WIT greetings interface becomes an exported namespace, and kebab-case names become camelCase (greet-all -> greetAll). In ./nodejs-host, create an index.js file with the content below:
import { greetings } from './greetings/greetings.js'
const bob = { name: 'Bob', age: 42, dog: { name: 'Wanda', breed: 'Beagle' } }
const sam = { name: 'Sam', age: 30, dog: { name: 'Rex', breed: 'Corgi' } }
console.log(greetings.greet(bob))
console.log(greetings.greetAll([bob, sam]))
WIT
records become plain JS objects, andlist<human>anArray— no memory management on the JS side: the promise of the component model.
Run index.js:
node index.js
Output:
👋 Hello Bob, your dog's name is Wanda
[ '🤓 Hello Bob and Wanda!', '🤓 Hello Sam and Rex!' ]
Conclusion
So we've built our 1st wasip2 component, and we know how to use it from Node.js. In an upcoming blogpost, we'll see how to use that same component from a Go host, with the wazy framework, a WebAssembly runtime for Go developed by Samy Fodil.
Source code: 01-make-a-wasip2-component
Written by

No comments yet. Be the first to comment!