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, tinymem, bench)
  • Loading branch information
lburgazzoli committed Apr 26, 2023
1 parent 28814da commit 14d3752
Show file tree
Hide file tree
Showing 13 changed files with 501 additions and 9 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
22 changes: 22 additions & 0 deletions examples/allocation/tinygo-malloc/README.md
Original file line number Diff line number Diff line change
@@ -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.
117 changes: 117 additions & 0 deletions examples/allocation/tinygo-malloc/greet.go
Original file line number Diff line number Diff line change
@@ -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))
}
}
18 changes: 18 additions & 0 deletions examples/allocation/tinygo-malloc/greet_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package main

import (
"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 wazero
func Test_main(t *testing.T) {
stdout, _ := maintester.TestMain(t, main, "greet", "wazero")
require.Equal(t, `wasm >> Hello, wazero!
go >> Hello, wazero!
`, stdout)
}
123 changes: 123 additions & 0 deletions examples/allocation/tinygo-malloc/testdata/greet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package main

import (
"fmt"
"unsafe"
)

// #include <stdlib.h>
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))
}
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.
10 changes: 2 additions & 8 deletions internal/integration_test/vs/bench_allocation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
}
Expand All @@ -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
}

Expand Down
Loading

0 comments on commit 14d3752

Please sign in to comment.