Pass complex parameters to Wasm functions

An issue with the WebAssembly spec is that it only supports a very limited number of data types. If you want to embed a WebAssembly function with complex call parameters or return values, you will need to manage memory pointers both on Go SDK and WebAssembly function sides. Such complex call parameters and return values include dynamic memory structures such as strings and byte arrays. In this section, we will discuss several examples.

Pass strings to Rust functions

In this example, we will demonstrate how to call a Rust-based WebAssembly function from a Go app. Specially, we will discuss how to pass string data.

An alternative approach to pass and return complex values to Rust functions in WebAssembly is to use the wasm-bindgen compiler tool. You can learn more here.

The Rust function takes a memory pointer for the string, and constructs the Rust string itself.

  1. #![allow(unused)]
  2. fn main() {
  3. use std::ffi::{CStr, CString};
  4. use std::mem;
  5. use std::os::raw::{c_char, c_void};
  6. #[no_mangle]
  7. pub extern fn allocate(size: usize) -> *mut c_void {
  8. let mut buffer = Vec::with_capacity(size);
  9. let pointer = buffer.as_mut_ptr();
  10. mem::forget(buffer);
  11. pointer as *mut c_void
  12. }
  13. #[no_mangle]
  14. pub extern fn deallocate(pointer: *mut c_void, capacity: usize) {
  15. unsafe {
  16. let _ = Vec::from_raw_parts(pointer, 0, capacity);
  17. }
  18. }
  19. #[no_mangle]
  20. pub extern fn greet(subject: *mut c_char) -> *mut c_char {
  21. let subject = unsafe { CStr::from_ptr(subject).to_bytes().to_vec() };
  22. let mut output = b"Hello, ".to_vec();
  23. output.extend(&subject);
  24. output.extend(&[b'!']);
  25. unsafe { CString::from_vec_unchecked(output) }.into_raw()
  26. }
  27. }

Use standard Rust compiler tools to compile the Rust source code into a WebAssembly bytecode application.

`cd rust_memory_greet cargo build --target wasm32-wasi # The output WASM will betarget/wasm32-wasi/debug/rust_memory_greet_lib.wasm`.

The Go SDK application must call allocate from the WasmEdge VM to get a pointer to the string parameter. It will then call the greet function in Rust with the pointer. After the function returns, the Go application will call deallocate to free the memory space.

