Skip to content

Commit

Permalink
examples(allocation): free memory after unmarshalling a result from t…
Browse files Browse the repository at this point in the history
…he guest (cgo)

Signed-off-by: Luca Burgazzoli <[email protected]>
  • Loading branch information
lburgazzoli committed May 2, 2023
1 parent c5871c7 commit 4f23ede
Show file tree
Hide file tree
Showing 16 changed files with 261 additions and 10 deletions.
14 changes: 13 additions & 1 deletion examples/allocation/tinygo/greet.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
21 changes: 20 additions & 1 deletion examples/allocation/tinygo/testdata/greet.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package main

// #include <stdlib.h>
import "C"

import (
"fmt"
"reflect"
Expand Down Expand Up @@ -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)
}

Expand All @@ -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))
}
Binary file modified examples/allocation/tinygo/testdata/greet.wasm
Binary file not shown.
Binary file modified examples/cli/testdata/cli.wasm
Binary file not shown.
Binary file modified imports/wasi_snapshot_preview1/example/testdata/tinygo/cat.wasm
Binary file not shown.
17 changes: 11 additions & 6 deletions internal/integration_test/vs/bench_allocation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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) {
Expand Down
89 changes: 89 additions & 0 deletions internal/integration_test/vs/bench_allocation_cgo.go
Original file line number Diff line number Diff line change
@@ -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)
}
8 changes: 8 additions & 0 deletions internal/integration_test/vs/compiler/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
12 changes: 12 additions & 0 deletions internal/integration_test/vs/interpreter/interpreter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
12 changes: 10 additions & 2 deletions internal/integration_test/vs/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions internal/integration_test/vs/wasmedge/wasmedge.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions internal/integration_test/vs/wasmedge/wasmedge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
9 changes: 9 additions & 0 deletions internal/integration_test/vs/wasmer/wasmer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
24 changes: 24 additions & 0 deletions internal/integration_test/vs/wasmer/wasmer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
9 changes: 9 additions & 0 deletions internal/integration_test/vs/wasmtime/wasmtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
24 changes: 24 additions & 0 deletions internal/integration_test/vs/wasmtime/wasmtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down

0 comments on commit 4f23ede

Please sign in to comment.