Photo by Daniel Schludi on Unsplash
Wazero Cookbook - Part Two: Host functions
My journey to free WASM from my browser
The Host Functions are functions defined inside the host program. From the WASM module perspective, a host function can be imported and called by the WASM module. A host function is like a superpower given by the host program to the WASM module.
So, today, we will learn how to develop "host functions" for the WASM modules with Wazero.
Requirements
You need to install the following:
GoLang and TinyGo (previous blog post: "Install Go and TinyGo").
Read the previous part: Wazero Cookbook - Part one: WASM function & Host application
And it's not mandatory, but it can help if you start from zero. You can read these former blog posts:
And now, it's time to start.
Chapter 3: Add a Print function
If you update the code of the WASM module from Chapter 2: "Create the new WASM module" to display a message from the WASM module by adding this line fmt.Println("🎃" + message)
(see below):
//export hello
func hello(valuePosition *uint32, length uint32) uint64 {
// read the memory to get the parameter
valueBytes := readBufferFromMemory(valuePosition, length)
message := "Hello " + string(valueBytes)
fmt.Println("🎃" + message)
// copy the value to memory
posSizePairValue := copyBufferToMemory([]byte(message))
// return the position and size
return posSizePairValue
}
When executing the module, you will never see the "pumpkin" message 😮. In fact, it's normal; according to the WASI specification, a wasm module is minimal. So, by default, it cannot display information in a terminal (it's why fmt.Println("🎃" + message)
does not work). But we can create a Print(something)
function into the host (the program calling the wasm module) and allow the wasm module to call this Print
function. And it will be our first host function.
Create a new host application.
I will reuse the code of Chapter 2: "Create the host application" to create the new host application.
On the host side:
We write a
PrintString
function.We export the
PrintString
function to the WASM runtime with an "export name":hostPrintString
(you can use the same name, but prefix the export name withhost
or something else helps me with the readability).
On the WASM module side*:
We import the
hostPrintString
functionWe use the
hostPrintString
function to print a message to the terminal from the WASM module.
👋 Every time the WASM module calls the hostPrintString
function, it will call the PrintString
function of the host.
👋 the hostPrintString
function will take as arguments the memory position and size of the data. Then, it will read the data from the WASM memory buffer (👀 have a look at Chapter 2: "Copy data to the WASM memory buffer").
Define the PrintString
host function
First, we will use the GoModuleFunc
api of Wazero, which is a convenience for defining an inlined function (== host function). And this is the source code of PrintString
:
var PrintString = api.GoModuleFunc(func(ctx context.Context, module api.Module, stack []uint64) {
// Get the position and size
// of the returned value 1️⃣
position := uint32(stack[0])
length := uint32(stack[1])
// Read the memory 2️⃣
buffer, ok := module.Memory().Read(position, length)
if !ok {
log.Panicf("Memory.Read(%d, %d) out of range", position, length)
}
// Display the message 3️⃣
fmt.Println(string(buffer))
})
- `stack []uint64` is used to pass the arguments and the
returned value of the function.
- 1️⃣ when `PrintString` is called by the **WASM module**
with `hostPrintString`, the arguments are the memory
position and size of the returned value.
- 2️⃣ the host can read the memory buffer thanks the position
and size of the returned value.
- 3️⃣ the host can display the message.
Export PrintString
as hostPrintString
To define the host function and make it available for the host, we will use the wazero.HostModuleBuilder
, like this:
builder := runtime.NewHostModuleBuilder("env")
builder.NewFunctionBuilder().
WithGoModuleFunction(PrintString, // 1️⃣
[]api.ValueType{
api.ValueTypeI32, // 2️⃣ string position
api.ValueTypeI32, // 3️⃣ string length
},
[]api.ValueType{api.ValueTypeI32}).
Export("hostPrintString") // 4️⃣
_, err := builder.Instantiate(ctx) // 5️⃣
if err != nil {
log.Panicln("Error with env module and host function(s):", err)
}
- 1️⃣+4️⃣ `PrintString` is exported as `hostPrintString`
- 2️⃣ the type of the 1st element of `stack []uint64`
- 3️⃣ the type of the 2nd element of `stack []uint64`
- 5️⃣ compile and instantiate the module
The entire source code looks like this:
// Package main of the host application
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)
// PrintString : print a string to the console
var PrintString = api.GoModuleFunc(func(ctx context.Context, module api.Module, stack []uint64) {
position := uint32(stack[0])
length := uint32(stack[1])
buffer, ok := module.Memory().Read(position, length)
if !ok {
log.Panicf("Memory.Read(%d, %d) out of range", position, length)
}
fmt.Println(string(buffer))
})
func main() {
// Choose the context to use for function calls.
ctx := context.Background()
// Create a new WebAssembly Runtime.
runtime := wazero.NewRuntime(ctx)
// This closes everything this Runtime created.
defer runtime.Close(ctx)
// START: Host functions
builder := runtime.NewHostModuleBuilder("env")
// hostPrintString
builder.NewFunctionBuilder().
WithGoModuleFunction(PrintString,
[]api.ValueType{
api.ValueTypeI32, // string position
api.ValueTypeI32, // string length
},
[]api.ValueType{api.ValueTypeI32}).
Export("hostPrintString")
_, err := builder.Instantiate(ctx)
if err != nil {
log.Panicln("Error with env module and host function(s):", err)
}
// END: Host functions
// Instantiate WASI
wasi_snapshot_preview1.MustInstantiate(ctx, runtime)
// Load the WebAssembly module
wasmPath := "../function/hello.wasm"
helloWasm, err := os.ReadFile(wasmPath)
if err != nil {
log.Panicln(err)
}
mod, err := runtime.Instantiate(ctx, helloWasm)
if err != nil {
log.Panicln(err)
}
// Get the reference to the WebAssembly function: "hello"
helloFunction := mod.ExportedFunction("hello")
// Function parameter
name := "Bob Morane"
nameSize := uint64(len(name))
// These function are exported by TinyGo
malloc := mod.ExportedFunction("malloc")
free := mod.ExportedFunction("free")
// Allocate Memory for "Bob Morane"
results, err := malloc.Call(ctx, nameSize)
if err != nil {
log.Panicln(err)
}
namePosition := results[0]
defer free.Call(ctx, namePosition)
// Copy "Bob Morane" to memory
if !mod.Memory().Write(uint32(namePosition), []byte(name)) {
log.Panicf("out of range of memory size")
}
// Now, we can call "hello" with the position
// and the size of "Bob Morane"
// the result type is []uint64
result, err := helloFunction.Call(ctx, namePosition, nameSize)
if err != nil {
log.Panicln(err)
}
// Extract the position and size of the returned value
valuePosition := uint32(result[0] >> 32)
valueSize := uint32(result[0])
// Read the value from the memory
if bytes, ok := mod.Memory().Read(valuePosition, valueSize); !ok {
log.Panicf("out of range of memory size")
} else {
fmt.Println("Returned value :", string(bytes))
}
}
You can build the host application with the following command:
go build
Create a new WASM module
I will reuse the code of Chapter 2: "Create the new WASM module" to create the new host application.
First, I need to make a reference to the "exported" hostPrintString
function:
//export hostPrintString
func hostPrintString(pos, sisze uint32) uint32
🖐🖐🖐 Yes, I know, it's "strange" to use
export
here, but in this specific case, actually,//export hostPrintString
means that the module imports the host function. And the second line allows providing the signature of the function.
Then, I create a helper to use the hostPrintString
host function:
func Print(message string) {
// Copy the message to
// the WASM memory buffer
buffer := []byte(message)
bufferPtr := &buffer[0]
unsafePtr := uintptr(unsafe.Pointer(bufferPtr))
// Get the memory position and size
// of the message
pos := uint32(unsafePtr)
size := uint32(len(buffer))
// Call the host function
hostPrintString(pos, size)
}
And now, I can print messages from my WASM module like that:
Print("👋 from the module: " + message)
The entire source code looks like that:
package main
import "unsafe"
func main () {}
//export hostPrintString
func hostPrintString(pos, sisze uint32) uint32
// Print a string
func Print(message string) {
buffer := []byte(message)
bufferPtr := &buffer[0]
unsafePtr := uintptr(unsafe.Pointer(bufferPtr))
pos := uint32(unsafePtr)
size := uint32(len(buffer))
hostPrintString(pos, size)
}
//export hello
func hello(valuePosition *uint32, length uint32) uint64 {
// read the memory to get the parameter
valueBytes := readBufferFromMemory(valuePosition, length)
message := "Hello " + string(valueBytes)
Print("👋 from the module: " + message)
// copy the value to memory
posSizePairValue := copyBufferToMemory([]byte(message))
// return the position and size
return posSizePairValue
}
// readBufferFromMemory returns a buffer from WebAssembly
func readBufferFromMemory(bufferPosition *uint32, length uint32) []byte {
subjectBuffer := make([]byte, length)
pointer := uintptr(unsafe.Pointer(bufferPosition))
for i := 0; i < int(length); i++ {
s := *(*int32)(unsafe.Pointer(pointer + uintptr(i)))
subjectBuffer[i] = byte(s)
}
return subjectBuffer
}
// copyBufferToMemory returns a single value
// (a kind of pair with position and length)
func copyBufferToMemory(buffer []byte) uint64 {
bufferPtr := &buffer[0]
unsafePtr := uintptr(unsafe.Pointer(bufferPtr))
ptr := uint32(unsafePtr)
size := uint32(len(buffer))
return (uint64(ptr) << uint64(32)) | uint64(size)
}
You can build the WASM module with the following command:
tinygo build -o hello.wasm \
-scheduler=none --no-debug \
-target wasi ./hello.go
Now, launch the host application with the following command:
./host # or `go run main.go`
Then, you should get this result:
👋 from the module: Hello Bob Morane
Returned value : Hello Bob Morane
🎉 And that's it. Thanks for reading this second part of Wazero recipes! I hope you enjoyed it. In the next blog post, we'll see how to implement a new host function to make HTTP requests, and always with Wazero.
All examples are available here: https://github.com/wasm-university/wazero-step-by-step/tree/main/chapter-03