diff --git a/Makefile b/Makefile index 8e36cf8c516..150ff3f0097 100644 --- a/Makefile +++ b/Makefile @@ -58,7 +58,7 @@ build.examples.as: build.examples.zig: examples/allocation/zig/testdata/greet.wasm imports/wasi_snapshot_preview1/example/testdata/zig/cat.wasm imports/wasi_snapshot_preview1/testdata/zig/wasi.wasm @cd internal/testing/dwarftestdata/testdata/zig; zig build; mv zig-out/*/main.wasm ./ # Need DWARF custom sections. -tinygo_sources := examples/basic/testdata/add.go examples/allocation/tinygo/testdata/greet.go examples/cli/testdata/cli.go imports/wasi_snapshot_preview1/example/testdata/tinygo/cat.go +tinygo_sources := examples/basic/testdata/add.go examples/allocation/tinygo/testdata/greet.go examples/allocation/tinygo-malloc/testdata/greet.go examples/cli/testdata/cli.go imports/wasi_snapshot_preview1/example/testdata/tinygo/cat.go .PHONY: build.examples.tinygo build.examples.tinygo: $(tinygo_sources) @for f in $^; do \ diff --git a/examples/allocation/tinygo-malloc/README.md b/examples/allocation/tinygo-malloc/README.md new file mode 100644 index 00000000000..1e26da6547d --- /dev/null +++ b/examples/allocation/tinygo-malloc/README.md @@ -0,0 +1,22 @@ +## TinyGo allocation example + +This example shows how to pass strings in and out of a Wasm function defined +in TinyGo, built with `tinygo build -o greet.wasm -scheduler=none -target=wasi greet.go` + +```bash +$ go run greet.go cgo wazero +cgo: wazero! +``` + +```bash +$ go run greet.go tinymem wazero +tinymem: wazero! +``` + +Under the covers, [greet.go](testdata/greet.go) does a few things of interest: +* Uses `unsafe.Pointer` to change a Go pointer to a numeric type. +* Uses `unsafe.Slice` to build back a string from a pointer, len pair. +* Uses CGO to allocate and free memory if `cgo` is passed as first argument. +* Uses [TinyMem](https://github.com/tetratelabs/tinymem) alike to allocate and free memory if `tinymem` is passed as first argument. + +See https://wazero.io/languages/tinygo/ for more tips. diff --git a/examples/allocation/tinygo-malloc/greet.go b/examples/allocation/tinygo-malloc/greet.go new file mode 100644 index 00000000000..481c9af4ea9 --- /dev/null +++ b/examples/allocation/tinygo-malloc/greet.go @@ -0,0 +1,117 @@ +package main + +import ( + "context" + _ "embed" + "fmt" + "log" + "os" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" +) + +// greetWasm was compiled using `tinygo build -o greet.wasm -scheduler=none --no-debug -target=wasi greet.go` +// +//go:embed testdata/greet.wasm +var greetWasm []byte + +// main shows how to interact with a WebAssembly function that was compiled +// from TinyGo. +// +// See README.md for a full description. +func main() { + // Choose the context to use for function calls. + ctx := context.Background() + + // Create a new WebAssembly Runtime. + r := wazero.NewRuntime(ctx) + defer r.Close(ctx) // This closes everything this Runtime created. + + // Note: testdata/greet.go doesn't use WASI, but TinyGo needs it to + // implement functions such as panic. + wasi_snapshot_preview1.MustInstantiate(ctx, r) + + // Instantiate a WebAssembly module that imports the "log" function defined + // in "env" and exports "memory" and functions we'll use in this example. + mod, err := r.InstantiateWithConfig(ctx, greetWasm, wazero.NewModuleConfig().WithStdout(os.Stdout)) + if err != nil { + log.Panicln(err) + } + + var free api.Function + var malloc api.Function + var greeting api.Function + + switch os.Args[1] { + case "cgo": + // These are undocumented, but exported. See tinygo-org/tinygo#2788 + malloc = mod.ExportedFunction("malloc") + free = mod.ExportedFunction("free") + greeting = mod.ExportedFunction("greeting_cgo") + case "tinymem": + malloc = mod.ExportedFunction("malloc_tinymem") + free = mod.ExportedFunction("free_tinymem") + greeting = mod.ExportedFunction("greeting_tinymem") + default: + log.Panicf("unsupported allocation mode %s", os.Args[1]) + } + + // Let's use the argument to this main function in Wasm. + name := os.Args[2] + nameSize := uint64(len(name)) + + // Instead of an arbitrary memory offset, use TinyGo's allocator. Notice + // there is nothing string-specific in this allocation function. The same + // function could be used to pass binary serialized data to Wasm. + results, err := malloc.Call(ctx, nameSize) + if err != nil { + log.Panicln(err) + } + namePtr := results[0] + + // This pointer is managed by TinyGo, but TinyGo is unaware of external usage. + // So, we have to free it when finished + defer func() { + _, err := free.Call(ctx, namePtr) + if err != nil { + log.Panicln(err) + } + }() + + // The pointer is a linear memory offset, which is where we write the name. + if !mod.Memory().Write(uint32(namePtr), []byte(name)) { + log.Panicf("Memory.Write(%d, %d) out of range of memory size %d", + namePtr, nameSize, mod.Memory().Size()) + } + + // Finally, we get the greeting message "greet" printed. This shows how to + // read-back something allocated by TinyGo. + ptrSize, err := greeting.Call(ctx, namePtr, nameSize) + if err != nil { + log.Panicln(err) + } + + greetingPtr := uint32(ptrSize[0] >> 32) + greetingSize := uint32(ptrSize[0]) + + // This pointer is managed by TinyGo, but TinyGo is unaware of external usage. + // So, we have to free it when finished + if greetingPtr != 0 { + defer func() { + _, err := free.Call(ctx, uint64(greetingPtr)) + if err != nil { + log.Panicln(err) + } + }() + } + + // The pointer is a linear memory offset, which is where we write the name. + if bytes, ok := mod.Memory().Read(greetingPtr, greetingSize); !ok { + log.Panicf("Memory.Read(%d, %d) out of range of memory size %d", + greetingPtr, greetingSize, mod.Memory().Size()) + } else { + fmt.Println(string(bytes)) + } +} diff --git a/examples/allocation/tinygo-malloc/greet_test.go b/examples/allocation/tinygo-malloc/greet_test.go new file mode 100644 index 00000000000..e775d594c64 --- /dev/null +++ b/examples/allocation/tinygo-malloc/greet_test.go @@ -0,0 +1,23 @@ +package main + +import ( + "strings" + "testing" + + "github.com/tetratelabs/wazero/internal/testing/maintester" + "github.com/tetratelabs/wazero/internal/testing/require" +) + +// Test_main ensures the following will work: +// +// go run greet.go cgo|tinymem wazero +func Test_main(t *testing.T) { + t.Run("cgo", func(t *testing.T) { + stdout, _ := maintester.TestMain(t, main, "greet", "cgo", "wazero") + require.Equal(t, `cgo: wazero!`, strings.Replace(stdout, "\n", "", -1)) + }) + t.Run("tinymem", func(t *testing.T) { + stdout, _ := maintester.TestMain(t, main, "greet", "tinymem", "wazero") + require.Equal(t, `tinymem: wazero!`, strings.Replace(stdout, "\n", "", -1)) + }) +} diff --git a/examples/allocation/tinygo-malloc/testdata/greet.go b/examples/allocation/tinygo-malloc/testdata/greet.go new file mode 100644 index 00000000000..3987b612124 --- /dev/null +++ b/examples/allocation/tinygo-malloc/testdata/greet.go @@ -0,0 +1,123 @@ +package main + +import ( + "fmt" + "unsafe" +) + +// #include +import "C" + +// main is required for TinyGo to compile to Wasm. +func main() {} + +// greeting gets a greeting for the name. +func greeting(name string) string { + return fmt.Sprint(name, "!") +} + +// ptrToString returns a string from WebAssembly compatible numeric types +// representing its pointer and length. +func ptrToString(ptr uint32, size uint32) string { + // Get a slice view of the underlying bytes in the stream. + s := unsafe.Slice((*byte)(unsafe.Pointer(uintptr(ptr))), size) + return *(*string)(unsafe.Pointer(&s)) +} + +// +// CGO memory management +// + +// stringToPtr_cgo returns a pointer and size pair for the given string in a way +// compatible with WebAssembly numeric types. The pointer is not automatically +// managed by tinygo but must be freed by the host. +func stringToPtr_cgo(s string) (uint32, uint32) { + if len(s) == 0 { + return 0, 0 + } + + size := C.ulong(len(s)) + ptr := unsafe.Pointer(C.malloc(size)) + + copy(unsafe.Slice((*byte)(ptr), size), []byte(s)) + + return uint32(uintptr(ptr)), uint32(len(s)) +} + +// _greeting_cgo is a WebAssembly export that accepts a string pointer (linear memory +// offset) and returns a pointer/size pair packed into a uint64. +// +// Note: +// - This uses an uint64 instead of two result values for compatibility with +// WebAssembly 1.0. +// - The pointer returned by the function must be freed by the host using builtin free +// as it has been allocated using CGO malloc +// +//export greeting_cgo +func _greeting_cgo(ptr, size uint32) (ptrSize uint64) { + name := ptrToString(ptr, size) + g := greeting("cgo: " + name) + ptr, size = stringToPtr_cgo(g) + return (uint64(ptr) << uint64(32)) | uint64(size) +} + +// +// TinyMem alike memory management (https://github.com/tetratelabs/tinymem) +// + +var alivePointers = map[uintptr]interface{}{} + +// stringToPtr_tinymem returns a pointer and size pair for the given string in a way +// compatible with WebAssembly numeric types. The pointer is not automatically +// managed by tinygo but must be freed by the host. +func stringToPtr_tinymem(s string) (uint32, uint32) { + + ptr := _malloc_tinymem(uint32(len(s))) + unsafePtr := unsafe.Pointer(ptr) + size := len(s) + + copy(unsafe.Slice((*byte)(unsafePtr), size), []byte(s)) + + return uint32(ptr), uint32(len(s)) +} + +// _greeting_tinymem is a WebAssembly export that accepts a string pointer (linear memory +// offset) and returns a pointer/size pair packed into a uint64. +// +// Note: +// - This uses an uint64 instead of two result values for compatibility with +// WebAssembly 1.0. +// - The pointer returned by the function must be freed by the host by using free_tinymem +// as it has been kept alive in a local map +// +//export greeting_tinymem +func _greeting_tinymem(ptr, size uint32) (ptrSize uint64) { + name := ptrToString(ptr, size) + g := greeting("tinymem: " + name) + ptr, size = stringToPtr_tinymem(g) + return (uint64(ptr) << uint64(32)) | uint64(size) +} + +// malloc_tinymem is a WebAssembly export that allocates a pointer (linear memory offset) +// that can be used for the given size in bytes. +// +// Note: This is an ownership transfer, which means the caller must call free +// when finished. +// +//export malloc_tinymem +func _malloc_tinymem(size uint32) uintptr { + buf := make([]byte, size) + ptr := &buf[0] + unsafePtr := uintptr(unsafe.Pointer(ptr)) + alivePointers[unsafePtr] = buf + + return unsafePtr +} + +// free_tinymem frees a uintptr returned by keepaliveBuf or allocate, allowing it +// to be garbage collected. +// +//export free_tinymem +func _free_tinymem(ptr uint32) { + delete(alivePointers, uintptr(ptr)) +} diff --git a/examples/allocation/tinygo-malloc/testdata/greet.wasm b/examples/allocation/tinygo-malloc/testdata/greet.wasm new file mode 100755 index 00000000000..324fa09139f Binary files /dev/null and b/examples/allocation/tinygo-malloc/testdata/greet.wasm differ diff --git a/examples/cli/testdata/cli.wasm b/examples/cli/testdata/cli.wasm index f38dbb3c76c..17fc332e061 100755 Binary files a/examples/cli/testdata/cli.wasm and b/examples/cli/testdata/cli.wasm differ diff --git a/imports/wasi_snapshot_preview1/example/testdata/tinygo/cat.wasm b/imports/wasi_snapshot_preview1/example/testdata/tinygo/cat.wasm index 2364369fbc9..730213bbbfc 100755 Binary files a/imports/wasi_snapshot_preview1/example/testdata/tinygo/cat.wasm and b/imports/wasi_snapshot_preview1/example/testdata/tinygo/cat.wasm differ diff --git a/internal/integration_test/vs/bench_allocation.go b/internal/integration_test/vs/bench_allocation.go index 290febb08a2..f0897f50357 100644 --- a/internal/integration_test/vs/bench_allocation.go +++ b/internal/integration_test/vs/bench_allocation.go @@ -24,7 +24,7 @@ func init() { allocationConfig = &RuntimeConfig{ ModuleName: "greet", ModuleWasm: allocationWasm, - FuncNames: []string{"malloc", "free", "greeting", "deallocate"}, + FuncNames: []string{"malloc", "free", "greet", "deallocate"}, NeedsWASI: true, // Needed for TinyGo LogFn: func(buf []byte) error { if !bytes.Equal(allocationResult, buf) { @@ -51,7 +51,7 @@ func allocationCall(m Module, _ int) error { } // Now, we can call "greeting", which reads the string we wrote to memory! - ptrSize, fnErr := m.CallI32I32_I64(testCtx, "greeting", namePtr, nameSize) + fnErr := m.CallI32I32_V(testCtx, "greet", namePtr, nameSize) if fnErr != nil { return fnErr } @@ -62,12 +62,6 @@ func allocationCall(m Module, _ int) error { return err } - // deallocate removes a uintptr from a map which stores a reference to the - // buffer ptrSize points to, allowing it to be garbage collected. - if err := m.CallI32_V(testCtx, "deallocate", uint32(ptrSize>>32)); err != nil { - return err - } - return nil } diff --git a/internal/integration_test/vs/bench_allocation_cgo.go b/internal/integration_test/vs/bench_allocation_cgo.go new file mode 100644 index 00000000000..54f0da33359 --- /dev/null +++ b/internal/integration_test/vs/bench_allocation_cgo.go @@ -0,0 +1,89 @@ +package vs + +import ( + "bytes" + _ "embed" + "fmt" + "testing" + + "github.com/tetratelabs/wazero/internal/testing/require" +) + +var ( + // allocationWasm is compiled from ../../../examples/allocation/tinygo-malloc/testdata/src/greet.go + // We can't use go:embed as it is outside this directory. Copying it isn't ideal due to size and drift. + allocationCGOWasmPath = "../../../examples/allocation/tinygo-malloc/testdata/greet.wasm" + allocationCGOWasm []byte + allocationCGOParam = "wazero" + allocationCGOResult = []byte("cgo: wazero!") + allocationCGOConfig *RuntimeConfig +) + +func init() { + allocationCGOWasm = readRelativeFile(allocationCGOWasmPath) + allocationCGOConfig = &RuntimeConfig{ + ModuleName: "greeting", + ModuleWasm: allocationCGOWasm, + FuncNames: []string{"malloc", "free", "greeting_cgo"}, + NeedsWASI: true, // Needed for TinyGo + LogFn: func(buf []byte) error { + if !bytes.Equal(allocationCGOResult, buf) { + return fmt.Errorf("expected %q, but was %q", allocationCGOResult, buf) + } + return nil + }, + } +} + +func AllocationCGOCall(m Module, _ int) error { + nameSize := uint32(len(allocationCGOParam)) + // Instead of an arbitrary memory offset, use TinyGo's allocator. Notice + // there is nothing string-specific in this allocation function. The same + // function could be used to pass binary serialized data to Wasm. + namePtr, err := m.CallI32_I32(testCtx, "malloc", nameSize) + if err != nil { + return err + } + + // The pointer is a linear memory offset, which is where we write the name. + if err = m.WriteMemory(namePtr, []byte(allocationCGOParam)); err != nil { + return err + } + + // Now, we can call "greeting", which reads the string we wrote to memory! + ptrSize, fnErr := m.CallI32I32_I64(testCtx, "greeting_cgo", namePtr, nameSize) + if fnErr != nil { + return fnErr + } + + // This pointer was allocated by TinyGo, but owned by Go, So, we have to + // deallocate it when finished + if err := m.CallI32_V(testCtx, "free", namePtr); err != nil { + return err + } + + // This pointer was allocated by TinyGo, but owned by Go, So, we have to + // deallocate it when finished + if err := m.CallI32_V(testCtx, "free", uint32(ptrSize>>32)); err != nil { + return err + } + + return nil +} + +func RunTestAllocationCGO(t *testing.T, runtime func() Runtime) { + testCall(t, runtime, allocationCGOConfig, testAllocationCGOCall) +} + +func testAllocationCGOCall(t *testing.T, m Module, instantiation, iteration int) { + err := AllocationCGOCall(m, iteration) + require.NoError(t, err, "instantiation[%d] iteration[%d] failed: %v", instantiation, iteration, err) +} + +func RunTestBenchmarkAllocationCGO_Call_CompilerFastest(t *testing.T, vsRuntime Runtime) { + runTestBenchmark_Call_CompilerFastest(t, allocationCGOConfig, "Allocation", AllocationCGOCall, vsRuntime) +} + +func RunBenchmarkAllocationCGO(b *testing.B, runtime func() Runtime) { + benchmark(b, runtime, allocationCGOConfig, AllocationCGOCall) +} diff --git a/internal/integration_test/vs/bench_allocation_tinymem.go b/internal/integration_test/vs/bench_allocation_tinymem.go new file mode 100644 index 00000000000..9df946b1c14 --- /dev/null +++ b/internal/integration_test/vs/bench_allocation_tinymem.go @@ -0,0 +1,89 @@ +package vs + +import ( + "bytes" + _ "embed" + "fmt" + "testing" + + "github.com/tetratelabs/wazero/internal/testing/require" +) + +var ( + // allocationWasm is compiled from ../../../examples/allocation/tinygo-malloc/testdata/src/greet.go + // We can't use go:embed as it is outside this directory. Copying it isn't ideal due to size and drift. + allocationTinyMemWasmPath = "../../../examples/allocation/tinygo-malloc/testdata/greet.wasm" + allocationTinyMemWasm []byte + allocationTinyMemParam = "wazero" + allocationTinyMemResult = []byte("tinymem: wazero!") + allocationTinyMemConfig *RuntimeConfig +) + +func init() { + allocationTinyMemWasm = readRelativeFile(allocationTinyMemWasmPath) + allocationTinyMemConfig = &RuntimeConfig{ + ModuleName: "greeting", + ModuleWasm: allocationTinyMemWasm, + FuncNames: []string{"malloc_tinymem", "free_tinymem", "greeting_tinymem"}, + NeedsWASI: true, // Needed for TinyGo + LogFn: func(buf []byte) error { + if !bytes.Equal(allocationTinyMemResult, buf) { + return fmt.Errorf("expected %q, but was %q", allocationTinyMemResult, buf) + } + return nil + }, + } +} + +func AllocationTinyMemCall(m Module, _ int) error { + nameSize := uint32(len(allocationTinyMemParam)) + // Instead of an arbitrary memory offset, use TinyGo's allocator. Notice + // there is nothing string-specific in this allocation function. The same + // function could be used to pass binary serialized data to Wasm. + namePtr, err := m.CallI32_I32(testCtx, "malloc_tinymem", nameSize) + if err != nil { + return err + } + + // The pointer is a linear memory offset, which is where we write the name. + if err = m.WriteMemory(namePtr, []byte(allocationTinyMemParam)); err != nil { + return err + } + + // Now, we can call "greeting", which reads the string we wrote to memory! + ptrSize, fnErr := m.CallI32I32_I64(testCtx, "greeting_tinymem", namePtr, nameSize) + if fnErr != nil { + return fnErr + } + + // This pointer was allocated by TinyGo, but owned by Go, So, we have to + // deallocate it when finished + if err := m.CallI32_V(testCtx, "free_tinymem", namePtr); err != nil { + return err + } + + // This pointer was allocated by TinyGo, but owned by Go, So, we have to + // deallocate it when finished + if err := m.CallI32_V(testCtx, "free_tinymem", uint32(ptrSize>>32)); err != nil { + return err + } + + return nil +} + +func RunTestAllocationTinyMem(t *testing.T, runtime func() Runtime) { + testCall(t, runtime, allocationTinyMemConfig, testAllocationTinyMemCall) +} + +func testAllocationTinyMemCall(t *testing.T, m Module, instantiation, iteration int) { + err := AllocationTinyMemCall(m, iteration) + require.NoError(t, err, "instantiation[%d] iteration[%d] failed: %v", instantiation, iteration, err) +} + +func RunTestBenchmarkAllocationTinyMem_Call_CompilerFastest(t *testing.T, vsRuntime Runtime) { + runTestBenchmark_Call_CompilerFastest(t, allocationTinyMemConfig, "Allocation", AllocationTinyMemCall, vsRuntime) +} + +func RunBenchmarkAllocationTinyMem(b *testing.B, runtime func() Runtime) { + benchmark(b, runtime, allocationTinyMemConfig, AllocationTinyMemCall) +} diff --git a/internal/integration_test/vs/compiler/compiler_test.go b/internal/integration_test/vs/compiler/compiler_test.go index 2c711f9e4d2..4ae38e9d16a 100644 --- a/internal/integration_test/vs/compiler/compiler_test.go +++ b/internal/integration_test/vs/compiler/compiler_test.go @@ -16,6 +16,22 @@ func BenchmarkAllocation(b *testing.B) { vs.RunBenchmarkAllocation(b, runtime) } +func TestAllocationCGO(t *testing.T) { + vs.RunTestAllocationCGO(t, runtime) +} + +func BenchmarkAllocationCGO(b *testing.B) { + vs.RunBenchmarkAllocationCGO(b, runtime) +} + +func TestAllocationTinyMem(t *testing.T) { + vs.RunTestAllocationTinyMem(t, runtime) +} + +func BenchmarkAllocationTinyMem(b *testing.B) { + vs.RunBenchmarkAllocationTinyMem(b, runtime) +} + func TestFactorial(t *testing.T) { vs.RunTestFactorial(t, runtime) } diff --git a/internal/integration_test/vs/interpreter/interpreter_test.go b/internal/integration_test/vs/interpreter/interpreter_test.go index 84090af8bff..3a574b54494 100644 --- a/internal/integration_test/vs/interpreter/interpreter_test.go +++ b/internal/integration_test/vs/interpreter/interpreter_test.go @@ -20,6 +20,30 @@ func TestBenchmarkAllocation_Call_CompilerFastest(t *testing.T) { vs.RunTestBenchmarkAllocation_Call_CompilerFastest(t, runtime()) } +func TestAllocationGCO(t *testing.T) { + vs.RunTestAllocationCGO(t, runtime) +} + +func BenchmarkAllocationCGO(b *testing.B) { + vs.RunBenchmarkAllocationCGO(b, runtime) +} + +func TestBenchmarkAllocationCGO_Call_CompilerFastest(t *testing.T) { + vs.RunTestBenchmarkAllocationCGO_Call_CompilerFastest(t, runtime()) +} + +func TestAllocationTinyMem(t *testing.T) { + vs.RunTestAllocationTinyMem(t, runtime) +} + +func BenchmarkAllocationTinyMem(b *testing.B) { + vs.RunBenchmarkAllocationTinyMem(b, runtime) +} + +func TestBenchmarkAllocationTinyMem_Call_CompilerFastest(t *testing.T) { + vs.RunTestBenchmarkAllocationTinyMem_Call_CompilerFastest(t, runtime()) +} + func TestFactorial(t *testing.T) { vs.RunTestFactorial(t, runtime) }