/wapc-go

Golang-based WebAssembly Host Runtime for waPC-compliant modules

Primary LanguageGoApache License 2.0Apache-2.0

waPC Host for Go

Gitter PkgGoDev Go Report Card go tests

This is the Golang implementation of the waPC standard for WebAssembly host runtimes. It allows any WebAssembly module to be loaded as a guest and receive requests for invocation as well as to make its own function requests of the host.

Example

The following is a simple example of synchronous, bidirectional procedure calls between a WebAssembly host runtime and the guest module.

package main

import (
	"context"
	"fmt"
	"os"
	"strings"

	"github.com/wapc/wapc-go"
	"github.com/wapc/wapc-go/engines/wazero"
)

func main() {
	if len(os.Args) < 2 {
		fmt.Println("usage: hello <name>")
		return
	}
	name := os.Args[1]
	ctx := context.Background()
	guest, err := os.ReadFile("hello/hello.wasm")
	if err != nil {
		panic(err)
	}

	engine := wazero.Engine()

	module, err := engine.New(ctx, host, guest, &wapc.ModuleConfig{
		Logger: wapc.PrintlnLogger,
		Stdout: os.Stdout,
		Stderr: os.Stderr,
	})
	if err != nil {
		panic(err)
	}
	defer module.Close(ctx)

	instance, err := module.Instantiate(ctx)
	if err != nil {
		panic(err)
	}
	defer instance.Close(ctx)

	result, err := instance.Invoke(ctx, "hello", []byte(name))
	if err != nil {
		panic(err)
	}

	fmt.Println(string(result))
}

func host(ctx context.Context, binding, namespace, operation string, payload []byte) ([]byte, error) {
	// Route the payload to any custom functionality accordingly.
	// You can even route to other waPC modules!!!
	switch namespace {
	case "example":
		switch operation {
		case "capitalize":
			name := string(payload)
			name = strings.Title(name)
			return []byte(name), nil
		}
	}
	return []byte("default"), nil
}

To see this in action, enter the following in your shell:

$ go run example/main.go waPC!
hello called
Hello, WaPC!

Alternatively you can use a Pool to manage a pool of instances.

	pool, err := wapc.NewPool(ctx, module, 10, func(instance wapc.Instance) error {
		// Do something to initialize this instance before use.
		return nil
	})
	if err != nil {
		panic(err)
	}
	defer pool.Close(ctx)

	for i := 0; i < 100; i++ {
		instance, err := pool.Get(10 * time.Millisecond)
		if err != nil {
			panic(err)
		}

		result, err := instance.Invoke(ctx, "hello", []byte("waPC"))
		if err != nil {
			panic(err)
		}

		fmt.Println(string(result))

		err = pool.Return(instance)
		if err != nil {
			panic(err)
		}
	}

While the above example uses wazero, wapc-go is decoupled (via wapc.Engine) and can be used with different runtimes.

Engines

Here are the supported wapc.Engine implementations, in alphabetical order:

Name Usage Build Tag Package
wasmer-go wasmer.Engine() wasmer github.com/wasmerio/wasmer-go
wasmtime-go wasmtime.Engine() wasmtime github.com/bytecodealliance/wasmtime-go
wazero wazero.Engine() N/A github.com/tetratelabs/wazero

For example, to switch the engine to wasmer, change example/main.go like below:

--- a/example/main.go
+++ b/example/main.go
@@ -7,7 +7,7 @@ import (
        "strings"

        "github.com/wapc/wapc-go"
-       "github.com/wapc/wapc-go/engines/wazero"
+       "github.com/wapc/wapc-go/engines/wasmer"
 )

 func main() {
@@ -22,7 +22,7 @@ func main() {
                panic(err)
        }

-       engine := wazero.Engine()
+       engine := wasmer.Engine()

        module, err := engine.New(ctx, code, hostCall)
        if err != nil {

Then, run with its build tag:

$ go run --tags wasmer example/main.go waPC!
hello called
Hello, WaPC!

Differences with wapc-rs (Rust)

Besides engine choices, there differences between this library and the Rust implementation:

  • Separate compilation (New) and instantiation (Instantiate) steps. This is to incur the cost of compilation once in a multi-instance scenario.
  • Pool for creating a pool of instances for a given Module.