diff --git a/examples/allocation/tinygo/README.md b/examples/allocation/tinygo/README.md index 7e2f9cbcd74..6e340f265b6 100644 --- a/examples/allocation/tinygo/README.md +++ b/examples/allocation/tinygo/README.md @@ -12,6 +12,6 @@ go >> Hello, 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 `reflect.StringHeader` to build back a string from a pointer, len pair. -* Relies on TinyGo not eagerly freeing pointers returned. +* Relies on CGO to allocate memory used to pass data from TinyGo to host. See https://wazero.io/languages/tinygo/ for more tips. 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..2eb717d5652 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 = stringToLeakedPtr(g) return (uint64(ptr) << uint64(32)) | uint64(size) } @@ -77,3 +80,15 @@ func stringToPtr(s string) (uint32, uint32) { unsafePtr := uintptr(unsafe.Pointer(ptr)) return uint32(unsafePtr), uint32(len(buf)) } + +// stringToLeakedPtr 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 stringToLeakedPtr(s string) (uint32, uint32) { + 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..6a7fa978690 100755 Binary files a/examples/allocation/tinygo/testdata/greet.wasm and b/examples/allocation/tinygo/testdata/greet.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/runtime.go b/internal/integration_test/vs/runtime.go index ef4c12fbea4..2c2a7b4e729 100644 --- a/internal/integration_test/vs/runtime.go +++ b/internal/integration_test/vs/runtime.go @@ -191,10 +191,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 {