wadge
is a WebAssembly "bridge" enabling native applications to invoke Wasm functions that are ordinarily provided by a Wasm runtime. Use cases for this project include:
- Testing WebAssembly applications natively, using existing development tools (e.g. debuggers)
- Extending native applications with WebAssembly plugins
Currently, wadge
supports Go applications written for compilation to WebAssembly components.
In the testing case, when you write a component using WebAssembly Interface Type (WIT) interfaces, standard Go tools like go test
don’t know how to natively stub the interfaces.
In order to maintain a typical, idiomatic Go development experience, you need a “bridge” between your Go toolchain and a WebAssembly runtime that makes interfaces “just work” when it's time to test your code.
wadge
(for “Wasm-bridge”) is a framework that bridges the gap, so you can work on your code in a Go-native way. This pattern can be extended beyond testing to extend native Go applications with WebAssembly functions.
wadge
handles interfaces belonging to the WebAssembly System Interface (WASI) 0.2 automatically, but also allows for testing of custom interfaces defined in WIT.
wadge
usescabish
to read and write values over FFI, passing to an embedded WebAssembly runtime (Wasmtime).- The
wadge-bindgen-go
utility walks the complete AST of your application and generates a binding file (bindings.wadge.go
) by default. - The bindings file utilizes
//go:linkname
directives to replace unimplemented functions with//go:wasmimport
directives—normally provided by the runtime—using a native implementation. In turn, the native implementation invokes those imports on a WebAssembly component running in an embeddedwadge
instance, operating in a "harness" pattern.
Add wadge
to your project:
go get go.wasmcloud.dev/wadge
Include the wadge
bindgen in your tools.go
like below:
//go:build tools
package main
import (
_ "github.com/bytecodealliance/wasm-tools-go/cmd/wit-bindgen-go"
_ "go.wasmcloud.dev/wadge/cmd/wadge-bindgen-go"
)
Write a test for the application in a new file called <filename>_test.go
(see example)
go mod tidy && go mod download
Generate wadge
bindings for your test:
go run go.wasmcloud.dev/wadge/cmd/wadge-bindgen-go
This generates bindings.wadge.go
.
Run go test
:
go test
PASS
ok example/http/test 0.296s
Writing tests with wadge
generally works as you would expect writing ordinary tests in Go:
- Use the standard
testing
package - Write tests in
<name>_test.go
files - Target functions with tests using the
TestTargetFunction
naming convention
For more on testing in Go, see the testing
package documentation.
See the examples/go/http
directory for examples of a simple HTTP server and associated test using wasi:http
.
There are a couple of wadge
-specific details to note:
RunTest
is written as a method of wadge
:
func TestIncomingHandler(t *testing.T) {
wadge.RunTest(t, func() {
req, err := http.NewRequest("", "test", nil)
if err != nil {
t.Fatalf("failed to create new HTTP request: %s", err)
}
wadgehttp
is used to call the component's export over the HandleIncomingRequest
interface of wasi:http
:
resp, err := wadgehttp.HandleIncomingRequest(incominghandler.Exports.Handle, req)
In the examples/go/http
test, the line above represents the only manual WASI-specific step: we pass in the component's export and a standard library HTTP request, and we receive a standard library HTTP response.
From this point on, we can test as usual—for example, making standard assert
statements:
assert.Equal(t, 200, resp.StatusCode)
assert.Equal(t, http.Header{
"foo": {
"bar",
"baz",
},
"key": {
"value",
},
}, resp.Header)
In order to write a test for a component that uses a custom interface defined in WIT, you can call wadge
in an init
function of your test file to use a custom instance of the platform harness: a component that exports all of the functionality imported by your application.
Typically the platform harness is instantiated automatically and exports standard WASI functionality. When using custom interfaces, you can create a new instance:
instance, err := wadge.NewInstance(&wadge.Config{
Wasm: component,
})
if err != nil {
log.Fatalf("failed to construct new instance: %s", err)
}
wadge.SetInstance(instance)
You can see this in practice in the wadge/tests/go/wasi
directory. The component in this directory uses custom fib
and leftpad
interfaces (defined in the /wit/
subfolder) and can be tested using go test
.
The init
function in the test file for this application is as follows:
func init() {
log.SetFlags(0)
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelDebug, ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
})))
instance, err := wadge.NewInstance(&wadge.Config{
Wasm: component,
})
if err != nil {
log.Fatalf("failed to construct new instance: %s", err)
}
wadge.SetInstance(instance)
}