Adds allocation examples in Rust and TinyGo (#475)

Signed-off-by: Adrian Cole <adrian@tetrate.io>
Co-authored-by: Takeshi Yoneda <takeshi@tetrate.io>
This commit is contained in:
Crypt Keeper
2022-04-21 18:22:22 +08:00
committed by GitHub
parent cbd22d49d2
commit b7b90e7dfd
18 changed files with 565 additions and 2 deletions

View File

@@ -2,6 +2,7 @@
The following example projects can help you practice WebAssembly with wazero:
* [allocation](allocation) - how to pass strings in and out of WebAssembly functions defined in Rust or TinyGo.
* [basic](basic) - how to use both WebAssembly and Go-defined functions.
* [import-go](import-go) - how to define, import and call a Go-defined function from a WebAssembly-defined function.
* [multiple-results](multiple-results) - how to return more than one result from WebAssembly or Go-defined functions.

View File

@@ -0,0 +1,25 @@
## Allocation examples
The examples in this directory deal with memory allocation concerns in
WebAssembly, e.g. How to pass strings in and out of WebAssembly functions.
```bash
$ go run greet.go wazero
wasm >> Hello, wazero!
go >> Hello, wazero!
```
While the below examples use strings, they are written in a way that would work
for binary serialization.
* [Rust](rust) - Calls Wasm built with `cargo build --release --target wasm32-unknown-unknown`
* [TinyGo](tinygo) - Calls Wasm built with `tinygo build -o X.wasm -scheduler=none --no-debug -target=wasi X.go`
Note: Each of the above languages differ in both terms of exports and runtime
behavior around allocation, because there is no WebAssembly specification for
it. For example, TinyGo exports allocation functions while Rust does not. Also,
Rust eagerly collects memory before returning from a Wasm function while TinyGo
does not.
We still try to keep the examples as close to the same as possible, and
highlight things to be aware of in the respective source and README files.

1
examples/allocation/rust/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
greet

View File

@@ -0,0 +1,23 @@
## Rust allocation example
This example shows how to pass strings in and out of a Wasm function defined
in Rust, built with `cargo build --release --target wasm32-unknown-unknown`
Ex.
```bash
$ go run greet.go wazero
Hello, wazero!
```
Under the covers, [lib.rs](testdata/src/lib.rs) does a few things of interest:
* Uses a WebAssembly-tuned memory allocator: [wee_alloc](https://github.com/rustwasm/wee_alloc).
* Exports wrapper functions to allocate and deallocate memory.
* Uses `&str` instead of CString (NUL-terminated strings).
* Uses `std::mem::forget` to prevent Rust from eagerly freeing pointers returned.
Note: We chose to not use CString because it keeps the example similar to how
you would track memory for arbitrary blobs. We also watched function signatures
carefully as Rust compiles different WebAssembly signatures depending on the
input type. All of this is Rust-specific, and wazero isn't a Rust project, but
we hope this gets you started. For next steps, consider reading the
[Rust and WebAssembly book](https://rustwasm.github.io/docs/book/).

View File

@@ -0,0 +1,108 @@
package main
import (
"context"
_ "embed"
"fmt"
"log"
"os"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
)
// greetWasm was compiled using `cargo build --release --target wasm32-unknown-unknown`
//go:embed testdata/greet.wasm
var greetWasm []byte
// main shows how to interact with a WebAssembly function that was compiled
// from Rust.
//
// 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()
// Instantiate a module named "env" that exports a function to log a string
// to the console.
env, err := r.NewModuleBuilder("env").
ExportFunction("log", logString).
Instantiate(ctx)
if err != nil {
log.Fatal(err)
}
defer env.Close()
// Instantiate a module named "greet" that imports the "log" function
// defined in "env".
mod, err := r.InstantiateModuleFromCode(ctx, greetWasm)
if err != nil {
log.Fatal(err)
}
defer mod.Close()
// Get a references to functions we'll use in this example.
greet := mod.ExportedFunction("greet")
greeting := mod.ExportedFunction("greeting")
allocate := mod.ExportedFunction("allocate")
deallocate := mod.ExportedFunction("deallocate")
// Let's use the argument to this main function in Wasm.
name := os.Args[1]
nameSize := uint64(len(name))
// Instead of an arbitrary memory offset, use Rust'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 := allocate.Call(ctx, nameSize)
if err != nil {
log.Fatal(err)
}
namePtr := results[0]
// This pointer was allocated by Rust, but owned by Go, So, we have to
// deallocate it when finished
defer deallocate.Call(ctx, namePtr, nameSize)
// The pointer is a linear memory offset, which is where we write the name.
if !mod.Memory().Write(uint32(namePtr), []byte(name)) {
log.Fatalf("Memory.Write(%d, %d) out of range of memory size %d",
namePtr, nameSize, mod.Memory().Size())
}
// Now, we can call "greet", which reads the string we wrote to memory!
_, err = greet.Call(ctx, namePtr, nameSize)
if err != nil {
log.Fatal(err)
}
// Finally, we get the greeting message "greet" printed. This shows how to
// read-back something allocated by Rust.
ptrSize, err := greeting.Call(ctx, namePtr, nameSize)
if err != nil {
log.Fatal(err)
}
greetingPtr := uint32(ptrSize[0] >> 32)
greetingSize := uint32(ptrSize[0])
// This pointer was allocated by Rust, but owned by Go, So, we have to
// deallocate it when finished
defer deallocate.Call(ctx, uint64(greetingPtr), uint64(greetingSize))
// The pointer is a linear memory offset, which is where we write the name.
if bytes, ok := mod.Memory().Read(greetingPtr, greetingSize); !ok {
log.Fatalf("Memory.Read(%d, %d) out of range of memory size %d",
greetingPtr, greetingSize, mod.Memory().Size())
} else {
fmt.Println("go >>", string(bytes))
}
}
func logString(m api.Module, offset, byteCount uint32) {
buf, ok := m.Memory().Read(offset, byteCount)
if !ok {
log.Fatalf("Memory.Read(%d, %d) out of range", offset, byteCount)
}
fmt.Println(string(buf))
}

View File

@@ -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)
}

