diff --git a/examples/allocation/tinygo/greet.go b/examples/allocation/tinygo/greet.go index 09e1831b017..22d76d64c5c 100644 --- a/examples/allocation/tinygo/greet.go +++ b/examples/allocation/tinygo/greet.go @@ -90,9 +90,21 @@ func main() { if err != nil { log.Panicln(err) } - // Note: This pointer is still owned by TinyGo, so don't try to free it! + 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", diff --git a/examples/allocation/tinygo/testdata/greet.go b/examples/allocation/tinygo/testdata/greet.go index d858ab38fe2..190c3dfbe4f 100644 --- a/examples/allocation/tinygo/testdata/greet.go +++ b/examples/allocation/tinygo/testdata/greet.go @@ -1,5 +1,8 @@ package main +// #include +import "C" + import ( "fmt" "reflect" @@ -53,7 +56,7 @@ func _greet(ptr, size uint32) { func _greeting(ptr, size uint32) (ptrSize uint64) { name := ptrToString(ptr, size) g := greeting(name) - ptr, size = stringToPtr(g) + ptr, size = stringToNativePtr(g) return (uint64(ptr) << uint64(32)) | uint64(size) } @@ -77,3 +80,19 @@ func stringToPtr(s string) (uint32, uint32) { unsafePtr := uintptr(unsafe.Pointer(ptr)) return uint32(unsafePtr), uint32(len(buf)) } + +// stringToNativePtr 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 hence it must be freed by the host. +func stringToNativePtr(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)) +} diff --git a/examples/allocation/tinygo/testdata/greet.wasm b/examples/allocation/tinygo/testdata/greet.wasm index 12efdc1fa2d..dba14dfe331 100755 Binary files a/examples/allocation/tinygo/testdata/greet.wasm and b/examples/allocation/tinygo/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 1c8d893c788..382b65d2225 100644 --- a/internal/integration_test/vs/bench_allocation.go +++ b/internal/integration_test/vs/bench_allocation.go @@ -37,7 +37,7 @@ func init() { func allocationCall(m Module, _ int) error { nameSize := uint32(len(allocationParam)) - // Instead of an arbitrary memory offset, use Rust's allocator. Notice + // 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) @@ -50,14 +50,19 @@ func allocationCall(m Module, _ int) error { return err } - // Now, we can call "greet", which reads the string we wrote to memory! - if err = m.CallI32I32_V(testCtx, "greet", namePtr, nameSize); err != nil { - return err + // Now, we can call "greeting", which reads the string we wrote to memory! + fnErr := m.CallI32I32_V(testCtx, "greet", namePtr, nameSize) + if fnErr != nil { + return fnErr } - // This pointer was allocated by Rust, but owned by Go, So, we have to + // This pointer was allocated by TinyGo, but owned by Go, So, we have to // deallocate it when finished - return m.CallI32_V(testCtx, "free", namePtr) + if err := m.CallI32_V(testCtx, "free", namePtr); err != nil { + return err + } + + return nil } func RunTestAllocation(t *testing.T, runtime func() Runtime) { 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..6e4ed82a4bb --- /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/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/testdata/greet.wasm" + allocationCGOWasm []byte + allocationCGOParam = "wazero" + allocationCGOResult = []byte("wasm >> Hello, wazero!") + allocationCGOConfig *RuntimeConfig +) + +func init() { + allocationCGOWasm = readRelativeFile(allocationCGOWasmPath) + allocationCGOConfig = &RuntimeConfig{ + ModuleName: "greeting", + ModuleWasm: allocationCGOWasm, + FuncNames: []string{"malloc", "free", "greeting"}, + 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", 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/compiler/compiler_test.go b/internal/integration_test/vs/compiler/compiler_test.go index 2c711f9e4d2..0739f08e634 100644 --- a/internal/integration_test/vs/compiler/compiler_test.go +++ b/internal/integration_test/vs/compiler/compiler_test.go @@ -16,6 +16,14 @@ 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 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..c68e9fbb4eb 100644 --- a/internal/integration_test/vs/interpreter/interpreter_test.go +++ b/internal/integration_test/vs/interpreter/interpreter_test.go @@ -20,6 +20,18 @@ 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 TestFactorial(t *testing.T) { vs.RunTestFactorial(t, runtime) } diff --git a/internal/integration_test/vs/runtime.go b/internal/integration_test/vs/runtime.go index ef4c12fbea4..7d875d349d4 100644 --- a/internal/integration_test/vs/runtime.go +++ b/internal/integration_test/vs/runtime.go @@ -37,6 +37,7 @@ type Runtime interface { type Module interface { CallI32_I32(ctx context.Context, funcName string, param uint32) (uint32, error) CallI32I32_V(ctx context.Context, funcName string, x, y uint32) error + CallI32I32_I64(ctx context.Context, funcName string, x, y uint32) (uint64, error) CallI32_V(ctx context.Context, funcName string, param uint32) error CallV_V(ctx context.Context, funcName string) error CallI64_I64(ctx context.Context, funcName string, param uint64) (uint64, error) @@ -183,6 +184,14 @@ func (m *wazeroModule) CallI32I32_V(ctx context.Context, funcName string, x, y u return } +func (m *wazeroModule) CallI32I32_I64(ctx context.Context, funcName string, x, y uint32) (uint64, error) { + if results, err := m.funcs[funcName].Call(ctx, uint64(x), uint64(y)); err != nil { + return 0, err + } else { + return results[0], nil + } +} + func (m *wazeroModule) CallI32_V(ctx context.Context, funcName string, param uint32) (err error) { _, err = m.funcs[funcName].Call(ctx, uint64(param)) return @@ -191,10 +200,9 @@ func (m *wazeroModule) CallI32_V(ctx context.Context, funcName string, param uin func (m *wazeroModule) CallI64_I64(ctx context.Context, funcName string, param uint64) (uint64, error) { if results, err := m.funcs[funcName].Call(ctx, param); err != nil { return 0, err - } else if len(results) > 0 { + } else { return results[0], nil } - return 0, nil } func (m *wazeroModule) WriteMemory(offset uint32, bytes []byte) error { diff --git a/internal/integration_test/vs/wasmedge/wasmedge.go b/internal/integration_test/vs/wasmedge/wasmedge.go index ee932268a9e..fc0a62e9644 100644 --- a/internal/integration_test/vs/wasmedge/wasmedge.go +++ b/internal/integration_test/vs/wasmedge/wasmedge.go @@ -155,6 +155,14 @@ func (m *wasmedgeModule) CallI32I32_V(_ context.Context, funcName string, x, y u return } +func (m *wasmedgeModule) CallI32I32_I64(_ context.Context, funcName string, x, y uint32) (uint64, error) { + if result, err := m.vm.Execute(funcName, int32(x), int32(y)); err != nil { + return 0, err + } else { + return uint64(result[0].(int64)), nil + } +} + func (m *wasmedgeModule) CallV_V(_ context.Context, funcName string) (err error) { _, err = m.vm.Execute(funcName) return diff --git a/internal/integration_test/vs/wasmedge/wasmedge_test.go b/internal/integration_test/vs/wasmedge/wasmedge_test.go index caf06517598..ed1d8d0de39 100644 --- a/internal/integration_test/vs/wasmedge/wasmedge_test.go +++ b/internal/integration_test/vs/wasmedge/wasmedge_test.go @@ -24,6 +24,30 @@ func TestBenchmarkAllocation_Call_CompilerFastest(t *testing.T) { vs.RunTestBenchmarkAllocation_Call_CompilerFastest(t, runtime()) } +func TestAllocationCGO(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) } diff --git a/internal/integration_test/vs/wasmer/wasmer.go b/internal/integration_test/vs/wasmer/wasmer.go index f11c237d617..306bb0bb1e5 100644 --- a/internal/integration_test/vs/wasmer/wasmer.go +++ b/internal/integration_test/vs/wasmer/wasmer.go @@ -170,6 +170,15 @@ func (m *wasmerModule) CallI32I32_V(_ context.Context, funcName string, x, y uin return } +func (m *wasmerModule) CallI32I32_I64(_ context.Context, funcName string, x, y uint32) (uint64, error) { + fn := m.funcs[funcName] + if result, err := fn.Call(int32(x), int32(y)); err != nil { + return 0, err + } else { + return uint64(result.(int64)), nil + } +} + func (m *wasmerModule) CallI32_V(_ context.Context, funcName string, param uint32) (err error) { fn := m.funcs[funcName] _, err = fn.Call(int32(param)) diff --git a/internal/integration_test/vs/wasmer/wasmer_test.go b/internal/integration_test/vs/wasmer/wasmer_test.go index 1782faa8c91..31db7beb29e 100644 --- a/internal/integration_test/vs/wasmer/wasmer_test.go +++ b/internal/integration_test/vs/wasmer/wasmer_test.go @@ -22,6 +22,30 @@ func TestBenchmarkAllocation_Call_CompilerFastest(t *testing.T) { vs.RunTestBenchmarkAllocation_Call_CompilerFastest(t, runtime()) } +func TestAllocationCGO(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) } diff --git a/internal/integration_test/vs/wasmtime/wasmtime.go b/internal/integration_test/vs/wasmtime/wasmtime.go index 3cd8614f015..26d277fca8a 100644 --- a/internal/integration_test/vs/wasmtime/wasmtime.go +++ b/internal/integration_test/vs/wasmtime/wasmtime.go @@ -170,6 +170,15 @@ func (m *wasmtimeModule) CallI32I32_V(_ context.Context, funcName string, x, y u return } +func (m *wasmtimeModule) CallI32I32_I64(_ context.Context, funcName string, x, y uint32) (uint64, error) { + fn := m.funcs[funcName] + if result, err := fn.Call(m.store, int32(x), int32(y)); err != nil { + return 0, err + } else { + return uint64(result.(int64)), nil + } +} + func (m *wasmtimeModule) CallV_V(_ context.Context, funcName string) (err error) { fn := m.funcs[funcName] _, err = fn.Call(m.store) diff --git a/internal/integration_test/vs/wasmtime/wasmtime_test.go b/internal/integration_test/vs/wasmtime/wasmtime_test.go index 8e6235f8eb3..5e29f2d9273 100644 --- a/internal/integration_test/vs/wasmtime/wasmtime_test.go +++ b/internal/integration_test/vs/wasmtime/wasmtime_test.go @@ -22,6 +22,30 @@ func TestBenchmarkAllocation_Call_CompilerFastest(t *testing.T) { vs.RunTestBenchmarkAllocation_Call_CompilerFastest(t, runtime()) } +func TestAllocationCGO(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) }