Skip to content

wapc/wapc-go

Repository files navigation

waPC Host for Go Gitter

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.