waPC Host for Go
June 29, 2024 ยท View on GitHub
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. Poolfor creating a pool of instances for a given Module.