View File

@@ -0,0 +1,2 @@
/target
Cargo.lock

View File

@@ -0,0 +1,21 @@
[package]
name = "greet"
version = "0.1.0"
edition = "2021"
[lib]
# cdylib builds a a %.wasm file with `cargo build --release --target wasm32-unknown-unknown`
crate-type = ["cdylib"]
[dependencies]
# wee_aloc is a WebAssembly optimized allocator, which is needed to use non-numeric types like strings.
# See https://docs.rs/wee_alloc/latest/wee_alloc/
wee_alloc = "0.4.5"
# Below settings dramatically reduce wasm output size
# See https://rustwasm.github.io/book/reference/code-size.html#optimizing-builds-for-code-sizewasm-opt -Oz -o
# See https://doc.rust-lang.org/cargo/reference/profiles.html#codegen-units
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1

BIN
examples/allocation/rust/testdata/greet.wasm vendored Executable file

Binary file not shown.

View File

@@ -0,0 +1,127 @@
extern crate alloc;
extern crate core;
extern crate wee_alloc;
use alloc::vec::Vec;
use std::slice;
use std::mem::MaybeUninit;
/// Prints a greeting to the console using [`log`].
fn greet(name: &String) {
log(&["wasm >> ", &greeting(name)].concat());
}
/// Gets a greeting for the name.
fn greeting(name: &String) -> String {
return ["Hello, ", &name, "!"].concat();
}
/// Logs a message to the console using [`_log`].
fn log(message: &String) {
unsafe {
let (ptr, len) = string_to_ptr(message);
_log(ptr, len);
}
}
#[link(wasm_import_module = "env")]
extern "C" {
/// WebAssembly import which prints a string (linear memory offset,
/// byteCount) to the console.
///
/// Note: This is not an ownership transfer: Rust still owns the pointer
/// and ensures it isn't deallocated during this call.
#[link_name = "log"]
fn _log(ptr: u32, size: u32);
}
/// WebAssembly export that accepts a string (linear memory offset, byteCount)
/// and calls [`greet`].
///
/// Note: The input parameters were returned by [`allocate`]. This is not an
/// ownership transfer, so the inputs can be reused after this call.
#[cfg_attr(
all(target_arch = "wasm32", target_os = "unknown"),
export_name = "greet"
)]
#[no_mangle]
pub unsafe extern "C" fn _greet(ptr: u32, len: u32) {
greet(&ptr_to_string(ptr, len));
}
/// WebAssembly export that accepts a string (linear memory offset, byteCount)
/// and returns a pointer/size pair packed into a u64.
///
/// Note: The return value is leaked to the caller, so it must call
/// [`deallocate`] when finished.
/// Note: This uses a u64 instead of two result values for compatibility with
/// WebAssembly 1.0.
#[cfg_attr(
all(target_arch = "wasm32", target_os = "unknown"),
export_name = "greeting"
)]
#[no_mangle]
pub unsafe extern "C" fn _greeting(ptr: u32, len: u32) -> u64 {
let name = &ptr_to_string(ptr, len);
let g = greeting(name);
let (ptr, len) = string_to_ptr(&g);
// Note: This changes ownership of the pointer to the external caller. If
// we didn't call forget, the caller would read back a corrupt value. Since
// we call forget, the caller must deallocate externally to prevent leaks.
std::mem::forget(g);
return ((ptr as u64) << 32) | len as u64;
}
/// Returns a string from WebAssembly compatible numeric types representing
/// its pointer and length.
unsafe fn ptr_to_string(ptr: u32, len: u32) -> String {
let slice = slice::from_raw_parts_mut(ptr as *mut u8, len as usize);
let utf8 = std::str::from_utf8_unchecked_mut(slice);
return String::from(utf8);
}
/// Returns a pointer and size pair for the given string in a way that is
/// compatible with WebAssembly numeric types.
///
/// Note: This doesn't change the ownership of the String. To intentionally
/// leak it, use [`std::mem::forget`] on the input after calling this.
unsafe fn string_to_ptr(s: &String) -> (u32, u32) {
return (s.as_ptr() as u32, s.len() as u32);
}
/// Set the global allocator to the WebAssembly optimized one.
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
/// WebAssembly export that allocates a pointer (linear memory offset) that can
/// be used for a string.
///
/// This is an ownership transfer, which means the caller must call
/// [`deallocate`] when finished.
#[cfg_attr(
all(target_arch = "wasm32", target_os = "unknown"),
export_name = "allocate"
)]
#[no_mangle]
pub extern "C" fn allocate(size: u32) -> *mut u8 {
// Allocate the amount of bytes needed.
let vec: Vec<MaybeUninit<u8>> = Vec::with_capacity(size as usize);
// into_raw leaks the memory to the caller.
Box::into_raw(vec.into_boxed_slice()) as *mut u8
}
/// WebAssembly export that deallocates a pointer of the given size (linear
/// memory offset, byteCount) allocated by [`allocate`].
#[cfg_attr(
all(target_arch = "wasm32", target_os = "unknown"),
export_name = "deallocate"
)]
#[no_mangle]
pub extern "C" fn deallocate(ptr: u32, size: u32) {
unsafe {
// Retake the pointer which allows its memory to be freed.
let _ = Vec::from_raw_parts(ptr as *mut u8, 0, size as usize);
}
}

