Files
extism/extism.go

524 lines
13 KiB
Go

package extism
import (
"encoding/json"
"errors"
"fmt"
"io"
"runtime/cgo"
"unsafe"
)
/*
#cgo CFLAGS: -I/usr/local/include
#cgo LDFLAGS: -L/usr/local/lib -lextism
#include <extism.h>
#include <stdlib.h>
int64_t extism_val_i64(ExtismValUnion* x){
return x->i64;
}
int32_t extism_val_i32(ExtismValUnion* x){
return x->i32;
}
float extism_val_f32(ExtismValUnion* x){
return x->f32;
}
double extism_val_f64(ExtismValUnion* x){
return x->f64;
}
void extism_val_set_i64(ExtismValUnion* x, int64_t i){
x->i64 = i;
}
void extism_val_set_i32(ExtismValUnion* x, int32_t i){
x->i32 = i;
}
void extism_val_set_f32(ExtismValUnion* x, float f){
x->f32 = f;
}
void extism_val_set_f64(ExtismValUnion* x, double f){
x->f64 = f;
}
*/
import "C"
// Context is used to manage Plugins
type Context struct {
pointer *C.ExtismContext
}
type ValType = C.ExtismValType
type Val = C.ExtismVal
type Size = C.ExtismSize
var (
I32 ValType = C.I32
I64 ValType = C.I64
F32 ValType = C.F32
F64 ValType = C.F64
V128 ValType = C.V128
FuncRef ValType = C.FuncRef
ExternRef ValType = C.ExternRef
)
// Function is used to define host functions
type Function struct {
pointer *C.ExtismFunction
userData cgo.Handle
}
// Free a function
func (f *Function) Free() {
C.extism_function_free(f.pointer)
f.pointer = nil
f.userData.Delete()
}
// NewFunction creates a new host function with the given name, input/outputs and optional user data, which can be an
// arbitrary `interface{}`
func NewFunction(name string, inputs []ValType, outputs []ValType, f unsafe.Pointer, userData interface{}) Function {
var function Function
function.userData = cgo.NewHandle(userData)
cname := C.CString(name)
ptr := unsafe.Pointer(function.userData)
var inputsPtr *C.ExtismValType = nil
if len(inputs) > 0 {
inputsPtr = (*C.ExtismValType)(&inputs[0])
}
var outputsPtr *C.ExtismValType = nil
if len(outputs) > 0 {
outputsPtr = (*C.ExtismValType)(&outputs[0])
}
function.pointer = C.extism_function_new(
cname,
inputsPtr,
C.uint64_t(len(inputs)),
outputsPtr,
C.uint64_t(len(outputs)),
(*[0]byte)(f),
ptr,
nil,
)
C.free(unsafe.Pointer(cname))
return function
}
func (f *Function) SetNamespace(s string) {
cstr := C.CString(s)
defer C.free(unsafe.Pointer(cstr))
C.extism_function_set_namespace(f.pointer, cstr)
}
func (f Function) WithNamespace(s string) Function {
f.SetNamespace(s)
return f
}
type CurrentPlugin struct {
pointer *C.ExtismCurrentPlugin
}
func GetCurrentPlugin(ptr unsafe.Pointer) CurrentPlugin {
return CurrentPlugin{
pointer: (*C.ExtismCurrentPlugin)(ptr),
}
}
func (p *CurrentPlugin) Memory(offs uint) []byte {
length := C.extism_current_plugin_memory_length(p.pointer, C.uint64_t(offs))
data := unsafe.Pointer(C.extism_current_plugin_memory(p.pointer))
return unsafe.Slice((*byte)(unsafe.Add(data, offs)), C.int(length))
}
// Alloc a new memory block of the given length, returning its offset
func (p *CurrentPlugin) Alloc(n uint) uint {
return uint(C.extism_current_plugin_memory_alloc(p.pointer, C.uint64_t(n)))
}
// Free the memory block specified by the given offset
func (p *CurrentPlugin) Free(offs uint) {
C.extism_current_plugin_memory_free(p.pointer, C.uint64_t(offs))
}
// Length returns the number of bytes allocated at the specified offset
func (p *CurrentPlugin) Length(offs uint) uint {
return uint(C.extism_current_plugin_memory_length(p.pointer, C.uint64_t(offs)))
}
// NewContext creates a new context, it should be freed using the `Free` method
func NewContext() Context {
p := C.extism_context_new()
return Context{
pointer: p,
}
}
// Free a context
func (ctx *Context) Free() {
C.extism_context_free(ctx.pointer)
ctx.pointer = nil
}
// Plugin is used to call WASM functions
type Plugin struct {
ctx *Context
id int32
functions []Function
}
type WasmData struct {
Data []byte `json:"data"`
Hash string `json:"hash,omitempty"`
Name string `json:"name,omitempty"`
}
type WasmFile struct {
Path string `json:"path"`
Hash string `json:"hash,omitempty"`
Name string `json:"name,omitempty"`
}
type WasmUrl struct {
Url string `json:"url"`
Hash string `json:"hash,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
Name string `json:"name,omitempty"`
Method string `json:"method,omitempty"`
}
type Wasm interface{}
type Manifest struct {
Wasm []Wasm `json:"wasm"`
Memory struct {
MaxPages uint32 `json:"max_pages,omitempty"`
} `json:"memory,omitempty"`
Config map[string]string `json:"config,omitempty"`
AllowedHosts []string `json:"allowed_hosts,omitempty"`
AllowedPaths map[string]string `json:"allowed_paths,omitempty"`
Timeout uint `json:"timeout_ms,omitempty"`
}
func makePointer(data []byte) unsafe.Pointer {
var ptr unsafe.Pointer = nil
if len(data) > 0 {
ptr = unsafe.Pointer(&data[0])
}
return ptr
}
// SetLogFile sets the log file and level, this is a global setting
func SetLogFile(filename string, level string) bool {
name := C.CString(filename)
l := C.CString(level)
r := C.extism_log_file(name, l)
C.free(unsafe.Pointer(name))
C.free(unsafe.Pointer(l))
return bool(r)
}
// ExtismVersion gets the Extism version string
func ExtismVersion() string {
return C.GoString(C.extism_version())
}
func register(ctx *Context, data []byte, functions []Function, wasi bool) (Plugin, error) {
ptr := makePointer(data)
functionPointers := []*C.ExtismFunction{}
for _, f := range functions {
functionPointers = append(functionPointers, f.pointer)
}
plugin := C.int32_t(-1)
if len(functions) == 0 {
plugin = C.extism_plugin_new(
ctx.pointer,
(*C.uchar)(ptr),
C.uint64_t(len(data)),
nil,
0,
C._Bool(wasi))
} else {
plugin = C.extism_plugin_new(
ctx.pointer,
(*C.uchar)(ptr),
C.uint64_t(len(data)),
&functionPointers[0],
C.uint64_t(len(functions)),
C._Bool(wasi),
)
}
if plugin < 0 {
err := C.extism_error(ctx.pointer, C.int32_t(-1))
msg := "Unknown"
if err != nil {
msg = C.GoString(err)
}
return Plugin{id: -1}, errors.New(
fmt.Sprintf("Unable to load plugin: %s", msg),
)
}
return Plugin{id: int32(plugin), ctx: ctx, functions: functions}, nil
}
func update(ctx *Context, plugin int32, data []byte, functions []Function, wasi bool) error {
ptr := makePointer(data)
functionPointers := []*C.ExtismFunction{}
for _, f := range functions {
functionPointers = append(functionPointers, f.pointer)
}
if len(functions) == 0 {
b := bool(C.extism_plugin_update(
ctx.pointer,
C.int32_t(plugin),
(*C.uchar)(ptr),
C.uint64_t(len(data)),
nil,
0,
C._Bool(wasi),
))
if b {
return nil
}
} else {
b := bool(C.extism_plugin_update(
ctx.pointer,
C.int32_t(plugin),
(*C.uchar)(ptr),
C.uint64_t(len(data)),
&functionPointers[0],
C.uint64_t(len(functions)),
C._Bool(wasi),
))
if b {
return nil
}
}
err := C.extism_error(ctx.pointer, C.int32_t(-1))
msg := "Unknown"
if err != nil {
msg = C.GoString(err)
}
return errors.New(
fmt.Sprintf("Unable to load plugin: %s", msg),
)
}
// NewPlugin creates a plugin in its own context
func NewPlugin(module io.Reader, functions []Function, wasi bool) (Plugin, error) {
ctx := NewContext()
return ctx.Plugin(module, functions, wasi)
}
// NewPlugin creates a plugin in its own context from a manifest
func NewPluginFromManifest(manifest Manifest, functions []Function, wasi bool) (Plugin, error) {
ctx := NewContext()
return ctx.PluginFromManifest(manifest, functions, wasi)
}
// PluginFromManifest creates a plugin from a `Manifest`
func (ctx *Context) PluginFromManifest(manifest Manifest, functions []Function, wasi bool) (Plugin, error) {
data, err := json.Marshal(manifest)
if err != nil {
return Plugin{id: -1}, err
}
return register(ctx, data, functions, wasi)
}
// Plugin creates a plugin from a WASM module
func (ctx *Context) Plugin(module io.Reader, functions []Function, wasi bool) (Plugin, error) {
wasm, err := io.ReadAll(module)
if err != nil {
return Plugin{id: -1}, err
}
return register(ctx, wasm, functions, wasi)
}
// Update a plugin with a new WASM module
func (p *Plugin) Update(module io.Reader, functions []Function, wasi bool) error {
wasm, err := io.ReadAll(module)
if err != nil {
return err
}
p.functions = functions
return update(p.ctx, p.id, wasm, functions, wasi)
}
// Update a plugin with a new Manifest
func (p *Plugin) UpdateManifest(manifest Manifest, functions []Function, wasi bool) error {
data, err := json.Marshal(manifest)
if err != nil {
return err
}
p.functions = functions
return update(p.ctx, p.id, data, functions, wasi)
}
// Set configuration values
func (plugin Plugin) SetConfig(data map[string][]byte) error {
s, err := json.Marshal(data)
if err != nil {
return err
}
ptr := makePointer(s)
C.extism_plugin_config(plugin.ctx.pointer, C.int(plugin.id), (*C.uchar)(ptr), C.uint64_t(len(s)))
return nil
}
// FunctionExists returns true when the named function is present in the plugin
func (plugin Plugin) FunctionExists(functionName string) bool {
name := C.CString(functionName)
b := C.extism_plugin_function_exists(plugin.ctx.pointer, C.int(plugin.id), name)
C.free(unsafe.Pointer(name))
return bool(b)
}
// Call a function by name with the given input, returning the output
func (plugin Plugin) Call(functionName string, input []byte) ([]byte, error) {
ptr := makePointer(input)
name := C.CString(functionName)
rc := C.extism_plugin_call(
plugin.ctx.pointer,
C.int32_t(plugin.id),
name,
(*C.uchar)(ptr),
C.uint64_t(len(input)),
)
C.free(unsafe.Pointer(name))
if rc != 0 {
err := C.extism_error(plugin.ctx.pointer, C.int32_t(plugin.id))
msg := "<unset by plugin>"
if err != nil {
msg = C.GoString(err)
}
return nil, errors.New(
fmt.Sprintf("Plugin error: %s, code: %d", msg, rc),
)
}
length := C.extism_plugin_output_length(plugin.ctx.pointer, C.int32_t(plugin.id))
if length > 0 {
x := C.extism_plugin_output_data(plugin.ctx.pointer, C.int32_t(plugin.id))
return unsafe.Slice((*byte)(x), C.int(length)), nil
}
return []byte{}, nil
}
// Free a plugin
func (plugin *Plugin) Free() {
if plugin.ctx.pointer == nil {
return
}
C.extism_plugin_free(plugin.ctx.pointer, C.int32_t(plugin.id))
plugin.id = -1
}
// Reset removes all registered plugins in a Context
func (ctx Context) Reset() {
C.extism_context_reset(ctx.pointer)
}
// ValGetI64 returns an I64 from an ExtismVal, it accepts a pointer to a C.ExtismVal
func ValGetI64(v unsafe.Pointer) int64 {
return int64(C.extism_val_i64(&(*Val)(v).v))
}
// ValGetUInt returns a uint from an ExtismVal, it accepts a pointer to a C.ExtismVal
func ValGetUInt(v unsafe.Pointer) uint {
return uint(C.extism_val_i64(&(*Val)(v).v))
}
// ValGetI32 returns an int32 from an ExtismVal, it accepts a pointer to a C.ExtismVal
func ValGetI32(v unsafe.Pointer) int32 {
return int32(C.extism_val_i32(&(*Val)(v).v))
}
// ValGetF32 returns a float32 from an ExtismVal, it accepts a pointer to a C.ExtismVal
func ValGetF32(v unsafe.Pointer) float32 {
return float32(C.extism_val_f32(&(*Val)(v).v))
}
// ValGetF32 returns a float64 from an ExtismVal, it accepts a pointer to a C.ExtismVal
func ValGetF64(v unsafe.Pointer) float64 {
return float64(C.extism_val_i64(&(*Val)(v).v))
}
// ValSetI64 stores an int64 in an ExtismVal, it accepts a pointer to a C.ExtismVal and the new value
func ValSetI64(v unsafe.Pointer, i int64) {
C.extism_val_set_i64(&(*Val)(v).v, C.int64_t(i))
}
// ValSetI32 stores an int32 in an ExtismVal, it accepts a pointer to a C.ExtismVal and the new value
func ValSetI32(v unsafe.Pointer, i int32) {
C.extism_val_set_i32(&(*Val)(v).v, C.int32_t(i))
}
// ValSetF32 stores a float32 in an ExtismVal, it accepts a pointer to a C.ExtismVal and the new value
func ValSetF32(v unsafe.Pointer, i float32) {
C.extism_val_set_f32(&(*Val)(v).v, C.float(i))
}
// ValSetF64 stores a float64 in an ExtismVal, it accepts a pointer to a C.ExtismVal and the new value
func ValSetF64(v unsafe.Pointer, f float64) {
C.extism_val_set_f64(&(*Val)(v).v, C.double(f))
}
func (p *CurrentPlugin) ReturnBytes(v unsafe.Pointer, b []byte) {
mem := p.Alloc(uint(len(b)))
ptr := p.Memory(mem)
copy(ptr, b)
ValSetI64(v, int64(mem))
}
func (p *CurrentPlugin) ReturnString(v unsafe.Pointer, s string) {
p.ReturnBytes(v, []byte(s))
}
func (p *CurrentPlugin) InputBytes(v unsafe.Pointer) []byte {
return p.Memory(ValGetUInt(v))
}
func (p *CurrentPlugin) InputString(v unsafe.Pointer) string {
return string(p.InputBytes(v))
}
type CancelHandle struct {
pointer *C.ExtismCancelHandle
}
func (p *Plugin) CancelHandle() CancelHandle {
pointer := C.extism_plugin_cancel_handle(p.ctx.pointer, C.int(p.id))
return CancelHandle{pointer}
}
func (c *CancelHandle) Cancel() bool {
return bool(C.extism_plugin_cancel(c.pointer))
}