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)