1
examples/allocation/tinygo/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
greet

View File

@@ -0,0 +1,28 @@
## 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`
Ex.
```bash
$ go run greet.go wazero
wasm >> Hello, wazero!
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.
Go does not export allocation functions, but when TinyGo generates WebAssembly,
it exports "malloc" and "free", which we use for that purpose. These are not
documented, so not necessarily a best practice. See the following issues for
updates:
* WebAssembly exports for allocation: https://github.com/tinygo-org/tinygo/issues/2788
* Memory ownership of TinyGo allocated pointers: https://github.com/tinygo-org/tinygo/issues/2787
Note: While folks here are familiar with TinyGo, wazero isn't a TinyGo project.
We hope this gets you started. For next steps, consider reading the
[TinyGo Using WebAssembly Guide](https://tinygo.org/docs/guides/webassembly/)
or joining the [#TinyGo channel on the Gophers Slack](https://github.com/tinygo-org/tinygo#getting-help).

View File

@@ -0,0 +1,115 @@
package main
import (
"context"
_ "embed"
"fmt"
"log"
"os"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/wasi"
)
// 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()
// Instantiate a module named "env" that exports a function to log a string
// to the console.
env, err := r.NewModuleBuilder("env").
ExportFunction("log", logString).
Instantiate(ctx)
if err != nil {
log.Fatal(err)
}
defer env.Close()
// Note: testdata/greet.go doesn't use WASI, but TinyGo needs it to
// implement functions such as panic.
wm, err := wasi.InstantiateSnapshotPreview1(ctx, r)
if err != nil {
log.Fatal(err)
}
defer wm.Close()
// Instantiate a module named "greet" that imports the "log" function
// defined in "env".
mod, err := r.InstantiateModuleFromCode(ctx, greetWasm)
if err != nil {
log.Fatal(err)
}
defer mod.Close()
// Get a references to functions we'll use in this example.
greet := mod.ExportedFunction("greet")
greeting := mod.ExportedFunction("greeting")
// These are undocumented, but exported. See tinygo-org/tinygo#2788
malloc := mod.ExportedFunction("malloc")
free := mod.ExportedFunction("free")
// Let's use the argument to this main function in Wasm.
name := os.Args[1]
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.Fatal(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 free.Call(ctx, namePtr)
// The pointer is a linear memory offset, which is where we write the name.
if !mod.Memory().Write(uint32(namePtr), []byte(name)) {
log.Fatalf("Memory.Write(%d, %d) out of range of memory size %d",
namePtr, nameSize, mod.Memory().Size())
}
// Now, we can call "greet", which reads the string we wrote to memory!
_, err = greet.Call(ctx, namePtr, nameSize)
if err != nil {
log.Fatal(err)
}
// 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.Fatal(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])
// The pointer is a linear memory offset, which is where we write the name.
if bytes, ok := mod.Memory().Read(greetingPtr, greetingSize); !ok {
log.Fatalf("Memory.Read(%d, %d) out of range of memory size %d",
greetingPtr, greetingSize, mod.Memory().Size())
} else {
fmt.Println("go >>", string(bytes))
}
}
func logString(m api.Module, offset, byteCount uint32) {
buf, ok := m.Memory().Read(offset, byteCount)
if !ok {
log.Fatalf("Memory.Read(%d, %d) out of range", offset, byteCount)
}
fmt.Println(string(buf))
}

View File

@@ -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)
}

View File

@@ -0,0 +1,75 @@
package main
import (
"fmt"
"reflect"
"unsafe"
)
// main is required for TinyGo to compile to Wasm.
func main() {}
// greet prints a greeting to the console.
func greet(name string) {
log(fmt.Sprint("wasm >> ", greeting(name)))
}
// log a message to the console using _log.
func log(message string) {
ptr, size := stringToPtr(message)
_log(ptr, size)
}
// _log is a WebAssembly import which prints a string (linear memory offset,
// byteCount) to the console.
//
// Note: In TinyGo "//export" on a func is actually an import!
//go:wasm-module env
//export log
func _log(ptr uint32, size uint32)
// greeting gets a greeting for the name.
func greeting(name string) string {
return fmt.Sprint("Hello, ", name, "!")
}
// _greet is a WebAssembly export that accepts a string pointer (linear
// memory offset) and calls greet.
//export greet
func _greet(ptr, size uint32) {
name := ptrToString(ptr, size)
greet(name)
}
// _greet 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 a uint64 instead of two result values for compatibility
// with WebAssembly 1.0.
//export greeting
func _greeting(ptr, size uint32) (ptrSize uint64) {
name := ptrToString(ptr, size)
g := greeting(name)
ptr, size = stringToPtr(g)
return (uint64(ptr) << uint64(32)) | uint64(size)
}
// ptrToString returns a string from WebAssembly compatible numeric
// types representing its pointer and length.
func ptrToString(ptr uint32, size uint32) (ret string) {
// Here, we want to get a string represented by the ptr and size. If we
// wanted a []byte, we'd use reflect.SliceHeader instead.
strHdr := (*reflect.StringHeader)(unsafe.Pointer(&ret))
strHdr.Data = uintptr(ptr)
strHdr.Len = uintptr(size)
return
}
// stringToPtr returns a pointer and size pair for the given string
// in a way that is compatible with WebAssembly numeric types.
func stringToPtr(s string) (uint32, uint32) {
buf := []byte(s)
ptr := &buf[0]
unsafePtr := uintptr(unsafe.Pointer(ptr))
return uint32(unsafePtr), uint32(len(buf))
}

BIN
examples/allocation/tinygo/testdata/greet.wasm vendored Executable file

Binary file not shown.

View File

@@ -12,5 +12,5 @@ import (
// go run cat.go ./test.txt
func Test_main(t *testing.T) {
stdout, _ := maintester.TestMain(t, main, "cat", "./test.txt")
require.Equal(t, "hello filesystem\n", stdout)
require.Equal(t, "greet filesystem\n", stdout)
}

View File

@@ -1 +1 @@
hello filesystem
greet filesystem