package main
import (
"context"
_ "embed"
"flag"
"fmt"
"log"
"os"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)
var (
wasmFile = flag.String("wasm", "greet.wasm", "path to the WebAssembly file")
say = flag.String("say", "Hello", "what to say")
)
func logString(_ context.Context, m api.Module, offset, byteCount uint32) {
buf, ok := m.Memory().Read(offset, byteCount)
if !ok {
log.Panicf("Memory.Read(%d, %d) out of range", offset, byteCount)
}
fmt.Println(string(buf))
}
func callHost(_ context.Context, m api.Module, offset, byteCount uint32) uint64 {
buf, ok := m.Memory().Read(offset, byteCount)
if !ok {
log.Panicf("Memory.Read(%d, %d) out of range", offset, byteCount)
}
// TODO
ret := "callHost " + string(buf)
// Allocate memory for the return value. Wasm component should free it.
ptr, err := m.ExportedFunction("malloc").Call(context.Background(), uint64(len(ret)))
if err != nil {
log.Panicln(err)
}
if !m.Memory().Write(uint32(ptr[0]), []byte(ret)) {
log.Panicf("Memory.Write(%d, %d) out of range of memory size %d", ptr[0], len(ret), m.Memory().Size())
}
retPtr := uint32(ptr[0])
retSize := uint32(len(ret))
return uint64(retPtr)<<32 | uint64(retSize)
}
func main() {
flag.Parse()
// Read the WebAssembly file.
wasm, err := os.ReadFile(*wasmFile)
if err != nil {
log.Panicln(err)
}
// 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.
_, err = r.NewHostModuleBuilder("env").
NewFunctionBuilder().WithFunc(callHost).Export("call_host").
NewFunctionBuilder().WithFunc(logString).Export("log").
Instantiate(ctx)
if err != nil {
log.Panicln(err)
}
// 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.Instantiate(ctx, wasm)
if err != nil {
log.Panicln(err)
}
doFunc := mod.ExportedFunction("do")
// These are undocumented, but exported. See tinygo-org/tinygo#2788
malloc := mod.ExportedFunction("malloc")
free := mod.ExportedFunction("free")
argSize := uint64(len(*say))
mallocRet, err := malloc.Call(context.Background(), argSize)
if err != nil {
log.Panicln(err)
}
argPtr := uint32(mallocRet[0])
if !mod.Memory().Write(uint32(argPtr), []byte(*say)) {
log.Panicf("Memory.Write(%d, %d) out of range of memory size %d",
argPtr, argSize, mod.Memory().Size())
}
ptrSize, err := doFunc.Call(ctx, uint64(argPtr), argSize)
if err != nil {
log.Panicln(err)
}
retPtr := uint32(ptrSize[0] >> 32)
retSize := 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 retPtr != 0 {
defer func() {
_, err := free.Call(ctx, uint64(retPtr))
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(retPtr, retSize); !ok {
log.Panicf("Memory.Read(%d, %d) out of range of memory size %d",
retPtr, retSize, mod.Memory().Size())
} else {
fmt.Println("go >>", string(bytes))
}
}
#!/bin/sh
#
tinygo build -o plugin.wasm -scheduler=none --no-debug -target=wasi main.go
package main
// #include <stdlib.h>
import "C"
import (
"encoding/json"
"unsafe"
)
/* helper functions */
//go:wasmimport env call_host
func _callHost(paramPtr, paramSize uint32) uint64
func callHost(buf string) string {
ptr, size := gostr_to_ptr(buf)
ret := _callHost(ptr, size)
retPtr, retSize := unpack_uint64_to_uint32(ret)
if retPtr != 0 {
defer free_ptr(retPtr)
}
return ptr_to_gostr(retPtr, retSize)
}
//go:wasmimport env log
func _log(ptr, size uint32)
func log(s string) {
ptr, size := gostr_to_ptr(s)
_log(ptr, size)
}
//export do
func _do(ptr, size uint32) (ptrSize uint64) {
// param's memory is managed by the host
param := ptr_to_gostr(ptr, size)
ret := do(param)
// need to dup the string because the host will free the pointer
ptr, size = dup_gostr(ret)
return (uint64(ptr) << uint64(32)) | uint64(size)
}
// ptr_to_gostr returns a string from WebAssembly compatible numeric types
// representing its pointer and length.
func ptr_to_gostr(ptr uint32, size uint32) string {
return unsafe.String((*byte)(unsafe.Pointer(uintptr(ptr))), size)
}
// gostr_to_ptr returns a pointer and size pair for the given string in a way
// compatible with WebAssembly numeric types.
// The returned pointer aliases the string hence the string must be kept alive
// until ptr is no longer needed.
func gostr_to_ptr(s string) (uint32, uint32) {
ptr := unsafe.Pointer(unsafe.StringData(s))
return uint32(uintptr(ptr)), uint32(len(s))
}
// dup_gostr returns a pointer and size pair for the given string in a way
// The pointer is not automatically managed by TinyGo hence it must be freed by the host.
func dup_gostr(s string) (uint32, uint32) {
size := C.ulong(len(s))
ptr := unsafe.Pointer(C.malloc(size))
copy(unsafe.Slice((*byte)(ptr), size), s)
return uint32(uintptr(ptr)), uint32(size)
}
// free_ptr frees the given pointer.
func free_ptr(ptr uint32) {
if ptr == 0 {
return
}
C.free(unsafe.Pointer(uintptr(ptr)))
}
func unpack_uint64_to_uint32(v uint64) (ptr uint32, size uint32) {
return uint32(v >> 32), uint32(v)
}
func pack_uint32_to_uint64(ptr, size uint32) uint64 {
return (uint64(ptr) << 32) | uint64(size)
}
func uint64_to_gostr(v uint64) (s string, ptr uint32) {
ptr, size := unpack_uint64_to_uint32(v)
return ptr_to_gostr(ptr, size), ptr
}
/* main functions */
func do(buf string) string {
jsonBuf := []byte(buf)
var data map[string]interface{}
err := json.Unmarshal(jsonBuf, &data)
if err != nil {
b, _ := json.Marshal(map[string]interface{}{
"error": err.Error(),
})
return string(b)
}
url := data["url"].(string)
hostRet := callHost(url)
log(hostRet)
ret, _ := json.MarshalIndent(data, "", " ")
return string(ret)
}
func main() {}