How to Use Wazero for Running Wasm Plugins in Go

Use Wazero by instantiating a `Runtime`, compiling your WebAssembly module from a file or bytes, and then instantiating it to call exported functions directly from your Go code.

Use Wazero by instantiating a Runtime, compiling your WebAssembly module from a file or bytes, and then instantiating it to call exported functions directly from your Go code. This approach allows you to load untrusted plugin logic in a sandboxed environment without needing a separate process or complex IPC.

Here is a minimal example of loading a Wasm plugin that exports a calculate function and invoking it:

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/tetratelabs/wazero"
	"github.com/tetratelabs/wazero/api"
)

func main() {
	ctx := context.Background()
	r := wazero.NewRuntime(ctx)
	defer r.Close(ctx)

	// Compile the Wasm module (assuming plugin.wasm exists)
	// In production, you might read this from a file or network
	wasm, err := r.CompileModule(ctx, []byte(`
		(module
			(func $calculate (param $a i32) (param $b i32) (result i32)
				local.get $a
				local.get $b
				i32.add
			)
			(export "calculate" (func $calculate))
		)
	`))
	if err != nil {
		log.Fatalf("failed to compile: %v", err)
	}

	// Instantiate the module
	instance, err := r.InstantiateModule(ctx, wasm, wazero.NewModuleConfig())
	if err != nil {
		log.Fatalf("failed to instantiate: %v", err)
	}
	defer instance.Close(ctx)

	// Call the exported function
	fn := instance.ExportedFunction("calculate")
	if fn == nil {
		log.Fatal("function 'calculate' not found")
	}

	result, err := fn.Call(ctx, 10, 20)
	if err != nil {
		log.Fatalf("call failed: %v", err)
	}

	fmt.Printf("Result: %d\n", result[0]) // Output: Result: 30
}

For more complex plugins that need to interact with your host application, you can pass Go functions into the Wasm module using wazero.NewModuleConfig().WithFunctions. This allows your plugin to call back into your Go code for database access, logging, or configuration.

// Define a Go function to be called from Wasm
func logMessage(ctx context.Context, mod api.Module, msg string) {
	fmt.Println("Plugin logged:", msg)
}

// Configure the runtime to expose this function to the Wasm module
config := wazero.NewModuleConfig().
	WithFunctions("env", "log", logMessage)

instance, err := r.InstantiateModule(ctx, wasm, config)

In this setup, your Wasm module can declare an import like (import "env" "log" (func $log (param i32 i32))) and call it as if it were a native Wasm function. Wazero handles the type conversion and context passing automatically.

Key benefits of this pattern include zero-copy performance for data passing, automatic memory management for the Wasm heap, and strict isolation since the plugin runs in a separate memory space. You don't need to manage threads or processes manually, making it ideal for serverless functions, sandboxed scripting, or extensible CLI tools. Just ensure you validate the Wasm binary before compilation if the source is untrusted, as Wazero executes the code immediately upon instantiation.