From 7611a8ab0dbd33ccbbc8c9483c5e28a9964d0116 Mon Sep 17 00:00:00 2001 From: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com> Date: Thu, 18 Aug 2022 21:26:27 +0800 Subject: [PATCH] Adds ConsoleLog and tests in a support matrix (#8) * Adds ConsoleLog and tests in a support matrix This adds `wapc.ConsoleLog` and stubs in the first end-to-end test for guest integration using wazero. This adds CI, reverse engineering a support matrix by the min and max supported Go versions based on latest TinyGo. Note: This is wider than Go's policy as Go no longer supports 1.16 which is the version required by TinyGo 0.17.0. Signed-off-by: Adrian Cole * typo Signed-off-by: Adrian Cole Signed-off-by: Adrian Cole --- .github/workflows/go-test.yaml | 49 ++++++++++ README.md | 2 +- go.mod | 2 +- imports.go | 3 + imports_webassembly.go | 5 ++ internal/Makefile | 9 ++ internal/e2e_test.go | 67 ++++++++++++++ internal/go.mod | 16 ++++ internal/go.sum | 17 ++++ internal/host_test.go | 113 ++++++++++++++++++++++++ internal/testdata/__console_log/main.go | 7 ++ wapc.go | 9 +- 12 files changed, 295 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/go-test.yaml create mode 100644 internal/Makefile create mode 100644 internal/e2e_test.go create mode 100644 internal/go.mod create mode 100644 internal/go.sum create mode 100644 internal/host_test.go create mode 100644 internal/testdata/__console_log/main.go diff --git a/.github/workflows/go-test.yaml b/.github/workflows/go-test.yaml new file mode 100644 index 0000000..08cfc8e --- /dev/null +++ b/.github/workflows/go-test.yaml @@ -0,0 +1,49 @@ +name: go tests + +on: + push: + tags: + - v* + branches: + - master + - main + pull_request: + +jobs: + # Note: TinyGo is not idempotent when generating wasm, so we don't check in + # %.wasm as a part of this job. + build: + runs-on: ubuntu-latest + strategy: + matrix: + go-version: # Note: Go only supports 2 versions: https://go.dev/doc/devel/release#policy + - "1.16" # Minimum Go version of latest TinyGo + - "1.18" # Latest + tinygo-version: # See https://github.com/tinygo-org/tinygo/releases + - "0.18.0" # First version to use wasi_snapshot_preview1 + - "0.25.0" # Latest + + steps: + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.18 + + - name: Install TinyGo + run: | # Installing via curl so commands are similar on OS/x + tinygo_version=${{ matrix.tinygo-version }} + curl -sSL https://github.com/tinygo-org/tinygo/releases/download/v${tinygo_version}/tinygo${tinygo_version}.linux-amd64.tar.gz | sudo tar -C /usr/local -xzf - + echo "TINYGOROOT=/usr/local/tinygo" >> $GITHUB_ENV + echo "/usr/local/tinygo/bin" >> $GITHUB_PATH + + - name: Checkout + uses: actions/checkout@v3 + + - name: Build example + run: tinygo build -o example/hello.wasm -scheduler=none --no-debug -target=wasi example/hello.go + + - name: Build test wasm + run: cd internal; make build-wasm + + - name: Test + run: go test -v ./... diff --git a/README.md b/README.md index d3a1d64..2896373 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ func hello(payload []byte) ([]byte, error) { ``` ```sh -tinygo build -o example/hello.wasm -target wasm -no-debug example/hello.go +tinygo build -o example/hello.wasm -scheduler=none --no-debug -target=wasi example/hello.go ``` ## Considerations diff --git a/go.mod b/go.mod index ff68340..548e655 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/wapc/wapc-guest-tinygo -go 1.15 +go 1.16 diff --git a/imports.go b/imports.go index 37c27fa..765011a 100644 --- a/imports.go +++ b/imports.go @@ -1,3 +1,4 @@ +//go:build !purego && !appengine && !wasm && !tinygo.wasm && !wasi // +build !purego,!appengine,!wasm,!tinygo.wasm,!wasi package wapc @@ -23,3 +24,5 @@ func hostResponse(ptr uintptr) {} func hostErrorLen() uint32 { return 0 } func hostError(ptr uintptr) {} + +func consoleLog(ptr uintptr, size uint32) {} diff --git a/imports_webassembly.go b/imports_webassembly.go index 7c902c0..c73d8dd 100644 --- a/imports_webassembly.go +++ b/imports_webassembly.go @@ -1,3 +1,4 @@ +//go:build wasm || tinygo.wasm || wasi // +build wasm tinygo.wasm wasi package wapc @@ -37,3 +38,7 @@ func hostErrorLen() uint32 //go:wasm-module wapc //go:export __host_error func hostError(ptr uintptr) + +//go:wasm-module wapc +//go:export __console_log +func consoleLog(ptr uintptr, size uint32) diff --git a/internal/Makefile b/internal/Makefile new file mode 100644 index 0000000..22d0af3 --- /dev/null +++ b/internal/Makefile @@ -0,0 +1,9 @@ +tinygo_sources := $(wildcard testdata/*/*.go) +.PHONY: build.wasm +build-wasm: $(tinygo_sources) + @echo "------------------" + @echo "Building Test Wasm" + @echo "------------------" + @for f in $^; do \ + tinygo build -o $$(echo $$f | sed -e 's/\.go/\.wasm/') -scheduler=none --no-debug -target=wasi $$f; \ + done diff --git a/internal/e2e_test.go b/internal/e2e_test.go new file mode 100644 index 0000000..858981c --- /dev/null +++ b/internal/e2e_test.go @@ -0,0 +1,67 @@ +package internal_test + +import ( + "context" + _ "embed" + "github.com/stretchr/testify/require" + "github.com/tetratelabs/wazero/api" + "testing" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/wasi_snapshot_preview1" +) + +// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors. +var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary") + +// consoleLogWasm was compiled from testdata/__console_log/main.go +// +//go:embed testdata/__console_log/main.wasm +var consoleLogWasm []byte + +func Test_EndToEnd(t *testing.T) { + type testCase struct { + name string + guest []byte + test func(t *testing.T, guest api.Module, host *wapcHost) + } + + tests := []testCase{ + { + name: "ConsoleLog", + guest: consoleLogWasm, + test: func(t *testing.T, guest api.Module, host *wapcHost) { + // main invokes ConsoleLog + require.Equal(t, []string{"msg", "msg1", "msg"}, host.consoleLogMessages) + }, + }, + } + + // Create a new WebAssembly Runtime. + r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfig(). + // WebAssembly 2.0 allows use of any version of TinyGo, including 0.24+. + WithWasmCore2()) + defer r.Close(testCtx) // This closes everything this Runtime created. + + // Instantiate WASI, which implements system I/O such as console output and + // is required for `tinygo build -target=wasi` + if _, err := wasi_snapshot_preview1.Instantiate(testCtx, r); err != nil { + t.Errorf("Error instantiating WASI - %v", err) + } + + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + h, host := instantiateWapcHost(t, r) + defer host.Close(testCtx) + + g, err := r.InstantiateModuleFromBinary(testCtx, tc.guest) + if err != nil { + t.Errorf("Error instantiating waPC guest - %v", err) + } + defer g.Close(testCtx) + + tc.test(t, g, h) + }) + } +} diff --git a/internal/go.mod b/internal/go.mod new file mode 100644 index 0000000..651ee76 --- /dev/null +++ b/internal/go.mod @@ -0,0 +1,16 @@ +module github.com/wapc/wapc-guest-tinygo/internal + +go 1.18 + +require ( + github.com/stretchr/testify v1.8.0 + github.com/tetratelabs/wazero v0.0.0-20220816233340-7d071a45d786 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/wapc/wapc-guest-tinygo => ../ diff --git a/internal/go.sum b/internal/go.sum new file mode 100644 index 0000000..480e2cb --- /dev/null +++ b/internal/go.sum @@ -0,0 +1,17 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/tetratelabs/wazero v0.0.0-20220816233340-7d071a45d786 h1:EycFERct5dJumsQUtx/HMpgEAAs348zwdS+aKtBT1eg= +github.com/tetratelabs/wazero v0.0.0-20220816233340-7d071a45d786/go.mod h1:CD5smBN5rGZo7UNe8aUiWyYE3bDWED/CQSonog9NSEg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/host_test.go b/internal/host_test.go new file mode 100644 index 0000000..c1e22ca --- /dev/null +++ b/internal/host_test.go @@ -0,0 +1,113 @@ +package internal_test + +import ( + "context" + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "testing" +) + +// instantiateWapcHost instantiates a test waPC host and returns it and a cleanup function. +func instantiateWapcHost(t *testing.T, r wazero.Runtime) (*wapcHost, api.Closer) { + h := &wapcHost{t: t} + // Export host functions (in the order defined in https://wapc.io/docs/spec/#required-host-exports) + if host, err := r.NewModuleBuilder("wapc"). + ExportFunction("__host_call", h.hostCall, + "__host_call", "bind_ptr", "bind_len", "ns_ptr", "ns_len", "cmd_ptr", "cmd_len", "payload_ptr", "payload_len"). + ExportFunction("__console_log", h.consoleLog, + "__console_log", "ptr", "len"). + ExportFunction("__guest_request", h.guestRequest, + "__guest_request", "op_ptr", "ptr"). + ExportFunction("__host_response", h.hostResponse, + "__host_response", "ptr"). + ExportFunction("__host_response_len", h.hostResponseLen, + "__host_response_len"). + ExportFunction("__guest_response", h.guestResponse, + "__guest_response", "ptr", "len"). + ExportFunction("__guest_error", h.guestError, + "__guest_error", "ptr", "len"). + ExportFunction("__host_error", h.hostError, + "__host_error", "ptr"). + ExportFunction("__host_error_len", h.hostErrorLen, + "__host_error_len"). + Instantiate(testCtx, r); err != nil { + t.Errorf("Error instantiating waPC host - %v", err) + return h, nil + } else { + return h, host + } +} + +type wapcHost struct { + t *testing.T + consoleLogMessages []string +} + +// hostCall is the WebAssembly function export "__host_call", which initiates a host using the callHandler using +// parameters read from linear memory (wasm.Memory). +func (w *wapcHost) hostCall(ctx context.Context, m api.Module, bindPtr, bindLen, nsPtr, nsLen, cmdPtr, cmdLen, payloadPtr, payloadLen uint32) int32 { + panic("TODO") +} + +// consoleLog is the WebAssembly function export "__console_log", which logs the message stored by the guest at the +// given offset (ptr) and length (len) in linear memory (wasm.Memory). +func (w *wapcHost) consoleLog(ctx context.Context, m api.Module, ptr, len uint32) { + msg := w.requireReadString(ctx, m.Memory(), "msg", ptr, len) + w.consoleLogMessages = append(w.consoleLogMessages, msg) +} + +// guestRequest is the WebAssembly function export "__guest_request", which writes the invokeContext.operation and +// invokeContext.guestReq to the given offsets (opPtr, ptr) in linear memory (wasm.Memory). +func (w *wapcHost) guestRequest(ctx context.Context, m api.Module, opPtr, ptr uint32) { + panic("TODO") +} + +// hostResponse is the WebAssembly function export "__host_response", which writes the invokeContext.hostResp to the +// given offset (ptr) in linear memory (wasm.Memory). +func (w *wapcHost) hostResponse(ctx context.Context, m api.Module, ptr uint32) { + panic("TODO") +} + +// hostResponse is the WebAssembly function export "__host_response_len", which returns the length of the current host +// response from invokeContext.hostResp. +func (w *wapcHost) hostResponseLen(ctx context.Context) uint32 { + panic("TODO") +} + +// guestResponse is the WebAssembly function export "__guest_response", which reads invokeContext.guestResp from the +// given offset (ptr) and length (len) in linear memory (wasm.Memory). +func (w *wapcHost) guestResponse(ctx context.Context, m api.Module, ptr, len uint32) { + panic("TODO") +} + +// guestError is the WebAssembly function export "__guest_error", which reads invokeContext.guestErr from the given +// offset (ptr) and length (len) in linear memory (wasm.Memory). +func (w *wapcHost) guestError(ctx context.Context, m api.Module, ptr, len uint32) { + panic("TODO") +} + +// hostError is the WebAssembly function export "__host_error", which writes the invokeContext.hostErr to the given +// offset (ptr) in linear memory (wasm.Memory). +func (w *wapcHost) hostError(ctx context.Context, m api.Module, ptr uint32) { + panic("TODO") +} + +// hostError is the WebAssembly function export "__host_error_len", which returns the length of the current host error +// from invokeContext.hostErr. +func (w *wapcHost) hostErrorLen(ctx context.Context) uint32 { + panic("TODO") +} + +// requireReadString is a convenience function that casts requireRead +func (w *wapcHost) requireReadString(ctx context.Context, mem api.Memory, fieldName string, offset, byteCount uint32) string { + return string(w.requireRead(ctx, mem, fieldName, offset, byteCount)) +} + +// requireRead is like api.Memory except that it panics if the offset and byteCount are out of range. +func (w *wapcHost) requireRead(ctx context.Context, mem api.Memory, fieldName string, offset, byteCount uint32) []byte { + buf, ok := mem.Read(ctx, offset, byteCount) + if !ok { + w.t.Fatalf("out of memory reading %s", fieldName) + } + return buf +} diff --git a/internal/testdata/__console_log/main.go b/internal/testdata/__console_log/main.go new file mode 100644 index 0000000..0275a01 --- /dev/null +++ b/internal/testdata/__console_log/main.go @@ -0,0 +1,7 @@ +package main + +func main() { + wapc.ConsoleLog("msg") + wapc.ConsoleLog("msg1") + wapc.ConsoleLog("msg") +} diff --git a/wapc.go b/wapc.go index b9acb4a..58c31b2 100644 --- a/wapc.go +++ b/wapc.go @@ -22,7 +22,7 @@ var ( allFunctions = Functions{} ) -// RegisterFunctions adds functions by name to the registery. +// RegisterFunctions adds functions by name to the registry. // This should be invoked in `main()`. func RegisterFunctions(functions Functions) { for name, fn := range functions { @@ -30,7 +30,7 @@ func RegisterFunctions(functions Functions) { } } -// RegisterFunction adds a single function by name to the registery. +// RegisterFunction adds a single function by name to the registry. // This should be invoked in `main()`. func RegisterFunction(name string, fn Function) { allFunctions[name] = fn @@ -62,6 +62,11 @@ func guestCall(operationSize uint32, payloadSize uint32) bool { return false } +// ConsoleLog writes the message the underlying waPC console logger. +func ConsoleLog(msg string) { + consoleLog(stringToPointer(msg), uint32(len(msg))) +} + // HostCall invokes an operation on the host. The host uses `namespace` and `operation` // to route to the `payload` to the appropriate operation. The host will return // a response payload if successful.