diff --git a/examples/README.md b/examples/README.md index 730031dd..146b0115 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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. diff --git a/examples/allocation/README.md b/examples/allocation/README.md new file mode 100644 index 00000000..fae2ecfe --- /dev/null +++ b/examples/allocation/README.md @@ -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. diff --git a/examples/allocation/rust/.gitignore b/examples/allocation/rust/.gitignore new file mode 100644 index 00000000..714a8539 --- /dev/null +++ b/examples/allocation/rust/.gitignore @@ -0,0 +1 @@ +greet diff --git a/examples/allocation/rust/README.md b/examples/allocation/rust/README.md new file mode 100644 index 00000000..d3ed932d --- /dev/null +++ b/examples/allocation/rust/README.md @@ -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/). diff --git a/examples/allocation/rust/greet.go b/examples/allocation/rust/greet.go new file mode 100644 index 00000000..67063820 --- /dev/null +++ b/examples/allocation/rust/greet.go @@ -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)) +} diff --git a/examples/allocation/rust/greet_test.go b/examples/allocation/rust/greet_test.go new file mode 100644 index 00000000..4a944c86 --- /dev/null +++ b/examples/allocation/rust/greet_test.go @@ -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) +} diff --git a/examples/allocation/rust/testdata/.gitignore b/examples/allocation/rust/testdata/.gitignore new file mode 100644 index 00000000..96ef6c0b --- /dev/null +++ b/examples/allocation/rust/testdata/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/examples/allocation/rust/testdata/Cargo.toml b/examples/allocation/rust/testdata/Cargo.toml new file mode 100644 index 00000000..db637e8d --- /dev/null +++ b/examples/allocation/rust/testdata/Cargo.toml @@ -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 diff --git a/examples/allocation/rust/testdata/greet.wasm b/examples/allocation/rust/testdata/greet.wasm new file mode 100755 index 00000000..03c31769 Binary files /dev/null and b/examples/allocation/rust/testdata/greet.wasm differ diff --git a/examples/allocation/rust/testdata/src/lib.rs b/examples/allocation/rust/testdata/src/lib.rs new file mode 100644 index 00000000..ee3af322 --- /dev/null +++ b/examples/allocation/rust/testdata/src/lib.rs @@ -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> = 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); + } +} diff --git a/examples/allocation/tinygo/.gitignore b/examples/allocation/tinygo/.gitignore new file mode 100644 index 00000000..714a8539 --- /dev/null +++ b/examples/allocation/tinygo/.gitignore @@ -0,0 +1 @@ +greet diff --git a/examples/allocation/tinygo/README.md b/examples/allocation/tinygo/README.md new file mode 100644 index 00000000..34464c27 --- /dev/null +++ b/examples/allocation/tinygo/README.md @@ -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). diff --git a/examples/allocation/tinygo/greet.go b/examples/allocation/tinygo/greet.go new file mode 100644 index 00000000..a0ae2c09 --- /dev/null +++ b/examples/allocation/tinygo/greet.go @@ -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)) +} diff --git a/examples/allocation/tinygo/greet_test.go b/examples/allocation/tinygo/greet_test.go new file mode 100644 index 00000000..4a944c86 --- /dev/null +++ b/examples/allocation/tinygo/greet_test.go @@ -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) +} diff --git a/examples/allocation/tinygo/testdata/greet.go b/examples/allocation/tinygo/testdata/greet.go new file mode 100644 index 00000000..2cba3514 --- /dev/null +++ b/examples/allocation/tinygo/testdata/greet.go @@ -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)) +} diff --git a/examples/allocation/tinygo/testdata/greet.wasm b/examples/allocation/tinygo/testdata/greet.wasm new file mode 100755 index 00000000..f2be0e1b Binary files /dev/null and b/examples/allocation/tinygo/testdata/greet.wasm differ diff --git a/examples/wasi/cat_test.go b/examples/wasi/cat_test.go index d4dc035a..05ddfdaa 100644 --- a/examples/wasi/cat_test.go +++ b/examples/wasi/cat_test.go @@ -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) } diff --git a/examples/wasi/testdata/test.txt b/examples/wasi/testdata/test.txt index d67ef599..c376dfb1 100644 --- a/examples/wasi/testdata/test.txt +++ b/examples/wasi/testdata/test.txt @@ -1 +1 @@ -hello filesystem +greet filesystem