package main import ( "fmt" "os" "strings" "github.com/second-state/WasmEdge-go/wasmedge" ) func main() { wasmedge.SetLogErrorLevel() conf := wasmedge.NewConfigure(wasmedge.WASI) store := wasmedge.NewStore() vm := wasmedge.NewVMWithConfigAndStore(conf, store) wasi := vm.GetImportModule(wasmedge.WASI) wasi.InitWasi( os.Args[1:], os.Environ(), []string{".:."}, ) err := vm.LoadWasmFile(os.Args[1]) if err != nil { fmt.Println("failed to load wasm") } vm.Validate() vm.Instantiate() subject := "WasmEdge" lengthOfSubject := len(subject) // Allocate memory for the subject, and get a pointer to it. // Include a byte for the NULL terminator we add below. allocateResult, _ := vm.Execute("allocate", int32(lengthOfSubject+1)) inputPointer := allocateResult[0].(int32) // Write the subject into the memory. mem := store.FindMemory("memory") memData, _ := mem.GetData(uint(inputPointer), uint(lengthOfSubject+1)) copy(memData, subject) // C-string terminates by NULL. memData[lengthOfSubject] = 0 // Run the `greet` function. Given the pointer to the subject. greetResult, _ := vm.Execute("greet", inputPointer) outputPointer := greetResult[0].(int32) pageSize := mem.GetPageSize() // Read the result of the `greet` function. memData, _ = mem.GetData(uint(0), uint(pageSize * 65536)) nth := 0 var output strings.Builder for { if memData[int(outputPointer) + nth] == 0 { break } output.WriteByte(memData[int(outputPointer) + nth]) nth++ } lengthOfOutput := nth fmt.Println(output.String()) // Deallocate the subject, and the output. vm.Execute("deallocate", inputPointer, int32(lengthOfSubject+1)) vm.Execute("deallocate", outputPointer, int32(lengthOfOutput+1)) vm.Release() store.Release() conf.Release() }

To build the Go SDK example, run the following commands.

go get github.com/second-state/WasmEdge-go/wasmedge@v0.10.0 go build greet_memory.go

Now you can use the Go application to run the WebAssembly plug-in compiled from Rust.

$ ./greet_memory rust_memory_greet_lib.wasm Hello, WasmEdge!

Pass strings to TinyGo functions

In this example, we will demonstrate how to call a TinyGo-based WebAssembly function from a Go app.

The TinyGo function takes a memory pointer for the string, and constructs the TinyGo string itself.

The empty main() is needed to the compiled WebAssembly program to set up WASI properly.

package main import ( "strings" "unsafe" ) func main() {} // export greet func greet(subject *int32) *byte { nth := 0 var subjectStr strings.Builder pointer := uintptr(unsafe.Pointer(subject)) for { s := *(*int32)(unsafe.Pointer(pointer + uintptr(nth))) if s == 0 { break; } subjectStr.WriteByte(byte(s)) nth++ } output := subjectStr.String() output = "Hello, " + output + "!" return &(([]byte)(output)[0]) }

Use the TinyGo compiler tools to compile the Go source code into a WebAssembly bytecode application.

tinygo build -o greet.wasm -target wasi greet.go

The Go SDK application must call malloc from the WasmEdge VM to get a pointer to the string parameter. It will then call the greet function in TinyGo with the pointer. After the function returns, the Go application will call free to free the memory space.

package main import ( "fmt" "os" "strings" "github.com/second-state/WasmEdge-go/wasmedge" ) func main() { wasmedge.SetLogErrorLevel() conf := wasmedge.NewConfigure(wasmedge.WASI) store := wasmedge.NewStore() vm := wasmedge.NewVMWithConfigAndStore(conf, store) wasi := vm.GetImportModule(wasmedge.WASI) wasi.InitWasi( os.Args[1:], os.Environ(), []string{".:."}, ) err := vm.LoadWasmFile(os.Args[1]) if err != nil { fmt.Println("failed to load wasm") } vm.Validate() vm.Instantiate() subject := "WasmEdge" lengthOfSubject := len(subject) // Allocate memory for the subject, and get a pointer to it. // Include a byte for the NULL terminator we add below. allocateResult, _ := vm.Execute("malloc", int32(lengthOfSubject+1)) inputPointer := allocateResult[0].(int32) // Write the subject into the memory. mem := store.FindMemory("memory") memData, _ := mem.GetData(uint(inputPointer), uint(lengthOfSubject+1)) copy(memData, subject) // C-string terminates by NULL. memData[lengthOfSubject] = 0 // Run the `greet` function. Given the pointer to the subject. greetResult, _ := vm.Execute("greet", inputPointer) outputPointer := greetResult[0].(int32) pageSize := mem.GetPageSize() // Read the result of the `greet` function. memData, _ = mem.GetData(uint(0), uint(pageSize * 65536)) nth := 0 var output strings.Builder for { if memData[int(outputPointer) + nth] == 0 { break } output.WriteByte(memData[int(outputPointer) + nth]) nth++ } fmt.Println(output.String()) // Deallocate the subject, and the output. vm.Execute("free", inputPointer) vm.Execute("free", outputPointer) vm.Release() store.Release() conf.Release() }

To build the Go SDK example, run the following commands.

go get github.com/second-state/WasmEdge-go/wasmedge@v0.10.0 go build greet_memory.go

Now you can use the Go application to run the WebAssembly plug-in compiled from TinyGo.

$ ./greet_memory greet.wasm Hello, WasmEdge!

Pass bytes to Rust functions

In this example, we will demonstrate how to call Rust-based WebAssembly functions and pass arrays to and from a Go app.

An alternative approach to pass and return complex values to Rust functions in WebAssembly is to use the wasm-bindgen compiler tool. You can learn more here.

The fib_array() function takes a array as a call parameter and fill it with a fibonacci sequence of numbers. Alternatively, the fib_array_return_memory() function returns a array of fibonacci sequence of numbers.

For the array in the call parameter, the Rust function fib_array() takes a memory pointer and constructs the Rust Vec using from_raw_parts. For the array return value, the Rust function fib_array_return_memory() simply returns the pointer.

  1. #![allow(unused)]
  2. fn main() {
  3. use std::mem;
  4. use std::os::raw::{c_void, c_int};
  5. #[no_mangle]
  6. pub extern fn allocate(size: usize) -> *mut c_void {
  7. let mut buffer = Vec::with_capacity(size);
  8. let pointer = buffer.as_mut_ptr();
  9. mem::forget(buffer);
  10. pointer as *mut c_void
  11. }
  12. #[no_mangle]
  13. pub extern fn deallocate(pointer: *mut c_void, capacity: usize) {
  14. unsafe {
  15. let _ = Vec::from_raw_parts(pointer, 0, capacity);
  16. }
  17. }
  18. #[no_mangle]
  19. pub extern fn fib_array(n: i32, p: *mut c_int) -> i32 {
  20. unsafe {
  21. let mut arr = Vec::<i32>::from_raw_parts(p, 0, (4*n) as usize);
  22. for i in 0..n {
  23. if i < 2 {
  24. arr.push(i);
  25. } else {
  26. arr.push(arr[(i - 1) as usize] + arr[(i - 2) as usize]);
  27. }
  28. }
  29. let r = arr[(n - 1) as usize];
  30. mem::forget(arr);
  31. r
  32. }
  33. }
  34. #[no_mangle]
  35. pub extern fn fib_array_return_memory(n: i32) -> *mut c_int {
  36. let mut arr = Vec::with_capacity((4 * n) as usize);
  37. let pointer = arr.as_mut_ptr();
  38. for i in 0..n {
  39. if i < 2 {
  40. arr.push(i);
  41. } else {
  42. arr.push(arr[(i - 1) as usize] + arr[(i - 2) as usize]);
  43. }
  44. }
  45. mem::forget(arr);
  46. pointer
  47. }
  48. }

Use standard Rust compiler tools to compile the Rust source code into a WebAssembly bytecode application.

cd rust_access_memory cargo build --target wasm32-wasi # The output WASM will be target/wasm32-wasi/debug/rust_access_memory_lib.wasm.

The Go SDK application must call allocate from the WasmEdge VM to get a pointer to the array. It will then call the fib_array() function in Rust and pass in the pointer. After the functions return, the Go application will use the WasmEdge store API to construct an array from the pointer in the call parameter (fib_array()) or in the return value (fib_array_return_memory()). The Go app will eventually call deallocate to free the memory space.

package main import ( "fmt" "os" "unsafe" "github.com/second-state/WasmEdge-go/wasmedge" ) func main() { wasmedge.SetLogErrorLevel() conf := wasmedge.NewConfigure(wasmedge.WASI) store := wasmedge.NewStore() vm := wasmedge.NewVMWithConfigAndStore(conf, store) wasi := vm.GetImportModule(wasmedge.WASI) wasi.InitWasi( os.Args[1:], os.Environ(), []string{".:."}, ) err := vm.LoadWasmFile(os.Args[1]) if err != nil { fmt.Println("failed to load wasm") } vm.Validate() vm.Instantiate() n := int32(10) p, err := vm.Execute("allocate", 4 * n) if err != nil { fmt.Println("allocate failed:", err) } fib, err := vm.Execute("fib_array", n, p[0]) if err != nil { fmt.Println("fib_rray failed:", err) } else { fmt.Println("fib_array() returned:", fib[0]) fmt.Printf("fib_array memory at: %p\n", unsafe.Pointer((uintptr)(p[0].(int32)))) mem := store.FindMemory("memory") if mem != nil { // int32 occupies 4 bytes fibArray, err := mem.GetData(uint(p[0].(int32)), uint(n * 4)) if err == nil && fibArray != nil { fmt.Println("fibArray:", fibArray) } } } fibP, err := vm.Execute("fib_array_return_memory", n) if err != nil { fmt.Println("fib_array_return_memory failed:", err) } else { fmt.Printf("fib_array_return_memory memory at: %p\n", unsafe.Pointer((uintptr)(fibP[0].(int32)))) mem := store.FindMemory("memory") if mem != nil { // int32 occupies 4 bytes fibArrayReturnMemory, err := mem.GetData(uint(fibP[0].(int32)), uint(n * 4)) if err == nil && fibArrayReturnMemory != nil { fmt.Println("fibArrayReturnMemory:", fibArrayReturnMemory) } } } _, err = vm.Execute("deallocate", p[0].(int32), 4 * n) if err != nil { fmt.Println("free failed:", err) } exitcode := wasi.WasiGetExitCode() if exitcode != 0 { fmt.Println("Go: Running wasm failed, exit code:", exitcode) } vm.Release() store.Release() conf.Release() }

To build the Go SDK example, run the following commands.

go get github.com/second-state/WasmEdge-go/wasmedge@v0.10.0 go build run.go

Now you can use the Go application to run the WebAssembly plug-in compiled from Rust.

$ ./run rust_access_memory_lib.wasm fib_array() returned: 34 fib_array memory at: 0x102d80 fibArray: [0 0 0 0 1 0 0 0 1 0 0 0 2 0 0 0 3 0 0 0 5 0 0 0 8 0 0 0 13 0 0 0 21 0 0 0 34 0 0 0] fib_array_return_memory memory at: 0x105430 fibArrayReturnMemory: [0 0 0 0 1 0 0 0 1 0 0 0 2 0 0 0 3 0 0 0 5 0 0 0 8 0 0 0 13 0 0 0 21 0 0 0 34 0 0 0]

Pass bytes to TinyGo functions

In this example, we will demonstrate how to call TinyGo-based WebAssembly functions and pass arrays to and from a Go app.

The fibArray function takes a array as a call parameter and fill it with a fibonacci sequence of numbers. Alternatively, the fibArrayReturnMemory function returns a array of fibonacci sequence of numbers.

package main import ( "fmt" "unsafe" ) func main() { println("in main") n := int32(10) arr := make([]int32, n) arrP := &arr[0] fmt.Printf("call fibArray(%d, %p) = %d\n", n, arrP, fibArray(n, arrP)) fmt.Printf("call fibArrayReturnMemory(%d) return %p\n", n, fibArrayReturnMemory(n)) } // export fibArray func fibArray(n int32, p *int32) int32 { arr := unsafe.Slice(p, n) for i := int32(0); i < n; i++ { switch { case i < 2: arr[i] = i default: arr[i] = arr[i-1] + arr[i-2] } } return arr[n-1] } // export fibArrayReturnMemory func fibArrayReturnMemory(n int32) *int32 { arr := make([]int32, n) for i := int32(0); i < n; i++ { switch { case i < 2: arr[i] = i default: arr[i] = arr[i-1] + arr[i-2] } } return &arr[0] }

Use the TinyGo compiler tools to compile the Go source code into a WebAssembly bytecode application.

tinygo build -o fib.wasm -target wasi fib.go

The Go SDK application must call malloc from the WasmEdge VM to get a pointer to the array. It will then call the fibArray() function in TinyGo with the pointer. After the functions return, the Go app uses the WasmEdge SDK’s store API to construct an array from the pointer in the call parameter (fibArray()) or in the return value (fibArrayReturnMemory()). The Go application will eventually call free to free the memory space.

package main import ( "fmt" "os" "unsafe" "github.com/second-state/WasmEdge-go/wasmedge" ) func main() { wasmedge.SetLogErrorLevel() conf := wasmedge.NewConfigure(wasmedge.WASI) store := wasmedge.NewStore() vm := wasmedge.NewVMWithConfigAndStore(conf, store) wasi := vm.GetImportModule(wasmedge.WASI) wasi.InitWasi( os.Args[1:], os.Environ(), []string{".:."}, ) err := vm.LoadWasmFile(os.Args[1]) if err != nil { fmt.Println("failed to load wasm") } vm.Validate() vm.Instantiate() n := int32(10) p, err := vm.Execute("malloc", n) if err != nil { fmt.Println("malloc failed:", err) } fib, err := vm.Execute("fibArray", n, p[0]) if err != nil { fmt.Println("fibArray failed:", err) } else { fmt.Println("fibArray() returned:", fib[0]) fmt.Printf("fibArray memory at: %p\n", unsafe.Pointer((uintptr)(p[0].(int32)))) mem := store.FindMemory("memory") if mem != nil { // int32 occupies 4 bytes fibArray, err := mem.GetData(uint(p[0].(int32)), uint(n * 4)) if err == nil && fibArray != nil { fmt.Println("fibArray:", fibArray) } } } fibP, err := vm.Execute("fibArrayReturnMemory", n) if err != nil { fmt.Println("fibArrayReturnMemory failed:", err) } else { fmt.Printf("fibArrayReturnMemory memory at: %p\n", unsafe.Pointer((uintptr)(fibP[0].(int32)))) mem := store.FindMemory("memory") if mem != nil { // int32 occupies 4 bytes fibArrayReturnMemory, err := mem.GetData(uint(fibP[0].(int32)), uint(n * 4)) if err == nil && fibArrayReturnMemory != nil { fmt.Println("fibArrayReturnMemory:", fibArrayReturnMemory) } } } _, err = vm.Execute("free", p...) if err != nil { fmt.Println("free failed:", err) } exitcode := wasi.WasiGetExitCode() if exitcode != 0 { fmt.Println("Go: Running wasm failed, exit code:", exitcode) } vm.Release() store.Release() conf.Release() }

To build the Go SDK example, run the following commands.

go get github.com/second-state/WasmEdge-go/wasmedge@v0.10.0 go build run.go

Now you can use the Go application to run the WebAssembly plug-in compiled from TinyGo.

$ ./run fib.wasm fibArray() returned: 34 fibArray memory at: 0x14d3c fibArray: [0 0 0 0 1 0 0 0 1 0 0 0 2 0 0 0 3 0 0 0 5 0 0 0 8 0 0 0 13 0 0 0 21 0 0 0 34 0 0 0] fibArrayReturnMemory memory at: 0x14d4c fibArrayReturnMemory: [0 0 0 0 1 0 0 0 1 0 0 0 2 0 0 0 3 0 0 0 5 0 0 0 8 0 0 0 13 0 0 0 21 0 0 0 34 0 0 0]