components-contrib/middleware/http/wasm/basic/basic.go

140 lines
3.8 KiB
Go

package basic
import (
"encoding/json"
"log"
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/wasi"
"github.com/valyala/fasthttp"
"github.com/dapr/components-contrib/middleware"
"github.com/dapr/kit/logger"
)
type middlewareMetadata struct {
Path string `json:"path"`
Runtime string `json:"runtime"`
}
// Middleware is an wasm basic middleware.
type Middleware struct {
logger logger.Logger
}
// NewMiddleware returns a new wasm basic middleware.
func NewMiddleware(logger logger.Logger) *Middleware {
return &Middleware{logger: logger}
}
// GetHandler returns the HTTP handler provided by wasm basic middleware.
func (m *Middleware) GetHandler(metadata middleware.Metadata) (func(h fasthttp.RequestHandler) fasthttp.RequestHandler, error) {
var (
meta *middlewareMetadata
err error
)
meta, err = m.getNativeMetadata(metadata)
if err != nil {
return nil, errors.Wrap(err, "failed to parse wasm basic metadata")
}
if meta.Runtime != "wazero" {
return nil, errors.Wrap(err, "only support wazero runtime")
}
path, err := filepath.Abs(meta.Path)
if err != nil {
return nil, errors.Wrap(err, "failed to find wasm basic file")
}
return func(h fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
// Create a new WebAssembly Runtime.
r := wazero.NewRuntime()
// TinyGo needs WASI to implement functions such as panic.
wm, err := wasi.InstantiateSnapshotPreview1(ctx, r)
if err != nil {
log.Fatal(err)
}
defer wm.Close(ctx)
wasmByte, err := os.ReadFile(path)
if err != nil {
log.Fatalf("wasm file %v", err)
}
mod, err := r.InstantiateModuleFromCode(ctx, wasmByte)
if err != nil {
log.Fatal(err)
}
defer mod.Close(ctx)
// These are undocumented, but exported. See tinygo-org/tinygo#2788
malloc := mod.ExportedFunction("malloc")
free := mod.ExportedFunction("free")
uriFunc := mod.ExportedFunction("run")
if uriFunc == nil {
log.Fatal(err)
}
uri := ctx.RequestURI()
uriLength := uint64(len(uri))
// 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, uriLength)
if err != nil {
log.Fatal(err)
}
uriPtr := 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, uriPtr)
// The pointer is a linear memory offset, which is where we write the value.
if !mod.Memory().Write(ctx, uint32(uriPtr), uri) {
log.Fatalf("Memory.Write(%d, %d) out of range of memory size %d",
uriPtr, uriLength, mod.Memory().Size(ctx))
}
// Now, we can call "uriFunc", which reads the string we wrote to memory!
ptrSize, err := uriFunc.Call(ctx, uriPtr, uriLength)
// if err != nil {
if err != nil {
log.Fatal(err)
}
// Note: This pointer is still owned by TinyGo, so don't try to free it!
retPtr := uint32(ptrSize[0] >> 32)
retSize := uint32(ptrSize[0])
// The pointer is a linear memory offset, which is where we write the name.
if bytes, ok := mod.Memory().Read(ctx, retPtr, retSize); !ok {
log.Fatalf("Memory.Read(%d, %d) out of range of memory size %d",
retPtr, retSize, mod.Memory().Size(ctx))
} else {
ctx.Request.SetRequestURIBytes(bytes)
}
h(ctx)
}
}, nil
}
func (m *Middleware) getNativeMetadata(metadata middleware.Metadata) (*middlewareMetadata, error) {
b, err := json.Marshal(metadata.Properties)
if err != nil {
return nil, err
}
var data middlewareMetadata
err = json.Unmarshal(b, &data)
if err != nil {
return nil, err
}
return &data, nil
}