feat: Add C API for host functions + support for C++, Python, Go, Node, OCaml (#195)

- New types:
  - `ExtismValType` - Enum of WebAssembly types
  - `ExtismValUnion` - A union of the possible WebAssembly types
  - `ExtismVal` - A struct with `ExtismValType` and `ExtismValUnion`
  - `ExtismFunction` - The host function wrapper type
  - `ExtismFunctionType` - The type of the host function callback
- `ExtismCurrentPlugin` - Provides access to the currently running
plugin from inside a host function

- New functions:
  - `extism_function_new` - Create a new `ExtismFunction`
  - `extism_function_free` - Free an `ExtismFunction`
- `extism_current_plugin_memory`, `extism_current_plugin_memory_alloc`,
`extism_current_plugin_memory_free`,
`extism_current_plugin_memory_length` - Manage plugin memory from inside
a host functions

- Updated functions
- `extism_plugin_new` and `extsim_plugin_update` - now accept two extra
parameters for `ExtismFunction*` array and length of that array

## Notes

- Host functions take a user-data argument, which is owned by the
resulting `ExtismFunction` and will be cleaned up when
`extism_function_free` is called (if a cleanup function was passed in
with the user data)
- Host functions in every SDK require working with `ExtismVal` arguments
directly, this is pretty low-level for what is kind of a high-level
feature. We could work on adding some types to the SDKs that make
working with pointers to plugin data more accessible, maybe something
similar to how the Rust PDK handes input/output data.
- In each language the host functions more-or-less share a signature:
`(CurrentPlugin plugin, Val inputs[], Val outputs[], userData)`
- C, C++, OCaml and Go take a single userData argument but Python and
Node take a "rest" argument which allows passing any number of user-data
values
- Go requires the host function to be exported:
f9eb5ed839/go/main.go (L13-L26)
- Zig and Ruby should be relatively simple to add host functions to next
but I haven't really looked into Elixir, .NET or Java yet.
- Also closes #20
This commit is contained in:
zach
2023-01-10 12:04:40 -08:00
committed by GitHub
parent cfb1317261
commit dc3d54e260
54 changed files with 2401 additions and 3449 deletions

View File

@@ -18,15 +18,12 @@ else
FEATURE_FLAGS=--features $(FEATURES)
endif
.PHONY: build
build:
cargo build --release $(FEATURE_FLAGS) --manifest-path libextism/Cargo.toml
lint:
cargo clippy --release --no-deps --manifest-path runtime/Cargo.toml
build:
cargo build --release $(FEATURE_FLAGS) --manifest-path libextism/Cargo.toml
debug:
RUSTFLAGS=-g $(MAKE) build

View File

@@ -9,6 +9,21 @@
#include <sys/stat.h>
#include <unistd.h>
void hello_world(ExtismCurrentPlugin *plugin, const struct ExtismVal *inputs,
uint64_t n_inputs, struct ExtismVal *outputs,
uint64_t n_outputs, void *data) {
puts("Hello from C!");
puts(data);
ExtismSize ptr_offs = inputs[0].v.i64;
uint8_t *buf = extism_current_plugin_memory(plugin) + ptr_offs;
uint64_t length = extism_current_plugin_memory_length(plugin, ptr_offs);
fwrite(buf, length, 1, stdout);
fputc('\n', stdout);
outputs[0].v.i64 = inputs[0].v.i64;
}
uint8_t *read_file(const char *filename, size_t *len) {
FILE *fp = fopen(filename, "rb");
@@ -41,13 +56,17 @@ int main(int argc, char *argv[]) {
ExtismContext *ctx = extism_context_new();
size_t len = 0;
uint8_t *data = read_file("../wasm/code.wasm", &len);
ExtismPlugin plugin = extism_plugin_new(ctx, data, len, false);
uint8_t *data = read_file("../wasm/code-functions.wasm", &len);
ExtismValType inputs[] = {I64};
ExtismValType outputs[] = {I64};
ExtismFunction *f = extism_function_new("hello_world", inputs, 1, outputs, 1,
hello_world, "Hello, again!", NULL);
ExtismPlugin plugin = extism_plugin_new(ctx, data, len, &f, 1, true);
free(data);
if (plugin < 0) {
puts(extism_error(ctx, -1));
exit(1);
}
assert(extism_plugin_call(ctx, plugin, "count_vowels", (uint8_t *)argv[1],
strlen(argv[1])) == 0);
ExtismSize out_len = extism_plugin_output_length(ctx, plugin);
@@ -56,6 +75,7 @@ int main(int argc, char *argv[]) {
write(STDOUT_FILENO, "\n", 1);
extism_plugin_free(ctx, plugin);
extism_function_free(f);
extism_context_free(ctx);
return 0;
}

View File

@@ -1,14 +1,14 @@
FLAGS=`pkg-config --cflags --libs jsoncpp gtest` -lextism -lpthread
build-example:
$(CXX) -std=c++11 -o example -I. example.cpp $(FLAGS)
$(CXX) -std=c++14 -o example -I. example.cpp $(FLAGS)
.PHONY: example
example: build-example
./example
build-test:
$(CXX) -std=c++11 -o test/test -I. test/test.cpp $(FLAGS)
$(CXX) -std=c++14 -o test/test -I. test/test.cpp $(FLAGS)
.PHONY: test
test: build-test

View File

@@ -14,10 +14,27 @@ std::vector<uint8_t> read(const char *filename) {
}
int main(int argc, char *argv[]) {
auto wasm = read("../wasm/code.wasm");
auto wasm = read("../wasm/code-functions.wasm");
Context context = Context();
std::string tmp = "Testing";
Plugin plugin = context.plugin(wasm);
// A lambda can be used as a host function
auto hello_world = [&tmp](CurrentPlugin plugin,
const std::vector<Val> &inputs,
std::vector<Val> &outputs, void *user_data) {
std::cout << "Hello from C++" << std::endl;
std::cout << (const char *)user_data << std::endl;
std::cout << tmp << std::endl;
outputs[0].v = inputs[0].v;
};
std::vector<Function> functions = {
Function("hello_world", {ValType::I64}, {ValType::I64}, hello_world,
(void *)"Hello again!",
[](void *x) { std::cout << "Free user data" << std::endl; }),
};
Plugin plugin = context.plugin(wasm, true, functions);
const char *input = argc > 1 ? argv[1] : "this is a test";
ExtismSize length = strlen(input);

View File

@@ -1,7 +1,9 @@
#pragma once
#include <functional>
#include <map>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
@@ -20,27 +22,62 @@ extern "C" {
namespace extism {
typedef std::map<std::string, std::string> Config;
class Wasm {
template <typename T> class ManifestKey {
bool is_set = false;
public:
std::string path;
std::string url;
T value;
ManifestKey(T x, bool is_set = false) : is_set(is_set) { value = x; }
void set(T x) {
value = x;
is_set = true;
}
bool empty() const { return is_set == false; }
};
class Wasm {
std::string _path;
std::string _url;
// TODO: add base64 encoded raw data
std::string hash;
ManifestKey<std::string> _hash =
ManifestKey<std::string>(std::string(), false);
public:
// Create Wasm pointing to a path
static Wasm path(std::string s, std::string hash = std::string()) {
Wasm w;
w._path = s;
if (!hash.empty()) {
w._hash.set(hash);
}
return w;
}
// Create Wasm pointing to a URL
static Wasm url(std::string s, std::string hash = std::string()) {
Wasm w;
w._url = s;
if (!hash.empty()) {
w._hash.set(hash);
}
return w;
}
#ifndef EXTISM_NO_JSON
Json::Value json() const {
Json::Value doc;
if (!this->path.empty()) {
doc["path"] = this->path;
if (!this->_path.empty()) {
doc["path"] = this->_path;
} else if (!this->_url.empty()) {
doc["url"] = this->_url;
}
if (!this->url.empty()) {
doc["url"] = this->url;
}
if (!this->hash.empty()) {
doc["hash"] = this->hash;
if (!this->_hash.empty()) {
doc["hash"] = this->_hash.value;
}
return doc;
@@ -52,18 +89,23 @@ class Manifest {
public:
Config config;
std::vector<Wasm> wasm;
std::vector<std::string> allowed_hosts;
std::map<std::string, std::string> allowed_paths;
uint64_t timeout_ms;
ManifestKey<std::vector<std::string>> allowed_hosts;
ManifestKey<std::map<std::string, std::string>> allowed_paths;
ManifestKey<uint64_t> timeout_ms;
Manifest() : timeout_ms(30000) {}
// Empty manifest
Manifest()
: timeout_ms(0, false), allowed_hosts(std::vector<std::string>(), false),
allowed_paths(std::map<std::string, std::string>(), false) {}
// Create manifest with a single Wasm from a path
static Manifest path(std::string s, std::string hash = std::string()) {
Manifest m;
m.add_wasm_path(s, hash);
return m;
}
// Create manifest with a single Wasm from a URL
static Manifest url(std::string s, std::string hash = std::string()) {
Manifest m;
m.add_wasm_url(s, hash);
@@ -92,7 +134,7 @@ public:
if (!this->allowed_hosts.empty()) {
Json::Value h;
for (auto s : this->allowed_hosts) {
for (auto s : this->allowed_hosts.value) {
h.append(s);
}
doc["allowed_hosts"] = h;
@@ -100,54 +142,63 @@ public:
if (!this->allowed_paths.empty()) {
Json::Value h;
for (auto k : this->allowed_paths) {
for (auto k : this->allowed_paths.value) {
h[k.first] = k.second;
}
doc["allowed_paths"] = h;
}
doc["timeout_ms"] = Json::Value(this->timeout_ms);
if (!this->timeout_ms.empty()) {
doc["timeout_ms"] = Json::Value(this->timeout_ms.value);
}
Json::FastWriter writer;
return writer.write(doc);
}
#endif
// Add Wasm from path
void add_wasm_path(std::string s, std::string hash = std::string()) {
Wasm w;
w.path = s;
w.hash = hash;
Wasm w = Wasm::path(s, hash);
this->wasm.push_back(w);
}
// Add Wasm from URL
void add_wasm_url(std::string u, std::string hash = std::string()) {
Wasm w;
w.url = u;
w.hash = hash;
Wasm w = Wasm::url(u, hash);
this->wasm.push_back(w);
}
void allow_host(std::string host) { this->allowed_hosts.push_back(host); }
// Add host to allowed hosts
void allow_host(std::string host) {
if (this->allowed_hosts.empty()) {
this->allowed_hosts.set(std::vector<std::string>{});
}
this->allowed_hosts.value.push_back(host);
}
// Add path to allowed paths
void allow_path(std::string src, std::string dest = std::string()) {
if (this->allowed_paths.empty()) {
this->allowed_paths.set(std::map<std::string, std::string>{});
}
if (dest.empty()) {
dest = src;
}
this->allowed_paths[src] = dest;
this->allowed_paths.value[src] = dest;
}
// Set timeout
void set_timeout_ms(uint64_t ms) { this->timeout_ms = ms; }
// Set config key/value
void set_config(std::string k, std::string v) { this->config[k] = v; }
};
class Error : public std::exception {
private:
std::string message;
class Error : public std::runtime_error {
public:
Error(std::string msg) : message(msg) {}
const char *what() { return message.c_str(); }
Error(std::string msg) : std::runtime_error(msg) {}
};
class Buffer {
@@ -166,14 +217,102 @@ public:
}
};
typedef ExtismValType ValType;
typedef ExtismValUnion ValUnion;
typedef ExtismVal Val;
class CurrentPlugin {
ExtismCurrentPlugin *pointer;
public:
CurrentPlugin(ExtismCurrentPlugin *p) : pointer(p) {}
uint8_t *memory() { return extism_current_plugin_memory(this->pointer); }
ExtismSize memory_length(uint64_t offs) {
return extism_current_plugin_memory_length(this->pointer, offs);
}
uint64_t alloc(ExtismSize size) {
return extism_current_plugin_memory_alloc(this->pointer, size);
}
void free(uint64_t offs) {
extism_current_plugin_memory_free(this->pointer, offs);
}
};
typedef std::function<void(CurrentPlugin, const std::vector<Val> &,
std::vector<Val> &, void *user_data)>
FunctionType;
struct UserData {
FunctionType func;
void *user_data = NULL;
std::function<void(void *)> free_user_data;
};
static void function_callback(ExtismCurrentPlugin *plugin,
const ExtismVal *inputs, ExtismSize n_inputs,
ExtismVal *outputs, ExtismSize n_outputs,
void *user_data) {
UserData *data = (UserData *)user_data;
const std::vector<Val> inp(inputs, inputs + n_inputs);
std::vector<Val> outp(outputs, outputs + n_outputs);
data->func(CurrentPlugin(plugin), inp, outp, data->user_data);
for (ExtismSize i = 0; i < n_outputs; i++) {
outputs[i] = outp[i];
}
}
static void free_user_data(void *user_data) {
UserData *data = (UserData *)user_data;
if (data->user_data != NULL && data->free_user_data != NULL) {
data->free_user_data(data->user_data);
}
}
class Function {
std::shared_ptr<ExtismFunction> func;
std::string name;
UserData user_data;
public:
Function(std::string name, const std::vector<ValType> inputs,
const std::vector<ValType> outputs, FunctionType f,
void *user_data = NULL, std::function<void(void *)> free = nullptr)
: name(name) {
this->user_data.func = f;
this->user_data.user_data = user_data;
this->user_data.free_user_data = free;
auto ptr = extism_function_new(
this->name.c_str(), inputs.data(), inputs.size(), outputs.data(),
outputs.size(), function_callback, &this->user_data, free_user_data);
this->func = std::shared_ptr<ExtismFunction>(ptr, extism_function_free);
}
Function(const Function &f) { this->func = f.func; }
ExtismFunction *get() { return this->func.get(); }
};
class Plugin {
std::shared_ptr<ExtismContext> context;
ExtismPlugin plugin;
std::vector<Function> functions;
public:
// Create a new plugin
Plugin(std::shared_ptr<ExtismContext> ctx, const uint8_t *wasm,
ExtismSize length, bool with_wasi = false) {
this->plugin = extism_plugin_new(ctx.get(), wasm, length, with_wasi);
ExtismSize length, bool with_wasi = false,
std::vector<Function> functions = std::vector<Function>())
: functions(functions) {
std::vector<const ExtismFunction *> ptrs;
for (auto i : this->functions) {
ptrs.push_back(i.get());
}
this->plugin = extism_plugin_new(ctx.get(), wasm, length, ptrs.data(),
ptrs.size(), with_wasi);
if (this->plugin < 0) {
const char *err = extism_error(ctx.get(), -1);
throw Error(err == nullptr ? "Unable to load plugin" : err);
@@ -182,11 +321,18 @@ public:
}
#ifndef EXTISM_NO_JSON
// Create a new plugin from Manifest
Plugin(std::shared_ptr<ExtismContext> ctx, const Manifest &manifest,
bool with_wasi = false) {
bool with_wasi = false, std::vector<Function> functions = {}) {
std::vector<const ExtismFunction *> ptrs;
for (auto i : this->functions) {
ptrs.push_back(i.get());
}
auto buffer = manifest.json();
this->plugin = extism_plugin_new(ctx.get(), (const uint8_t *)buffer.c_str(),
buffer.size(), with_wasi);
this->plugin =
extism_plugin_new(ctx.get(), (const uint8_t *)buffer.c_str(),
buffer.size(), ptrs.data(), ptrs.size(), with_wasi);
if (this->plugin < 0) {
const char *err = extism_error(ctx.get(), -1);
throw Error(err == nullptr ? "Unable to load plugin from manifest" : err);
@@ -204,9 +350,15 @@ public:
ExtismContext *get_context() const { return this->context.get(); }
void update(const uint8_t *wasm, size_t length, bool with_wasi = false) {
void update(const uint8_t *wasm, size_t length, bool with_wasi = false,
std::vector<Function> functions = {}) {
this->functions = functions;
std::vector<const ExtismFunction *> ptrs;
for (auto i : this->functions) {
ptrs.push_back(i.get());
}
bool b = extism_plugin_update(this->context.get(), this->plugin, wasm,
length, with_wasi);
length, ptrs.data(), ptrs.size(), with_wasi);
if (!b) {
const char *err = extism_error(this->context.get(), -1);
throw Error(err == nullptr ? "Unable to update plugin" : err);
@@ -214,11 +366,17 @@ public:
}
#ifndef EXTISM_NO_JSON
void update(const Manifest &manifest, bool with_wasi = false) {
void update(const Manifest &manifest, bool with_wasi = false,
std::vector<Function> functions = {}) {
this->functions = functions;
std::vector<const ExtismFunction *> ptrs;
for (auto i : this->functions) {
ptrs.push_back(i.get());
}
auto buffer = manifest.json();
bool b = extism_plugin_update(this->context.get(), this->plugin,
(const uint8_t *)buffer.c_str(),
buffer.size(), with_wasi);
bool b = extism_plugin_update(
this->context.get(), this->plugin, (const uint8_t *)buffer.c_str(),
buffer.size(), ptrs.data(), ptrs.size(), with_wasi);
if (!b) {
const char *err = extism_error(this->context.get(), -1);
throw Error(err == nullptr ? "Unable to update plugin" : err);
@@ -251,6 +409,7 @@ public:
this->config(json.c_str(), json.size());
}
// Call a plugin
Buffer call(const std::string &func, const uint8_t *input,
ExtismSize input_length) const {
int32_t rc = extism_plugin_call(this->context.get(), this->plugin,
@@ -271,15 +430,19 @@ public:
return Buffer(ptr, length);
}
// Call a plugin function with std::vector<uint8_t> input
Buffer call(const std::string &func,
const std::vector<uint8_t> &input) const {
return this->call(func, input.data(), input.size());
}
Buffer call(const std::string &func, const std::string &input) const {
// Call a plugin function with string input
Buffer call(const std::string &func,
const std::string &input = std::string()) const {
return this->call(func, (const uint8_t *)input.c_str(), input.size());
}
// Returns true if the specified function exists
bool function_exists(const std::string &func) const {
return extism_plugin_function_exists(this->context.get(), this->plugin,
func.c_str());
@@ -290,38 +453,49 @@ class Context {
public:
std::shared_ptr<ExtismContext> pointer;
// Create a new context;
Context() {
this->pointer = std::shared_ptr<ExtismContext>(extism_context_new(),
extism_context_free);
}
Plugin plugin(const uint8_t *wasm, size_t length,
bool with_wasi = false) const {
return Plugin(this->pointer, wasm, length, with_wasi);
// Create plugin from uint8_t*
Plugin plugin(const uint8_t *wasm, size_t length, bool with_wasi = false,
std::vector<Function> functions = {}) const {
return Plugin(this->pointer, wasm, length, with_wasi, functions);
}
Plugin plugin(const std::string &str, bool with_wasi = false) const {
// Create plugin from std::string
Plugin plugin(const std::string &str, bool with_wasi = false,
std::vector<Function> functions = {}) const {
return Plugin(this->pointer, (const uint8_t *)str.c_str(), str.size(),
with_wasi);
with_wasi, functions);
}
Plugin plugin(const std::vector<uint8_t> &data,
bool with_wasi = false) const {
return Plugin(this->pointer, data.data(), data.size(), with_wasi);
// Create plugin from uint8_t vector
Plugin plugin(const std::vector<uint8_t> &data, bool with_wasi = false,
std::vector<Function> functions = {}) const {
return Plugin(this->pointer, data.data(), data.size(), with_wasi,
functions);
}
#ifndef EXTISM_NO_JSON
Plugin plugin(const Manifest &manifest, bool with_wasi = false) const {
return Plugin(this->pointer, manifest, with_wasi);
// Create plugin from Manifest
Plugin plugin(const Manifest &manifest, bool with_wasi = false,
std::vector<Function> functions = {}) const {
return Plugin(this->pointer, manifest, with_wasi, functions);
}
#endif
// Remove all plugins
void reset() { extism_context_reset(this->pointer.get()); }
};
// Set global log file for plugins
inline bool set_log_file(const char *filename, const char *level) {
return extism_log_file(filename, level);
}
// Get libextism version
inline std::string version() { return std::string(extism_version()); }
} // namespace extism

View File

@@ -38,7 +38,7 @@ public class Context : IDisposable
{
fixed (byte* wasmPtr = wasm)
{
var plugin = LibExtism.extism_plugin_new(NativeHandle, wasmPtr, wasm.Length, withWasi);
var plugin = LibExtism.extism_plugin_new(NativeHandle, wasmPtr, wasm.Length, null, 0, withWasi);
return new Plugin(this, plugin);
}
}

View File

@@ -27,10 +27,12 @@ internal static class LibExtism
/// <param name="context">Pointer to the context the plugin will be associated with.</param>
/// <param name="wasm">A WASM module (wat or wasm) or a JSON encoded manifest.</param>
/// <param name="wasmSize">The length of the `wasm` parameter.</param>
/// <param name="functions">Array of host function pointers.</param>
/// <param name="nFunctions">Number of host functions.</param>
/// <param name="withWasi">Enables/disables WASI.</param>
/// <returns></returns>
[DllImport("extism")]
unsafe public static extern IntPtr extism_plugin_new(IntPtr context, byte* wasm, int wasmSize, bool withWasi);
unsafe public static extern IntPtr extism_plugin_new(IntPtr context, byte* wasm, int wasmSize, IntPtr *functions, int nFunctions, bool withWasi);
/// <summary>
/// Update a plugin, keeping the existing ID.
@@ -41,10 +43,12 @@ internal static class LibExtism
/// <param name="plugin">Pointer to the plugin you want to update.</param>
/// <param name="wasm">A WASM module (wat or wasm) or a JSON encoded manifest.</param>
/// <param name="wasmLength">The length of the `wasm` parameter.</param>
/// <param name="functions">Array of host function pointers.</param>
/// <param name="nFunctions">Number of host functions.</param>
/// <param name="withWasi">Enables/disables WASI.</param>
/// <returns></returns>
[DllImport("extism")]
unsafe public static extern bool extism_plugin_update(IntPtr context, IntPtr plugin, byte* wasm, int wasmLength, bool withWasi);
unsafe public static extern bool extism_plugin_update(IntPtr context, IntPtr plugin, byte* wasm, int wasmLength, IntPtr *functions, int nFunctions, bool withWasi);
/// <summary>
/// Remove a plugin from the registry and free associated memory.

View File

@@ -35,7 +35,7 @@ public class Plugin : IDisposable
fixed (byte* wasmPtr = wasm)
{
return LibExtism.extism_plugin_update(_context.NativeHandle, NativeHandle, wasmPtr, wasm.Length, withWasi);
return LibExtism.extism_plugin_update(_context.NativeHandle, NativeHandle, wasmPtr, wasm.Length, null, 0, withWasi);
}
}

View File

@@ -27,12 +27,7 @@ fn load(env: Env, _: Term) -> bool {
}
fn to_rustler_error(extism_error: extism::Error) -> rustler::Error {
match extism_error {
extism::Error::UnableToLoadPlugin(msg) => rustler::Error::Term(Box::new(msg)),
extism::Error::Message(msg) => rustler::Error::Term(Box::new(msg)),
extism::Error::Json(json_err) => rustler::Error::Term(Box::new(json_err.to_string())),
extism::Error::Runtime(e) => rustler::Error::Term(Box::new(e.to_string())),
}
rustler::Error::Term(Box::new(extism_error.to_string()))
}
#[rustler::nif]
@@ -61,7 +56,7 @@ fn plugin_new_with_manifest(
wasi: bool,
) -> Result<i32, rustler::Error> {
let context = &ctx.ctx.write().unwrap();
let result = match Plugin::new(context, manifest_payload, wasi) {
let result = match Plugin::new(context, manifest_payload, [], wasi) {
Err(e) => Err(to_rustler_error(e)),
Ok(plugin) => {
let plugin_id = plugin.as_i32();
@@ -107,7 +102,7 @@ fn plugin_update_manifest(
) -> Result<(), rustler::Error> {
let context = &ctx.ctx.read().unwrap();
let mut plugin = unsafe { Plugin::from_id(plugin_id, context) };
let result = match plugin.update(manifest_payload, wasi) {
let result = match plugin.update(manifest_payload, [], wasi) {
Ok(()) => Ok(()),
Err(e) => Err(to_rustler_error(e)),
};

160
extism.go
View File

@@ -9,7 +9,8 @@ import (
)
/*
#cgo pkg-config: libextism.pc
#cgo CFLAGS: -I/usr/local/include
#cgo LDFLAGS: -L/usr/local/lib -lextism
#include <extism.h>
#include <stdlib.h>
*/
@@ -20,6 +21,69 @@ 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
FuncRef ValType = C.FuncRef
ExternRef ValType = C.ExternRef
)
// Function is used to define host functions
type Function struct {
pointer *C.ExtismFunction
userData interface{}
}
// Free a function
func (f *Function) Free() {
C.extism_function_free(f.pointer)
f.pointer = nil
}
// 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 = userData
cname := C.CString(name)
function.pointer = C.extism_function_new(
cname,
(*C.ExtismValType)(&inputs[0]),
C.uint64_t(len(inputs)),
(*C.ExtismValType)(&outputs[0]),
C.uint64_t(len(outputs)),
(*[0]byte)(f),
unsafe.Pointer(&function.userData),
nil,
)
C.free(unsafe.Pointer(cname))
return function
}
type CurrentPlugin struct {
pointer *C.ExtismCurrentPlugin
}
func GetCurrentPlugin(ptr *C.ExtismCurrentPlugin) CurrentPlugin {
return CurrentPlugin{
pointer: 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))
}
// NewContext creates a new context, it should be freed using the `Free` method
func NewContext() Context {
p := C.extism_context_new()
@@ -96,14 +160,32 @@ func ExtismVersion() string {
return C.GoString(C.extism_version())
}
func register(ctx *Context, data []byte, wasi bool) (Plugin, error) {
func register(ctx *Context, data []byte, functions []Function, wasi bool) (Plugin, error) {
ptr := makePointer(data)
plugin := C.extism_plugin_new(
ctx.pointer,
(*C.uchar)(ptr),
C.uint64_t(len(data)),
C._Bool(wasi),
)
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))
@@ -120,18 +202,41 @@ func register(ctx *Context, data []byte, wasi bool) (Plugin, error) {
return Plugin{id: int32(plugin), ctx: ctx}, nil
}
func update(ctx *Context, plugin int32, data []byte, wasi bool) error {
func update(ctx *Context, plugin int32, data []byte, functions []Function, wasi bool) error {
ptr := makePointer(data)
b := bool(C.extism_plugin_update(
ctx.pointer,
C.int32_t(plugin),
(*C.uchar)(ptr),
C.uint64_t(len(data)),
C._Bool(wasi),
))
functionPointers := []*C.ExtismFunction{}
for _, f := range functions {
functionPointers = append(functionPointers, f.pointer)
}
if b {
return nil
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))
@@ -146,43 +251,43 @@ func update(ctx *Context, plugin int32, data []byte, wasi bool) error {
}
// PluginFromManifest creates a plugin from a `Manifest`
func (ctx *Context) PluginFromManifest(manifest Manifest, wasi bool) (Plugin, error) {
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, wasi)
return register(ctx, data, functions, wasi)
}
// Plugin creates a plugin from a WASM module
func (ctx *Context) Plugin(module io.Reader, wasi bool) (Plugin, error) {
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, wasi)
return register(ctx, wasm, functions, wasi)
}
// Update a plugin with a new WASM module
func (p *Plugin) Update(module io.Reader, wasi bool) error {
func (p *Plugin) Update(module io.Reader, functions []Function, wasi bool) error {
wasm, err := io.ReadAll(module)
if err != nil {
return err
}
return update(p.ctx, p.id, wasm, wasi)
return update(p.ctx, p.id, wasm, functions, wasi)
}
// Update a plugin with a new Manifest
func (p *Plugin) UpdateManifest(manifest Manifest, wasi bool) error {
func (p *Plugin) UpdateManifest(manifest Manifest, functions []Function, wasi bool) error {
data, err := json.Marshal(manifest)
if err != nil {
return err
}
return update(p.ctx, p.id, data, wasi)
return update(p.ctx, p.id, data, functions, wasi)
}
// Set configuration values
@@ -233,8 +338,7 @@ func (plugin Plugin) Call(functionName string, input []byte) ([]byte, error) {
if length > 0 {
x := C.extism_plugin_output_data(plugin.ctx.pointer, C.int32_t(plugin.id))
y := (*[]byte)(unsafe.Pointer(&x))
return []byte((*y)[0:length]), nil
return unsafe.Slice((*byte)(x), C.int(length)), nil
}
return []byte{}, nil

View File

@@ -4,10 +4,27 @@ import (
"encoding/json"
"fmt"
"os"
"unsafe"
"github.com/extism/extism"
)
/*
#include <extism.h>
EXTISM_GO_FUNCTION(hello_world);
*/
import "C"
//export hello_world
func hello_world(plugin *C.ExtismCurrentPlugin, inputs *C.ExtismVal, nInputs C.ExtismSize, outputs *C.ExtismVal, nOutputs C.ExtismSize, userData unsafe.Pointer) {
fmt.Println("Hello from Go!")
s := *(*interface{})(userData)
fmt.Println(s.(string))
inputSlice := unsafe.Slice(inputs, nInputs)
outputSlice := unsafe.Slice(outputs, nOutputs)
outputSlice[0] = inputSlice[0]
}
func main() {
version := extism.ExtismVersion()
fmt.Println("Extism Version: ", version)
@@ -22,9 +39,10 @@ func main() {
} else {
data = []byte("testing from go -> wasm shared memory...")
}
manifest := extism.Manifest{Wasm: []extism.Wasm{extism.WasmFile{Path: "../wasm/code.wasm"}}}
plugin, err := ctx.PluginFromManifest(manifest, false)
manifest := extism.Manifest{Wasm: []extism.Wasm{extism.WasmFile{Path: "../wasm/code-functions.wasm"}}}
f := extism.NewFunction("hello_world", []extism.ValType{extism.I64}, []extism.ValType{extism.I64}, C.hello_world, "Hello again!")
defer f.Free()
plugin, err := ctx.PluginFromManifest(manifest, []extism.Function{f}, true)
if err != nil {
fmt.Println(err)
os.Exit(1)

View File

@@ -70,7 +70,7 @@ plugin c wasm useWasi =
do
withForeignPtr ctx (\ctx -> do
p <- unsafeUseAsCString wasm (\s ->
extism_plugin_new ctx (castPtr s) length wasi)
extism_plugin_new ctx (castPtr s) length nullPtr 0 wasi )
if p < 0 then do
err <- extism_error ctx (-1)
e <- peekCString err
@@ -92,7 +92,7 @@ update (Plugin (Context ctx) id) wasm useWasi =
do
withForeignPtr ctx (\ctx -> do
b <- unsafeUseAsCString wasm (\s ->
extism_plugin_update ctx id (castPtr s) length wasi)
extism_plugin_update ctx id (castPtr s) length nullPtr 0 wasi)
if b <= 0 then do
err <- extism_error ctx (-1)
e <- peekCString err

View File

@@ -9,18 +9,19 @@ import Data.Int
import Data.Word
newtype ExtismContext = ExtismContext () deriving Show
newtype ExtismFunction = ExtismFunction () deriving Show
foreign import ccall unsafe "extism.h extism_context_new" extism_context_new :: IO (Ptr ExtismContext)
foreign import ccall unsafe "extism.h &extism_context_free" extism_context_free :: FunPtr (Ptr ExtismContext -> IO ())
foreign import ccall unsafe "extism.h extism_plugin_new" extism_plugin_new :: Ptr ExtismContext -> Ptr Word8 -> Word64 -> CBool -> IO Int32
foreign import ccall unsafe "extism.h extism_plugin_update" extism_plugin_update :: Ptr ExtismContext -> Int32 -> Ptr Word8 -> Word64 -> CBool -> IO CBool
foreign import ccall unsafe "extism.h extism_plugin_call" extism_plugin_call :: Ptr ExtismContext -> Int32 -> CString -> Ptr Word8 -> Word64 -> IO Int32
foreign import ccall unsafe "extism.h extism_plugin_function_exists" extism_plugin_function_exists :: Ptr ExtismContext -> Int32 -> CString -> IO CBool
foreign import ccall unsafe "extism.h extism_error" extism_error :: Ptr ExtismContext -> Int32 -> IO CString
foreign import ccall unsafe "extism.h extism_plugin_output_length" extism_plugin_output_length :: Ptr ExtismContext -> Int32 -> IO Word64
foreign import ccall unsafe "extism.h extism_plugin_output_data" extism_plugin_output_data :: Ptr ExtismContext -> Int32 -> IO (Ptr Word8)
foreign import ccall unsafe "extism.h extism_log_file" extism_log_file :: CString -> CString -> IO CBool
foreign import ccall unsafe "extism.h extism_plugin_config" extism_plugin_config :: Ptr ExtismContext -> Int32 -> Ptr Word8 -> Int64 -> IO CBool
foreign import ccall unsafe "extism.h extism_plugin_free" extism_plugin_free :: Ptr ExtismContext -> Int32 -> IO ()
foreign import ccall unsafe "extism.h extism_context_reset" extism_context_reset :: Ptr ExtismContext -> IO ()
foreign import ccall unsafe "extism.h extism_version" extism_version :: IO CString
foreign import ccall safe "extism.h extism_context_new" extism_context_new :: IO (Ptr ExtismContext)
foreign import ccall safe "extism.h &extism_context_free" extism_context_free :: FunPtr (Ptr ExtismContext -> IO ())
foreign import ccall safe "extism.h extism_plugin_new" extism_plugin_new :: Ptr ExtismContext -> Ptr Word8 -> Word64 -> Ptr (Ptr ExtismFunction) -> Word64 -> CBool -> IO Int32
foreign import ccall safe "extism.h extism_plugin_update" extism_plugin_update :: Ptr ExtismContext -> Int32 -> Ptr Word8 -> Word64 -> Ptr (Ptr ExtismFunction) -> Word64 -> CBool -> IO CBool
foreign import ccall safe "extism.h extism_plugin_call" extism_plugin_call :: Ptr ExtismContext -> Int32 -> CString -> Ptr Word8 -> Word64 -> IO Int32
foreign import ccall safe "extism.h extism_plugin_function_exists" extism_plugin_function_exists :: Ptr ExtismContext -> Int32 -> CString -> IO CBool
foreign import ccall safe "extism.h extism_error" extism_error :: Ptr ExtismContext -> Int32 -> IO CString
foreign import ccall safe "extism.h extism_plugin_output_length" extism_plugin_output_length :: Ptr ExtismContext -> Int32 -> IO Word64
foreign import ccall safe "extism.h extism_plugin_output_data" extism_plugin_output_data :: Ptr ExtismContext -> Int32 -> IO (Ptr Word8)
foreign import ccall safe "extism.h extism_log_file" extism_log_file :: CString -> CString -> IO CBool
foreign import ccall safe "extism.h extism_plugin_config" extism_plugin_config :: Ptr ExtismContext -> Int32 -> Ptr Word8 -> Int64 -> IO CBool
foreign import ccall safe "extism.h extism_plugin_free" extism_plugin_free :: Ptr ExtismContext -> Int32 -> IO ()
foreign import ccall safe "extism.h extism_context_reset" extism_context_reset :: Ptr ExtismContext -> IO ()
foreign import ccall safe "extism.h extism_version" extism_version :: IO CString

View File

@@ -56,10 +56,12 @@ public interface LibExtism extends Library {
* @param contextPointer pointer to the {@link Context}.
* @param wasm is a WASM module (wat or wasm) or a JSON encoded manifest
* @param wasmSize the length of the `wasm` parameter
* @param functions host functions
* @param nFunctions the number of host functions
* @param withWASI enables/disables WASI
* @return id of the plugin or {@literal -1} in case of error
*/
int extism_plugin_new(long contextPointer, byte[] wasm, long wasmSize, boolean withWASI);
int extism_plugin_new(Pointer contextPointer, byte[] wasm, long wasmSize, Pointer functions, int nFunctions, boolean withWASI);
/**
* Returns the Extism version string
@@ -117,10 +119,12 @@ public interface LibExtism extends Library {
* @param pluginIndex
* @param wasm
* @param length
* @param functions host functions
* @param nFunctions the number of host functions
* @param withWASI
* @return {@literal true} if update was successful
*/
boolean extism_plugin_update(Pointer contextPointer, int pluginIndex, byte[] wasm, int length, boolean withWASI);
boolean extism_plugin_update(Pointer contextPointer, int pluginIndex, byte[] wasm, int length, Pointer functions, int nFunctions, boolean withWASI);
/**
* Remove a plugin from the registry and free associated memory.

View File

@@ -36,7 +36,7 @@ public class Plugin implements AutoCloseable {
Objects.requireNonNull(manifestBytes, "manifestBytes");
Pointer contextPointer = context.getPointer();
int index = LibExtism.INSTANCE.extism_plugin_new(contextPointer, manifestBytes, manifestBytes.length, withWASI);
int index = LibExtism.INSTANCE.extism_plugin_new(contextPointer, manifestBytes, manifestBytes.length, null, 0, withWASI);
if (index == -1) {
String error = context.error(this);
throw new ExtismException(error);
@@ -125,7 +125,7 @@ public class Plugin implements AutoCloseable {
*/
public boolean update(byte[] manifestBytes, boolean withWASI) {
Objects.requireNonNull(manifestBytes, "manifestBytes");
return LibExtism.INSTANCE.extism_plugin_update(context.getPointer(), index, manifestBytes, manifestBytes.length, withWASI);
return LibExtism.INSTANCE.extism_plugin_update(context.getPointer(), index, manifestBytes, manifestBytes.length, null, 0, withWASI);
}
/**

View File

@@ -1,10 +0,0 @@
prefix=/usr/local
exec_prefix=${prefix}
includedir=${prefix}/include
libdir=${exec_prefix}/lib
Name: extism
Description: The Extism universal plug-in system.
Version: 0.1.0
Cflags: -I${includedir}
Libs: -L${libdir} -lextism

View File

@@ -1,9 +1,33 @@
const { withContext, Context } = require('./dist/index.js');
const { readFileSync } = require('fs');
const {
withContext,
Context,
HostFunction,
ValType,
} = require("./dist/index.js");
const { readFileSync } = require("fs");
function f(currentPlugin, inputs, outputs, userData) {
let mem = currentPlugin.memory(inputs[0].v.i64);
console.log(mem.length);
console.log(mem.toString());
console.log("Hello from Javascript!");
console.log(userData);
outputs[0] = inputs[0];
}
let hello_world = new HostFunction(
"hello_world",
[ValType.I64],
[ValType.I64],
f,
"Hello again!"
);
let functions = [hello_world];
withContext(async function (context) {
let wasm = readFileSync("../wasm/code.wasm");
let p = context.plugin(wasm);
let wasm = readFileSync("../wasm/code-functions.wasm");
let p = context.plugin(wasm, true, functions);
if (!p.functionExists("count_vowels")) {
console.log("no function 'count_vowels' in wasm");
@@ -16,7 +40,7 @@ withContext(async function (context) {
});
// or, use a context like this:
let ctx = new Context();
let wasm = readFileSync("../wasm/code.wasm");
let p = ctx.plugin(wasm);
// let ctx = new Context();
// let wasm = readFileSync("../wasm/code.wasm");
// let p = ctx.plugin(wasm, wasi = true);
// ... where the context can be passed around to various functions etc.

3114
node/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,12 +21,17 @@
},
"scripts": {
"prepare": "npm run build",
"example": "node example.js",
"example": "npm run build && node example.js",
"build": "tsc",
"test": "jest --coverage"
},
"dependencies": {
"ffi-napi": "^4.0.3"
"@types/ref-array-di": "^1.2.5",
"ffi-napi": "^4.0.3",
"ref-array-di": "^1.2.2",
"ref-napi": "^3.0.3",
"ref-struct-di": "^1.1.1",
"ref-union-di": "^1.0.1"
},
"publishConfig": {
"access": "public"

View File

@@ -1,30 +1,96 @@
import ffi from "ffi-napi";
import ref from "ref-napi";
import path from "path";
const context = "void*";
var ArrayType = require("ref-array-di")(ref);
var StructType = require("ref-struct-di")(ref);
var UnionType = require("ref-union-di")(ref);
const opaque = ref.types.void;
const context = ref.refType(opaque);
const function_t = ref.refType(opaque);
const pluginIndex = ref.types.int32;
let ValTypeArray = ArrayType(ref.types.int);
let PtrArray = new ArrayType("void*");
let ValUnion = new UnionType({
i32: ref.types.uint32,
i64: ref.types.uint64,
f32: ref.types.float,
f64: ref.types.double,
});
/**
* Val struct, low-level WebAssembly values
*/
let Val = new StructType({
t: ref.types.int,
v: ValUnion,
});
/**
* Array of `Val`
*/
let ValArray = ArrayType(Val);
const _functions = {
extism_context_new: [context, []],
extism_context_free: ["void", [context]],
extism_plugin_new: ["int32", [context, "string", "uint64", "bool"]],
extism_plugin_new: [
pluginIndex,
[context, "string", "uint64", PtrArray, "uint64", "bool"],
],
extism_plugin_update: [
"bool",
[context, "int32", "string", "uint64", "bool"],
[context, pluginIndex, "string", "uint64", PtrArray, "uint64", "bool"],
],
extism_error: ["char*", [context, "int32"]],
extism_error: ["string", [context, pluginIndex]],
extism_plugin_call: [
"int32",
[context, "int32", "string", "string", "uint64"],
[context, pluginIndex, "string", "string", "uint64"],
],
extism_plugin_output_length: ["uint64", [context, "int32"]],
extism_plugin_output_data: ["uint8*", [context, "int32"]],
extism_plugin_output_length: ["uint64", [context, pluginIndex]],
extism_plugin_output_data: ["uint8*", [context, pluginIndex]],
extism_log_file: ["bool", ["string", "char*"]],
extism_plugin_function_exists: ["bool", [context, "int32", "string"]],
extism_plugin_config: ["void", [context, "int32", "char*", "uint64"]],
extism_plugin_free: ["void", [context, "int32"]],
extism_plugin_function_exists: ["bool", [context, pluginIndex, "string"]],
extism_plugin_config: ["void", [context, pluginIndex, "char*", "uint64"]],
extism_plugin_free: ["void", [context, pluginIndex]],
extism_context_reset: ["void", [context]],
extism_version: ["char*", []],
extism_version: ["string", []],
extism_function_new: [
function_t,
[
"string",
ValTypeArray,
"uint64",
ValTypeArray,
"uint64",
"void*",
"void*",
"void*",
],
],
extism_function_free: ["void", [function_t]],
extism_current_plugin_memory: ["uint8*", ["void*"]],
extism_current_plugin_memory_alloc: ["uint64", ["void*", "uint64"]],
extism_current_plugin_memory_length: ["uint64", ["void*", "uint64"]],
extism_current_plugin_memory_free: ["void", ["void*", "uint64"]],
};
/**
* An enumeration of all possible `Val` types
*/
export enum ValType {
I32 = 0,
I64,
F32,
F64,
FuncRef,
ExternRef,
}
interface LibExtism {
extism_context_new: () => Buffer;
extism_context_free: (ctx: Buffer) => void;
@@ -32,6 +98,8 @@ interface LibExtism {
ctx: Buffer,
data: string | Buffer,
data_len: number,
functions: Buffer,
nfunctions: number,
wasi: boolean
) => number;
extism_plugin_update: (
@@ -39,9 +107,11 @@ interface LibExtism {
plugin_id: number,
data: string | Buffer,
data_len: number,
functions: Buffer,
nfunctions: number,
wasi: boolean
) => boolean;
extism_error: (ctx: Buffer, plugin_id: number) => Buffer;
extism_error: (ctx: Buffer, plugin_id: number) => string;
extism_plugin_call: (
ctx: Buffer,
plugin_id: number,
@@ -50,7 +120,7 @@ interface LibExtism {
input_len: number
) => number;
extism_plugin_output_length: (ctx: Buffer, plugin_id: number) => number;
extism_plugin_output_data: (ctx: Buffer, plugin_id: Number) => Uint8Array;
extism_plugin_output_data: (ctx: Buffer, plugin_id: number) => Uint8Array;
extism_log_file: (file: string, level: string) => boolean;
extism_plugin_function_exists: (
ctx: Buffer,
@@ -65,7 +135,22 @@ interface LibExtism {
) => void;
extism_plugin_free: (ctx: Buffer, plugin_id: number) => void;
extism_context_reset: (ctx: Buffer) => void;
extism_version: () => Buffer;
extism_version: () => string;
extism_function_new: (
name: string,
inputs: Buffer,
nInputs: number,
outputs: Buffer,
nOutputs: number,
f: Buffer,
user_data: Buffer | null,
free: Buffer | null
) => Buffer;
extism_function_free: (f: Buffer) => void;
extism_current_plugin_memory: (p: Buffer) => Buffer;
extism_current_plugin_memory_alloc: (p: Buffer, n: number) => number;
extism_current_plugin_memory_length: (p: Buffer, n: number) => number;
extism_current_plugin_memory_free: (p: Buffer, n: number) => void;
}
function locate(paths: string[]): LibExtism {
@@ -110,19 +195,19 @@ export function setLogFile(filename: string, level?: string) {
* @returns The version string of the Extism runtime
*/
export function extismVersion(): string {
return lib.extism_version().toString();
return lib.extism_version();
}
// @ts-ignore
const pluginRegistry = new FinalizationRegistry(({ id, pointer }) => {
if (id && pointer) lib.extism_plugin_free(pointer, id);
});
// @ts-ignore
const contextRegistry = new FinalizationRegistry((pointer) => {
if (pointer) lib.extism_context_free(pointer);
});
// @ts-ignore
const functionRegistry = new FinalizationRegistry((pointer) => {
if (pointer) lib.extism_function_free(pointer);
});
/**
* Represents a path or url to a WASM module
*/
@@ -161,7 +246,7 @@ export type ManifestWasm = ManifestWasmFile | ManifestWasmData;
/**
* The manifest which describes the {@link Plugin} code and
* runtime constraints.
*
*
* @see [Extism > Concepts > Manifest](https://extism.org/docs/concepts/manifest)
*/
export type Manifest = {
@@ -214,7 +299,7 @@ export class Context {
*/
constructor() {
this.pointer = lib.extism_context_new();
contextRegistry.register(this, this.pointer, this);
contextRegistry.register(this, this.pointer, this.pointer);
}
/**
@@ -225,19 +310,24 @@ export class Context {
* @param config - Config details for the plugin
* @returns A new Plugin scoped to this Context
*/
plugin(manifest: ManifestData, wasi: boolean = false, config?: PluginConfig) {
return new Plugin(this, manifest, wasi, config);
plugin(
manifest: ManifestData,
wasi: boolean = false,
functions: HostFunction[] = [],
config?: PluginConfig
) {
return new Plugin(this, manifest, wasi, functions, config);
}
/**
* Frees the context. Should be called after the context is not needed to reclaim the memory.
*/
free() {
contextRegistry.unregister(this.pointer);
if (this.pointer) {
contextRegistry.unregister(this);
lib.extism_context_free(this.pointer);
this.pointer = null;
}
this.pointer = null;
}
/**
@@ -268,12 +358,156 @@ export async function withContext(f: (ctx: Context) => Promise<any>) {
}
}
/**
* Provides access to the plugin that is currently running from inside a {@link HostFunction}
*/
export class CurrentPlugin {
pointer: Buffer;
constructor(pointer: Buffer) {
this.pointer = pointer;
}
/**
* Access plugin's memory
* @param offset - The offset in memory
* @returns a pointer to the provided offset
*/
memory(offset: number): Buffer {
let length = lib.extism_current_plugin_memory_length(this.pointer, offset);
return Buffer.from(
lib.extism_current_plugin_memory(this.pointer).buffer,
offset,
length
);
}
/**
* Allocate a new memory block
* @param n - The number of bytes to allocate
* @returns the offset to the newly allocated block
*/
memoryAlloc(n: number): number {
return lib.extism_current_plugin_memory_alloc(this.pointer, n);
}
/**
* Free a memory block
* @param offset - The offset of the block to free
*/
memoryFree(offset: number) {
return lib.extism_current_plugin_memory_free(this.pointer, offset);
}
/**
* Get the length of a memory block
* @param offset - The offset of the block
* @returns the length of the block specified by `offset`
*/
memoryLength(offset: number): number {
return lib.extism_current_plugin_memory_length(this.pointer, offset);
}
}
/**
* Allows for the host to define functions that can be called from WebAseembly
*/
export class HostFunction {
callback: any;
pointer: Buffer | null;
name: string;
userData: any[];
inputs: typeof ValTypeArray;
outputs: typeof ValTypeArray;
constructor(
name: string,
inputs: ValType[],
outputs: ValType[],
f: any,
...userData: any
) {
this.userData = userData;
this.callback = ffi.Callback(
"void",
[
"void*",
ref.refType(Val),
"uint64",
ref.refType(Val),
"uint64",
"void*",
],
(
currentPlugin: Buffer,
inputs: Buffer,
nInputs: number,
outputs: Buffer,
nOutputs: number,
user_data
) => {
let inputArr = [];
let outputArr = [];
for (var i = 0; i < nInputs; i++) {
inputArr.push(Val.get(inputs, i));
}
for (var i = 0; i < nOutputs; i++) {
outputArr.push(Val.get(outputs, i));
}
f(
new CurrentPlugin(currentPlugin),
inputArr,
outputArr,
...this.userData
);
for (var i = 0; i < nOutputs; i++) {
Val.set(outputs, i, outputArr[i]);
}
}
);
this.name = name;
this.inputs = new ValTypeArray(inputs);
this.outputs = new ValTypeArray(outputs);
this.pointer = lib.extism_function_new(
this.name,
this.inputs,
this.inputs.length,
this.outputs,
this.outputs.length,
this.callback,
null,
null
);
this.userData = userData;
functionRegistry.register(this, this.pointer, this.pointer);
}
/**
* Free a host function - this should be called to cleanup the associated resources
*/
free() {
functionRegistry.unregister(this.pointer);
if (this.pointer === null) {
return;
}
lib.extism_function_free(this.pointer);
this.pointer = null;
}
}
/**
* A Plugin represents an instance of your WASM program from the given manifest.
*/
export class Plugin {
id: number;
ctx: Context;
functions: typeof PtrArray;
token: { id: number; pointer: Buffer };
/**
* Constructor for a plugin. @see {@link Context#plugin}.
@@ -281,12 +515,14 @@ export class Plugin {
* @param ctx - The context to manage this plugin
* @param manifest - The {@link Manifest}
* @param wasi - Set to true to enable WASI support
* @param functions - An array of {@link HostFunction}
* @param config - The plugin config
*/
constructor(
ctx: Context,
manifest: ManifestData,
wasi: boolean = false,
functions: HostFunction[] = [],
config?: PluginConfig
) {
let dataRaw: string | Buffer;
@@ -298,10 +534,16 @@ export class Plugin {
throw Error(`Unknown manifest type ${typeof manifest}`);
}
if (!ctx.pointer) throw Error("No Context set");
this.functions = new PtrArray(functions.length);
for (var i = 0; i < functions.length; i++) {
this.functions[i] = functions[i].pointer;
}
let plugin = lib.extism_plugin_new(
ctx.pointer,
dataRaw,
Buffer.byteLength(dataRaw, 'utf-8'),
Buffer.byteLength(dataRaw, "utf-8"),
this.functions,
functions.length,
wasi
);
if (plugin < 0) {
@@ -312,16 +554,17 @@ export class Plugin {
throw `Unable to load plugin: ${err.toString()}`;
}
this.id = plugin;
this.token = { id: this.id, pointer: ctx.pointer };
this.ctx = ctx;
pluginRegistry.register(
this,
{ id: this.id, pointer: this.ctx.pointer },
this
);
if (config != null) {
let s = JSON.stringify(config);
lib.extism_plugin_config(ctx.pointer, this.id, s, Buffer.byteLength(s, 'utf-8'),);
lib.extism_plugin_config(
ctx.pointer,
this.id,
s,
Buffer.byteLength(s, "utf-8")
);
}
}
@@ -330,9 +573,15 @@ export class Plugin {
*
* @param manifest - The new {@link Manifest} data
* @param wasi - Set to true to enable WASI support
* @param functions - An array of {@link HostFunction}
* @param config - The new plugin config
*/
update(manifest: ManifestData, wasi: boolean = false, config?: PluginConfig) {
update(
manifest: ManifestData,
wasi: boolean = false,
functions: HostFunction[] = [],
config?: PluginConfig
) {
let dataRaw: string | Buffer;
if (Buffer.isBuffer(manifest) || typeof manifest === "string") {
dataRaw = manifest;
@@ -342,11 +591,17 @@ export class Plugin {
throw Error("Unknown manifest type type");
}
if (!this.ctx.pointer) throw Error("No Context set");
this.functions = new PtrArray(functions.length);
for (var i = 0; i < functions.length; i++) {
this.functions[i] = functions[i].pointer;
}
const ok = lib.extism_plugin_update(
this.ctx.pointer,
this.id,
dataRaw,
Buffer.byteLength(dataRaw, 'utf-8'),
Buffer.byteLength(dataRaw, "utf-8"),
this.functions,
functions.length,
wasi
);
if (!ok) {
@@ -359,7 +614,12 @@ export class Plugin {
if (config != null) {
let s = JSON.stringify(config);
lib.extism_plugin_config(this.ctx.pointer, this.id, s, Buffer.byteLength(s, 'utf-8'),);
lib.extism_plugin_config(
this.ctx.pointer,
this.id,
s,
Buffer.byteLength(s, "utf-8")
);
}
}
@@ -393,7 +653,7 @@ export class Plugin {
*
* @param functionName - The name of the function
* @param input - The input data
* @returns A Buffer repreesentation of the output
*@returns A Buffer repreesentation of the output
*/
async call(functionName: string, input: string | Buffer): Promise<Buffer> {
return new Promise<Buffer>((resolve, reject) => {
@@ -403,7 +663,7 @@ export class Plugin {
this.id,
functionName,
input.toString(),
Buffer.byteLength(input, 'utf-8'),
Buffer.byteLength(input, "utf-8")
);
if (rc !== 0) {
var err = lib.extism_error(this.ctx.pointer, this.id);
@@ -427,8 +687,7 @@ export class Plugin {
* Free a plugin, this should be called when the plugin is no longer needed
*/
free() {
if (this.ctx.pointer && this.id !== -1) {
pluginRegistry.unregister(this);
if (this.ctx.pointer && this.id >= 0) {
lib.extism_plugin_free(this.ctx.pointer, this.id);
this.id = -1;
}

View File

@@ -40,13 +40,60 @@ let context = ptr void
let extism_context_new = fn "extism_context_new" (void @-> returning context)
let extism_context_free = fn "extism_context_free" (context @-> returning void)
module Extism_val_type = struct
type t = I32 | I64 | F32 | F64 | FuncRef | ExternRef
let to_int = function
| I32 -> 0
| I64 -> 1
| F32 -> 2
| F64 -> 3
| FuncRef -> 4
| ExternRef -> 5
let of_int = function
| 0 -> I32
| 1 -> I64
| 2 -> F32
| 3 -> F64
| 4 -> FuncRef
| 5 -> ExternRef
| n -> invalid_arg ("Extism_val_type.of_int: " ^ string_of_int n)
let t : t typ = view ~read:of_int ~write:to_int int
end
module Extism_val_union = struct
type t
let t : t union typ = union "ExtismValUnion"
let i32 = field t "i32" int32_t
let i64 = field t "i64" int64_t
let f32 = field t "f32" float
let f64 = field t "f64" double
let () = seal t
end
module Extism_val = struct
type t
let t : t structure typ = structure "ExtismVal"
let ty = field t "t" Extism_val_type.t
let v = field t "v" Extism_val_union.t
let () = seal t
end
let extism_plugin_new =
fn "extism_plugin_new"
(context @-> string @-> uint64_t @-> bool @-> returning int32_t)
(context @-> string @-> uint64_t
@-> ptr (ptr void)
@-> uint64_t @-> bool @-> returning int32_t)
let extism_plugin_update =
fn "extism_plugin_update"
(context @-> int32_t @-> string @-> uint64_t @-> bool @-> returning bool)
(context @-> int32_t @-> string @-> uint64_t
@-> ptr (ptr void)
@-> uint64_t @-> bool @-> returning bool)
let extism_plugin_config =
fn "extism_plugin_config"
@@ -84,3 +131,35 @@ let extism_context_reset = fn "extism_context_reset" (context @-> returning void
let extism_plugin_function_exists =
fn "extism_plugin_function_exists"
(context @-> int32_t @-> string @-> returning bool)
let extism_function_type =
Foreign.funptr ~runtime_lock:true
(ptr void @-> ptr Extism_val.t @-> uint64_t @-> ptr Extism_val.t
@-> uint64_t @-> ptr void @-> returning void)
let extism_free_user_data =
Foreign.funptr_opt ~runtime_lock:true (ptr void @-> returning void)
let extism_function_new =
fn "extism_function_new"
(string @-> ptr Extism_val_type.t @-> uint64_t @-> ptr Extism_val_type.t
@-> uint64_t @-> extism_function_type @-> ptr void @-> extism_free_user_data
@-> returning (ptr void))
let extism_function_free =
fn "extism_function_free" (ptr void @-> returning void)
let extism_current_plugin_memory =
fn "extism_current_plugin_memory" (ptr void @-> returning (ptr uint8_t))
let extism_current_plugin_memory_length =
fn "extism_current_plugin_memory_length"
(ptr void @-> uint64_t @-> returning uint64_t)
let extism_current_plugin_memory_alloc =
fn "extism_current_plugin_memory_alloc"
(ptr void @-> uint64_t @-> returning uint64_t)
let extism_current_plugin_memory_free =
fn "extism_current_plugin_memory_free"
(ptr void @-> uint64_t @-> returning void)

View File

@@ -0,0 +1,51 @@
open Ctypes
type t = unit ptr
type offs = Unsigned.uint64
type len = Unsigned.uint64
let memory t = Bindings.extism_current_plugin_memory t
let length t offs = Bindings.extism_current_plugin_memory_length t offs
let alloc t len = Bindings.extism_current_plugin_memory_alloc t len
let free t offs = Bindings.extism_current_plugin_memory_free t offs
module Memory = struct
let get_bigstring t offs : Bigstringaf.t =
let length = length t offs in
let p = memory t +@ Unsigned.UInt64.to_int offs in
bigarray_of_ptr array1
(Unsigned.UInt64.to_int length)
Bigarray.Char
(coerce (ptr uint8_t) (ptr char) p)
let get_string t offs =
let length = length t offs in
let p = memory t +@ Unsigned.UInt64.to_int offs in
Ctypes.string_from_ptr
(coerce (ptr uint8_t) (ptr char) p)
~length:(Unsigned.UInt64.to_int length)
let set_bigstring t offs bs =
let length =
min (Unsigned.UInt64.to_int @@ length t offs) (Bigstringaf.length bs)
in
let p =
coerce (ptr uint8_t) (ptr char)
@@ (memory t +@ Unsigned.UInt64.to_int offs)
in
for i = 0 to length - 1 do
p +@ i <-@ Bigstringaf.unsafe_get bs i
done
let set_string t offs s =
let length =
min (Unsigned.UInt64.to_int @@ length t offs) (String.length s)
in
let p =
coerce (ptr uint8_t) (ptr char)
@@ (memory t +@ Unsigned.UInt64.to_int offs)
in
for i = 0 to length - 1 do
p +@ i <-@ String.unsafe_get s i
done
end

View File

@@ -2,7 +2,7 @@
(name extism)
(public_name extism)
(inline_tests
(deps test/code.wasm))
(deps test/code.wasm test/code-functions.wasm))
(libraries ctypes.foreign bigstringaf extism-manifest)
(preprocess
(pps ppx_yojson_conv ppx_inline_test)))

View File

@@ -8,3 +8,4 @@ let () =
| _ -> None)
let unwrap = function Ok x -> x | Error t -> raise (Error t)
let throw e = raise (Error e)

View File

@@ -2,6 +2,9 @@ module Manifest = Extism_manifest
module Error = Error
module Context = Context
module Plugin = Plugin
module Function = Function
module Current_plugin = Current_plugin
include Types
let with_context = Plugin.with_context
let extism_version = Bindings.extism_version

View File

@@ -1,36 +1,181 @@
(** Extism bindings for OCaml *)
(** Returns the libextism version, not the version of the OCaml library *)
val extism_version : unit -> string
(** Returns the libextism version, not the version of the OCaml library *)
module Manifest = Extism_manifest
module Error : sig
type t = [`Msg of string]
type t = [ `Msg of string ]
exception Error of t
val unwrap: ('a, t) result -> 'a
val unwrap : ('a, t) result -> 'a
val throw : t -> 'a
end
(** [Val_type] enumerates every possible argument/result type *)
module Val_type : sig
type t = I32 | I64 | F32 | F64 | FuncRef | ExternRef (** Value type *)
val t : t Ctypes.typ
val of_int : int -> t
val to_int : t -> int
end
(** [Val] represents low-level WebAssembly values *)
module Val : sig
type t
(** Val *)
val t : t Ctypes.typ
val ty : t -> Val_type.t
(** [ty v] returns the [Val_type.t] for the value [v] *)
val of_i32 : int32 -> t
(** Create an i32 [Val] *)
val of_i64 : int64 -> t
(** Create an i64 [Val] *)
val of_f32 : float -> t
(** Create an f32 [Val] *)
val of_f64 : float -> t
(** Create an f64 [Val] *)
val to_i32 : t -> int32 option
(** Get an int32 from [Val] if the type matches *)
val to_i64 : t -> int64 option
(** Get an int64 from [Val] if the type matches *)
val to_f32 : t -> float option
(** Get a f32 from [Val] if the type matches *)
val to_f64 : t -> float option
(** Get an f64 from [Val] if the type matches *)
val to_i32_exn : t -> int32
(** Same as [to_i32] but raises an exception if the types don't match*)
val to_i64_exn : t -> int64
(** Same as [to_i64] but raises an exception if the types don't match*)
val to_f32_exn : t -> float
(** Same as [to_f32] but raises an exception if the types don't match*)
val to_f64_exn : t -> float
(** Same as [to_f64] but raises an exception if the types don't match*)
end
(** [Val_array] is used for input/output parameters for host functions *)
module Val_array : sig
type t = Val.t Ctypes.CArray.t
(** [Val_array] type *)
val get : t -> int -> Val.t
(** Get an index *)
val set : t -> int -> Val.t -> unit
(** Set an index *)
val length : t -> int
(** Get the number of items in a [Val_array]*)
val ( .$[] ) : t -> int -> Val.t
(** Syntax for [get] *)
val ( .$[]<- ) : t -> int -> Val.t -> unit
(** Syntax for [set] *)
end
(** [Current_plugin] represents the plugin that is currently running, it should
it should only be used from a host function *)
module Current_plugin : sig
type t
(** Opaque type, wraps [ExtismCurrentPlugin] *)
type offs = Unsigned.uint64
(** Memory offset type *)
type len = Unsigned.uint64
(** Memory length type *)
val memory : t -> Unsigned.uint8 Ctypes.ptr
(** Get pointer to entire plugin memory *)
val length : t -> offs -> len
(** Get the length of an allocated block of memory *)
val alloc : t -> len -> offs
(** Allocate a new block of memory *)
val free : t -> offs -> unit
(** Free an allocated block of memory *)
(** Some helpter functions for reading/writing memory *)
module Memory : sig
val get_string : t -> offs -> string
(** Get a string from memory stored at the provided offset *)
val get_bigstring : t -> offs -> Bigstringaf.t
(** Get a bigstring from memory stored at the provided offset *)
val set_string : t -> offs -> string -> unit
(** Store a string into memory at the provided offset *)
val set_bigstring : t -> offs -> Bigstringaf.t -> unit
(** Store a bigstring into memory at the provided offset *)
end
end
(** [Function] is used to create new a new function, which can be called
from a WebAssembly plugin *)
module Function : sig
type t
(** Function type *)
val v :
string ->
Val_type.t list ->
Val_type.t list ->
user_data:'a ->
(Current_plugin.t -> Val_array.t -> Val_array.t -> 'a -> unit) ->
t
(** Create a new function, [Function.v name args returns ~user_data f] creates
a new [Function] with the given [name], [args] specifies the argument types,
[returns] specifies the return types, [user_data] is used to pass arbitrary
OCaml values into the function and [f] is the OCaml function that will be
called.
*)
val free : t -> unit
(** Free a function *)
val free_all : t list -> unit
(** Free a list of functions *)
end
(** [Context] is used to group plugins *)
module Context : sig
(** Context type *)
type t
(** Context type *)
(** Create a new context *)
val create : unit -> t
(** Create a new context *)
val free : t -> unit
(** Free a context. All plugins will be removed and the value should not be
accessed after this call *)
val free : t -> unit
(** Reset a context. All plugins will be removed *)
val reset : t -> unit
(** Reset a context. All plugins will be removed *)
end
(** Execute a function with a fresh context and free it after *)
val with_context : (Context.t -> 'a) -> 'a
(** Execute a function with a fresh context and free it after *)
val set_log_file :
?level:[ `Error | `Warn | `Info | `Debug | `Trace ] -> string -> bool
@@ -39,40 +184,45 @@ val set_log_file :
module Plugin : sig
type t
(** Make a new plugin from raw WebAssembly or JSON encoded manifest *)
val make :
?config:Manifest.config ->
?wasi:bool ->
?functions:Function.t list ->
Context.t ->
string ->
(t, Error.t) result
(** Make a new plugin from raw WebAssembly or JSON encoded manifest *)
(** Make a new plugin from a [Manifest] *)
val of_manifest :
?wasi:bool -> Context.t -> Manifest.t -> (t, Error.t) result
?wasi:bool ->
?functions:Function.t list ->
Context.t ->
Manifest.t ->
(t, Error.t) result
(** Make a new plugin from a [Manifest] *)
(** Update a plugin from raw WebAssembly or JSON encoded manifest *)
val update :
t ->
?config:(string * string option) list ->
?wasi:bool ->
?functions:Function.t list ->
string ->
(unit, [ `Msg of string ]) result
(** Update a plugin from raw WebAssembly or JSON encoded manifest *)
val update_manifest : t -> ?wasi:bool -> Manifest.t -> (unit, Error.t) result
(** Update a plugin from a [Manifest] *)
val update_manifest :
t -> ?wasi:bool -> Manifest.t -> (unit, Error.t) result
(** Call a function, uses [Bigstringaf.t] for input/output *)
val call_bigstring :
t -> name:string -> Bigstringaf.t -> (Bigstringaf.t, Error.t) result
(** Call a function, uses [Bigstringaf.t] for input/output *)
(** Call a function, uses [string] for input/output *)
val call : t -> name:string -> string -> (string, Error.t) result
(** Call a function, uses [string] for input/output *)
(** Drop a plugin *)
val free : t -> unit
(** Drop a plugin *)
(** Check if a function is exported by a plugin *)
val function_exists : t -> string -> bool
(** Check if a function is exported by a plugin *)
end

40
ocaml/lib/function.ml Normal file
View File

@@ -0,0 +1,40 @@
open Ctypes
type t = {
mutable pointer : unit ptr;
mutable user_data : unit ptr;
name : string;
}
let free t =
let () =
if not (is_null t.user_data) then
let () = Root.release t.user_data in
t.user_data <- null
in
if not (is_null t.pointer) then
let () = Bindings.extism_function_free t.pointer in
t.pointer <- null
let free_all l = List.iter free l
let v name inputs outputs ~user_data f =
let inputs = CArray.of_list Bindings.Extism_val_type.t inputs in
let n_inputs = Unsigned.UInt64.of_int (CArray.length inputs) in
let outputs = CArray.of_list Bindings.Extism_val_type.t outputs in
let n_outputs = Unsigned.UInt64.of_int (CArray.length outputs) in
let free' = Some Root.release in
let user_data = Root.create user_data in
let f current inputs n_inputs outputs n_outputs user_data =
let user_data = Root.get user_data in
let inputs = CArray.from_ptr inputs (Unsigned.UInt64.to_int n_inputs) in
let outputs = CArray.from_ptr outputs (Unsigned.UInt64.to_int n_outputs) in
f current inputs outputs user_data
in
let pointer =
Bindings.extism_function_new name (CArray.start inputs) n_inputs
(CArray.start outputs) n_outputs f user_data free'
in
let t = { pointer; user_data; name } in
Gc.finalise free t;
t

View File

@@ -1,6 +1,6 @@
module Manifest = Extism_manifest
type t = { id : int32; ctx : Context.t }
type t = { id : int32; ctx : Context.t; mutable functions : Function.t list }
let with_context f =
let ctx = Context.create () in
@@ -26,10 +26,15 @@ let free t =
if not (Ctypes.is_null t.ctx.pointer) then
Bindings.extism_plugin_free t.ctx.pointer t.id
let make ?config ?(wasi = false) ctx wasm =
let make ?config ?(wasi = false) ?(functions = []) ctx wasm =
let func_ptrs = List.map (fun x -> x.Function.pointer) functions in
let arr = Ctypes.CArray.of_list Ctypes.(ptr void) func_ptrs in
let n_funcs = Ctypes.CArray.length arr in
let id =
Bindings.extism_plugin_new ctx.Context.pointer wasm
(Unsigned.UInt64.of_int (String.length wasm))
(Ctypes.CArray.start arr)
(Unsigned.UInt64.of_int n_funcs)
wasi
in
if id < 0l then
@@ -37,28 +42,33 @@ let make ?config ?(wasi = false) ctx wasm =
| None -> Error (`Msg "extism_plugin_call failed")
| Some msg -> Error (`Msg msg)
else
let t = { id; ctx } in
let t = { id; ctx; functions } in
if not (set_config t config) then Error (`Msg "call to set_config failed")
else
let () = Gc.finalise free t in
Ok t
let of_manifest ?wasi ctx manifest =
let of_manifest ?wasi ?functions ctx manifest =
let data = Manifest.json manifest in
make ctx ?wasi data
make ctx ?wasi ?functions data
let%test "free plugin" =
let manifest = Manifest.v [ Manifest.file "test/code.wasm" ] in
with_context (fun ctx ->
let plugin = of_manifest ctx manifest |> Result.get_ok in
let plugin = of_manifest ctx manifest |> Error.unwrap in
free plugin;
true)
let update plugin ?config ?(wasi = false) wasm =
let { id; ctx } = plugin in
let update plugin ?config ?(wasi = false) ?(functions = []) wasm =
let { id; ctx; _ } = plugin in
let func_ptrs = List.map (fun x -> x.Function.pointer) functions in
let arr = Ctypes.CArray.of_list Ctypes.(ptr void) func_ptrs in
let n_funcs = Ctypes.CArray.length arr in
let ok =
Bindings.extism_plugin_update ctx.pointer id wasm
(Unsigned.UInt64.of_int (String.length wasm))
(Ctypes.CArray.start arr)
(Unsigned.UInt64.of_int n_funcs)
wasi
in
if not ok then
@@ -77,11 +87,11 @@ let%test "update plugin manifest and config" =
let manifest = Manifest.v [ Manifest.file "test/code.wasm" ] in
with_context (fun ctx ->
let config = [ ("a", Some "1") ] in
let plugin = of_manifest ctx manifest |> Result.get_ok in
let plugin = of_manifest ctx manifest |> Error.unwrap in
let manifest = Manifest.with_config manifest config in
update_manifest plugin manifest |> Result.is_ok)
let call' f { id; ctx } ~name input len =
let call' f { id; ctx; _ } ~name input len =
let rc = f ctx.pointer id name input len in
if rc <> 0l then
match Bindings.extism_error ctx.pointer id with
@@ -105,10 +115,10 @@ let call_bigstring (t : t) ~name input =
let%test "call_bigstring" =
let manifest = Manifest.v [ Manifest.file "test/code.wasm" ] in
with_context (fun ctx ->
let plugin = of_manifest ctx manifest |> Result.get_ok in
let plugin = of_manifest ctx manifest |> Error.unwrap in
call_bigstring plugin ~name:"count_vowels"
(Bigstringaf.of_string ~off:0 ~len:14 "this is a test")
|> Result.get_ok |> Bigstringaf.to_string = "{\"count\": 4}")
|> Error.unwrap |> Bigstringaf.to_string = "{\"count\": 4}")
let call (t : t) ~name input =
let len = String.length input in
@@ -118,16 +128,40 @@ let call (t : t) ~name input =
let%test "call" =
let manifest = Manifest.v [ Manifest.file "test/code.wasm" ] in
with_context (fun ctx ->
let plugin = of_manifest ctx manifest |> Result.get_ok in
let plugin = of_manifest ctx manifest |> Error.unwrap in
call plugin ~name:"count_vowels" "this is a test"
|> Result.get_ok = "{\"count\": 4}")
|> Error.unwrap = "{\"count\": 4}")
let function_exists { id; ctx } name =
let%test "call_functions" =
let open Types.Val_type in
let hello_world =
Function.v "hello_world" [ I64 ] [ I64 ] ~user_data:"Hello again!"
@@ fun plugin inputs outputs user_data ->
let open Types.Val_array in
let s =
Current_plugin.Memory.get_string plugin
(Unsigned.UInt64.of_int64 @@ Types.Val.to_i64_exn inputs.$[0])
in
let () = print_endline "Hello from OCaml!" in
let () = print_endline user_data in
let () = print_endline s in
outputs.$[0] <- inputs.$[0]
in
let functions = [ hello_world ] in
let manifest = Manifest.v [ Manifest.file "test/code-functions.wasm" ] in
with_context (fun ctx ->
let plugin =
of_manifest ctx manifest ~functions ~wasi:true |> Error.unwrap
in
call plugin ~name:"count_vowels" "this is a test"
|> Error.unwrap = "{\"count\": 4}")
let function_exists { id; ctx; _ } name =
Bindings.extism_plugin_function_exists ctx.pointer id name
let%test "function exists" =
let manifest = Manifest.v [ Manifest.file "test/code.wasm" ] in
with_context (fun ctx ->
let plugin = of_manifest ctx manifest |> Result.get_ok in
let plugin = of_manifest ctx manifest |> Error.unwrap in
function_exists plugin "count_vowels"
&& not (function_exists plugin "function_does_not_exist"))

Binary file not shown.

92
ocaml/lib/types.ml Normal file
View File

@@ -0,0 +1,92 @@
open Ctypes
module Val_type = Bindings.Extism_val_type
module Val = struct
type t = (Bindings.Extism_val.t, [ `Struct ]) Ctypes.structured
let t = Bindings.Extism_val.t
let of_i32 (x : int32) : t =
let u = Ctypes.make Bindings.Extism_val_union.t in
u @. Bindings.Extism_val_union.i32 <-@ x;
let t = Ctypes.make Bindings.Extism_val.t in
t @. Bindings.Extism_val.ty <-@ Val_type.I32;
t @. Bindings.Extism_val.v <-@ u;
t
let of_i64 (x : int64) : t =
let u = Ctypes.make Bindings.Extism_val_union.t in
u @. Bindings.Extism_val_union.i64 <-@ x;
let t = Ctypes.make Bindings.Extism_val.t in
t @. Bindings.Extism_val.ty <-@ Val_type.I64;
t @. Bindings.Extism_val.v <-@ u;
t
let of_f32 (x : float) : t =
let u = Ctypes.make Bindings.Extism_val_union.t in
u @. Bindings.Extism_val_union.f32 <-@ x;
let t = Ctypes.make Bindings.Extism_val.t in
t @. Bindings.Extism_val.ty <-@ Val_type.F32;
t @. Bindings.Extism_val.v <-@ u;
t
let of_f64 (x : float) : t =
let u = Ctypes.make Bindings.Extism_val_union.t in
u @. Bindings.Extism_val_union.f64 <-@ x;
let t = Ctypes.make Bindings.Extism_val.t in
t @. Bindings.Extism_val.ty <-@ Val_type.F64;
t @. Bindings.Extism_val.v <-@ u;
t
let to_i32 t : int32 option =
let ty = t @. Bindings.Extism_val.ty in
let v = t @. Bindings.Extism_val.v in
match !@ty with
| Bindings.Extism_val_type.I32 ->
Some !@(!@v @. Bindings.Extism_val_union.i32)
| _ -> None
let to_i64 t : int64 option =
let ty = t @. Bindings.Extism_val.ty in
let v = t @. Bindings.Extism_val.v in
match !@ty with
| Bindings.Extism_val_type.I64 ->
Some !@(!@v @. Bindings.Extism_val_union.i64)
| _ -> None
let to_f32 t : float option =
let ty = t @. Bindings.Extism_val.ty in
let v = t @. Bindings.Extism_val.v in
match !@ty with
| Bindings.Extism_val_type.F32 ->
Some !@(!@v @. Bindings.Extism_val_union.f32)
| _ -> None
let to_f64 t : float option =
let ty = t @. Bindings.Extism_val.ty in
let v = t @. Bindings.Extism_val.v in
match !@ty with
| Bindings.Extism_val_type.F64 ->
Some !@(!@v @. Bindings.Extism_val_union.f64)
| _ -> None
let ty t = !@(t @. Bindings.Extism_val.ty)
let make_exn f x =
match f x with Some x -> x | None -> Error.throw (`Msg "invalid type")
let to_i32_exn = make_exn to_i32
let to_i64_exn = make_exn to_i64
let to_f32_exn = make_exn to_f32
let to_f64_exn = make_exn to_f64
end
module Val_array = struct
type t = Val.t Ctypes.CArray.t
let get t i = Ctypes.CArray.get t i
let set t i x = Ctypes.CArray.set t i x
let length t = Ctypes.CArray.length t
let ( .$[] ) = get
let ( .$[]<- ) = set
end

View File

@@ -1,39 +1,42 @@
(** Memory options *)
type memory = { max_pages : int option } [@@deriving yojson]
(** Memory options *)
(** Key/value dictionary *)
type dict = (string * string) list [@@deriving yojson]
(** Key/value dictionary *)
(** Key/value dictionary with optional values *)
type config = (string * string option) list [@@deriving yojson]
(** Key/value dictionary with optional values *)
(** WebAssembly file *)
type wasm_file = {
path : string;
name : string option; [@yojson.option]
hash : string option; [@yojson.option]
} [@@deriving yojson]
}
[@@deriving yojson]
(** WebAssembly file *)
(** WebAssembly module data *)
type wasm_data = {
data : string;
name : string option; [@yojson.option]
hash : string option; [@yojson.option]
} [@@deriving yojson]
}
[@@deriving yojson]
(** WebAssembly module data *)
(** WebAssembly URL *)
type wasm_url = {
url : string;
headers : dict option; [@yojson.option]
name : string option; [@yojson.option]
meth : string option; [@yojson.option] [@key "method"]
hash : string option; [@yojson.option]
} [@@deriving yojson]
}
[@@deriving yojson]
(** WebAssembly URL *)
(** WebAssembly from a file, module data or URL *)
type wasm = File of wasm_file | Data of wasm_data | Url of wasm_url [@@deriving yojson]
type wasm = File of wasm_file | Data of wasm_data | Url of wasm_url
[@@deriving yojson]
(** Manifest type *)
type t = {
wasm : wasm list;
memory : memory option;
@@ -41,15 +44,16 @@ type t = {
allowed_hosts : string list option;
allowed_paths : dict option;
timeout_ms : int option;
} [@@deriving yojson]
}
[@@deriving yojson]
(** Manifest type *)
(** Create [wasm] from filename *)
val file : ?name:string -> ?hash:string -> string -> wasm
(** Create [wasm] from filename *)
(** Create [wasm] from WebAssembly module data *)
val data : ?name:string -> ?hash:string -> string -> wasm
(** Create [wasm] from WebAssembly module data *)
(** Create [wasm] from URL *)
val url :
?headers:(string * string) list ->
?name:string ->
@@ -57,8 +61,8 @@ val url :
?hash:string ->
string ->
wasm
(** Create [wasm] from URL *)
(** Create new manifest *)
val v :
?config:config ->
?memory:memory ->
@@ -67,9 +71,10 @@ val v :
?timeout_ms:int ->
wasm list ->
t
(** Create new manifest *)
(** Convert manifest to JSON *)
val json : t -> string
(** Convert manifest to JSON *)
(** Updates a manifest config *)
val with_config : t -> config -> t
(** Updates a manifest config *)

View File

@@ -29,7 +29,7 @@ class Plugin
$data = string_to_bytes($data);
}
$id = $this->lib->extism_plugin_new($ctx->pointer, $data, count($data), (int)$wasi);
$id = $this->lib->extism_plugin_new($ctx->pointer, $data, count($data), null, 0, (int)$wasi);
if ($id < 0) {
$err = $this->lib->extism_error($ctx->pointer, -1);
throw new \Exception("Extism: unable to load plugin: " . $err);
@@ -96,7 +96,7 @@ class Plugin
$data = string_to_bytes($data);
}
$ok = $this->lib->extism_plugin_update($this->context->pointer, $this->id, $data, count($data), (int)$wasi);
$ok = $this->lib->extism_plugin_update($this->context->pointer, $this->id, $data, count($data), null, 0, (int)$wasi);
if (!$ok) {
$err = $this->lib->extism_error($this->context->pointer, -1);
throw new \Exception("Extism: unable to update plugin: " . $err);

View File

@@ -1,36 +0,0 @@
#pragma once
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
typedef int32_t ExtismPlugin;
typedef uint64_t ExtismSize;
ExtismPlugin extism_plugin_register(const uint8_t *wasm, ExtismSize wasm_size, bool with_wasi);
bool extism_plugin_update(ExtismPlugin index,
const uint8_t *wasm,
ExtismSize wasm_size,
bool with_wasi);
bool extism_plugin_config(ExtismPlugin plugin, const uint8_t *json, ExtismSize json_size);
bool extism_function_exists(ExtismPlugin plugin, const char *func_name);
int32_t extism_call(ExtismPlugin plugin_id,
const char *func_name,
const uint8_t *data,
ExtismSize data_len);
const char *extism_error(ExtismPlugin plugin);
ExtismSize extism_output_length(ExtismPlugin plugin);
void extism_output_get(ExtismPlugin plugin, uint8_t *buf, ExtismSize len);
bool extism_log_file(const char *filename, const char *log_level);
const char *extism_version();

View File

@@ -3,21 +3,42 @@ import json
import hashlib
sys.path.append(".")
from extism import Context
from extism import Context, Function, host_fn, ValType
if len(sys.argv) > 1:
data = sys.argv[1].encode()
else:
data = b"some data from python!"
@host_fn
def hello_world(plugin, input, output, context, a_string):
mem = plugin.memory_at_offset(input[0])
print("Hello from Python!")
print(a_string)
print(input)
print(plugin.memory(mem)[:])
output[0] = input[0]
# a Context provides a scope for plugins to be managed within. creating multiple contexts
# is expected and groups plugins based on source/tenant/lifetime etc.
with Context() as context:
wasm = open("../wasm/code.wasm", "rb").read()
wasm = open("../wasm/code-functions.wasm", "rb").read()
hash = hashlib.sha256(wasm).hexdigest()
config = {"wasm": [{"data": wasm, "hash": hash}], "memory": {"max": 5}}
plugin = context.plugin(config)
functions = [
Function(
"hello_world",
[ValType.I64],
[ValType.I64],
hello_world,
context,
"Hello again!",
)
]
plugin = context.plugin(config, wasi=True, functions=functions)
# Call `count_vowels`
j = json.loads(plugin.call("count_vowels", data))
print("Number of vowels:", j["count"])

View File

@@ -1 +1,10 @@
from .extism import Error, Plugin, set_log_file, Context, extism_version
from .extism import (
Error,
Plugin,
set_log_file,
Context,
extism_version,
host_fn,
Function,
ValType,
)

View File

@@ -3,6 +3,7 @@ import os
from base64 import b64encode
from cffi import FFI
from typing import Union
from enum import Enum
class Error(Exception):
@@ -124,6 +125,15 @@ def _wasm(plugin):
return wasm
class Memory:
def __init__(self, offs, length):
self.offset = offs
self.length = length
def __len__(self):
return self.length
class Context:
"""
Context is used to store and manage plugins. You need a context to create
@@ -161,7 +171,9 @@ class Context:
"""Remove all registered plugins"""
_lib.extism_context_reset(self.pointer)
def plugin(self, manifest: Union[str, bytes, dict], wasi=False, config=None):
def plugin(
self, manifest: Union[str, bytes, dict], wasi=False, config=None, functions=None
):
"""
Register a new plugin from a WASM module or JSON encoded manifest
@@ -173,13 +185,40 @@ class Context:
Set to `True` to enable WASI support
config : dict
The plugin config dictionary
functions: list
Additional host functions
Returns
-------
Plugin
The created plugin
"""
return Plugin(self, manifest, wasi, config)
return Plugin(self, manifest, wasi, config, functions)
class Function:
def __init__(self, name: str, args, returns, f, *user_data):
self.pointer = None
args = [a.value for a in args]
returns = [r.value for r in returns]
if len(user_data) > 0:
self.user_data = _ffi.new_handle(user_data)
else:
self.user_data = _ffi.NULL
self.pointer = _lib.extism_function_new(
name.encode(),
args,
len(args),
returns,
len(returns),
f,
self.user_data,
_ffi.NULL,
)
def __del__(self):
if self.pointer is not None:
_lib.extism_function_free(self.pointer)
class Plugin:
@@ -190,7 +229,12 @@ class Plugin:
"""
def __init__(
self, context: Context, plugin: Union[str, bytes, dict], wasi=False, config=None
self,
context: Context,
plugin: Union[str, bytes, dict],
wasi=False,
config=None,
functions=None,
):
"""
Construct a Plugin. Please use Context#plugin instead.
@@ -199,7 +243,16 @@ class Plugin:
wasm = _wasm(plugin)
# Register plugin
self.plugin = _lib.extism_plugin_new(context.pointer, wasm, len(wasm), wasi)
if functions is not None:
functions = [f.pointer for f in functions]
ptr = _ffi.new("ExtismFunction*[]", functions)
self.plugin = _lib.extism_plugin_new(
context.pointer, wasm, len(wasm), ptr, len(functions), wasi
)
else:
self.plugin = _lib.extism_plugin_new(
context.pointer, wasm, len(wasm), _ffi.NULL, 0, wasi
)
self.ctx = context
@@ -213,7 +266,9 @@ class Plugin:
s = json.dumps(config).encode()
_lib.extism_plugin_config(self.ctx.pointer, self.plugin, s, len(s))
def update(self, manifest: Union[str, bytes, dict], wasi=False, config=None):
def update(
self, manifest: Union[str, bytes, dict], wasi=False, config=None, functions=None
):
"""
Update a plugin with a new WASM module or manifest
@@ -227,9 +282,22 @@ class Plugin:
The plugin config dictionary
"""
wasm = _wasm(manifest)
ok = _lib.extism_plugin_update(
self.ctx.pointer, self.plugin, wasm, len(wasm), wasi
)
if functions is not None:
functions = [f.pointer for f in functions]
ptr = _ffi.new("ExtismFunction*[]", functions)
ok = _lib.extism_plugin_update(
self.ctx.pointer,
self.plugin,
wasm,
len(wasm),
ptr,
len(functions),
wasi,
)
else:
ok = _lib.extism_plugin_update(
self.ctx.pointer, self.plugin, wasm, len(wasm), _ffi.NULL, 0, wasi
)
if not ok:
error = _lib.extism_error(self.ctx.pointer, -1)
if error != _ffi.NULL:
@@ -310,3 +378,120 @@ class Plugin:
def __exit__(self, type, exc, traceback):
self.__del__()
def _convert_value(x):
if x.t == 0:
return Val(ValType.I32, x.v.i32)
elif x.t == 1:
return Val(ValType.I64, x.v.i64)
elif x.t == 2:
return Val(ValType.F32, x.v.f32)
elif x.y == 3:
return Val(ValType.F64, x.v.f64)
return None
def _convert_output(x, v):
if v.t.value != x.t:
raise Error(f"Output type mismatch, got {v.t} but expected {x.t}")
if v.t == ValType.I32:
x.v.i32 = int(v.value)
elif v.t == ValType.I64:
x.v.i64 = int(v.value)
elif x.t == ValType.F32:
x.v.f32 = float(v.value)
elif x.t == ValType.F64:
x.v.f64 = float(v.value)
else:
raise Error("Unsupported return type: " + str(x.t))
class ValType(Enum):
I32 = 0
I64 = 1
F32 = 2
F64 = 3
FUNC_REF = 4
EXTERN_REF = 5
class Val:
"""
Low-level WebAssembly value
"""
def __init__(self, t: ValType, v):
self.t = t
self.value = v
def __repr__(self):
return f"Val({self.t}, {self.value})"
class CurrentPlugin:
"""
Wraps the current plugin from inside a host function
"""
def __init__(self, p):
self.pointer = p
def memory(self, mem: Memory):
"""Access a block of memory"""
p = _lib.extism_current_plugin_memory(self.pointer)
if p == 0:
return None
return _ffi.buffer(p + mem.offset, mem.length)
def alloc(self, n):
"""Allocate a new block of memory of [n] bytes"""
offs = _lib.extism_current_plugin_memory_alloc(self.pointer, n)
return Memory(offs, n)
def free(self, mem):
"""Free a block of memory"""
return _lib.extism_current_plugin_memory_free(self.pointer, mem.offset)
def memory_at_offset(self, offs):
"""Get a block of memory at the specified offset"""
if isinstance(offs, Val):
offs = offs.value
len = _lib.extism_current_plugin_memory_length(self.pointer, offs)
return Memory(offs, len)
def host_fn(func):
"""
A decorator for creating host functions, this decorator wraps a function that takes the following parameters:
- current plugin: `CurrentPlugin`
- inputs: `List[Val]`
- user_data: any number of values passed as user data
The function should return a list of `Val`
"""
@_ffi.callback(
"void(ExtismCurrentPlugin*, const ExtismVal*, ExtismSize, ExtismVal*, ExtismSize, void*)"
)
def handle_args(current, inputs, n_inputs, outputs, n_outputs, user_data):
inp = []
outp = []
for i in range(n_inputs):
inp.append(_convert_value(inputs[i]))
for i in range(n_outputs):
outp.append(_convert_value(outputs[i]))
if user_data == _ffi.NULL:
output = func(CurrentPlugin(current), inp, outp)
else:
udata = _ffi.from_handle(user_data)
func(CurrentPlugin(current), inp, outp, *udata)
for i in range(n_outputs):
_convert_output(outputs[i], outp[i])
return handle_args

View File

@@ -7,7 +7,6 @@ from os.path import join, dirname
class TestExtism(unittest.TestCase):
def test_context_new(self):
ctx = extism.Context()
self.assertIsNotNone(ctx)
@@ -20,8 +19,7 @@ class TestExtism(unittest.TestCase):
self.assertEqual(j["count"], 4)
j = json.loads(plugin.call("count_vowels", "this is a test again"))
self.assertEqual(j["count"], 7)
j = json.loads(plugin.call("count_vowels",
"this is a test thrice"))
j = json.loads(plugin.call("count_vowels", "this is a test thrice"))
self.assertEqual(j["count"], 6)
j = json.loads(plugin.call("count_vowels", "🌎hello🌎world🌎"))
self.assertEqual(j["count"], 3)
@@ -44,8 +42,9 @@ class TestExtism(unittest.TestCase):
def test_errors_on_unknown_function(self):
with extism.Context() as ctx:
plugin = ctx.plugin(self._manifest())
self.assertRaises(extism.Error,
lambda: plugin.call("i_dont_exist", "someinput"))
self.assertRaises(
extism.Error, lambda: plugin.call("i_dont_exist", "someinput")
)
def test_can_free_plugin(self):
with extism.Context() as ctx:
@@ -54,12 +53,13 @@ class TestExtism(unittest.TestCase):
def test_errors_on_bad_manifest(self):
with extism.Context() as ctx:
self.assertRaises(extism.Error,
lambda: ctx.plugin({"invalid_manifest": True}))
self.assertRaises(
extism.Error, lambda: ctx.plugin({"invalid_manifest": True})
)
plugin = ctx.plugin(self._manifest())
self.assertRaises(
extism.Error,
lambda: plugin.update({"invalid_manifest": True}))
extism.Error, lambda: plugin.update({"invalid_manifest": True})
)
def test_extism_version(self):
self.assertIsNotNone(extism.extism_version())
@@ -68,36 +68,25 @@ class TestExtism(unittest.TestCase):
with extism.Context() as ctx:
plugin = ctx.plugin(self._loop_manifest())
start = datetime.now()
self.assertRaises(extism.Error,
lambda: plugin.call("infinite_loop", b""))
self.assertRaises(extism.Error, lambda: plugin.call("infinite_loop", b""))
end = datetime.now()
self.assertLess(end, start + timedelta(seconds=1.01),
"plugin timeout exceeded 1000ms expectation")
self.assertLess(
end,
start + timedelta(seconds=1.01),
"plugin timeout exceeded 1000ms expectation",
)
def _manifest(self):
wasm = self._count_vowels_wasm()
hash = hashlib.sha256(wasm).hexdigest()
return {
"wasm": [{
"data": wasm,
"hash": hash
}],
"memory": {
"max_pages": 5
}
}
return {"wasm": [{"data": wasm, "hash": hash}], "memory": {"max_pages": 5}}
def _loop_manifest(self):
wasm = self._infinite_loop_wasm()
hash = hashlib.sha256(wasm).hexdigest()
return {
"wasm": [{
"data": wasm,
"hash": hash
}],
"memory": {
"max_pages": 5
},
"wasm": [{"data": wasm, "hash": hash}],
"memory": {"max_pages": 5},
"timeout_ms": 1000,
}

View File

@@ -131,7 +131,7 @@ module Extism
end
code = FFI::MemoryPointer.new(:char, wasm.bytesize)
code.put_bytes(0, wasm)
@plugin = C.extism_plugin_new(context.pointer, code, wasm.bytesize, wasi)
@plugin = C.extism_plugin_new(context.pointer, code, wasm.bytesize, nil, 0, wasi)
if @plugin < 0
err = C.extism_error(@context.pointer, -1)
if err&.empty?
@@ -161,7 +161,7 @@ module Extism
end
code = FFI::MemoryPointer.new(:char, wasm.bytesize)
code.put_bytes(0, wasm)
ok = C.extism_plugin_update(@context.pointer, @plugin, code, wasm.bytesize, wasi)
ok = C.extism_plugin_update(@context.pointer, @plugin, code, wasm.bytesize, nil, 0, wasi)
if !ok
err = C.extism_error(@context.pointer, @plugin)
if err&.empty?
@@ -230,8 +230,8 @@ module Extism
ffi_lib "extism"
attach_function :extism_context_new, [], :pointer
attach_function :extism_context_free, [:pointer], :void
attach_function :extism_plugin_new, [:pointer, :pointer, :uint64, :bool], :int32
attach_function :extism_plugin_update, [:pointer, :int32, :pointer, :uint64, :bool], :bool
attach_function :extism_plugin_new, [:pointer, :pointer, :uint64, :pointer, :uint64, :bool], :int32
attach_function :extism_plugin_update, [:pointer, :int32, :pointer, :uint64, :pointer, :uint64, :bool], :bool
attach_function :extism_error, [:pointer, :int32], :string
attach_function :extism_plugin_call, [:pointer, :int32, :string, :pointer, :uint64], :int32
attach_function :extism_plugin_function_exists, [:pointer, :int32, :string], :bool

View File

@@ -1,4 +1,6 @@
fn main() {
let fn_macro = "#define EXTISM_FUNCTION(N) extern void N(ExtismCurrentPlugin*, const ExtismVal*, ExtismSize, ExtismVal*, ExtismSize, void*)";
let go_fn_macro = "#define EXTISM_GO_FUNCTION(N) extern void N(ExtismCurrentPlugin*, ExtismVal*, ExtismSize, ExtismVal*, ExtismSize, void*)";
if let Ok(bindings) = cbindgen::Builder::new()
.with_crate(".")
.with_language(cbindgen::Language::C)
@@ -6,9 +8,15 @@ fn main() {
.with_sys_include("stdint.h")
.with_sys_include("stdbool.h")
.with_pragma_once(true)
.with_after_include(fn_macro)
.with_after_include(go_fn_macro)
.rename_item("Size", "ExtismSize")
.rename_item("PluginIndex", "ExtismPlugin")
.rename_item("Context", "ExtismContext")
.rename_item("ValType", "ExtismValType")
.rename_item("ValUnion", "ExtismValUnion")
.rename_item("Plugin", "ExtismCurrentPlugin")
.with_style(cbindgen::Style::Type)
.generate()
{
bindings.write_to_file("extism.h");

View File

@@ -2,36 +2,163 @@
#include <stdint.h>
#include <stdbool.h>
#define EXTISM_GO_FUNCTION(N) extern void N(ExtismCurrentPlugin*, ExtismVal*, ExtismSize, ExtismVal*, ExtismSize, void*)
/**
* A list of all possible value types in WebAssembly.
*/
typedef enum {
/**
* Signed 32 bit integer.
*/
I32,
/**
* Signed 64 bit integer.
*/
I64,
/**
* Floating point 32 bit integer.
*/
F32,
/**
* Floating point 64 bit integer.
*/
F64,
/**
* A 128 bit number.
*/
V128,
/**
* A reference to a Wasm function.
*/
FuncRef,
/**
* A reference to opaque data in the Wasm instance.
*/
ExternRef,
} ExtismValType;
/**
* A `Context` is used to store and manage plugins
*/
typedef struct ExtismContext ExtismContext;
typedef int32_t ExtismPlugin;
/**
* Wraps host functions
*/
typedef struct ExtismFunction ExtismFunction;
/**
* Plugin contains everything needed to execute a WASM function
*/
typedef struct ExtismCurrentPlugin ExtismCurrentPlugin;
typedef uint64_t ExtismSize;
/**
* A union type for host function argument/return values
*/
typedef union {
int32_t i32;
int64_t i64;
float f32;
double f64;
} ExtismValUnion;
/**
* `ExtismVal` holds the type and value of a function argument/return
*/
typedef struct {
ExtismValType t;
ExtismValUnion v;
} ExtismVal;
/**
* Host function signature
*/
typedef void (*ExtismFunctionType)(ExtismCurrentPlugin *plugin, const ExtismVal *inputs, ExtismSize n_inputs, ExtismVal *outputs, ExtismSize n_outputs, void *data);
typedef int32_t ExtismPlugin;
/**
* Create a new context
*/
struct ExtismContext *extism_context_new(void);
ExtismContext *extism_context_new(void);
/**
* Free a context
*/
void extism_context_free(struct ExtismContext *ctx);
void extism_context_free(ExtismContext *ctx);
/**
* Create a new plugin
* Returns a pointer to the memory of the currently running plugin
* NOTE: this should only be called from host functions.
*/
uint8_t *extism_current_plugin_memory(ExtismCurrentPlugin *plugin);
/**
* Allocate a memory block in the currently running plugin
* NOTE: this should only be called from host functions.
*/
uint64_t extism_current_plugin_memory_alloc(ExtismCurrentPlugin *plugin, ExtismSize n);
/**
* Get the length of an allocated block
* NOTE: this should only be called from host functions.
*/
ExtismSize extism_current_plugin_memory_length(ExtismCurrentPlugin *plugin, ExtismSize n);
/**
* Free an allocated memory block
* NOTE: this should only be called from host functions.
*/
void extism_current_plugin_memory_free(ExtismCurrentPlugin *plugin, uint64_t ptr);
/**
* Create a new host function
*
* Arguments
* - `name`: function name, this should be valid UTF-8
* - `inputs`: argument types
* - `n_inputs`: number of argument types
* - `outputs`: return types
* - `n_outputs`: number of return types
* - `func`: the function to call
* - `user_data`: a pointer that will be passed to the function when it's called
* this value should live as long as the function exists
* - `free_user_data`: a callback to release the `user_data` value when the resulting
* `ExtismFunction` is freed.
*
* Returns a new `ExtismFunction` or `null` if the `name` argument is invalid.
*/
ExtismFunction *extism_function_new(const char *name,
const ExtismValType *inputs,
ExtismSize n_inputs,
const ExtismValType *outputs,
ExtismSize n_outputs,
ExtismFunctionType func,
void *user_data,
void (*free_user_data)(void *_));
/**
* Free an `ExtismFunction`
*/
void extism_function_free(ExtismFunction *ptr);
/**
* Create a new plugin with additional host functions
*
* `wasm`: is a WASM module (wat or wasm) or a JSON encoded manifest
* `wasm_size`: the length of the `wasm` parameter
* `functions`: an array of `ExtismFunction*`
* `n_functions`: the number of functions provided
* `with_wasi`: enables/disables WASI
*/
ExtismPlugin extism_plugin_new(struct ExtismContext *ctx,
ExtismPlugin extism_plugin_new(ExtismContext *ctx,
const uint8_t *wasm,
ExtismSize wasm_size,
const ExtismFunction **functions,
ExtismSize n_functions,
bool with_wasi);
/**
@@ -42,26 +169,28 @@ ExtismPlugin extism_plugin_new(struct ExtismContext *ctx,
*
* Memory for this plugin will be reset upon update
*/
bool extism_plugin_update(struct ExtismContext *ctx,
bool extism_plugin_update(ExtismContext *ctx,
ExtismPlugin index,
const uint8_t *wasm,
ExtismSize wasm_size,
const ExtismFunction **functions,
ExtismSize nfunctions,
bool with_wasi);
/**
* Remove a plugin from the registry and free associated memory
*/
void extism_plugin_free(struct ExtismContext *ctx, ExtismPlugin plugin);
void extism_plugin_free(ExtismContext *ctx, ExtismPlugin plugin);
/**
* Remove all plugins from the registry
*/
void extism_context_reset(struct ExtismContext *ctx);
void extism_context_reset(ExtismContext *ctx);
/**
* Update plugin config values, this will merge with the existing values
*/
bool extism_plugin_config(struct ExtismContext *ctx,
bool extism_plugin_config(ExtismContext *ctx,
ExtismPlugin plugin,
const uint8_t *json,
ExtismSize json_size);
@@ -69,9 +198,7 @@ bool extism_plugin_config(struct ExtismContext *ctx,
/**
* Returns true if `func_name` exists
*/
bool extism_plugin_function_exists(struct ExtismContext *ctx,
ExtismPlugin plugin,
const char *func_name);
bool extism_plugin_function_exists(ExtismContext *ctx, ExtismPlugin plugin, const char *func_name);
/**
* Call a function
@@ -80,7 +207,7 @@ bool extism_plugin_function_exists(struct ExtismContext *ctx,
* `data`: is the input data
* `data_len`: is the length of `data`
*/
int32_t extism_plugin_call(struct ExtismContext *ctx,
int32_t extism_plugin_call(ExtismContext *ctx,
ExtismPlugin plugin_id,
const char *func_name,
const uint8_t *data,
@@ -90,17 +217,17 @@ int32_t extism_plugin_call(struct ExtismContext *ctx,
* Get the error associated with a `Context` or `Plugin`, if `plugin` is `-1` then the context
* error will be returned
*/
const char *extism_error(struct ExtismContext *ctx, ExtismPlugin plugin);
const char *extism_error(ExtismContext *ctx, ExtismPlugin plugin);
/**
* Get the length of a plugin's output data
*/
ExtismSize extism_plugin_output_length(struct ExtismContext *ctx, ExtismPlugin plugin);
ExtismSize extism_plugin_output_length(ExtismContext *ctx, ExtismPlugin plugin);
/**
* Get the length of a plugin's output data
*/
const uint8_t *extism_plugin_output_data(struct ExtismContext *ctx, ExtismPlugin plugin);
const uint8_t *extism_plugin_output_data(ExtismContext *ctx, ExtismPlugin plugin);
/**
* Set log file and level

View File

@@ -1,3 +1,4 @@
use std::cell::UnsafeCell;
use std::collections::{BTreeMap, VecDeque};
use crate::*;
@@ -7,7 +8,7 @@ static mut TIMER: std::sync::Mutex<Option<Timer>> = std::sync::Mutex::new(None);
/// A `Context` is used to store and manage plugins
pub struct Context {
/// Plugin registry
pub plugins: BTreeMap<PluginIndex, Plugin>,
pub plugins: BTreeMap<PluginIndex, UnsafeCell<Plugin>>,
/// Error message
pub error: Option<std::ffi::CString>,
@@ -90,29 +91,17 @@ impl Context {
return -1;
}
};
self.plugins.insert(id, plugin);
self.plugins.insert(id, UnsafeCell::new(plugin));
id
}
pub fn new_plugin(&mut self, data: impl AsRef<[u8]>, with_wasi: bool) -> PluginIndex {
let plugin = match Plugin::new(data, with_wasi) {
Ok(x) => x,
Err(e) => {
error!("Error creating Plugin: {:?}", e);
self.set_error(e);
return -1;
}
};
self.insert(plugin)
}
pub fn new_plugin_with_functions(
pub fn new_plugin<'a>(
&mut self,
data: impl AsRef<[u8]>,
imports: impl IntoIterator<Item = Function>,
imports: impl IntoIterator<Item = &'a Function>,
with_wasi: bool,
) -> PluginIndex {
let plugin = match Plugin::new_with_functions(data, imports, with_wasi) {
let plugin = match Plugin::new(data, imports, with_wasi) {
Ok(x) => x,
Err(e) => {
error!("Error creating Plugin: {:?}", e);
@@ -136,8 +125,11 @@ impl Context {
}
/// Get a plugin from the context
pub fn plugin(&mut self, id: PluginIndex) -> Option<&mut Plugin> {
self.plugins.get_mut(&id)
pub fn plugin(&mut self, id: PluginIndex) -> Option<*mut Plugin> {
match self.plugins.get_mut(&id) {
Some(x) => Some(x.get_mut()),
None => None,
}
}
pub fn plugin_exists(&mut self, id: PluginIndex) -> bool {

View File

@@ -2,6 +2,7 @@ use crate::{Error, Internal};
/// A list of all possible value types in WebAssembly.
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
#[repr(C)]
pub enum ValType {
// NB: the ordering here is intended to match the ordering in
// `wasmtime_types::WasmType` to help improve codegen when converting.
@@ -51,53 +52,146 @@ impl From<ValType> for wasmtime::ValType {
}
}
#[allow(clippy::type_complexity)]
pub struct Function(
pub(crate) String,
pub(crate) wasmtime::FuncType,
pub(crate) Box<
dyn Fn(
wasmtime::Caller<Internal>,
&[wasmtime::Val],
&mut [wasmtime::Val],
) -> Result<(), Error>
+ Sync
+ Send,
>,
);
pub type Val = wasmtime::Val;
pub struct UserData {
ptr: *mut std::ffi::c_void,
free: Option<extern "C" fn(_: *mut std::ffi::c_void)>,
is_any: bool,
}
extern "C" fn free_any(ptr: *mut std::ffi::c_void) {
let ptr = ptr as *mut dyn std::any::Any;
unsafe { drop(Box::from_raw(ptr)) }
}
impl UserData {
pub fn new_pointer(
ptr: *mut std::ffi::c_void,
free: Option<extern "C" fn(_: *mut std::ffi::c_void)>,
) -> Self {
UserData {
ptr,
free,
is_any: false,
}
}
pub fn new<T: std::any::Any>(x: T) -> Self {
let ptr = Box::into_raw(Box::new(x)) as *mut _;
UserData {
ptr,
free: Some(free_any),
is_any: true,
}
}
pub fn is_null(&self) -> bool {
self.ptr.is_null()
}
pub fn as_ptr(&self) -> *mut std::ffi::c_void {
self.ptr
}
pub(crate) fn make_copy(&self) -> Self {
UserData {
ptr: self.ptr,
free: None,
is_any: self.is_any,
}
}
pub fn any(&self) -> Option<&dyn std::any::Any> {
if !self.is_any || self.is_null() {
return None;
}
unsafe { Some(&*self.ptr) }
}
pub fn any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
if !self.is_any || self.is_null() {
return None;
}
unsafe { Some(&mut *self.ptr) }
}
}
impl Default for UserData {
fn default() -> Self {
UserData {
ptr: std::ptr::null_mut(),
free: None,
is_any: false,
}
}
}
impl Drop for UserData {
fn drop(&mut self) {
if self.ptr.is_null() {
return;
}
if let Some(free) = self.free {
free(self.ptr);
}
self.ptr = std::ptr::null_mut();
}
}
unsafe impl Send for UserData {}
unsafe impl Sync for UserData {}
type FunctionInner = dyn Fn(wasmtime::Caller<Internal>, &[wasmtime::Val], &mut [wasmtime::Val]) -> Result<(), Error>
+ Sync
+ Send;
#[derive(Clone)]
pub struct Function {
pub(crate) name: String,
pub(crate) ty: wasmtime::FuncType,
pub(crate) f: std::sync::Arc<FunctionInner>,
pub(crate) _user_data: std::sync::Arc<UserData>,
}
impl Function {
pub fn new<F>(
name: impl Into<String>,
args: impl IntoIterator<Item = ValType>,
returns: impl IntoIterator<Item = ValType>,
user_data: Option<UserData>,
f: F,
) -> Function
where
F: 'static
+ Fn(
wasmtime::Caller<Internal>,
&[wasmtime::Val],
&mut [wasmtime::Val],
) -> Result<(), Error>
+ Fn(&mut crate::Plugin, &[Val], &mut [Val], UserData) -> Result<(), Error>
+ Sync
+ Send,
{
Function(
name.into(),
wasmtime::FuncType::new(
let user_data = user_data.unwrap_or_default();
let data = UserData::new_pointer(user_data.ptr, None);
Function {
name: name.into(),
ty: wasmtime::FuncType::new(
args.into_iter().map(wasmtime::ValType::from),
returns.into_iter().map(wasmtime::ValType::from),
),
Box::new(f),
)
f: std::sync::Arc::new(move |mut caller, inp, outp| {
f(caller.data_mut().plugin_mut(), inp, outp, data.make_copy())
}),
_user_data: std::sync::Arc::new(user_data),
}
}
pub fn name(&self) -> &str {
&self.0
&self.name
}
pub fn ty(&self) -> &wasmtime::FuncType {
&self.1
&self.ty
}
}

View File

@@ -12,9 +12,9 @@ pub mod sdk;
mod timer;
pub use context::Context;
pub use function::{Function, ValType};
pub use function::{Function, UserData, Val, ValType};
pub use manifest::Manifest;
pub use memory::{MemoryBlock, PluginMemory};
pub use memory::{MemoryBlock, PluginMemory, ToMemoryBlock};
pub use plugin::{Internal, Plugin, Wasi};
pub use plugin_ref::PluginRef;
pub(crate) use timer::{Timer, TimerAction};

View File

@@ -255,7 +255,7 @@ impl PluginMemory {
}
/// Get memory as a mutable slice of bytes
pub fn data_mut(&mut self) -> &[u8] {
pub fn data_mut(&mut self) -> &mut [u8] {
self.memory.data_mut(&mut self.store)
}

View File

@@ -96,14 +96,9 @@ const EXPORT_MODULE_NAME: &str = "env";
impl Plugin {
/// Create a new plugin from the given WASM code
pub fn new(wasm: impl AsRef<[u8]>, with_wasi: bool) -> Result<Plugin, Error> {
Self::new_with_functions(wasm, [], with_wasi)
}
/// Create a new plugin from the given WASM code and imported functions
pub fn new_with_functions(
pub fn new<'a>(
wasm: impl AsRef<[u8]>,
imports: impl IntoIterator<Item = Function>,
imports: impl IntoIterator<Item = &'a Function>,
with_wasi: bool,
) -> Result<Plugin, Error> {
let engine = Engine::new(
@@ -192,7 +187,9 @@ impl Plugin {
for f in &mut imports {
let name = f.name().to_string();
let func = Func::new(&mut memory.store, f.ty().clone(), f.2);
let func = Func::new(&mut memory.store, f.ty().clone(), unsafe {
&*std::sync::Arc::as_ptr(&f.f)
});
linker.define(EXPORT_MODULE_NAME, &name, func)?;
}
}

View File

@@ -3,8 +3,9 @@ use crate::*;
// PluginRef is used to access a plugin from a context-scoped plugin registry
pub struct PluginRef<'a> {
pub id: PluginIndex,
plugin: &'a mut Plugin,
pub(crate) epoch_timer_tx: std::sync::mpsc::SyncSender<TimerAction>,
plugin: *mut Plugin,
_t: std::marker::PhantomData<&'a ()>,
}
impl<'a> PluginRef<'a> {
@@ -15,7 +16,7 @@ impl<'a> PluginRef<'a> {
pub fn init(mut self, data: *const u8, data_len: usize) -> Self {
trace!("PluginRef::init: {}", self.id,);
self.as_mut().memory.reset();
self.plugin.set_input(data, data_len);
self.as_mut().set_input(data, data_len);
self
}
@@ -40,19 +41,23 @@ impl<'a> PluginRef<'a> {
// `unwrap` is okay here because we already checked with `ctx.plugin_exists` above
let plugin = ctx.plugin(plugin_id).unwrap();
if clear_error {
trace!("Clearing plugin error: {plugin_id}");
plugin.clear_error();
}
// Reinstantiate plugin after calling _start because according to the WASI
// applicate ABI _start should be called "at most once":
// https://github.com/WebAssembly/WASI/blob/main/legacy/application-abi.md
if plugin.should_reinstantiate {
plugin.should_reinstantiate = false;
if let Err(e) = plugin.reinstantiate() {
error!("Failed to reinstantiate: {e:?}");
return plugin.error(format!("Failed to reinstantiate: {e:?}"), None);
{
let plugin = unsafe { &mut *plugin };
if clear_error {
trace!("Clearing plugin error: {plugin_id}");
plugin.clear_error();
}
// Reinstantiate plugin after calling _start because according to the WASI
// applicate ABI _start should be called "at most once":
// https://github.com/WebAssembly/WASI/blob/main/legacy/application-abi.md
if plugin.should_reinstantiate {
plugin.should_reinstantiate = false;
if let Err(e) = plugin.reinstantiate() {
error!("Failed to reinstantiate: {e:?}");
return plugin.error(format!("Failed to reinstantiate: {e:?}"), None);
}
}
}
@@ -60,25 +65,26 @@ impl<'a> PluginRef<'a> {
id: plugin_id,
plugin,
epoch_timer_tx,
_t: std::marker::PhantomData,
})
}
}
impl<'a> AsRef<Plugin> for PluginRef<'a> {
fn as_ref(&self) -> &Plugin {
self.plugin
unsafe { &*self.plugin }
}
}
impl<'a> AsMut<Plugin> for PluginRef<'a> {
fn as_mut(&mut self) -> &mut Plugin {
self.plugin
unsafe { &mut *self.plugin }
}
}
impl<'a> Drop for PluginRef<'a> {
fn drop(&mut self) {
trace!("Dropping plugin {}", self.id);
trace!("Dropping PluginRef {}", self.id);
// Cleanup?
}
}

View File

@@ -5,6 +5,74 @@ use std::str::FromStr;
use crate::*;
/// A union type for host function argument/return values
#[repr(C)]
pub union ValUnion {
i32: i32,
i64: i64,
f32: f32,
f64: f64,
// TODO: v128, ExternRef, FuncRef
}
/// `ExtismVal` holds the type and value of a function argument/return
#[repr(C)]
pub struct ExtismVal {
t: ValType,
v: ValUnion,
}
/// Wraps host functions
pub struct ExtismFunction(Function);
impl From<Function> for ExtismFunction {
fn from(x: Function) -> Self {
ExtismFunction(x)
}
}
/// Host function signature
pub type ExtismFunctionType = extern "C" fn(
plugin: *mut Plugin,
inputs: *const ExtismVal,
n_inputs: Size,
outputs: *mut ExtismVal,
n_outputs: Size,
data: *mut std::ffi::c_void,
);
impl From<&wasmtime::Val> for ExtismVal {
fn from(value: &wasmtime::Val) -> Self {
match value.ty() {
wasmtime::ValType::I32 => ExtismVal {
t: ValType::I32,
v: ValUnion {
i32: value.unwrap_i32(),
},
},
wasmtime::ValType::I64 => ExtismVal {
t: ValType::I64,
v: ValUnion {
i64: value.unwrap_i64(),
},
},
wasmtime::ValType::F32 => ExtismVal {
t: ValType::F32,
v: ValUnion {
f32: value.unwrap_f32(),
},
},
wasmtime::ValType::F64 => ExtismVal {
t: ValType::F64,
v: ValUnion {
f64: value.unwrap_f64(),
},
},
t => todo!("{}", t),
}
}
}
/// Create a new context
#[no_mangle]
pub unsafe extern "C" fn extism_context_new() -> *mut Context {
@@ -22,22 +90,192 @@ pub unsafe extern "C" fn extism_context_free(ctx: *mut Context) {
drop(Box::from_raw(ctx))
}
/// Create a new plugin
/// Returns a pointer to the memory of the currently running plugin
/// NOTE: this should only be called from host functions.
#[no_mangle]
pub unsafe extern "C" fn extism_current_plugin_memory(plugin: *mut Plugin) -> *mut u8 {
if plugin.is_null() {
return std::ptr::null_mut();
}
let plugin = &mut *plugin;
plugin.memory.data_mut().as_mut_ptr()
}
/// Allocate a memory block in the currently running plugin
/// NOTE: this should only be called from host functions.
#[no_mangle]
pub unsafe extern "C" fn extism_current_plugin_memory_alloc(plugin: *mut Plugin, n: Size) -> u64 {
if plugin.is_null() {
return 0;
}
let plugin = &mut *plugin;
let mem = match plugin.memory.alloc(n as usize) {
Ok(x) => x,
Err(e) => return plugin.error(e, 0),
};
mem.offset as u64
}
/// Get the length of an allocated block
/// NOTE: this should only be called from host functions.
#[no_mangle]
pub unsafe extern "C" fn extism_current_plugin_memory_length(plugin: *mut Plugin, n: Size) -> Size {
if plugin.is_null() {
return 0;
}
let plugin = &mut *plugin;
match plugin.memory.block_length(n as usize) {
Some(x) => x as Size,
None => 0,
}
}
/// Free an allocated memory block
/// NOTE: this should only be called from host functions.
#[no_mangle]
pub unsafe extern "C" fn extism_current_plugin_memory_free(plugin: *mut Plugin, ptr: u64) {
if plugin.is_null() {
return;
}
let plugin = &mut *plugin;
plugin.memory.free(ptr as usize);
}
/// Create a new host function
///
/// Arguments
/// - `name`: function name, this should be valid UTF-8
/// - `inputs`: argument types
/// - `n_inputs`: number of argument types
/// - `outputs`: return types
/// - `n_outputs`: number of return types
/// - `func`: the function to call
/// - `user_data`: a pointer that will be passed to the function when it's called
/// this value should live as long as the function exists
/// - `free_user_data`: a callback to release the `user_data` value when the resulting
/// `ExtismFunction` is freed.
///
/// Returns a new `ExtismFunction` or `null` if the `name` argument is invalid.
#[no_mangle]
pub unsafe extern "C" fn extism_function_new(
name: *const std::ffi::c_char,
inputs: *const ValType,
n_inputs: Size,
outputs: *const ValType,
n_outputs: Size,
func: ExtismFunctionType,
user_data: *mut std::ffi::c_void,
free_user_data: Option<extern "C" fn(_: *mut std::ffi::c_void)>,
) -> *mut ExtismFunction {
let name = match std::ffi::CStr::from_ptr(name).to_str() {
Ok(x) => x.to_string(),
Err(_) => {
return std::ptr::null_mut();
}
};
let inputs = if inputs.is_null() || n_inputs == 0 {
&[]
} else {
std::slice::from_raw_parts(inputs, n_inputs as usize)
}
.to_vec();
let output_types = if outputs.is_null() || n_outputs == 0 {
&[]
} else {
std::slice::from_raw_parts(outputs, n_outputs as usize)
}
.to_vec();
let user_data = UserData::new_pointer(user_data, free_user_data);
let f = Function::new(
name,
inputs,
output_types.clone(),
Some(user_data),
move |plugin, inputs, outputs, user_data| {
let inputs: Vec<_> = inputs.iter().map(ExtismVal::from).collect();
let mut output_tmp: Vec<_> = output_types
.iter()
.map(|t| ExtismVal {
t: t.clone(),
v: ValUnion { i64: 0 },
})
.collect();
func(
plugin,
inputs.as_ptr(),
inputs.len() as Size,
output_tmp.as_mut_ptr(),
output_tmp.len() as Size,
user_data.as_ptr(),
);
for (tmp, out) in output_tmp.iter().zip(outputs.iter_mut()) {
match tmp.t {
ValType::I32 => *out = Val::I32(tmp.v.i32),
ValType::I64 => *out = Val::I64(tmp.v.i64),
ValType::F32 => *out = Val::F32(tmp.v.f32 as u32),
ValType::F64 => *out = Val::F64(tmp.v.f64 as u64),
_ => todo!(),
}
}
Ok(())
},
);
Box::into_raw(Box::new(ExtismFunction(f)))
}
/// Free an `ExtismFunction`
#[no_mangle]
pub unsafe extern "C" fn extism_function_free(ptr: *mut ExtismFunction) {
drop(Box::from_raw(ptr))
}
/// Create a new plugin with additional host functions
///
/// `wasm`: is a WASM module (wat or wasm) or a JSON encoded manifest
/// `wasm_size`: the length of the `wasm` parameter
/// `functions`: an array of `ExtismFunction*`
/// `n_functions`: the number of functions provided
/// `with_wasi`: enables/disables WASI
#[no_mangle]
pub unsafe extern "C" fn extism_plugin_new(
ctx: *mut Context,
wasm: *const u8,
wasm_size: Size,
functions: *mut *const ExtismFunction,
n_functions: Size,
with_wasi: bool,
) -> PluginIndex {
trace!("Call to extism_plugin_new with wasm pointer {:?}", wasm);
let ctx = &mut *ctx;
let data = std::slice::from_raw_parts(wasm, wasm_size as usize);
ctx.new_plugin(data, with_wasi)
let mut funcs = vec![];
if !functions.is_null() {
for i in 0..n_functions {
unsafe {
let f = *functions.add(i as usize);
if f.is_null() {
continue;
}
let f = &*f;
funcs.push(&f.0);
}
}
}
ctx.new_plugin(data, funcs, with_wasi)
}
/// Update a plugin, keeping the existing ID
@@ -52,13 +290,31 @@ pub unsafe extern "C" fn extism_plugin_update(
index: PluginIndex,
wasm: *const u8,
wasm_size: Size,
functions: *mut *const ExtismFunction,
nfunctions: Size,
with_wasi: bool,
) -> bool {
trace!("Call to extism_plugin_update with wasm pointer {:?}", wasm);
let ctx = &mut *ctx;
let data = std::slice::from_raw_parts(wasm, wasm_size as usize);
let plugin = match Plugin::new(data, with_wasi) {
let mut funcs = vec![];
if !functions.is_null() {
for i in 0..nfunctions {
unsafe {
let f = *functions.add(i as usize);
if f.is_null() {
continue;
}
let f = &*f;
funcs.push(&f.0);
}
}
}
let plugin = match Plugin::new(data, funcs, with_wasi) {
Ok(x) => x,
Err(e) => {
error!("Error creating Plugin: {:?}", e);
@@ -72,7 +328,8 @@ pub unsafe extern "C" fn extism_plugin_update(
return false;
}
ctx.plugins.insert(index, plugin);
ctx.plugins
.insert(index, std::cell::UnsafeCell::new(plugin));
info!("Plugin updated: {index}");
true
@@ -245,7 +502,7 @@ pub unsafe extern "C" fn extism_plugin_call(
}
// Call the function
let mut results = vec![Val::null(); n_results];
let mut results = vec![wasmtime::Val::null(); n_results];
let res = func.call(
&mut plugin_ref.as_mut().memory.store,
&[],

View File

@@ -13,4 +13,4 @@ extism-manifest = { version = "0.1.0", path = "../manifest" }
extism-runtime = { version = "0.1.0", path = "../runtime"}
serde_json = "1"
log = "0.4"
thiserror = "1"
anyhow = "1"

View File

@@ -1,5 +1,7 @@
pub use extism_manifest::{self as manifest, Manifest};
pub use extism_runtime::{sdk as bindings, Function, ValType};
pub use extism_runtime::{
sdk as bindings, Function, MemoryBlock, Plugin as CurrentPlugin, UserData, Val, ValType,
};
mod context;
mod plugin;
@@ -8,18 +10,7 @@ mod plugin_builder;
pub use context::Context;
pub use plugin::Plugin;
pub use plugin_builder::PluginBuilder;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Unable to load plugin: {0}")]
UnableToLoadPlugin(String),
#[error("{0}")]
Message(String),
#[error("JSON: {0}")]
Json(#[from] serde_json::Error),
#[error("Runtime: {0}")]
Runtime(#[from] extism_runtime::Error),
}
pub type Error = anyhow::Error;
/// Gets the version of Extism
pub fn extism_version() -> String {
@@ -44,14 +35,32 @@ mod tests {
use super::*;
use std::time::Instant;
const WASM: &[u8] = include_bytes!("../../wasm/code.wasm");
const WASM: &[u8] = include_bytes!("../../wasm/code-functions.wasm");
fn hello_world(
_plugin: &mut CurrentPlugin,
inputs: &[Val],
outputs: &mut [Val],
_user_data: UserData,
) -> Result<(), Error> {
outputs[0] = inputs[0].clone();
Ok(())
}
#[test]
fn it_works() {
let wasm_start = Instant::now();
set_log_file("test.log", Some(log::Level::Info));
let context = Context::new();
let mut plugin = Plugin::new(&context, WASM, false).unwrap();
let f = Function::new(
"hello_world",
[ValType::I64],
[ValType::I64],
None,
hello_world,
);
let functions = [&f];
let mut plugin = Plugin::new(&context, WASM, functions, true).unwrap();
println!("register loaded plugin: {:?}", wasm_start.elapsed());
let repeat = 1182;
@@ -143,14 +152,32 @@ mod tests {
use std::io::Write;
std::thread::spawn(|| {
let context = Context::new();
let mut plugin = Plugin::new(&context, WASM, false).unwrap();
let f = Function::new(
"hello_world",
[ValType::I64],
[ValType::I64],
None,
hello_world,
);
let mut plugin = Plugin::new(&context, WASM, [&f], true).unwrap();
let output = plugin.call("count_vowels", "this is a test").unwrap();
std::io::stdout().write_all(output).unwrap();
});
std::thread::spawn(|| {
let f = Function::new(
"hello_world",
[ValType::I64],
[ValType::I64],
None,
hello_world,
);
let g = f.clone();
std::thread::spawn(move || {
let context = Context::new();
let mut plugin = PluginBuilder::new_with_module(WASM)
.with_function(&g)
.with_wasi(true)
.build(&context)
.unwrap();
let output = plugin.call("count_vowels", "this is a test aaa").unwrap();
@@ -158,7 +185,7 @@ mod tests {
});
let context = Context::new();
let mut plugin = Plugin::new(&context, WASM, false).unwrap();
let mut plugin = Plugin::new(&context, WASM, [&f], true).unwrap();
let output = plugin.call("count_vowels", "abc123").unwrap();
std::io::stdout().write_all(output).unwrap();
}

View File

@@ -23,32 +23,27 @@ impl<'a> Plugin<'a> {
pub fn new_with_manifest(
ctx: &'a Context,
manifest: &Manifest,
functions: impl IntoIterator<Item = &'a extism_runtime::Function>,
wasi: bool,
) -> Result<Plugin<'a>, Error> {
let data = serde_json::to_vec(manifest)?;
Self::new(ctx, data, wasi)
}
/// Create a new plugin from the given manifest and import functions
pub fn new_with_manifest_and_functions(
ctx: &'a Context,
manifest: &Manifest,
imports: impl IntoIterator<Item = extism_runtime::Function>,
wasi: bool,
) -> Result<Plugin<'a>, Error> {
let data = serde_json::to_vec(manifest)?;
Self::new_with_functions(ctx, data, imports, wasi)
Self::new(ctx, data, functions, wasi)
}
/// Create a new plugin from a WASM module
pub fn new(ctx: &'a Context, data: impl AsRef<[u8]>, wasi: bool) -> Result<Plugin, Error> {
let plugin = ctx.lock().new_plugin(data, wasi);
pub fn new(
ctx: &'a Context,
data: impl AsRef<[u8]>,
functions: impl IntoIterator<Item = &'a Function>,
wasi: bool,
) -> Result<Plugin, Error> {
let plugin = ctx.lock().new_plugin(data, functions, wasi);
if plugin < 0 {
let err = unsafe { bindings::extism_error(&mut *ctx.lock(), -1) };
let buf = unsafe { std::ffi::CStr::from_ptr(err) };
let buf = buf.to_str().unwrap().to_string();
return Err(Error::UnableToLoadPlugin(buf));
let buf = buf.to_str().unwrap();
return Err(Error::msg(buf));
}
Ok(Plugin {
@@ -57,36 +52,40 @@ impl<'a> Plugin<'a> {
})
}
/// Create a new plugin from a WASM module with imported functions
pub fn new_with_functions(
ctx: &'a Context,
data: impl AsRef<[u8]>,
imports: impl IntoIterator<Item = extism_runtime::Function>,
/// Update a plugin with the given manifest
pub fn update_with_manifest(
&mut self,
manifest: &Manifest,
functions: impl IntoIterator<Item = &'a Function>,
wasi: bool,
) -> Result<Plugin, Error> {
let plugin = ctx.lock().new_plugin_with_functions(data, imports, wasi);
if plugin < 0 {
let err = unsafe { bindings::extism_error(&mut *ctx.lock(), -1) };
let buf = unsafe { std::ffi::CStr::from_ptr(err) };
let buf = buf.to_str().unwrap().to_string();
return Err(Error::UnableToLoadPlugin(buf));
}
Ok(Plugin {
id: plugin,
context: ctx,
})
) -> Result<(), Error> {
let data = serde_json::to_vec(manifest)?;
self.update(data, functions, wasi)
}
/// Update a plugin with the given WASM module
pub fn update(&mut self, data: impl AsRef<[u8]>, wasi: bool) -> Result<(), Error> {
pub fn update(
&mut self,
data: impl AsRef<[u8]>,
functions: impl IntoIterator<Item = &'a Function>,
wasi: bool,
) -> Result<(), Error> {
let functions = functions
.into_iter()
.map(|x| bindings::ExtismFunction::from(x.clone()))
.collect::<Vec<_>>();
let mut functions = functions
.into_iter()
.map(|x| &x as *const _)
.collect::<Vec<_>>();
let b = unsafe {
bindings::extism_plugin_update(
&mut *self.context.lock(),
self.id,
data.as_ref().as_ptr(),
data.as_ref().len() as u64,
functions.as_mut_ptr(),
functions.len() as u64,
wasi,
)
};
@@ -97,16 +96,10 @@ impl<'a> Plugin<'a> {
let err = unsafe { bindings::extism_error(&mut *self.context.lock(), -1) };
if !err.is_null() {
let s = unsafe { std::ffi::CStr::from_ptr(err) };
return Err(Error::Message(s.to_str().unwrap().to_string()));
return Err(Error::msg(s.to_str().unwrap()));
}
Err(Error::Message("extism_plugin_update failed".to_string()))
}
/// Update a plugin with the given manifest
pub fn update_manifest(&mut self, manifest: &Manifest, wasi: bool) -> Result<(), Error> {
let data = serde_json::to_vec(manifest)?;
self.update(data, wasi)
Err(Error::msg("extism_plugin_update failed"))
}
/// Set configuration values
@@ -158,10 +151,10 @@ impl<'a> Plugin<'a> {
let err = unsafe { bindings::extism_error(&mut *self.context.lock(), self.id) };
if !err.is_null() {
let s = unsafe { std::ffi::CStr::from_ptr(err) };
return Err(Error::Message(s.to_str().unwrap().to_string()));
return Err(Error::msg(s.to_str().unwrap()));
}
return Err(Error::Message("extism_call failed".to_string()));
return Err(Error::msg("extism_call failed"));
}
let out_len =

View File

@@ -6,13 +6,13 @@ enum Source {
}
/// PluginBuilder is used to configure and create `Plugin` instances
pub struct PluginBuilder {
pub struct PluginBuilder<'a> {
source: Source,
wasi: bool,
functions: Vec<Function>,
functions: Vec<&'a Function>,
}
impl PluginBuilder {
impl<'a> PluginBuilder<'a> {
/// Create a new `PluginBuilder` with the given WebAssembly module
pub fn new_with_module(data: impl Into<Vec<u8>>) -> Self {
PluginBuilder {
@@ -38,23 +38,23 @@ impl PluginBuilder {
}
/// Add a single host function
pub fn with_function(mut self, f: Function) -> Self {
pub fn with_function(mut self, f: &'a Function) -> Self {
self.functions.push(f);
self
}
/// Add multiple host functions
pub fn with_functions(mut self, f: impl IntoIterator<Item = Function>) -> Self {
pub fn with_functions(mut self, f: impl IntoIterator<Item = &'a Function>) -> Self {
self.functions.extend(f);
self
}
pub fn build(self, context: &Context) -> Result<Plugin, Error> {
pub fn build(self, context: &'a Context) -> Result<Plugin<'a>, Error> {
match self.source {
Source::Manifest(m) => {
Plugin::new_with_manifest_and_functions(context, &m, self.functions, self.wasi)
Plugin::new_with_manifest(context, &m, self.functions, self.wasi)
}
Source::Data(d) => Plugin::new_with_functions(context, &d, self.functions, self.wasi),
Source::Data(d) => Plugin::new(context, &d, self.functions, self.wasi),
}
}
}

View File

@@ -51,7 +51,11 @@ class Visitor(c_ast.NodeVisitor):
if hasattr(t.type, 'name'):
type_name = t.type.name
else:
type_name = t.type.names[0]
try:
type_name = t.type.names[0]
except:
continue
const = hasattr(t.type, 'quals') and 'const' in t.type.quals
t = Type(type_name, const=const, pointer=is_ptr)
dest.append(Arg(name, t))

BIN
wasm/code-functions.wasm Executable file

Binary file not shown.

View File

@@ -16,7 +16,7 @@ pub const Plugin = struct {
pub fn init(ctx: *Context, data: []const u8, wasi: bool) !Plugin {
ctx.mutex.lock();
defer ctx.mutex.unlock();
const plugin = c.extism_plugin_new(ctx.ctx, toCstr(data), @as(u64, data.len), wasi);
const plugin = c.extism_plugin_new(ctx.ctx, toCstr(data), @as(u64, data.len), null, 0, wasi);
if (plugin < 0) {
const err_c = c.extism_error(ctx.ctx, @as(i32, -1));
const err = std.mem.span(err_c);
@@ -76,7 +76,7 @@ pub const Plugin = struct {
pub fn update(self: *Plugin, data: []const u8, wasi: bool) !void {
self.ctx.mutex.lock();
defer self.ctx.mutex.unlock();
const res = c.extism_plugin_update(self.ctx.ctx, self.id, toCstr(data), @intCast(u64, data.len), wasi);
const res = c.extism_plugin_update(self.ctx.ctx, self.id, toCstr(data), @intCast(u64, data.len), null, 0, wasi);
if (res) return;
const err_c = c.extism_error(self.ctx.ctx, @as(i32, -1));
const err = std.mem.span(err_c);