DEV Community

Cover image for Writing components in Go with TinyGo compiler - WebAssembly Component Model
Tophe
Tophe

Posted on

Writing components in Go with TinyGo compiler - WebAssembly Component Model

Setup

Required tools for TinyGo WebAssembly Component Model support:

  • TinyGo compiler v0.39.0+: Compiles Go code to WebAssembly components
  • wkg: Resolves and bundles WIT dependencies into a single package
  • wasm-tools: Converts WebAssembly modules to components

Build Process

Go plugins require a specific build process due to TinyGo's Component Model requirements:

1. Prepare WIT files for TinyGo

TinyGo has specific requirements that differ from other languages. The prepare-wit-files.sh script (a custom script for this project) copies WIT files from crates/pluginlab/wit to go_modules/wit and uncomments TinyGo-specific lines:

./scripts/prepare-wit-files.sh -i crates/pluginlab/wit -o go_modules/wit -s "SPECIFIC TinyGo" 
Enter fullscreen mode Exit fullscreen mode

Why this workflow exists: The go_modules/wit directory is gitignored and contains modified versions of the WIT files specifically for TinyGo. This workflow maintains crates/pluginlab/wit as the single source of truth for all WIT definitions, while automatically generating TinyGo-compatible versions.

Why this is needed: The wasip2 target of TinyGo assumes that component is targeting wasi:cli/command@0.2.0 and requires explicit inclusion of WASI imports. The script searches for lines containing "SPECIFIC TinyGo" and uncomments them:

world plugin-api {  // The wasip2 target of TinyGo assumes that the component is targeting // wasi:cli/command@0.2.0 world (part of wasi:cli), so it needs to // include the imports from that world. // It's only included for the versions of the wit files destined for TinyGo. - // include wasi:cli/imports@0.2.0; // SPECIFIC TinyGo - DO NOT CHANGE THIS LINE + include wasi:cli/imports@0.2.0; // SPECIFIC TinyGo - DO NOT CHANGE THIS LINE  import http-client; import host-state-plugin; export plugin; } 
Enter fullscreen mode Exit fullscreen mode

2. Bundle WIT dependencies with wkg

Why wkg is used: TinyGo cannot resolve WIT imports on its own - it needs all dependencies to be either bundled into a single WASM file or pre-resolved into a local directory. For example, when TinyGo sees include wasi:cli/imports@0.2.0;, it doesn't know where to find that external package or what's inside it. The wkg tool handles dependency resolution by fetching all imported WIT interfaces and creating a complete WIT package:

cd go_modules/ wkg wit build 
Enter fullscreen mode Exit fullscreen mode

This creates repl:api.wasm containing all the WIT definitions needed by the plugin. The command also generates a wkg.lock file that locks the dependency versions for reproducible builds.

3. Generate Go bindings

Use wit-bindgen-go to generate Go code from the bundled WIT package:

cd go_modules/plugin-name/ go tool wit-bindgen-go generate --world plugin-api --out internal ../repl:api.wasm 
Enter fullscreen mode Exit fullscreen mode

This creates the internal/ directory with all the generated Go bindings for the plugin interfaces.

4. Compile with TinyGo

TinyGo compiles the Go code directly to a WebAssembly component:

cd go_modules/plugin-name/ tinygo build -target=wasip2 --wit-package ../repl:api.wasm --wit-world plugin-api -o plugin-name-go.wasm main.go 
Enter fullscreen mode Exit fullscreen mode

File Structure

Project organization: Go plugins follow this structure in the repo:

go_modules/ wit/ # Modified WIT files for TinyGo plugin-api.wit # Contains TinyGo-specific includes shared.wit # Shared types and interfaces host-api.wit # Host API definitions repl:api.wasm # Bundled WIT package (from wkg) wkg.lock # Lock file for WIT dependencies (from wkg) plugin-echo/ # Plugin directory go.mod # Go module with wit-bindgen-go tool main.go # Plugin implementation internal/ # Generated bindings (from wit-bindgen-go) repl/api/plugin/ # Plugin interface bindings repl/api/transport/ # Transport type bindings wasi/ # WASI interface bindings plugin-echo-go.wasm # Final WebAssembly component 
Enter fullscreen mode Exit fullscreen mode

Plugin Implementation

Standard pattern: Go plugins use the init() function to register their exports, following the WebAssembly Component Model pattern:

package main import ( "webassembly-repl/plugin-echo/internal/repl/api/plugin" "webassembly-repl/plugin-echo/internal/repl/api/transport" "go.bytecodealliance.org/cm" ) func init() { plugin.Exports.Name = func() string { return "echogo" } plugin.Exports.Man = func() string { return `... some man text ...` } plugin.Exports.Run = func(payload string) cm.Result[plugin.PluginResponse, plugin.PluginResponse, struct{}] { response := plugin.PluginResponse{ Status: transport.ReplStatusSuccess, Stdout: cm.Some(payload), Stderr: cm.None[string](), } return cm.OK[cm.Result[plugin.PluginResponse, plugin.PluginResponse, struct{}]](response) } } // main is required for the wasip2 target func main() {} 
Enter fullscreen mode Exit fullscreen mode

Go Module Configuration

Required setup: The go.mod file must include the wit-bindgen-go tool as a Go tool dependency:

module webassembly-repl/plugin-echo go 1.24 tool go.bytecodealliance.org/cmd/wit-bindgen-go // ... other dependencies 
Enter fullscreen mode Exit fullscreen mode

This allows the project to use go tool wit-bindgen-go commands for generating bindings.

📎 Here are links to:

Top comments (0)