mirror of
https://github.com/extism/extism.git
synced 2026-01-09 13:57:55 -05:00
refactor!: Remove context, unify extism-runtime and extism crates (#421)
- Removes the `ExtismContext` type from runtime and all SDKs - Removed SDK functions: `extism_context_new`, `extism_context_reset`, `extism_context_free` - All SDKs have been updated, but there are still some TODOs below - Removes `extism_plugin_update` - Plugins can no longer be updated - a new plugin should be created instead - Adds `extism_plugin_id` to uniquely identify plugins - Merges the `extism-runtime` and `extism` crates (there is no longer an `extism-runtime` crate) - Makes `extism::Manifest` an alias for `extism_manifest::Manifest` instead of a distinct type - Adds `MemoryHandle` type to SDKs to refer to blocks of Extism memory that can be accessed in host functions - Improves thread-safety of Plugins, adds C++ test to call a single plugin from multiple threads. - Expands wasmtime bounds to include 12.0
This commit is contained in:
22
.github/workflows/ci-rust.yml
vendored
22
.github/workflows/ci-rust.yml
vendored
@@ -12,9 +12,8 @@ on:
|
||||
name: Rust CI
|
||||
|
||||
env:
|
||||
RUNTIME_CRATE: extism-runtime
|
||||
RUNTIME_CRATE: extism
|
||||
LIBEXTISM_CRATE: libextism
|
||||
RUST_SDK_CRATE: extism
|
||||
|
||||
jobs:
|
||||
lib:
|
||||
@@ -86,20 +85,9 @@ jobs:
|
||||
- name: Lint
|
||||
run: cargo clippy --release --all-features --no-deps -p ${{ env.RUNTIME_CRATE }}
|
||||
- name: Test
|
||||
run: cargo test --release -p ${{ env.RUNTIME_CRATE }}
|
||||
- name: Test all features
|
||||
run: cargo test --all-features --release -p ${{ env.RUNTIME_CRATE }}
|
||||
- name: Test no features
|
||||
run: cargo test --no-default-features --release -p ${{ env.RUNTIME_CRATE }}
|
||||
|
||||
rust:
|
||||
name: Rust
|
||||
needs: lib
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
rust:
|
||||
- stable
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/extism
|
||||
- name: Test Rust Host SDK
|
||||
run: LD_LIBRARY_PATH=/usr/local/lib cargo test --release -p ${{ env.RUST_SDK_CRATE }}
|
||||
|
||||
6
.github/workflows/release-rust.yaml
vendored
6
.github/workflows/release-rust.yaml
vendored
@@ -26,14 +26,12 @@ jobs:
|
||||
# order of crate publication matter: manifest, runtime, rust
|
||||
cargo publish --manifest-path manifest/Cargo.toml
|
||||
# allow for crates.io to update so dependant crates can locate extism-manifest
|
||||
sleep 5
|
||||
sleep 10
|
||||
|
||||
- name: Release Rust Host SDK
|
||||
- name: Release Runtime
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_API_TOKEN }}
|
||||
run: |
|
||||
|
||||
cargo publish --manifest-path runtime/Cargo.toml --no-verify
|
||||
cargo publish --manifest-path rust/Cargo.toml
|
||||
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
version = 0.24.1
|
||||
version = 0.26.0
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
members = [
|
||||
"manifest",
|
||||
"runtime",
|
||||
"rust",
|
||||
"libextism",
|
||||
]
|
||||
exclude = ["kernel"]
|
||||
|
||||
25
c/main.c
25
c/main.c
@@ -53,30 +53,29 @@ int main(int argc, char *argv[]) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
ExtismContext *ctx = extism_context_new();
|
||||
|
||||
size_t len = 0;
|
||||
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, (const ExtismFunction **)&f, 1, true);
|
||||
|
||||
char *errmsg = NULL;
|
||||
ExtismPlugin *plugin = extism_plugin_new(
|
||||
data, len, (const ExtismFunction **)&f, 1, true, &errmsg);
|
||||
free(data);
|
||||
if (plugin < 0) {
|
||||
puts(extism_error(ctx, -1));
|
||||
if (plugin == NULL) {
|
||||
puts(errmsg);
|
||||
extism_plugin_new_error_free(errmsg);
|
||||
exit(1);
|
||||
}
|
||||
assert(extism_plugin_call(ctx, plugin, "count_vowels", (uint8_t *)argv[1],
|
||||
|
||||
assert(extism_plugin_call(plugin, "count_vowels", (uint8_t *)argv[1],
|
||||
strlen(argv[1])) == 0);
|
||||
ExtismSize out_len = extism_plugin_output_length(ctx, plugin);
|
||||
const uint8_t *output = extism_plugin_output_data(ctx, plugin);
|
||||
ExtismSize out_len = extism_plugin_output_length(plugin);
|
||||
const uint8_t *output = extism_plugin_output_data(plugin);
|
||||
write(STDOUT_FILENO, output, out_len);
|
||||
write(STDOUT_FILENO, "\n", 1);
|
||||
|
||||
extism_plugin_free(ctx, plugin);
|
||||
extism_function_free(f);
|
||||
extism_context_free(ctx);
|
||||
extism_plugin_free(plugin);
|
||||
return 0;
|
||||
}
|
||||
|
||||
183
cpp/extism.hpp
183
cpp/extism.hpp
@@ -221,6 +221,7 @@ public:
|
||||
typedef ExtismValType ValType;
|
||||
typedef ExtismValUnion ValUnion;
|
||||
typedef ExtismVal Val;
|
||||
typedef uint64_t MemoryHandle;
|
||||
|
||||
class CurrentPlugin {
|
||||
ExtismCurrentPlugin *pointer;
|
||||
@@ -229,16 +230,18 @@ public:
|
||||
CurrentPlugin(ExtismCurrentPlugin *p) : pointer(p) {}
|
||||
|
||||
uint8_t *memory() { return extism_current_plugin_memory(this->pointer); }
|
||||
ExtismSize memory_length(uint64_t offs) {
|
||||
uint8_t *memory(MemoryHandle offs) { return this->memory() + offs; }
|
||||
|
||||
ExtismSize memoryLength(MemoryHandle offs) {
|
||||
return extism_current_plugin_memory_length(this->pointer, offs);
|
||||
}
|
||||
|
||||
uint64_t alloc(ExtismSize size) {
|
||||
MemoryHandle 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);
|
||||
void free(MemoryHandle handle) {
|
||||
extism_current_plugin_memory_free(this->pointer, handle);
|
||||
}
|
||||
|
||||
void returnString(Val &output, const std::string &s) {
|
||||
@@ -256,7 +259,7 @@ public:
|
||||
return nullptr;
|
||||
}
|
||||
if (length != nullptr) {
|
||||
*length = this->memory_length(inp.v.i64);
|
||||
*length = this->memoryLength(inp.v.i64);
|
||||
}
|
||||
return this->memory() + inp.v.i64;
|
||||
}
|
||||
@@ -318,7 +321,7 @@ public:
|
||||
this->func = std::shared_ptr<ExtismFunction>(ptr, extism_function_free);
|
||||
}
|
||||
|
||||
void set_namespace(std::string s) {
|
||||
void setNamespace(std::string s) {
|
||||
extism_function_set_namespace(this->func.get(), s.c_str());
|
||||
}
|
||||
|
||||
@@ -336,111 +339,51 @@ public:
|
||||
};
|
||||
|
||||
class Plugin {
|
||||
std::shared_ptr<ExtismContext> context;
|
||||
ExtismPlugin plugin;
|
||||
std::vector<Function> functions;
|
||||
|
||||
public:
|
||||
ExtismPlugin *plugin;
|
||||
// Create a new plugin
|
||||
Plugin(const uint8_t *wasm, ExtismSize length, bool with_wasi = false,
|
||||
std::vector<Function> functions = std::vector<Function>(),
|
||||
std::shared_ptr<ExtismContext> ctx = std::shared_ptr<ExtismContext>(
|
||||
extism_context_new(), extism_context_free))
|
||||
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);
|
||||
|
||||
char *errmsg = nullptr;
|
||||
this->plugin = extism_plugin_new(wasm, length, ptrs.data(), ptrs.size(),
|
||||
with_wasi, &errmsg);
|
||||
if (this->plugin == nullptr) {
|
||||
std::string s(errmsg);
|
||||
extism_plugin_new_error_free(errmsg);
|
||||
throw Error(s);
|
||||
}
|
||||
this->context = ctx;
|
||||
}
|
||||
|
||||
Plugin(const std::string &str, bool with_wasi = false,
|
||||
std::vector<Function> functions = {},
|
||||
std::shared_ptr<ExtismContext> ctx = std::shared_ptr<ExtismContext>(
|
||||
extism_context_new(), extism_context_free))
|
||||
: Plugin((const uint8_t *)str.c_str(), str.size(), with_wasi, functions,
|
||||
ctx) {}
|
||||
std::vector<Function> functions = {})
|
||||
: Plugin((const uint8_t *)str.c_str(), str.size(), with_wasi, functions) {
|
||||
}
|
||||
|
||||
Plugin(const std::vector<uint8_t> &data, bool with_wasi = false,
|
||||
std::vector<Function> functions = {},
|
||||
std::shared_ptr<ExtismContext> ctx = std::shared_ptr<ExtismContext>(
|
||||
extism_context_new(), extism_context_free))
|
||||
: Plugin(data.data(), data.size(), with_wasi, functions, ctx) {}
|
||||
std::vector<Function> functions = {})
|
||||
: Plugin(data.data(), data.size(), with_wasi, functions) {}
|
||||
|
||||
CancelHandle cancel_handle() {
|
||||
return CancelHandle(
|
||||
extism_plugin_cancel_handle(this->context.get(), this->id()));
|
||||
CancelHandle cancelHandle() {
|
||||
return CancelHandle(extism_plugin_cancel_handle(this->plugin));
|
||||
}
|
||||
|
||||
#ifndef EXTISM_NO_JSON
|
||||
// Create a new plugin from Manifest
|
||||
Plugin(const Manifest &manifest, bool with_wasi = false,
|
||||
std::vector<Function> functions = {},
|
||||
std::shared_ptr<ExtismContext> ctx = std::shared_ptr<ExtismContext>(
|
||||
extism_context_new(), extism_context_free)) {
|
||||
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(), 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);
|
||||
}
|
||||
this->context = ctx;
|
||||
}
|
||||
#endif
|
||||
std::vector<Function> functions = {})
|
||||
: Plugin(manifest.json().c_str(), with_wasi, functions) {}
|
||||
|
||||
~Plugin() {
|
||||
extism_plugin_free(this->context.get(), this->plugin);
|
||||
this->plugin = -1;
|
||||
}
|
||||
|
||||
ExtismPlugin id() const { return this->plugin; }
|
||||
|
||||
ExtismContext *get_context() const { return this->context.get(); }
|
||||
|
||||
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, 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);
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef EXTISM_NO_JSON
|
||||
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(), 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);
|
||||
}
|
||||
extism_plugin_free(this->plugin);
|
||||
this->plugin = nullptr;
|
||||
}
|
||||
|
||||
void config(const Config &data) {
|
||||
@@ -457,10 +400,9 @@ public:
|
||||
#endif
|
||||
|
||||
void config(const char *json, size_t length) {
|
||||
bool b = extism_plugin_config(this->context.get(), this->plugin,
|
||||
(const uint8_t *)json, length);
|
||||
bool b = extism_plugin_config(this->plugin, (const uint8_t *)json, length);
|
||||
if (!b) {
|
||||
const char *err = extism_error(this->context.get(), this->plugin);
|
||||
const char *err = extism_plugin_error(this->plugin);
|
||||
throw Error(err == nullptr ? "Unable to update plugin config" : err);
|
||||
}
|
||||
}
|
||||
@@ -472,10 +414,10 @@ public:
|
||||
// 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,
|
||||
func.c_str(), input, input_length);
|
||||
int32_t rc =
|
||||
extism_plugin_call(this->plugin, func.c_str(), input, input_length);
|
||||
if (rc != 0) {
|
||||
const char *error = extism_error(this->context.get(), this->plugin);
|
||||
const char *error = extism_plugin_error(this->plugin);
|
||||
if (error == nullptr) {
|
||||
throw Error("extism_call failed");
|
||||
}
|
||||
@@ -483,10 +425,8 @@ public:
|
||||
throw Error(error);
|
||||
}
|
||||
|
||||
ExtismSize length =
|
||||
extism_plugin_output_length(this->context.get(), this->plugin);
|
||||
const uint8_t *ptr =
|
||||
extism_plugin_output_data(this->context.get(), this->plugin);
|
||||
ExtismSize length = extism_plugin_output_length(this->plugin);
|
||||
const uint8_t *ptr = extism_plugin_output_data(this->plugin);
|
||||
return Buffer(ptr, length);
|
||||
}
|
||||
|
||||
@@ -503,56 +443,13 @@ public:
|
||||
}
|
||||
|
||||
// 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());
|
||||
bool functionExists(const std::string &func) const {
|
||||
return extism_plugin_function_exists(this->plugin, func.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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(wasm, length, with_wasi, functions, this->pointer);
|
||||
}
|
||||
|
||||
// Create plugin from std::string
|
||||
Plugin plugin(const std::string &str, bool with_wasi = false,
|
||||
std::vector<Function> functions = {}) const {
|
||||
return Plugin((const uint8_t *)str.c_str(), str.size(), with_wasi,
|
||||
functions, this->pointer);
|
||||
}
|
||||
|
||||
// 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(data.data(), data.size(), with_wasi, functions,
|
||||
this->pointer);
|
||||
}
|
||||
|
||||
#ifndef EXTISM_NO_JSON
|
||||
// Create plugin from Manifest
|
||||
Plugin plugin(const Manifest &manifest, bool with_wasi = false,
|
||||
std::vector<Function> functions = {}) const {
|
||||
return Plugin(manifest, with_wasi, functions, this->pointer);
|
||||
}
|
||||
#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) {
|
||||
inline bool setLogFile(const char *filename, const char *level) {
|
||||
return extism_log_file(filename, level);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "../extism.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <thread>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
@@ -15,16 +16,10 @@ const std::string code = "../../wasm/code.wasm";
|
||||
namespace {
|
||||
using namespace extism;
|
||||
|
||||
TEST(Context, Basic) {
|
||||
Context context;
|
||||
ASSERT_NE(context.pointer, nullptr);
|
||||
}
|
||||
|
||||
TEST(Plugin, Manifest) {
|
||||
Manifest manifest = Manifest::path(code);
|
||||
manifest.set_config("a", "1");
|
||||
|
||||
ASSERT_NO_THROW(Plugin plugin(manifest));
|
||||
Plugin plugin(manifest);
|
||||
|
||||
Buffer buf = plugin.call("count_vowels", "this is a test");
|
||||
@@ -37,19 +32,17 @@ TEST(Plugin, BadManifest) {
|
||||
}
|
||||
|
||||
TEST(Plugin, Bytes) {
|
||||
Context context;
|
||||
auto wasm = read(code.c_str());
|
||||
ASSERT_NO_THROW(Plugin plugin = context.plugin(wasm));
|
||||
Plugin plugin = context.plugin(wasm);
|
||||
ASSERT_NO_THROW(Plugin plugin(wasm));
|
||||
Plugin plugin(wasm);
|
||||
|
||||
Buffer buf = plugin.call("count_vowels", "this is another test");
|
||||
ASSERT_EQ(buf.string(), "{\"count\": 6}");
|
||||
}
|
||||
|
||||
TEST(Plugin, UpdateConfig) {
|
||||
Context context;
|
||||
auto wasm = read(code.c_str());
|
||||
Plugin plugin = context.plugin(wasm);
|
||||
Plugin plugin(wasm);
|
||||
|
||||
Config config;
|
||||
config["abc"] = "123";
|
||||
@@ -57,12 +50,11 @@ TEST(Plugin, UpdateConfig) {
|
||||
}
|
||||
|
||||
TEST(Plugin, FunctionExists) {
|
||||
Context context;
|
||||
auto wasm = read(code.c_str());
|
||||
Plugin plugin = context.plugin(wasm);
|
||||
Plugin plugin(wasm);
|
||||
|
||||
ASSERT_FALSE(plugin.function_exists("bad_function"));
|
||||
ASSERT_TRUE(plugin.function_exists("count_vowels"));
|
||||
ASSERT_FALSE(plugin.functionExists("bad_function"));
|
||||
ASSERT_TRUE(plugin.functionExists("count_vowels"));
|
||||
}
|
||||
|
||||
TEST(Plugin, HostFunction) {
|
||||
@@ -85,6 +77,38 @@ TEST(Plugin, HostFunction) {
|
||||
ASSERT_EQ((std::string)buf, "test");
|
||||
}
|
||||
|
||||
void callThread(Plugin *plugin) {
|
||||
auto buf = plugin->call("count_vowels", "aaa").string();
|
||||
ASSERT_EQ(buf.size(), 10);
|
||||
ASSERT_EQ(buf, "testing123");
|
||||
}
|
||||
|
||||
TEST(Plugin, MultipleThreads) {
|
||||
auto wasm = read("../../wasm/code-functions.wasm");
|
||||
auto t = std::vector<ValType>{ValType::I64};
|
||||
Function hello_world =
|
||||
Function("hello_world", t, t,
|
||||
[](CurrentPlugin plugin, const std::vector<Val> ¶ms,
|
||||
std::vector<Val> &results, void *user_data) {
|
||||
auto offs = plugin.alloc(10);
|
||||
memcpy(plugin.memory() + offs, "testing123", 10);
|
||||
results[0].v.i64 = (int64_t)offs;
|
||||
});
|
||||
auto functions = std::vector<Function>{
|
||||
hello_world,
|
||||
};
|
||||
Plugin plugin(wasm, true, functions);
|
||||
|
||||
std::vector<std::thread> threads;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
threads.push_back(std::thread(callThread, &plugin));
|
||||
}
|
||||
|
||||
for (auto &th : threads) {
|
||||
th.join();
|
||||
}
|
||||
}
|
||||
|
||||
}; // namespace
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
|
||||
@@ -4,8 +4,6 @@ using Extism.Sdk.Native;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
var context = new Context();
|
||||
|
||||
var userData = Marshal.StringToHGlobalAnsi("Hello again!");
|
||||
|
||||
using var helloWorld = new HostFunction(
|
||||
@@ -30,7 +28,7 @@ void HelloWorld(CurrentPlugin plugin, Span<ExtismVal> inputs, Span<ExtismVal> ou
|
||||
}
|
||||
|
||||
var wasm = File.ReadAllBytes("./code-functions.wasm");
|
||||
using var plugin = context.CreatePlugin(wasm, new[] { helloWorld }, withWasi: true);
|
||||
using var plugin = new Plugin(wasm, new[] { helloWorld }, withWasi: true);
|
||||
|
||||
var output = Encoding.UTF8.GetString(
|
||||
plugin.CallFunction("count_vowels", Encoding.UTF8.GetBytes("Hello World!"))
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Extism.Sdk.Native;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an Extism context through which you can load <see cref="Plugin"/>s.
|
||||
/// </summary>
|
||||
public unsafe class Context : IDisposable
|
||||
{
|
||||
private readonly ConcurrentDictionary<int, Plugin> _plugins = new ConcurrentDictionary<int, Plugin>();
|
||||
|
||||
private const int DisposedMarker = 1;
|
||||
|
||||
private int _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new Extism Context.
|
||||
/// </summary>
|
||||
public Context()
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
NativeHandle = LibExtism.extism_context_new();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Native pointer to the Extism Context.
|
||||
/// </summary>
|
||||
internal LibExtism.ExtismContext* NativeHandle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Loads an Extism <see cref="Plugin"/>.
|
||||
/// </summary>
|
||||
/// <param name="wasm">A WASM module (wat or wasm) or a JSON encoded manifest.</param>
|
||||
/// <param name="functions">List of host functions expected by the plugin.</param>
|
||||
/// <param name="withWasi">Enable/Disable WASI.</param>
|
||||
public Plugin CreatePlugin(ReadOnlySpan<byte> wasm, HostFunction[] functions, bool withWasi)
|
||||
{
|
||||
CheckNotDisposed();
|
||||
|
||||
var functionHandles = functions.Select(f => f.NativeHandle).ToArray();
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* wasmPtr = wasm)
|
||||
fixed (IntPtr* functionsPtr = functionHandles)
|
||||
{
|
||||
var index = LibExtism.extism_plugin_new(NativeHandle, wasmPtr, wasm.Length, functionsPtr, functions.Length, withWasi);
|
||||
if (index == -1)
|
||||
{
|
||||
var errorMsg = GetError();
|
||||
if (errorMsg != null)
|
||||
{
|
||||
throw new ExtismException(errorMsg);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ExtismException("Failed to create plugin.");
|
||||
}
|
||||
}
|
||||
|
||||
return _plugins[index] = new Plugin(this, functions, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a plugin by index.
|
||||
/// </summary>
|
||||
/// <param name="index">Index of plugin.</param>
|
||||
/// <returns></returns>
|
||||
public Plugin GetPlugin(int index)
|
||||
{
|
||||
return _plugins[index];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove all plugins from this <see cref="Context"/>'s registry.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
CheckNotDisposed();
|
||||
|
||||
LibExtism.extism_context_reset(NativeHandle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get this this <see cref="Context"/>'s last error.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
internal string? GetError()
|
||||
{
|
||||
CheckNotDisposed();
|
||||
|
||||
var result = LibExtism.extism_error(NativeHandle, -1);
|
||||
return Marshal.PtrToStringUTF8(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Frees all resources held by this Context.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, DisposedMarker) == DisposedMarker)
|
||||
{
|
||||
// Already disposed.
|
||||
return;
|
||||
}
|
||||
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throw an appropriate exception if the plugin has been disposed.
|
||||
/// </summary>
|
||||
/// <exception cref="ObjectDisposedException"></exception>
|
||||
protected void CheckNotDisposed()
|
||||
{
|
||||
Interlocked.MemoryBarrier();
|
||||
if (_disposed == DisposedMarker)
|
||||
{
|
||||
ThrowDisposedException();
|
||||
}
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
private static void ThrowDisposedException()
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(Context));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Frees all resources held by this Context.
|
||||
/// </summary>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// Free up any managed resources here
|
||||
}
|
||||
|
||||
foreach (var plugin in _plugins.Values)
|
||||
{
|
||||
plugin.Dispose();
|
||||
}
|
||||
|
||||
// Free up unmanaged resources
|
||||
LibExtism.extism_context_free(NativeHandle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Destructs the current Context and frees all resources used by it.
|
||||
/// </summary>
|
||||
~Context()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the Extism version string.
|
||||
/// </summary>
|
||||
public static string GetExtismVersion()
|
||||
{
|
||||
var pointer = LibExtism.extism_version();
|
||||
return Marshal.PtrToStringUTF8(pointer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set Extism's log file and level. This is applied for all <see cref="Context"/>s.
|
||||
/// </summary>
|
||||
/// <param name="logPath">Log file; can be 'stdout' or 'stderr' to write logs to the console.</param>
|
||||
/// <param name="level">The log level to write at.</param>
|
||||
public static bool SetExtismLogFile(string logPath, LogLevel level)
|
||||
{
|
||||
var logLevel = level switch
|
||||
{
|
||||
LogLevel.Error => LibExtism.LogLevels.Error,
|
||||
LogLevel.Warning => LibExtism.LogLevels.Warn,
|
||||
LogLevel.Info => LibExtism.LogLevels.Info,
|
||||
LogLevel.Debug => LibExtism.LogLevels.Debug,
|
||||
LogLevel.Trace => LibExtism.LogLevels.Trace,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
|
||||
return LibExtism.extism_log_file(logPath, logLevel);
|
||||
}
|
||||
}
|
||||
@@ -97,10 +97,10 @@ public struct ExtismVal
|
||||
internal static class LibExtism
|
||||
{
|
||||
/// <summary>
|
||||
/// A `Context` is used to store and manage plugins.
|
||||
/// An Extism Plugin
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct ExtismContext { }
|
||||
internal struct ExtismPlugin { }
|
||||
|
||||
/// <summary>
|
||||
/// Host function signature
|
||||
@@ -180,24 +180,9 @@ internal static class LibExtism
|
||||
[DllImport("extism", EntryPoint = "extism_function_free")]
|
||||
internal static extern void extism_function_free(IntPtr ptr);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new context.
|
||||
/// </summary>
|
||||
/// <returns>A pointer to the newly created context.</returns>
|
||||
[DllImport("extism")]
|
||||
unsafe internal static extern ExtismContext* extism_context_new();
|
||||
|
||||
/// <summary>
|
||||
/// Remove a context from the registry and free associated memory.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
[DllImport("extism")]
|
||||
unsafe internal static extern void extism_context_free(ExtismContext* context);
|
||||
|
||||
/// <summary>
|
||||
/// Load a WASM plugin.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
@@ -205,98 +190,68 @@ internal static class LibExtism
|
||||
/// <param name="withWasi">Enables/disables WASI.</param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
unsafe internal static extern int extism_plugin_new(ExtismContext* context, byte* wasm, int wasmSize, IntPtr* functions, int nFunctions, bool withWasi);
|
||||
|
||||
/// <summary>
|
||||
/// Update a plugin, keeping the existing ID.
|
||||
/// Similar to <see cref="extism_plugin_new"/> but takes an `plugin` argument to specify which plugin to update.
|
||||
/// Memory for this plugin will be reset upon update.
|
||||
/// </summary>
|
||||
/// <param name="context">Pointer to the context the plugin is associated with.</param>
|
||||
/// <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="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 internal static extern bool extism_plugin_update(ExtismContext* context, int plugin, byte* wasm, long wasmSize, Span<IntPtr> functions, long nFunctions, bool withWasi);
|
||||
unsafe internal static extern ExtismPlugin* extism_plugin_new(byte* wasm, int wasmSize, IntPtr* functions, int nFunctions, bool withWasi, IntPtr* errmsg);
|
||||
|
||||
/// <summary>
|
||||
/// Remove a plugin from the registry and free associated memory.
|
||||
/// </summary>
|
||||
/// <param name="context">Pointer to the context the plugin is associated with.</param>
|
||||
/// <param name="plugin">Pointer to the plugin you want to free.</param>
|
||||
[DllImport("extism")]
|
||||
unsafe internal static extern void extism_plugin_free(ExtismContext* context, int plugin);
|
||||
|
||||
/// <summary>
|
||||
/// Remove all plugins from the registry.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
[DllImport("extism")]
|
||||
unsafe internal static extern void extism_context_reset(ExtismContext* context);
|
||||
unsafe internal static extern void extism_plugin_free(ExtismPlugin* plugin);
|
||||
|
||||
/// <summary>
|
||||
/// Update plugin config values, this will merge with the existing values.
|
||||
/// </summary>
|
||||
/// <param name="context">Pointer to the context the plugin is associated with.</param>
|
||||
/// <param name="plugin">Pointer to the plugin you want to update the configurations for.</param>
|
||||
/// <param name="json">The configuration JSON encoded in UTF8.</param>
|
||||
/// <param name="jsonLength">The length of the `json` parameter.</param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
unsafe internal static extern bool extism_plugin_config(ExtismContext* context, int plugin, byte* json, int jsonLength);
|
||||
unsafe internal static extern bool extism_plugin_config(ExtismPlugin* plugin, byte* json, int jsonLength);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if funcName exists.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="plugin"></param>
|
||||
/// <param name="funcName"></param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
unsafe internal static extern bool extism_plugin_function_exists(ExtismContext* context, int plugin, string funcName);
|
||||
unsafe internal static extern bool extism_plugin_function_exists(ExtismPlugin* plugin, string funcName);
|
||||
|
||||
/// <summary>
|
||||
/// Call a function.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="plugin"></param>
|
||||
/// <param name="funcName">The function to call.</param>
|
||||
/// <param name="data">Input data.</param>
|
||||
/// <param name="dataLen">The length of the `data` parameter.</param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
unsafe internal static extern int extism_plugin_call(ExtismContext* context, int plugin, string funcName, byte* data, int dataLen);
|
||||
unsafe internal static extern int extism_plugin_call(ExtismPlugin* plugin, string funcName, byte* data, int dataLen);
|
||||
|
||||
/// <summary>
|
||||
/// Get the error associated with a Context or Plugin, if plugin is -1 then the context error will be returned.
|
||||
/// Get the error associated with a Plugin
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="plugin">A plugin pointer, or -1 for the context error.</param>
|
||||
/// <param name="plugin">A plugin pointer</param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
unsafe internal static extern IntPtr extism_error(ExtismContext* context, nint plugin);
|
||||
unsafe internal static extern IntPtr extism_plugin_error(ExtismPlugin* plugin);
|
||||
|
||||
/// <summary>
|
||||
/// Get the length of a plugin's output data.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="plugin"></param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
unsafe internal static extern long extism_plugin_output_length(ExtismContext* context, int plugin);
|
||||
unsafe internal static extern long extism_plugin_output_length(ExtismPlugin* plugin);
|
||||
|
||||
/// <summary>
|
||||
/// Get the plugin's output data.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="plugin"></param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
unsafe internal static extern IntPtr extism_plugin_output_data(ExtismContext* context, int plugin);
|
||||
unsafe internal static extern IntPtr extism_plugin_output_data(ExtismPlugin* plugin);
|
||||
|
||||
/// <summary>
|
||||
/// Set log file and level.
|
||||
@@ -308,11 +263,11 @@ internal static class LibExtism
|
||||
internal static extern bool extism_log_file(string filename, string logLevel);
|
||||
|
||||
/// <summary>
|
||||
/// Get the Extism version string.
|
||||
/// Get the Extism Plugin ID, a 16-bit UUID in host order
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism", EntryPoint = "extism_version")]
|
||||
internal static extern IntPtr extism_version();
|
||||
// [DllImport("extism")]
|
||||
// unsafe internal static extern IntPtr extism_plugin_id(ExtismPlugin* plugin);
|
||||
|
||||
/// <summary>
|
||||
/// Extism Log Levels
|
||||
|
||||
@@ -6,51 +6,43 @@ namespace Extism.Sdk.Native;
|
||||
/// <summary>
|
||||
/// Represents a WASM Extism plugin.
|
||||
/// </summary>
|
||||
public class Plugin : IDisposable
|
||||
public unsafe class Plugin : IDisposable
|
||||
{
|
||||
private const int DisposedMarker = 1;
|
||||
|
||||
private readonly Context _context;
|
||||
private readonly HostFunction[] _functions;
|
||||
private int _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Native pointer to the Extism Plugin.
|
||||
/// </summary>
|
||||
internal LibExtism.ExtismPlugin* NativeHandle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a and load a plug-in
|
||||
/// Using this constructor will give the plug-in it's own internal Context
|
||||
/// </summary>
|
||||
/// <param name="wasm">A WASM module (wat or wasm) or a JSON encoded manifest.</param>
|
||||
/// <param name="functions">List of host functions expected by the plugin.</param>
|
||||
/// <param name="withWasi">Enable/Disable WASI.</param>
|
||||
public static Plugin Create(ReadOnlySpan<byte> wasm, HostFunction[] functions, bool withWasi) {
|
||||
var context = new Context();
|
||||
return context.CreatePlugin(wasm, functions, withWasi);
|
||||
}
|
||||
|
||||
internal Plugin(Context context, HostFunction[] functions, int index)
|
||||
{
|
||||
_context = context;
|
||||
public Plugin(ReadOnlySpan<byte> wasm, HostFunction[] functions, bool withWasi) {
|
||||
_functions = functions;
|
||||
Index = index;
|
||||
}
|
||||
var functionHandles = functions.Select(f => f.NativeHandle).ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// A pointer to the native Plugin struct.
|
||||
/// </summary>
|
||||
internal int Index { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Update a plugin, keeping the existing ID.
|
||||
/// </summary>
|
||||
/// <param name="wasm">The plugin WASM bytes.</param>
|
||||
/// <param name="withWasi">Enable/Disable WASI.</param>
|
||||
unsafe public bool Update(ReadOnlySpan<byte> wasm, bool withWasi)
|
||||
{
|
||||
CheckNotDisposed();
|
||||
|
||||
var functions = _functions.Select(f => f.NativeHandle).ToArray();
|
||||
fixed (byte* wasmPtr = wasm)
|
||||
unsafe
|
||||
{
|
||||
return LibExtism.extism_plugin_update(_context.NativeHandle, Index, wasmPtr, wasm.Length, functions, 0, withWasi);
|
||||
fixed (byte* wasmPtr = wasm)
|
||||
fixed (IntPtr* functionsPtr = functionHandles)
|
||||
{
|
||||
NativeHandle = LibExtism.extism_plugin_new(wasmPtr, wasm.Length, functionsPtr, functions.Length, withWasi, null);
|
||||
if (NativeHandle == null)
|
||||
{
|
||||
throw new ExtismException("Unable to create plugin");
|
||||
// TODO: handle error
|
||||
// var s = Marshal.PtrToStringUTF8(result);
|
||||
// LibExtism.extism_plugin_new_error_free(errmsg);
|
||||
// throw new ExtismException(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +56,7 @@ public class Plugin : IDisposable
|
||||
|
||||
fixed (byte* jsonPtr = json)
|
||||
{
|
||||
return LibExtism.extism_plugin_config(_context.NativeHandle, Index, jsonPtr, json.Length);
|
||||
return LibExtism.extism_plugin_config(NativeHandle, jsonPtr, json.Length);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +67,7 @@ public class Plugin : IDisposable
|
||||
{
|
||||
CheckNotDisposed();
|
||||
|
||||
return LibExtism.extism_plugin_function_exists(_context.NativeHandle, Index, name);
|
||||
return LibExtism.extism_plugin_function_exists(NativeHandle, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -93,7 +85,7 @@ public class Plugin : IDisposable
|
||||
|
||||
fixed (byte* dataPtr = data)
|
||||
{
|
||||
int response = LibExtism.extism_plugin_call(_context.NativeHandle, Index, functionName, dataPtr, data.Length);
|
||||
int response = LibExtism.extism_plugin_call(NativeHandle, functionName, dataPtr, data.Length);
|
||||
if (response == 0)
|
||||
{
|
||||
return OutputData();
|
||||
@@ -121,7 +113,7 @@ public class Plugin : IDisposable
|
||||
{
|
||||
CheckNotDisposed();
|
||||
|
||||
return (int)LibExtism.extism_plugin_output_length(_context.NativeHandle, Index);
|
||||
return (int)LibExtism.extism_plugin_output_length(NativeHandle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -135,7 +127,7 @@ public class Plugin : IDisposable
|
||||
|
||||
unsafe
|
||||
{
|
||||
var ptr = LibExtism.extism_plugin_output_data(_context.NativeHandle, Index).ToPointer();
|
||||
var ptr = LibExtism.extism_plugin_output_data(NativeHandle).ToPointer();
|
||||
return new Span<byte>(ptr, length);
|
||||
}
|
||||
}
|
||||
@@ -148,7 +140,7 @@ public class Plugin : IDisposable
|
||||
{
|
||||
CheckNotDisposed();
|
||||
|
||||
var result = LibExtism.extism_error(_context.NativeHandle, Index);
|
||||
var result = LibExtism.extism_plugin_error(NativeHandle);
|
||||
return Marshal.PtrToStringUTF8(result);
|
||||
}
|
||||
|
||||
@@ -197,7 +189,7 @@ public class Plugin : IDisposable
|
||||
}
|
||||
|
||||
// Free up unmanaged resources
|
||||
LibExtism.extism_plugin_free(_context.NativeHandle, Index);
|
||||
LibExtism.extism_plugin_free(NativeHandle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -10,25 +10,12 @@ namespace Extism.Sdk.Tests;
|
||||
|
||||
public class BasicTests
|
||||
{
|
||||
[Fact]
|
||||
public void CountHelloWorldVowelsWithoutContext()
|
||||
{
|
||||
var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
|
||||
var wasm = File.ReadAllBytes(Path.Combine(binDirectory, "code.wasm"));
|
||||
using var plugin = Plugin.Create(wasm, Array.Empty<HostFunction>(), withWasi: true);
|
||||
|
||||
var response = plugin.CallFunction("count_vowels", Encoding.UTF8.GetBytes("Hello World"));
|
||||
Assert.Equal("{\"count\": 3}", Encoding.UTF8.GetString(response));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CountHelloWorldVowels()
|
||||
{
|
||||
using var context = new Context();
|
||||
|
||||
var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
|
||||
var wasm = File.ReadAllBytes(Path.Combine(binDirectory, "code.wasm"));
|
||||
using var plugin = context.CreatePlugin(wasm, Array.Empty<HostFunction>(), withWasi: true);
|
||||
using var plugin = new Plugin(wasm, Array.Empty<HostFunction>(), withWasi: true);
|
||||
|
||||
var response = plugin.CallFunction("count_vowels", Encoding.UTF8.GetBytes("Hello World"));
|
||||
Assert.Equal("{\"count\": 3}", Encoding.UTF8.GetString(response));
|
||||
@@ -37,8 +24,6 @@ public class BasicTests
|
||||
[Fact]
|
||||
public void CountVowelsHostFunctions()
|
||||
{
|
||||
using var context = new Context();
|
||||
|
||||
var userData = Marshal.StringToHGlobalAnsi("Hello again!");
|
||||
|
||||
using var helloWorld = new HostFunction(
|
||||
@@ -51,7 +36,7 @@ public class BasicTests
|
||||
|
||||
var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
|
||||
var wasm = File.ReadAllBytes(Path.Combine(binDirectory, "code-functions.wasm"));
|
||||
using var plugin = context.CreatePlugin(wasm, new[] { helloWorld }, withWasi: true);
|
||||
using var plugin = new Plugin(wasm, new[] { helloWorld }, withWasi: true);
|
||||
|
||||
var response = plugin.CallFunction("count_vowels", Encoding.UTF8.GetBytes("Hello World"));
|
||||
Assert.Equal("{\"count\": 3}", Encoding.UTF8.GetString(response));
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
(extism-manifest (= :version))
|
||||
(ppx_inline_test (>= v0.15.0))
|
||||
(cmdliner (>= 1.1.1))
|
||||
(uuidm (>= 0.9.0))
|
||||
)
|
||||
(tags
|
||||
(topics wasm plugin)))
|
||||
|
||||
@@ -5,7 +5,7 @@ prepare:
|
||||
mix compile
|
||||
|
||||
test: prepare
|
||||
mix test
|
||||
mix test -v
|
||||
|
||||
clean:
|
||||
mix clean
|
||||
|
||||
@@ -23,12 +23,9 @@ end
|
||||
### Example
|
||||
|
||||
```elixir
|
||||
# Create a context for which plugins can be allocated and cleaned
|
||||
ctx = Extism.Context.new()
|
||||
|
||||
# point to some wasm code, this is the count_vowels example that ships with extism
|
||||
manifest = %{ wasm: [ %{ path: "/Users/ben/code/extism/wasm/code.wasm" } ]}
|
||||
{:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
|
||||
{:ok, plugin} = Extism.Plugin.new(manifest, false)
|
||||
# {:ok,
|
||||
# %Extism.Plugin{
|
||||
# resource: 0,
|
||||
@@ -38,36 +35,20 @@ manifest = %{ wasm: [ %{ path: "/Users/ben/code/extism/wasm/code.wasm" } ]}
|
||||
# {:ok, "{\"count\": 4}"}
|
||||
{:ok, result} = JSON.decode(output)
|
||||
# {:ok, %{"count" => 4}}
|
||||
|
||||
# free up the context and any plugins we allocated
|
||||
Extism.Context.free(ctx)
|
||||
```
|
||||
|
||||
### Modules
|
||||
|
||||
The two primary modules you should learn are:
|
||||
The primary modules you should learn is:
|
||||
|
||||
* [Extism.Context](Extism.Context.html)
|
||||
* [Extism.Plugin](Extism.Plugin.html)
|
||||
|
||||
#### Context
|
||||
|
||||
The [Context](Extism.Context.html) can be thought of as a session. You need a context to interact with the Extism runtime. The context holds your plugins and when you free the context, it frees your plugins. It's important to free up your context and plugins when you are done with them.
|
||||
|
||||
```elixir
|
||||
ctx = Extism.Context.new()
|
||||
# frees all the plugins
|
||||
Extism.Context.reset(ctx)
|
||||
# frees the context and all its plugins
|
||||
Extism.Context.free(ctx)
|
||||
```
|
||||
|
||||
#### Plugin
|
||||
|
||||
The [Plugin](Extism.Plugin.html) represents an instance of your WASM program from the given manifest.
|
||||
The key method to know here is [Extism.Plugin#call](Extism.Plugin.html#call/3) which takes a function name to invoke and some input data, and returns the results from the plugin.
|
||||
|
||||
```elixir
|
||||
{:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
|
||||
{:ok, plugin} = Extism.Plugin.new(manifest, false)
|
||||
{:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
|
||||
```
|
||||
|
||||
@@ -4,7 +4,7 @@ defmodule Extism.CancelHandle do
|
||||
thread while running.
|
||||
"""
|
||||
defstruct [
|
||||
# The actual NIF Resource. PluginIndex and the context
|
||||
# The actual NIF Resource
|
||||
handle: nil
|
||||
]
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
defmodule Extism.Context do
|
||||
@moduledoc """
|
||||
A Context is needed to create plugins. The Context is where your plugins
|
||||
live. Freeing the context frees all of the plugins in its scope.
|
||||
"""
|
||||
|
||||
defstruct [
|
||||
# The actual NIF Resource. A pointer in this case
|
||||
ptr: nil
|
||||
]
|
||||
|
||||
def wrap_resource(ptr) do
|
||||
%__MODULE__{
|
||||
ptr: ptr
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a new context.
|
||||
"""
|
||||
def new() do
|
||||
ptr = Extism.Native.context_new()
|
||||
Extism.Context.wrap_resource(ptr)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Resets the context. This has the effect of freeing all the plugins created so far.
|
||||
"""
|
||||
def reset(ctx) do
|
||||
Extism.Native.context_reset(ctx.ptr)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Frees the context from memory and all of its plugins.
|
||||
"""
|
||||
def free(ctx) do
|
||||
Extism.Native.context_free(ctx.ptr)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create a new plugin from a WASM module or manifest
|
||||
|
||||
## Examples:
|
||||
|
||||
iex> ctx = Extism.Context.new()
|
||||
iex> manifest = %{ wasm: [ %{ path: "/Users/ben/code/extism/wasm/code.wasm" } ]}
|
||||
iex> {:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
|
||||
|
||||
## Parameters
|
||||
|
||||
- ctx: The Context to manage this plugin
|
||||
- manifest: The String or Map of the WASM module or [manifest](https://extism.org/docs/concepts/manifest)
|
||||
- wasi: A bool you set to true if you want WASI support
|
||||
|
||||
"""
|
||||
def new_plugin(ctx, manifest, wasi \\ false) do
|
||||
{:ok, manifest_payload} = JSON.encode(manifest)
|
||||
|
||||
case Extism.Native.plugin_new_with_manifest(ctx.ptr, manifest_payload, wasi) do
|
||||
{:error, err} -> {:error, err}
|
||||
res -> {:ok, Extism.Plugin.wrap_resource(ctx, res)}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -7,16 +7,12 @@ defmodule Extism.Native do
|
||||
otp_app: :extism,
|
||||
crate: :extism_nif
|
||||
|
||||
def context_new(), do: error()
|
||||
def context_reset(_ctx), do: error()
|
||||
def context_free(_ctx), do: error()
|
||||
def plugin_new_with_manifest(_ctx, _manifest, _wasi), do: error()
|
||||
def plugin_call(_ctx, _plugin_id, _name, _input), do: error()
|
||||
def plugin_update_manifest(_ctx, _plugin_id, _manifest, _wasi), do: error()
|
||||
def plugin_has_function(_ctx, _plugin_id, _function_name), do: error()
|
||||
def plugin_free(_ctx, _plugin_id), do: error()
|
||||
def plugin_new_with_manifest(_manifest, _wasi), do: error()
|
||||
def plugin_call(_plugin, _name, _input), do: error()
|
||||
def plugin_has_function(_plugin, _function_name), do: error()
|
||||
def plugin_free(_plugin), do: error()
|
||||
def set_log_file(_filename, _level), do: error()
|
||||
def plugin_cancel_handle(_ctx, _plugin_id), do: error()
|
||||
def plugin_cancel_handle(_plugin), do: error()
|
||||
def plugin_cancel(_handle), do: error()
|
||||
|
||||
defp error, do: :erlang.nif_error(:nif_not_loaded)
|
||||
|
||||
@@ -3,28 +3,25 @@ defmodule Extism.Plugin do
|
||||
A Plugin represents an instance of your WASM program from the given manifest.
|
||||
"""
|
||||
defstruct [
|
||||
# The actual NIF Resource. PluginIndex and the context
|
||||
plugin_id: nil,
|
||||
ctx: nil
|
||||
# The actual NIF Resource
|
||||
plugin: nil,
|
||||
]
|
||||
|
||||
def wrap_resource(ctx, plugin_id) do
|
||||
def wrap_resource(plugin) do
|
||||
%__MODULE__{
|
||||
ctx: ctx,
|
||||
plugin_id: plugin_id
|
||||
plugin: plugin
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a new plugin
|
||||
"""
|
||||
def new(manifest, wasi \\ false, context \\ nil) do
|
||||
ctx = context || Extism.Context.new()
|
||||
def new(manifest, wasi \\ false) do
|
||||
{:ok, manifest_payload} = JSON.encode(manifest)
|
||||
|
||||
case Extism.Native.plugin_new_with_manifest(ctx.ptr, manifest_payload, wasi) do
|
||||
case Extism.Native.plugin_new_with_manifest(manifest_payload, wasi) do
|
||||
{:error, err} -> {:error, err}
|
||||
res -> {:ok, Extism.Plugin.wrap_resource(ctx, res)}
|
||||
res -> {:ok, Extism.Plugin.wrap_resource(res)}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -49,49 +46,24 @@ defmodule Extism.Plugin do
|
||||
|
||||
"""
|
||||
def call(plugin, name, input) do
|
||||
case Extism.Native.plugin_call(plugin.ctx.ptr, plugin.plugin_id, name, input) do
|
||||
case Extism.Native.plugin_call(plugin.plugin, name, input) do
|
||||
{:error, err} -> {:error, err}
|
||||
res -> {:ok, res}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates the manifest of the given plugin
|
||||
|
||||
## Parameters
|
||||
|
||||
- ctx: The Context to manage this plugin
|
||||
- manifest: The String or Map of the WASM module or [manifest](https://extism.org/docs/concepts/manifest)
|
||||
- wasi: A bool you set to true if you want WASI support
|
||||
|
||||
|
||||
"""
|
||||
def update(plugin, manifest, wasi) when is_map(manifest) do
|
||||
{:ok, manifest_payload} = JSON.encode(manifest)
|
||||
|
||||
case Extism.Native.plugin_update_manifest(
|
||||
plugin.ctx.ptr,
|
||||
plugin.plugin_id,
|
||||
manifest_payload,
|
||||
wasi
|
||||
) do
|
||||
{:error, err} -> {:error, err}
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Frees the plugin
|
||||
"""
|
||||
def free(plugin) do
|
||||
Extism.Native.plugin_free(plugin.ctx.ptr, plugin.plugin_id)
|
||||
Extism.Native.plugin_free(plugin.plugin)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the given plugin responds to the given function name
|
||||
"""
|
||||
def has_function(plugin, function_name) do
|
||||
Extism.Native.plugin_has_function(plugin.ctx.ptr, plugin.plugin_id, function_name)
|
||||
Extism.Native.plugin_has_function(plugin.plugin, function_name)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -99,6 +71,6 @@ defimpl Inspect, for: Extim.Plugin do
|
||||
import Inspect.Algebra
|
||||
|
||||
def inspect(dict, opts) do
|
||||
concat(["#Extism.Plugin<", to_doc(dict.plugin_id, opts), ">"])
|
||||
concat(["#Extism.Plugin<", to_doc(dict.plugin, opts), ">"])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,5 +14,5 @@ crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
rustler = "0.28.0"
|
||||
extism = "0.5.0"
|
||||
extism = {path = "../../../runtime"} # "0.5.0"
|
||||
log = "0.4"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use extism::{Context, Plugin};
|
||||
use extism::Plugin;
|
||||
use rustler::{Atom, Env, ResourceArc, Term};
|
||||
use std::mem;
|
||||
use std::path::Path;
|
||||
use std::str;
|
||||
use std::str::FromStr;
|
||||
@@ -14,11 +13,11 @@ mod atoms {
|
||||
}
|
||||
}
|
||||
|
||||
struct ExtismContext {
|
||||
ctx: RwLock<Context>,
|
||||
struct ExtismPlugin {
|
||||
plugin: RwLock<Option<Plugin>>,
|
||||
}
|
||||
unsafe impl Sync for ExtismContext {}
|
||||
unsafe impl Send for ExtismContext {}
|
||||
unsafe impl Sync for ExtismPlugin {}
|
||||
unsafe impl Send for ExtismPlugin {}
|
||||
|
||||
struct ExtismCancelHandle {
|
||||
handle: RwLock<extism::CancelHandle>,
|
||||
@@ -28,7 +27,7 @@ unsafe impl Sync for ExtismCancelHandle {}
|
||||
unsafe impl Send for ExtismCancelHandle {}
|
||||
|
||||
fn load(env: Env, _: Term) -> bool {
|
||||
rustler::resource!(ExtismContext, env);
|
||||
rustler::resource!(ExtismPlugin, env);
|
||||
rustler::resource!(ExtismCancelHandle, env);
|
||||
true
|
||||
}
|
||||
@@ -37,111 +36,73 @@ fn to_rustler_error(extism_error: extism::Error) -> rustler::Error {
|
||||
rustler::Error::Term(Box::new(extism_error.to_string()))
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
fn context_new() -> ResourceArc<ExtismContext> {
|
||||
ResourceArc::new(ExtismContext {
|
||||
ctx: RwLock::new(Context::new()),
|
||||
})
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
fn context_reset(ctx: ResourceArc<ExtismContext>) {
|
||||
let context = &mut ctx.ctx.write().unwrap();
|
||||
context.reset()
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
fn context_free(ctx: ResourceArc<ExtismContext>) {
|
||||
let context = ctx.ctx.read().unwrap();
|
||||
std::mem::drop(context)
|
||||
fn freed_error() -> rustler::Error {
|
||||
rustler::Error::Term(Box::new("Plugin has already been freed".to_string()))
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
fn plugin_new_with_manifest(
|
||||
ctx: ResourceArc<ExtismContext>,
|
||||
manifest_payload: String,
|
||||
wasi: bool,
|
||||
) -> Result<i32, rustler::Error> {
|
||||
let context = ctx.ctx.write().unwrap();
|
||||
let result = match Plugin::new(&context, manifest_payload, [], wasi) {
|
||||
) -> Result<ResourceArc<ExtismPlugin>, rustler::Error> {
|
||||
let result = match Plugin::new(manifest_payload, [], wasi) {
|
||||
Err(e) => Err(to_rustler_error(e)),
|
||||
Ok(plugin) => {
|
||||
let plugin_id = plugin.as_i32();
|
||||
// this forget should be safe because the context will clean up
|
||||
// all it's plugins when it is dropped
|
||||
mem::forget(plugin);
|
||||
Ok(plugin_id)
|
||||
}
|
||||
Ok(plugin) => Ok(ResourceArc::new(ExtismPlugin {
|
||||
plugin: RwLock::new(Some(plugin)),
|
||||
})),
|
||||
};
|
||||
result
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
fn plugin_call(
|
||||
ctx: ResourceArc<ExtismContext>,
|
||||
plugin_id: i32,
|
||||
plugin: ResourceArc<ExtismPlugin>,
|
||||
name: String,
|
||||
input: String,
|
||||
) -> Result<String, rustler::Error> {
|
||||
let context = &ctx.ctx.read().unwrap();
|
||||
let mut plugin = unsafe { Plugin::from_id(plugin_id, context) };
|
||||
let result = match plugin.call(name, input) {
|
||||
Err(e) => Err(to_rustler_error(e)),
|
||||
Ok(result) => match str::from_utf8(result) {
|
||||
Ok(output) => Ok(output.to_string()),
|
||||
Err(_e) => Err(rustler::Error::Term(Box::new(
|
||||
"Could not read output from plugin",
|
||||
))),
|
||||
},
|
||||
};
|
||||
// this forget should be safe because the context will clean up
|
||||
// all it's plugins when it is dropped
|
||||
mem::forget(plugin);
|
||||
result
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
fn plugin_update_manifest(
|
||||
ctx: ResourceArc<ExtismContext>,
|
||||
plugin_id: i32,
|
||||
manifest_payload: String,
|
||||
wasi: bool,
|
||||
) -> 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) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) => Err(to_rustler_error(e)),
|
||||
};
|
||||
// this forget should be safe because the context will clean up
|
||||
// all it's plugins when it is dropped
|
||||
mem::forget(plugin);
|
||||
result
|
||||
let mut plugin = plugin.plugin.write().unwrap();
|
||||
if let Some(plugin) = &mut *plugin {
|
||||
let result = match plugin.call(name, input) {
|
||||
Err(e) => Err(to_rustler_error(e)),
|
||||
Ok(result) => match str::from_utf8(result) {
|
||||
Ok(output) => Ok(output.to_string()),
|
||||
Err(_e) => Err(rustler::Error::Term(Box::new(
|
||||
"Could not read output from plugin",
|
||||
))),
|
||||
},
|
||||
};
|
||||
result
|
||||
} else {
|
||||
Err(freed_error())
|
||||
}
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
fn plugin_cancel_handle(
|
||||
ctx: ResourceArc<ExtismContext>,
|
||||
plugin_id: i32,
|
||||
plugin: ResourceArc<ExtismPlugin>,
|
||||
) -> Result<ResourceArc<ExtismCancelHandle>, rustler::Error> {
|
||||
let context = &ctx.ctx.read().unwrap();
|
||||
let plugin = unsafe { Plugin::from_id(plugin_id, context) };
|
||||
let handle = plugin.cancel_handle();
|
||||
Ok(ResourceArc::new(ExtismCancelHandle {
|
||||
handle: RwLock::new(handle),
|
||||
}))
|
||||
let mut plugin = plugin.plugin.write().unwrap();
|
||||
if let Some(plugin) = &mut *plugin {
|
||||
let handle = plugin.cancel_handle();
|
||||
Ok(ResourceArc::new(ExtismCancelHandle {
|
||||
handle: RwLock::new(handle),
|
||||
}))
|
||||
} else {
|
||||
Err(freed_error())
|
||||
}
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
fn plugin_cancel(handle: ResourceArc<ExtismCancelHandle>) -> bool {
|
||||
handle.handle.read().unwrap().cancel()
|
||||
handle.handle.read().unwrap().cancel().is_ok()
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
fn plugin_free(ctx: ResourceArc<ExtismContext>, plugin_id: i32) -> Result<(), rustler::Error> {
|
||||
let context = &ctx.ctx.read().unwrap();
|
||||
let plugin = unsafe { Plugin::from_id(plugin_id, context) };
|
||||
std::mem::drop(plugin);
|
||||
fn plugin_free(plugin: ResourceArc<ExtismPlugin>) -> Result<(), rustler::Error> {
|
||||
let mut plugin = plugin.plugin.write().unwrap();
|
||||
if let Some(plugin) = plugin.take() {
|
||||
drop(plugin);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -153,42 +114,34 @@ fn set_log_file(filename: String, log_level: String) -> Result<Atom, rustler::Er
|
||||
"{} not a valid log level",
|
||||
log_level
|
||||
)))),
|
||||
Ok(level) => {
|
||||
if extism::set_log_file(path, Some(level)) {
|
||||
Ok(atoms::ok())
|
||||
} else {
|
||||
Err(rustler::Error::Term(Box::new(
|
||||
"Did not set log file, received false from the API.",
|
||||
)))
|
||||
}
|
||||
}
|
||||
Ok(level) => match extism::set_log_file(path, level) {
|
||||
Ok(()) => Ok(atoms::ok()),
|
||||
Err(e) => Err(rustler::Error::Term(Box::new(format!(
|
||||
"Did not set log file: {e:?}"
|
||||
)))),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
fn plugin_has_function(
|
||||
ctx: ResourceArc<ExtismContext>,
|
||||
plugin_id: i32,
|
||||
plugin: ResourceArc<ExtismPlugin>,
|
||||
function_name: String,
|
||||
) -> Result<bool, rustler::Error> {
|
||||
let context = &ctx.ctx.read().unwrap();
|
||||
let plugin = unsafe { Plugin::from_id(plugin_id, context) };
|
||||
let has_function = plugin.has_function(function_name);
|
||||
// this forget should be safe because the context will clean up
|
||||
// all it's plugins when it is dropped
|
||||
mem::forget(plugin);
|
||||
Ok(has_function)
|
||||
let mut plugin = plugin.plugin.write().unwrap();
|
||||
if let Some(plugin) = &mut *plugin {
|
||||
let has_function = plugin.function_exists(function_name);
|
||||
Ok(has_function)
|
||||
} else {
|
||||
Err(freed_error())
|
||||
}
|
||||
}
|
||||
|
||||
rustler::init!(
|
||||
"Elixir.Extism.Native",
|
||||
[
|
||||
context_new,
|
||||
context_reset,
|
||||
context_free,
|
||||
plugin_new_with_manifest,
|
||||
plugin_call,
|
||||
plugin_update_manifest,
|
||||
plugin_has_function,
|
||||
plugin_cancel_handle,
|
||||
plugin_cancel,
|
||||
|
||||
@@ -2,33 +2,21 @@ defmodule ExtismTest do
|
||||
use ExUnit.Case
|
||||
doctest Extism
|
||||
|
||||
test "context create & reset" do
|
||||
ctx = Extism.Context.new()
|
||||
path = Path.join([__DIR__, "../../wasm/code.wasm"])
|
||||
manifest = %{wasm: [%{path: path}]}
|
||||
{:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
|
||||
Extism.Context.reset(ctx)
|
||||
# we should expect an error after resetting context
|
||||
{:error, _err} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
|
||||
end
|
||||
|
||||
defp new_plugin() do
|
||||
ctx = Extism.Context.new()
|
||||
path = Path.join([__DIR__, "../../wasm/code.wasm"])
|
||||
manifest = %{wasm: [%{path: path}]}
|
||||
{:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
|
||||
{ctx, plugin}
|
||||
{:ok, plugin} = Extism.Plugin.new(manifest, false)
|
||||
plugin
|
||||
end
|
||||
|
||||
test "counts vowels" do
|
||||
{ctx, plugin} = new_plugin()
|
||||
plugin = new_plugin()
|
||||
{:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
|
||||
assert JSON.decode(output) == {:ok, %{"count" => 4}}
|
||||
Extism.Context.free(ctx)
|
||||
end
|
||||
|
||||
test "can make multiple calls on a plugin" do
|
||||
{ctx, plugin} = new_plugin()
|
||||
plugin = new_plugin()
|
||||
{:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
|
||||
assert JSON.decode(output) == {:ok, %{"count" => 4}}
|
||||
{:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "this is a test again")
|
||||
@@ -37,37 +25,24 @@ defmodule ExtismTest do
|
||||
assert JSON.decode(output) == {:ok, %{"count" => 6}}
|
||||
{:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "🌎hello🌎world🌎")
|
||||
assert JSON.decode(output) == {:ok, %{"count" => 3}}
|
||||
Extism.Context.free(ctx)
|
||||
end
|
||||
|
||||
test "can free a plugin" do
|
||||
{ctx, plugin} = new_plugin()
|
||||
plugin = new_plugin()
|
||||
{:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
|
||||
assert JSON.decode(output) == {:ok, %{"count" => 4}}
|
||||
Extism.Plugin.free(plugin)
|
||||
# Expect an error when calling a plugin that was freed
|
||||
{:error, _err} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
|
||||
Extism.Context.free(ctx)
|
||||
end
|
||||
|
||||
test "can update manifest" do
|
||||
{ctx, plugin} = new_plugin()
|
||||
path = Path.join([__DIR__, "../../wasm/code.wasm"])
|
||||
manifest = %{wasm: [%{path: path}]}
|
||||
assert Extism.Plugin.update(plugin, manifest, true) == :ok
|
||||
Extism.Context.free(ctx)
|
||||
end
|
||||
|
||||
test "errors on bad manifest" do
|
||||
ctx = Extism.Context.new()
|
||||
{:error, _msg} = Extism.Context.new_plugin(ctx, %{"wasm" => 123}, false)
|
||||
Extism.Context.free(ctx)
|
||||
{:error, _msg} = Extism.Plugin.new(%{"wasm" => 123}, false)
|
||||
end
|
||||
|
||||
test "errors on unknown function" do
|
||||
{ctx, plugin} = new_plugin()
|
||||
plugin = new_plugin()
|
||||
{:error, _msg} = Extism.Plugin.call(plugin, "unknown", "this is a test")
|
||||
Extism.Context.free(ctx)
|
||||
end
|
||||
|
||||
test "set_log_file" do
|
||||
@@ -75,9 +50,8 @@ defmodule ExtismTest do
|
||||
end
|
||||
|
||||
test "has_function" do
|
||||
{ctx, plugin} = new_plugin()
|
||||
plugin = new_plugin()
|
||||
assert Extism.Plugin.has_function(plugin, "count_vowels")
|
||||
assert !Extism.Plugin.has_function(plugin, "unknown")
|
||||
Extism.Context.free(ctx)
|
||||
end
|
||||
end
|
||||
|
||||
203
extism.go
203
extism.go
@@ -52,11 +52,6 @@ void extism_val_set_f64(ExtismValUnion* x, double f){
|
||||
*/
|
||||
import "C"
|
||||
|
||||
// Context is used to manage Plugins
|
||||
type Context struct {
|
||||
pointer *C.ExtismContext
|
||||
}
|
||||
|
||||
type ValType = C.ExtismValType
|
||||
|
||||
type Val = C.ExtismVal
|
||||
@@ -81,9 +76,11 @@ type Function struct {
|
||||
|
||||
// Free a function
|
||||
func (f *Function) Free() {
|
||||
C.extism_function_free(f.pointer)
|
||||
f.pointer = nil
|
||||
f.userData.Delete()
|
||||
if f.pointer != nil {
|
||||
C.extism_function_free(f.pointer)
|
||||
f.pointer = nil
|
||||
f.userData.Delete()
|
||||
}
|
||||
}
|
||||
|
||||
// NewFunction creates a new host function with the given name, input/outputs and optional user data, which can be an
|
||||
@@ -136,45 +133,32 @@ func GetCurrentPlugin(ptr unsafe.Pointer) CurrentPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *CurrentPlugin) Memory(offs uint) []byte {
|
||||
type MemoryHandle = uint
|
||||
|
||||
func (p *CurrentPlugin) Memory(offs MemoryHandle) []byte {
|
||||
length := C.extism_current_plugin_memory_length(p.pointer, C.uint64_t(offs))
|
||||
data := unsafe.Pointer(C.extism_current_plugin_memory(p.pointer))
|
||||
return unsafe.Slice((*byte)(unsafe.Add(data, offs)), C.int(length))
|
||||
}
|
||||
|
||||
// Alloc a new memory block of the given length, returning its offset
|
||||
func (p *CurrentPlugin) Alloc(n uint) uint {
|
||||
func (p *CurrentPlugin) Alloc(n uint) MemoryHandle {
|
||||
return uint(C.extism_current_plugin_memory_alloc(p.pointer, C.uint64_t(n)))
|
||||
}
|
||||
|
||||
// Free the memory block specified by the given offset
|
||||
func (p *CurrentPlugin) Free(offs uint) {
|
||||
func (p *CurrentPlugin) Free(offs MemoryHandle) {
|
||||
C.extism_current_plugin_memory_free(p.pointer, C.uint64_t(offs))
|
||||
}
|
||||
|
||||
// Length returns the number of bytes allocated at the specified offset
|
||||
func (p *CurrentPlugin) Length(offs uint) uint {
|
||||
return uint(C.extism_current_plugin_memory_length(p.pointer, C.uint64_t(offs)))
|
||||
}
|
||||
|
||||
// NewContext creates a new context, it should be freed using the `Free` method
|
||||
func NewContext() Context {
|
||||
p := C.extism_context_new()
|
||||
return Context{
|
||||
pointer: p,
|
||||
}
|
||||
}
|
||||
|
||||
// Free a context
|
||||
func (ctx *Context) Free() {
|
||||
C.extism_context_free(ctx.pointer)
|
||||
ctx.pointer = nil
|
||||
func (p *CurrentPlugin) Length(offs MemoryHandle) int {
|
||||
return int(C.extism_current_plugin_memory_length(p.pointer, C.uint64_t(offs)))
|
||||
}
|
||||
|
||||
// Plugin is used to call WASM functions
|
||||
type Plugin struct {
|
||||
ctx *Context
|
||||
id int32
|
||||
ptr *C.ExtismPlugin
|
||||
functions []Function
|
||||
}
|
||||
|
||||
@@ -234,175 +218,99 @@ func ExtismVersion() string {
|
||||
return C.GoString(C.extism_version())
|
||||
}
|
||||
|
||||
func register(ctx *Context, data []byte, functions []Function, wasi bool) (Plugin, error) {
|
||||
func register(data []byte, functions []Function, wasi bool) (Plugin, error) {
|
||||
ptr := makePointer(data)
|
||||
functionPointers := []*C.ExtismFunction{}
|
||||
for _, f := range functions {
|
||||
functionPointers = append(functionPointers, f.pointer)
|
||||
}
|
||||
plugin := C.int32_t(-1)
|
||||
|
||||
var plugin *C.ExtismPlugin
|
||||
errmsg := (*C.char)(nil)
|
||||
if len(functions) == 0 {
|
||||
plugin = C.extism_plugin_new(
|
||||
ctx.pointer,
|
||||
(*C.uchar)(ptr),
|
||||
C.uint64_t(len(data)),
|
||||
nil,
|
||||
0,
|
||||
C._Bool(wasi))
|
||||
C._Bool(wasi),
|
||||
&errmsg)
|
||||
} 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),
|
||||
&errmsg,
|
||||
)
|
||||
}
|
||||
|
||||
if plugin < 0 {
|
||||
err := C.extism_error(ctx.pointer, C.int32_t(-1))
|
||||
msg := "Unknown"
|
||||
if err != nil {
|
||||
msg = C.GoString(err)
|
||||
}
|
||||
|
||||
return Plugin{id: -1}, errors.New(
|
||||
if plugin == nil {
|
||||
msg := C.GoString(errmsg)
|
||||
C.extism_plugin_new_error_free(errmsg)
|
||||
return Plugin{}, errors.New(
|
||||
fmt.Sprintf("Unable to load plugin: %s", msg),
|
||||
)
|
||||
}
|
||||
|
||||
return Plugin{id: int32(plugin), ctx: ctx, functions: functions}, nil
|
||||
return Plugin{ptr: plugin, functions: functions}, nil
|
||||
}
|
||||
|
||||
func update(ctx *Context, plugin int32, data []byte, functions []Function, wasi bool) error {
|
||||
ptr := makePointer(data)
|
||||
functionPointers := []*C.ExtismFunction{}
|
||||
for _, f := range functions {
|
||||
functionPointers = append(functionPointers, f.pointer)
|
||||
}
|
||||
|
||||
if len(functions) == 0 {
|
||||
b := bool(C.extism_plugin_update(
|
||||
ctx.pointer,
|
||||
C.int32_t(plugin),
|
||||
(*C.uchar)(ptr),
|
||||
C.uint64_t(len(data)),
|
||||
nil,
|
||||
0,
|
||||
C._Bool(wasi),
|
||||
))
|
||||
|
||||
if b {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
b := bool(C.extism_plugin_update(
|
||||
ctx.pointer,
|
||||
C.int32_t(plugin),
|
||||
(*C.uchar)(ptr),
|
||||
C.uint64_t(len(data)),
|
||||
&functionPointers[0],
|
||||
C.uint64_t(len(functions)),
|
||||
C._Bool(wasi),
|
||||
))
|
||||
|
||||
if b {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
err := C.extism_error(ctx.pointer, C.int32_t(-1))
|
||||
msg := "Unknown"
|
||||
if err != nil {
|
||||
msg = C.GoString(err)
|
||||
}
|
||||
|
||||
return errors.New(
|
||||
fmt.Sprintf("Unable to load plugin: %s", msg),
|
||||
)
|
||||
}
|
||||
|
||||
// NewPlugin creates a plugin in its own context
|
||||
// NewPlugin creates a plugin
|
||||
func NewPlugin(module io.Reader, functions []Function, wasi bool) (Plugin, error) {
|
||||
ctx := NewContext()
|
||||
return ctx.Plugin(module, functions, wasi)
|
||||
wasm, err := io.ReadAll(module)
|
||||
if err != nil {
|
||||
return Plugin{}, err
|
||||
}
|
||||
|
||||
return register(wasm, functions, wasi)
|
||||
}
|
||||
|
||||
// NewPlugin creates a plugin in its own context from a manifest
|
||||
// NewPlugin creates a plugin from a manifest
|
||||
func NewPluginFromManifest(manifest Manifest, functions []Function, wasi bool) (Plugin, error) {
|
||||
ctx := NewContext()
|
||||
return ctx.PluginFromManifest(manifest, functions, wasi)
|
||||
}
|
||||
|
||||
// PluginFromManifest creates a plugin from a `Manifest`
|
||||
func (ctx *Context) PluginFromManifest(manifest Manifest, functions []Function, wasi bool) (Plugin, error) {
|
||||
data, err := json.Marshal(manifest)
|
||||
if err != nil {
|
||||
return Plugin{id: -1}, err
|
||||
return Plugin{}, err
|
||||
}
|
||||
|
||||
return register(ctx, data, functions, wasi)
|
||||
}
|
||||
|
||||
// Plugin creates a plugin from a WASM module
|
||||
func (ctx *Context) Plugin(module io.Reader, functions []Function, wasi bool) (Plugin, error) {
|
||||
wasm, err := io.ReadAll(module)
|
||||
if err != nil {
|
||||
return Plugin{id: -1}, err
|
||||
}
|
||||
|
||||
return register(ctx, wasm, functions, wasi)
|
||||
}
|
||||
|
||||
// Update a plugin with a new WASM module
|
||||
func (p *Plugin) Update(module io.Reader, functions []Function, wasi bool) error {
|
||||
wasm, err := io.ReadAll(module)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.functions = functions
|
||||
return update(p.ctx, p.id, wasm, functions, wasi)
|
||||
}
|
||||
|
||||
// Update a plugin with a new Manifest
|
||||
func (p *Plugin) UpdateManifest(manifest Manifest, functions []Function, wasi bool) error {
|
||||
data, err := json.Marshal(manifest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.functions = functions
|
||||
return update(p.ctx, p.id, data, functions, wasi)
|
||||
return register(data, functions, wasi)
|
||||
}
|
||||
|
||||
// Set configuration values
|
||||
func (plugin Plugin) SetConfig(data map[string][]byte) error {
|
||||
if plugin.ptr == nil {
|
||||
return errors.New("Cannot set config, Plugin already freed")
|
||||
}
|
||||
s, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ptr := makePointer(s)
|
||||
C.extism_plugin_config(plugin.ctx.pointer, C.int(plugin.id), (*C.uchar)(ptr), C.uint64_t(len(s)))
|
||||
C.extism_plugin_config(plugin.ptr, (*C.uchar)(ptr), C.uint64_t(len(s)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// FunctionExists returns true when the named function is present in the plugin
|
||||
func (plugin Plugin) FunctionExists(functionName string) bool {
|
||||
if plugin.ptr == nil {
|
||||
return false
|
||||
}
|
||||
name := C.CString(functionName)
|
||||
b := C.extism_plugin_function_exists(plugin.ctx.pointer, C.int(plugin.id), name)
|
||||
b := C.extism_plugin_function_exists(plugin.ptr, name)
|
||||
C.free(unsafe.Pointer(name))
|
||||
return bool(b)
|
||||
}
|
||||
|
||||
// Call a function by name with the given input, returning the output
|
||||
func (plugin Plugin) Call(functionName string, input []byte) ([]byte, error) {
|
||||
if plugin.ptr == nil {
|
||||
return []byte{}, errors.New("Plugin has already been freed")
|
||||
}
|
||||
ptr := makePointer(input)
|
||||
name := C.CString(functionName)
|
||||
rc := C.extism_plugin_call(
|
||||
plugin.ctx.pointer,
|
||||
C.int32_t(plugin.id),
|
||||
plugin.ptr,
|
||||
name,
|
||||
(*C.uchar)(ptr),
|
||||
C.uint64_t(len(input)),
|
||||
@@ -410,7 +318,7 @@ func (plugin Plugin) Call(functionName string, input []byte) ([]byte, error) {
|
||||
C.free(unsafe.Pointer(name))
|
||||
|
||||
if rc != 0 {
|
||||
err := C.extism_error(plugin.ctx.pointer, C.int32_t(plugin.id))
|
||||
err := C.extism_plugin_error(plugin.ptr)
|
||||
msg := "<unset by plugin>"
|
||||
if err != nil {
|
||||
msg = C.GoString(err)
|
||||
@@ -421,10 +329,10 @@ func (plugin Plugin) Call(functionName string, input []byte) ([]byte, error) {
|
||||
)
|
||||
}
|
||||
|
||||
length := C.extism_plugin_output_length(plugin.ctx.pointer, C.int32_t(plugin.id))
|
||||
length := C.extism_plugin_output_length(plugin.ptr)
|
||||
|
||||
if length > 0 {
|
||||
x := C.extism_plugin_output_data(plugin.ctx.pointer, C.int32_t(plugin.id))
|
||||
x := C.extism_plugin_output_data(plugin.ptr)
|
||||
return unsafe.Slice((*byte)(x), C.int(length)), nil
|
||||
}
|
||||
|
||||
@@ -433,16 +341,11 @@ func (plugin Plugin) Call(functionName string, input []byte) ([]byte, error) {
|
||||
|
||||
// Free a plugin
|
||||
func (plugin *Plugin) Free() {
|
||||
if plugin.ctx.pointer == nil {
|
||||
if plugin.ptr == nil {
|
||||
return
|
||||
}
|
||||
C.extism_plugin_free(plugin.ctx.pointer, C.int32_t(plugin.id))
|
||||
plugin.id = -1
|
||||
}
|
||||
|
||||
// Reset removes all registered plugins in a Context
|
||||
func (ctx Context) Reset() {
|
||||
C.extism_context_reset(ctx.pointer)
|
||||
C.extism_plugin_free(plugin.ptr)
|
||||
plugin.ptr = nil
|
||||
}
|
||||
|
||||
// ValGetI64 returns an I64 from an ExtismVal, it accepts a pointer to a C.ExtismVal
|
||||
@@ -514,7 +417,7 @@ type CancelHandle struct {
|
||||
}
|
||||
|
||||
func (p *Plugin) CancelHandle() CancelHandle {
|
||||
pointer := C.extism_plugin_cancel_handle(p.ctx.pointer, C.int(p.id))
|
||||
pointer := C.extism_plugin_cancel_handle(p.ptr)
|
||||
return CancelHandle{pointer}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ depends: [
|
||||
"extism-manifest" {= version}
|
||||
"ppx_inline_test" {>= "v0.15.0"}
|
||||
"cmdliner" {>= "1.1.1"}
|
||||
"uuidm" {>= "0.9.0"}
|
||||
"odoc" {with-doc}
|
||||
]
|
||||
build: [
|
||||
|
||||
@@ -35,16 +35,8 @@ func expectVowelCount(plugin Plugin, input string, count int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestCreateAndFreeContext(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
ctx.Free()
|
||||
}
|
||||
|
||||
func TestCallPlugin(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
defer ctx.Free()
|
||||
|
||||
plugin, err := ctx.PluginFromManifest(manifest(false), []Function{}, false)
|
||||
plugin, err := NewPluginFromManifest(manifest(false), []Function{}, false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -61,10 +53,7 @@ func TestCallPlugin(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFreePlugin(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
defer ctx.Free()
|
||||
|
||||
plugin, err := ctx.PluginFromManifest(manifest(false), []Function{}, false)
|
||||
plugin, err := NewPluginFromManifest(manifest(false), []Function{}, false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -80,52 +69,8 @@ func TestFreePlugin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextReset(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
defer ctx.Free()
|
||||
|
||||
plugin, err := ctx.PluginFromManifest(manifest(false), []Function{}, false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := expectVowelCount(plugin, "this is a test", 4); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// reset the context dropping all plugins
|
||||
ctx.Reset()
|
||||
|
||||
if err := expectVowelCount(plugin, "this is a test", 4); err == nil {
|
||||
t.Fatal("Expected an error after plugin was freed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanUpdateAManifest(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
defer ctx.Free()
|
||||
|
||||
plugin, err := ctx.PluginFromManifest(manifest(false), []Function{}, false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := expectVowelCount(plugin, "this is a test", 4); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
plugin.UpdateManifest(manifest(false), []Function{}, false)
|
||||
|
||||
// can still call the plugin
|
||||
if err := expectVowelCount(plugin, "this is a test", 4); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFunctionExists(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
defer ctx.Free()
|
||||
|
||||
plugin, err := ctx.PluginFromManifest(manifest(false), []Function{}, false)
|
||||
plugin, err := NewPluginFromManifest(manifest(false), []Function{}, false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -139,10 +84,7 @@ func TestFunctionExists(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestErrorsOnUnknownFunction(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
defer ctx.Free()
|
||||
|
||||
plugin, err := ctx.PluginFromManifest(manifest(false), []Function{}, false)
|
||||
plugin, err := NewPluginFromManifest(manifest(false), []Function{}, false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -162,10 +104,7 @@ func TestCancel(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
ctx := NewContext()
|
||||
defer ctx.Free()
|
||||
|
||||
plugin, err := ctx.PluginFromManifest(manifest, []Function{}, false)
|
||||
plugin, err := NewPluginFromManifest(manifest, []Function{}, false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
module Main where
|
||||
|
||||
import Extism
|
||||
import Extism.CurrentPlugin
|
||||
import Extism.HostFunction
|
||||
import Extism.Manifest(manifest, wasmFile)
|
||||
|
||||
unwrap (Right x) = x
|
||||
unwrap (Left (ExtismError msg)) = do
|
||||
error msg
|
||||
|
||||
hello plugin params msg = do
|
||||
putStrLn "Hello from Haskell!"
|
||||
putStrLn msg
|
||||
@@ -15,10 +11,11 @@ hello plugin params msg = do
|
||||
return [toI64 offs]
|
||||
|
||||
main = do
|
||||
setLogFile "stdout" Error
|
||||
setLogFile "stdout" LogError
|
||||
let m = manifest [wasmFile "../wasm/code-functions.wasm"]
|
||||
f <- hostFunction "hello_world" [I64] [I64] hello "Hello, again"
|
||||
plugin <- unwrap <$> createPluginFromManifest m [f] True
|
||||
plugin <- unwrap <$> pluginFromManifest m [f] True
|
||||
id <- pluginID plugin
|
||||
print id
|
||||
res <- unwrap <$> call plugin "count_vowels" (toByteString "this is a test")
|
||||
putStrLn (fromByteString res)
|
||||
free plugin
|
||||
|
||||
@@ -11,7 +11,7 @@ category: Plugins, WebAssembly
|
||||
extra-doc-files: CHANGELOG.md
|
||||
|
||||
library
|
||||
exposed-modules: Extism Extism.CurrentPlugin
|
||||
exposed-modules: Extism Extism.HostFunction
|
||||
reexported-modules: Extism.Manifest
|
||||
hs-source-dirs: src
|
||||
other-modules: Extism.Bindings
|
||||
@@ -22,7 +22,8 @@ library
|
||||
base >= 4.16.1 && < 5,
|
||||
bytestring >= 0.11.3 && <= 0.12,
|
||||
json >= 0.10 && <= 0.11,
|
||||
extism-manifest >= 0.0.0 && < 0.4.0
|
||||
extism-manifest >= 0.0.0 && < 0.4.0,
|
||||
uuid >= 1.3 && < 2
|
||||
|
||||
test-suite extism-example
|
||||
type: exitcode-stdio-1.0
|
||||
|
||||
@@ -9,7 +9,7 @@ import Data.ByteString.Internal (c2w, w2c)
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
import qualified Data.ByteString.Char8 as BS (unpack)
|
||||
|
||||
data Nullable a = Null | NotNull a
|
||||
data Nullable a = Null | NotNull a deriving Eq
|
||||
|
||||
makeArray x = JSArray [showJSON a | a <- x]
|
||||
isNull JSNull = True
|
||||
@@ -49,7 +49,7 @@ instance JSON a => JSON (Nullable a) where
|
||||
readJSON x = readJSON x
|
||||
|
||||
|
||||
newtype Base64 = Base64 B.ByteString
|
||||
newtype Base64 = Base64 B.ByteString deriving (Eq, Show)
|
||||
|
||||
instance JSON Base64 where
|
||||
showJSON (Base64 bs) = showJSON (BS.unpack $ B64.encode bs)
|
||||
|
||||
@@ -9,6 +9,7 @@ newtype Memory = Memory
|
||||
{
|
||||
memoryMaxPages :: Nullable Int
|
||||
}
|
||||
deriving Eq
|
||||
|
||||
instance JSON Memory where
|
||||
showJSON (Memory max) =
|
||||
@@ -26,6 +27,7 @@ data HTTPRequest = HTTPRequest
|
||||
, headers :: Nullable [(String, String)]
|
||||
, method :: Nullable String
|
||||
}
|
||||
deriving Eq
|
||||
|
||||
makeKV x =
|
||||
object [(k, showJSON v) | (k, v) <- x]
|
||||
@@ -55,6 +57,7 @@ data WasmFile = WasmFile
|
||||
, fileName :: Nullable String
|
||||
, fileHash :: Nullable String
|
||||
}
|
||||
deriving Eq
|
||||
|
||||
instance JSON WasmFile where
|
||||
showJSON (WasmFile path name hash) =
|
||||
@@ -80,9 +83,7 @@ data WasmData = WasmData
|
||||
, dataName :: Nullable String
|
||||
, dataHash :: Nullable String
|
||||
}
|
||||
|
||||
|
||||
|
||||
deriving Eq
|
||||
|
||||
instance JSON WasmData where
|
||||
showJSON (WasmData bytes name hash) =
|
||||
@@ -110,6 +111,7 @@ data WasmURL = WasmURL
|
||||
, urlName :: Nullable String
|
||||
, urlHash :: Nullable String
|
||||
}
|
||||
deriving Eq
|
||||
|
||||
|
||||
instance JSON WasmURL where
|
||||
@@ -127,7 +129,7 @@ instance JSON WasmURL where
|
||||
Just req -> Ok (WasmURL req name hash)
|
||||
|
||||
-- | Specifies where to get WASM module data
|
||||
data Wasm = File WasmFile | Data WasmData | URL WasmURL
|
||||
data Wasm = File WasmFile | Data WasmData | URL WasmURL deriving Eq
|
||||
|
||||
instance JSON Wasm where
|
||||
showJSON x =
|
||||
|
||||
@@ -1,60 +1,69 @@
|
||||
module Extism (
|
||||
module Extism,
|
||||
module Extism.Manifest,
|
||||
ValType(..),
|
||||
Val(..)
|
||||
Function(..),
|
||||
Plugin(..),
|
||||
CancelHandle(..),
|
||||
LogLevel(..),
|
||||
Error(..),
|
||||
Result(..),
|
||||
toByteString,
|
||||
fromByteString,
|
||||
extismVersion,
|
||||
plugin,
|
||||
pluginFromManifest,
|
||||
isValid,
|
||||
setConfig,
|
||||
setLogFile,
|
||||
functionExists,
|
||||
call,
|
||||
cancelHandle,
|
||||
cancel,
|
||||
pluginID,
|
||||
unwrap
|
||||
) where
|
||||
|
||||
import Data.Int
|
||||
import Data.Word
|
||||
import Control.Monad (void)
|
||||
import Foreign.ForeignPtr
|
||||
import Foreign.C.String
|
||||
import Foreign.Ptr
|
||||
import Foreign.Marshal.Array
|
||||
import Foreign.Marshal.Alloc
|
||||
import Foreign.Storable
|
||||
import Foreign.StablePtr
|
||||
import Foreign.Concurrent
|
||||
import Foreign.Marshal.Utils (copyBytes, moveBytes)
|
||||
import Data.ByteString as B
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
import Data.ByteString.Internal (c2w, w2c)
|
||||
import Data.ByteString.Unsafe (unsafeUseAsCString)
|
||||
import Data.Bifunctor (second)
|
||||
import Text.JSON (encode, toJSObject, showJSON)
|
||||
import qualified Text.JSON (encode, toJSObject, showJSON)
|
||||
import Extism.Manifest (Manifest, toString)
|
||||
import Extism.Bindings
|
||||
import qualified Data.UUID (UUID, fromByteString)
|
||||
|
||||
-- | Context for managing plugins
|
||||
newtype Context = Context (ForeignPtr ExtismContext)
|
||||
|
||||
-- | Host function
|
||||
data Function = Function (ForeignPtr ExtismFunction) (StablePtr ())
|
||||
-- | Host function, see 'Extism.HostFunction.hostFunction'
|
||||
data Function = Function (ForeignPtr ExtismFunction) (StablePtr ()) deriving Eq
|
||||
|
||||
-- | Plugins can be used to call WASM function
|
||||
data Plugin = Plugin Context Int32 [Function]
|
||||
newtype Plugin = Plugin (ForeignPtr ExtismPlugin) deriving Eq
|
||||
|
||||
-- | Cancellation handle for Plugins
|
||||
newtype CancelHandle = CancelHandle (Ptr ExtismCancelHandle)
|
||||
|
||||
-- | Access the plugin that is currently executing from inside a host function
|
||||
type CurrentPlugin = Ptr ExtismCurrentPlugin
|
||||
|
||||
-- | Log level
|
||||
data LogLevel = Error | Warn | Info | Debug | Trace deriving (Show)
|
||||
data LogLevel = LogError | LogWarn | LogInfo | LogDebug | LogTrace deriving (Show, Eq)
|
||||
|
||||
-- | Extism error
|
||||
newtype Error = ExtismError String deriving Show
|
||||
newtype Error = ExtismError String deriving (Show, Eq)
|
||||
|
||||
-- | Result type
|
||||
type Result a = Either Error a
|
||||
|
||||
-- | Helper function to convert a 'String' to a 'ByteString'
|
||||
toByteString :: String -> ByteString
|
||||
toByteString x = B.pack (Prelude.map c2w x)
|
||||
toByteString :: String -> B.ByteString
|
||||
toByteString x = B.pack (map c2w x)
|
||||
|
||||
-- | Helper function to convert a 'ByteString' to a 'String'
|
||||
fromByteString :: ByteString -> String
|
||||
fromByteString bs = Prelude.map w2c $ B.unpack bs
|
||||
fromByteString :: B.ByteString -> String
|
||||
fromByteString bs = map w2c $ B.unpack bs
|
||||
|
||||
-- | Get the Extism version string
|
||||
extismVersion :: () -> IO String
|
||||
@@ -62,119 +71,55 @@ extismVersion () = do
|
||||
v <- extism_version
|
||||
peekCString v
|
||||
|
||||
-- | Remove all registered plugins in a 'Context'
|
||||
reset :: Context -> IO ()
|
||||
reset (Context ctx) =
|
||||
withForeignPtr ctx extism_context_reset
|
||||
|
||||
-- | Create a new 'Context'
|
||||
newContext :: IO Context
|
||||
newContext = do
|
||||
ptr <- extism_context_new
|
||||
fptr <- Foreign.ForeignPtr.newForeignPtr extism_context_free ptr
|
||||
return (Context fptr)
|
||||
|
||||
-- | Execute a function with a new 'Context' that is destroyed when it returns
|
||||
withContext :: (Context -> IO a) -> IO a
|
||||
withContext f = do
|
||||
ctx <- newContext
|
||||
f ctx
|
||||
|
||||
-- | Execute a function with the provided 'Plugin' as a parameter, then frees the 'Plugin'
|
||||
-- | before returning the result.
|
||||
withPlugin :: (Plugin -> IO a) -> Plugin -> IO a
|
||||
withPlugin f plugin = do
|
||||
res <- f plugin
|
||||
free plugin
|
||||
return res
|
||||
|
||||
-- | Create a 'Plugin' from a WASM module, `useWasi` determines if WASI should
|
||||
-- | be linked
|
||||
plugin :: Context -> B.ByteString -> [Function] -> Bool -> IO (Result Plugin)
|
||||
plugin c wasm functions useWasi =
|
||||
let nfunctions = fromIntegral (Prelude.length functions) in
|
||||
plugin :: B.ByteString -> [Function] -> Bool -> IO (Result Plugin)
|
||||
plugin wasm functions useWasi =
|
||||
let nfunctions = fromIntegral (length functions) in
|
||||
let length = fromIntegral (B.length wasm) in
|
||||
let wasi = fromInteger (if useWasi then 1 else 0) in
|
||||
let Context ctx = c in
|
||||
do
|
||||
funcs <- Prelude.mapM (\(Function ptr _) -> withForeignPtr ptr (\x -> do return x)) functions
|
||||
withForeignPtr ctx (\ctx -> do
|
||||
funcs <- mapM (\(Function ptr _) -> withForeignPtr ptr (\x -> do return x)) functions
|
||||
alloca (\e-> do
|
||||
let errmsg = (e :: Ptr CString)
|
||||
p <- unsafeUseAsCString wasm (\s ->
|
||||
withArray funcs (\funcs ->
|
||||
extism_plugin_new ctx (castPtr s) length funcs nfunctions wasi ))
|
||||
if p < 0 then do
|
||||
err <- extism_error ctx (-1)
|
||||
e <- peekCString err
|
||||
extism_plugin_new (castPtr s) length funcs nfunctions wasi errmsg ))
|
||||
if p == nullPtr then do
|
||||
err <- peek errmsg
|
||||
e <- peekCString err
|
||||
extism_plugin_new_error_free err
|
||||
return $ Left (ExtismError e)
|
||||
else
|
||||
return $ Right (Plugin c p functions))
|
||||
|
||||
-- | Create a 'Plugin' with its own 'Context'
|
||||
createPlugin :: B.ByteString -> [Function] -> Bool -> IO (Result Plugin)
|
||||
createPlugin c functions useWasi = do
|
||||
ctx <- newContext
|
||||
plugin ctx c functions useWasi
|
||||
else do
|
||||
ptr <- Foreign.Concurrent.newForeignPtr p (extism_plugin_free p)
|
||||
return $ Right (Plugin ptr))
|
||||
|
||||
-- | Create a 'Plugin' from a 'Manifest'
|
||||
pluginFromManifest :: Context -> Manifest -> [Function] -> Bool -> IO (Result Plugin)
|
||||
pluginFromManifest ctx manifest functions useWasi =
|
||||
pluginFromManifest :: Manifest -> [Function] -> Bool -> IO (Result Plugin)
|
||||
pluginFromManifest manifest functions useWasi =
|
||||
let wasm = toByteString $ toString manifest in
|
||||
plugin ctx wasm functions useWasi
|
||||
|
||||
-- | Create a 'Plugin' with its own 'Context' from a 'Manifest'
|
||||
createPluginFromManifest :: Manifest -> [Function] -> Bool -> IO (Result Plugin)
|
||||
createPluginFromManifest manifest functions useWasi = do
|
||||
ctx <- newContext
|
||||
pluginFromManifest ctx manifest functions useWasi
|
||||
|
||||
-- | Update a 'Plugin' with a new WASM module
|
||||
update :: Plugin -> B.ByteString -> [Function] -> Bool -> IO (Result Plugin)
|
||||
update (Plugin (Context ctx) id _) wasm functions useWasi =
|
||||
let nfunctions = fromIntegral (Prelude.length functions) in
|
||||
let length = fromIntegral (B.length wasm) in
|
||||
let wasi = fromInteger (if useWasi then 1 else 0) in
|
||||
do
|
||||
funcs <- Prelude.mapM (\(Function ptr _ ) -> withForeignPtr ptr (\x -> do return x)) functions
|
||||
withForeignPtr ctx (\ctx' -> do
|
||||
b <- unsafeUseAsCString wasm (\s ->
|
||||
withArray funcs (\funcs ->
|
||||
extism_plugin_update ctx' id (castPtr s) length funcs nfunctions wasi))
|
||||
if b <= 0 then do
|
||||
err <- extism_error ctx' (-1)
|
||||
e <- peekCString err
|
||||
return $ Left (ExtismError e)
|
||||
else
|
||||
return (Right (Plugin (Context ctx) id functions)))
|
||||
|
||||
-- | Update a 'Plugin' with a new 'Manifest'
|
||||
updateManifest :: Plugin -> Manifest -> [Function] -> Bool -> IO (Result Plugin)
|
||||
updateManifest plugin manifest functions useWasi =
|
||||
let wasm = toByteString $ toString manifest in
|
||||
update plugin wasm functions useWasi
|
||||
plugin wasm functions useWasi
|
||||
|
||||
-- | Check if a 'Plugin' is valid
|
||||
isValid :: Plugin -> Bool
|
||||
isValid (Plugin _ p _) = p >= 0
|
||||
isValid :: Plugin -> IO Bool
|
||||
isValid (Plugin p) = withForeignPtr p (\x -> return (x /= nullPtr))
|
||||
|
||||
-- | Set configuration values for a plugin
|
||||
setConfig :: Plugin -> [(String, Maybe String)] -> IO Bool
|
||||
setConfig (Plugin (Context ctx) plugin _) x =
|
||||
if plugin < 0
|
||||
then return False
|
||||
else
|
||||
let obj = toJSObject [(k, showJSON v) | (k, v) <- x] in
|
||||
let bs = toByteString (encode obj) in
|
||||
let length = fromIntegral (B.length bs) in
|
||||
unsafeUseAsCString bs (\s -> do
|
||||
withForeignPtr ctx (\ctx -> do
|
||||
b <- extism_plugin_config ctx plugin (castPtr s) length
|
||||
return $ b /= 0))
|
||||
setConfig (Plugin plugin) x =
|
||||
let obj = Text.JSON.toJSObject [(k, Text.JSON.showJSON v) | (k, v) <- x] in
|
||||
let bs = toByteString (Text.JSON.encode obj) in
|
||||
let length = fromIntegral (B.length bs) in
|
||||
unsafeUseAsCString bs (\s -> do
|
||||
withForeignPtr plugin (\plugin-> do
|
||||
b <- extism_plugin_config plugin (castPtr s) length
|
||||
return $ b /= 0))
|
||||
|
||||
levelStr Error = "error"
|
||||
levelStr Debug = "debug"
|
||||
levelStr Warn = "warn"
|
||||
levelStr Trace = "trace"
|
||||
levelStr Info = "info"
|
||||
levelStr LogError = "error"
|
||||
levelStr LogDebug = "debug"
|
||||
levelStr LogWarn = "warn"
|
||||
levelStr LogTrace = "trace"
|
||||
levelStr LogInfo = "info"
|
||||
|
||||
-- | Set the log file and level, this is a global configuration
|
||||
setLogFile :: String -> LogLevel -> IO Bool
|
||||
@@ -187,43 +132,46 @@ setLogFile filename level =
|
||||
|
||||
-- | Check if a function exists in the given plugin
|
||||
functionExists :: Plugin -> String -> IO Bool
|
||||
functionExists (Plugin (Context ctx) plugin _) name = do
|
||||
withForeignPtr ctx (\ctx -> do
|
||||
b <- withCString name (extism_plugin_function_exists ctx plugin)
|
||||
functionExists (Plugin plugin) name = do
|
||||
withForeignPtr plugin (\plugin -> do
|
||||
b <- withCString name (extism_plugin_function_exists plugin)
|
||||
if b == 1 then return True else return False)
|
||||
|
||||
--- | Call a function provided by the given plugin
|
||||
call :: Plugin -> String -> B.ByteString -> IO (Result B.ByteString)
|
||||
call (Plugin (Context ctx) plugin _) name input =
|
||||
call (Plugin plugin) name input =
|
||||
let length = fromIntegral (B.length input) in
|
||||
do
|
||||
withForeignPtr ctx (\ctx -> do
|
||||
withForeignPtr plugin (\plugin -> do
|
||||
rc <- withCString name (\name ->
|
||||
unsafeUseAsCString input (\input ->
|
||||
extism_plugin_call ctx plugin name (castPtr input) length))
|
||||
err <- extism_error ctx plugin
|
||||
extism_plugin_call plugin name (castPtr input) length))
|
||||
err <- extism_error plugin
|
||||
if err /= nullPtr
|
||||
then do e <- peekCString err
|
||||
return $ Left (ExtismError e)
|
||||
else if rc == 0
|
||||
then do
|
||||
length <- extism_plugin_output_length ctx plugin
|
||||
ptr <- extism_plugin_output_data ctx plugin
|
||||
buf <- packCStringLen (castPtr ptr, fromIntegral length)
|
||||
length <- extism_plugin_output_length plugin
|
||||
ptr <- extism_plugin_output_data plugin
|
||||
buf <- B.packCStringLen (castPtr ptr, fromIntegral length)
|
||||
return $ Right buf
|
||||
else return $ Left (ExtismError "Call failed"))
|
||||
|
||||
-- | Free a 'Plugin', this will automatically be called for every plugin
|
||||
-- | associated with a 'Context' when that 'Context' is freed
|
||||
free :: Plugin -> IO ()
|
||||
free (Plugin (Context ctx) plugin _) =
|
||||
withForeignPtr ctx (`extism_plugin_free` plugin)
|
||||
-- | Call a function with a string argument and return a string
|
||||
callString :: Plugin -> String -> String -> IO (Result String)
|
||||
callString p name input = do
|
||||
res <- call p name (toByteString input)
|
||||
case res of
|
||||
Left x -> return $ Left x
|
||||
Right x -> return $ Right (fromByteString x)
|
||||
|
||||
|
||||
-- | Create a new 'CancelHandle' that can be used to cancel a running plugin
|
||||
-- | from another thread.
|
||||
cancelHandle :: Plugin -> IO CancelHandle
|
||||
cancelHandle (Plugin (Context ctx) plugin _) = do
|
||||
handle <- withForeignPtr ctx (`extism_plugin_cancel_handle` plugin)
|
||||
cancelHandle (Plugin plugin) = do
|
||||
handle <- withForeignPtr plugin extism_plugin_cancel_handle
|
||||
return (CancelHandle handle)
|
||||
|
||||
-- | Cancel a running plugin using a 'CancelHandle'
|
||||
@@ -231,58 +179,16 @@ cancel :: CancelHandle -> IO Bool
|
||||
cancel (CancelHandle handle) =
|
||||
extism_plugin_cancel handle
|
||||
|
||||
pluginID :: Plugin -> IO Data.UUID.UUID
|
||||
pluginID (Plugin plugin) =
|
||||
withForeignPtr plugin (\plugin -> do
|
||||
ptr <- extism_plugin_id plugin
|
||||
buf <- B.packCStringLen (castPtr ptr, 16)
|
||||
case Data.UUID.fromByteString (BL.fromStrict buf) of
|
||||
Nothing -> error "Invalid Plugin ID"
|
||||
Just x -> return x)
|
||||
|
||||
-- | Create a new 'Function' that can be called from a 'Plugin'
|
||||
hostFunction :: String -> [ValType] -> [ValType] -> (CurrentPlugin -> [Val] -> a -> IO [Val]) -> a -> IO Function
|
||||
hostFunction name params results f v =
|
||||
let nparams = fromIntegral $ Prelude.length params in
|
||||
let nresults = fromIntegral $ Prelude.length results in
|
||||
do
|
||||
cb <- callbackWrap (callback f :: CCallback)
|
||||
free <- freePtrWrap freePtr
|
||||
userData <- newStablePtr (v, free, cb)
|
||||
let userDataPtr = castStablePtrToPtr userData
|
||||
x <- withCString name (\name -> do
|
||||
withArray params (\params ->
|
||||
withArray results (\results -> do
|
||||
extism_function_new name params nparams results nresults cb userDataPtr free)))
|
||||
let freeFn = extism_function_free x
|
||||
fptr <- Foreign.Concurrent.newForeignPtr x freeFn
|
||||
return $ Function fptr (castPtrToStablePtr userDataPtr)
|
||||
|
||||
|
||||
-- | Create a new I32 'Val'
|
||||
toI32 :: Integral a => a -> Val
|
||||
toI32 x = ValI32 (fromIntegral x)
|
||||
|
||||
-- | Create a new I64 'Val'
|
||||
toI64 :: Integral a => a -> Val
|
||||
toI64 x = ValI64 (fromIntegral x)
|
||||
|
||||
-- | Create a new F32 'Val'
|
||||
toF32 :: Float -> Val
|
||||
toF32 = ValF32
|
||||
|
||||
-- | Create a new F64 'Val'
|
||||
toF64 :: Double -> Val
|
||||
toF64 = ValF64
|
||||
|
||||
-- | Get I32 'Val'
|
||||
fromI32 :: Integral a => Val -> Maybe a
|
||||
fromI32 (ValI32 x) = Just (fromIntegral x)
|
||||
fromI32 _ = Nothing
|
||||
|
||||
-- | Get I64 'Val'
|
||||
fromI64 :: Integral a => Val -> Maybe a
|
||||
fromI64 (ValI64 x) = Just (fromIntegral x)
|
||||
fromI64 _ = Nothing
|
||||
|
||||
-- | Get F32 'Val'
|
||||
fromF32 :: Val -> Maybe Float
|
||||
fromF32 (ValF32 x) = Just x
|
||||
fromF32 _ = Nothing
|
||||
|
||||
-- | Get F64 'Val'
|
||||
fromF64 :: Val -> Maybe Double
|
||||
fromF64 (ValF64 x) = Just x
|
||||
fromF64 _ = Nothing
|
||||
|
||||
unwrap (Right x) = x
|
||||
unwrap (Left (ExtismError msg)) = do
|
||||
error msg
|
||||
|
||||
@@ -13,7 +13,7 @@ import Foreign.StablePtr
|
||||
|
||||
type FreeCallback = Ptr () -> IO ()
|
||||
|
||||
newtype ExtismContext = ExtismContext () deriving Show
|
||||
newtype ExtismPlugin = ExtismPlugin () deriving Show
|
||||
newtype ExtismFunction = ExtismFunction () deriving Show
|
||||
newtype ExtismCancelHandle = ExtismCancelHandle () deriving Show
|
||||
newtype ExtismCurrentPlugin = ExtismCurrentPlugin () deriving Show
|
||||
@@ -79,21 +79,19 @@ instance Storable ValType where
|
||||
poke ptr x = do
|
||||
pokeByteOff ptr 0 (intOfValType x)
|
||||
|
||||
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_plugin_new" extism_plugin_new :: Ptr Word8 -> Word64 -> Ptr (Ptr ExtismFunction) -> Word64 -> CBool -> Ptr CString -> IO (Ptr ExtismPlugin)
|
||||
foreign import ccall safe "extism.h extism_plugin_call" extism_plugin_call :: Ptr ExtismPlugin -> CString -> Ptr Word8 -> Word64 -> IO Int32
|
||||
foreign import ccall safe "extism.h extism_plugin_function_exists" extism_plugin_function_exists :: Ptr ExtismPlugin -> CString -> IO CBool
|
||||
foreign import ccall safe "extism.h extism_plugin_error" extism_error :: Ptr ExtismPlugin -> IO CString
|
||||
foreign import ccall safe "extism.h extism_plugin_output_length" extism_plugin_output_length :: Ptr ExtismPlugin -> IO Word64
|
||||
foreign import ccall safe "extism.h extism_plugin_output_data" extism_plugin_output_data :: Ptr ExtismPlugin -> 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_plugin_config" extism_plugin_config :: Ptr ExtismPlugin -> Ptr Word8 -> Int64 -> IO CBool
|
||||
foreign import ccall safe "extism.h extism_plugin_free" extism_plugin_free :: Ptr ExtismPlugin -> IO ()
|
||||
foreign import ccall safe "extism.h extism_plugin_new_error_free" extism_plugin_new_error_free :: CString -> IO ()
|
||||
foreign import ccall safe "extism.h extism_version" extism_version :: IO CString
|
||||
foreign import ccall safe "extism.h extism_plugin_cancel_handle" extism_plugin_cancel_handle :: Ptr ExtismContext -> Int32 -> IO (Ptr ExtismCancelHandle)
|
||||
foreign import ccall safe "extism.h extism_plugin_id" extism_plugin_id :: Ptr ExtismPlugin -> IO (Ptr Word8)
|
||||
foreign import ccall safe "extism.h extism_plugin_cancel_handle" extism_plugin_cancel_handle :: Ptr ExtismPlugin -> IO (Ptr ExtismCancelHandle)
|
||||
foreign import ccall safe "extism.h extism_plugin_cancel" extism_plugin_cancel :: Ptr ExtismCancelHandle -> IO Bool
|
||||
|
||||
foreign import ccall safe "extism.h extism_function_new" extism_function_new :: CString -> Ptr ValType -> Word64 -> Ptr ValType -> Word64 -> FunPtr CCallback -> Ptr () -> FunPtr FreeCallback -> IO (Ptr ExtismFunction)
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
module Extism.CurrentPlugin where
|
||||
|
||||
import Extism
|
||||
import Extism.Bindings
|
||||
import Data.Word
|
||||
import Data.ByteString as B
|
||||
import Foreign.Ptr
|
||||
import Foreign.Marshal.Array
|
||||
|
||||
-- | Allocate a new handle of the given size
|
||||
memoryAlloc :: CurrentPlugin -> Word64 -> IO Word64
|
||||
memoryAlloc = extism_current_plugin_memory_alloc
|
||||
|
||||
-- | Get the length of a handle, returns 0 if the handle is invalid
|
||||
memoryLength :: CurrentPlugin -> Word64 -> IO Word64
|
||||
memoryLength = extism_current_plugin_memory_length
|
||||
|
||||
-- | Free allocated memory
|
||||
memoryFree :: CurrentPlugin -> Word64 -> IO ()
|
||||
memoryFree = extism_current_plugin_memory_free
|
||||
|
||||
-- | Access a pointer to the entire memory region
|
||||
memory :: CurrentPlugin -> IO (Ptr Word8)
|
||||
memory = extism_current_plugin_memory
|
||||
|
||||
-- | Access a pointer the a specific offset in memory
|
||||
memoryOffset :: CurrentPlugin -> Word64 -> IO (Ptr Word8)
|
||||
memoryOffset plugin offs = do
|
||||
x <- extism_current_plugin_memory plugin
|
||||
return $ plusPtr x (fromIntegral offs)
|
||||
|
||||
-- | Access the data associated with a handle as a 'ByteString'
|
||||
memoryBytes :: CurrentPlugin -> Word64 -> IO B.ByteString
|
||||
memoryBytes plugin offs = do
|
||||
ptr <- memoryOffset plugin offs
|
||||
len <- memoryLength plugin offs
|
||||
arr <- peekArray (fromIntegral len) ptr
|
||||
return $ B.pack arr
|
||||
|
||||
-- | Allocate memory and copy an existing 'ByteString' into it
|
||||
allocBytes :: CurrentPlugin -> B.ByteString -> IO Word64
|
||||
allocBytes plugin s = do
|
||||
let length = B.length s
|
||||
offs <- memoryAlloc plugin (fromIntegral length)
|
||||
ptr <- memoryOffset plugin offs
|
||||
pokeArray ptr (B.unpack s)
|
||||
return offs
|
||||
|
||||
157
haskell/src/Extism/HostFunction.hs
Normal file
157
haskell/src/Extism/HostFunction.hs
Normal file
@@ -0,0 +1,157 @@
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
|
||||
module Extism.HostFunction(
|
||||
CurrentPlugin(..),
|
||||
ValType(..),
|
||||
Val(..),
|
||||
MemoryHandle,
|
||||
memoryAlloc,
|
||||
memoryLength,
|
||||
memoryFree,
|
||||
memory,
|
||||
memoryOffset,
|
||||
memoryBytes,
|
||||
memoryString,
|
||||
allocBytes,
|
||||
allocString,
|
||||
toI32,
|
||||
toI64,
|
||||
toF32,
|
||||
toF64,
|
||||
fromI32,
|
||||
fromI64,
|
||||
fromF32,
|
||||
fromF64,
|
||||
hostFunction
|
||||
) where
|
||||
|
||||
import Extism
|
||||
import Extism.Bindings
|
||||
import Data.Word
|
||||
import qualified Data.ByteString as B
|
||||
import Foreign.Ptr
|
||||
import Foreign.ForeignPtr
|
||||
import Foreign.C.String
|
||||
import Foreign.StablePtr
|
||||
import Foreign.Concurrent
|
||||
import Foreign.Marshal.Array
|
||||
import qualified Data.ByteString.Internal as BS (c2w)
|
||||
|
||||
-- | Access the plugin that is currently executing from inside a host function
|
||||
type CurrentPlugin = Ptr ExtismCurrentPlugin
|
||||
|
||||
-- | A memory handle represents an allocated block of Extism memory
|
||||
newtype MemoryHandle = MemoryHandle Word64 deriving (Num, Enum, Eq, Ord, Real, Integral, Show)
|
||||
|
||||
-- | Allocate a new handle of the given size
|
||||
memoryAlloc :: CurrentPlugin -> Word64 -> IO MemoryHandle
|
||||
memoryAlloc p n = MemoryHandle <$> extism_current_plugin_memory_alloc p n
|
||||
|
||||
-- | Get the length of a handle, returns 0 if the handle is invalid
|
||||
memoryLength :: CurrentPlugin -> MemoryHandle -> IO Word64
|
||||
memoryLength p (MemoryHandle offs) = extism_current_plugin_memory_length p offs
|
||||
|
||||
-- | Free allocated memory
|
||||
memoryFree :: CurrentPlugin -> MemoryHandle -> IO ()
|
||||
memoryFree p (MemoryHandle offs) = extism_current_plugin_memory_free p offs
|
||||
|
||||
-- | Access a pointer to the entire memory region
|
||||
memory :: CurrentPlugin -> IO (Ptr Word8)
|
||||
memory = extism_current_plugin_memory
|
||||
|
||||
-- | Access the pointer for the given 'MemoryHandle'
|
||||
memoryOffset :: CurrentPlugin -> MemoryHandle -> IO (Ptr Word8)
|
||||
memoryOffset plugin (MemoryHandle offs) = do
|
||||
x <- extism_current_plugin_memory plugin
|
||||
return $ plusPtr x (fromIntegral offs)
|
||||
|
||||
-- | Access the data associated with a handle as a 'ByteString'
|
||||
memoryBytes :: CurrentPlugin -> MemoryHandle -> IO B.ByteString
|
||||
memoryBytes plugin offs = do
|
||||
ptr <- memoryOffset plugin offs
|
||||
len <- memoryLength plugin offs
|
||||
arr <- peekArray (fromIntegral len) ptr
|
||||
return $ B.pack arr
|
||||
|
||||
|
||||
-- | Access the data associated with a handle as a 'String'
|
||||
memoryString :: CurrentPlugin -> MemoryHandle -> IO String
|
||||
memoryString plugin offs = do
|
||||
ptr <- memoryOffset plugin offs
|
||||
len <- memoryLength plugin offs
|
||||
arr <- peekArray (fromIntegral len) ptr
|
||||
return $ fromByteString $ B.pack arr
|
||||
|
||||
-- | Allocate memory and copy an existing 'ByteString' into it
|
||||
allocBytes :: CurrentPlugin -> B.ByteString -> IO MemoryHandle
|
||||
allocBytes plugin s = do
|
||||
let length = B.length s
|
||||
offs <- memoryAlloc plugin (fromIntegral length)
|
||||
ptr <- memoryOffset plugin offs
|
||||
pokeArray ptr (B.unpack s)
|
||||
return offs
|
||||
|
||||
|
||||
-- | Allocate memory and copy an existing 'String' into it
|
||||
allocString :: CurrentPlugin -> String -> IO MemoryHandle
|
||||
allocString plugin s = do
|
||||
let length = Prelude.length s
|
||||
offs <- memoryAlloc plugin (fromIntegral length)
|
||||
ptr <- memoryOffset plugin offs
|
||||
pokeArray ptr (Prelude.map BS.c2w s)
|
||||
return offs
|
||||
|
||||
-- | Create a new I32 'Val'
|
||||
toI32 :: Integral a => a -> Val
|
||||
toI32 x = ValI32 (fromIntegral x)
|
||||
|
||||
-- | Create a new I64 'Val'
|
||||
toI64 :: Integral a => a -> Val
|
||||
toI64 x = ValI64 (fromIntegral x)
|
||||
|
||||
-- | Create a new F32 'Val'
|
||||
toF32 :: Float -> Val
|
||||
toF32 = ValF32
|
||||
|
||||
-- | Create a new F64 'Val'
|
||||
toF64 :: Double -> Val
|
||||
toF64 = ValF64
|
||||
|
||||
-- | Get I32 'Val'
|
||||
fromI32 :: Integral a => Val -> Maybe a
|
||||
fromI32 (ValI32 x) = Just (fromIntegral x)
|
||||
fromI32 _ = Nothing
|
||||
|
||||
-- | Get I64 'Val'
|
||||
fromI64 :: Integral a => Val -> Maybe a
|
||||
fromI64 (ValI64 x) = Just (fromIntegral x)
|
||||
fromI64 _ = Nothing
|
||||
|
||||
-- | Get F32 'Val'
|
||||
fromF32 :: Val -> Maybe Float
|
||||
fromF32 (ValF32 x) = Just x
|
||||
fromF32 _ = Nothing
|
||||
|
||||
-- | Get F64 'Val'
|
||||
fromF64 :: Val -> Maybe Double
|
||||
fromF64 (ValF64 x) = Just x
|
||||
fromF64 _ = Nothing
|
||||
|
||||
-- | Create a new 'Function' that can be called from a 'Plugin'
|
||||
hostFunction :: String -> [ValType] -> [ValType] -> (CurrentPlugin -> [Val] -> a -> IO [Val]) -> a -> IO Function
|
||||
hostFunction name params results f v =
|
||||
let nparams = fromIntegral $ length params in
|
||||
let nresults = fromIntegral $ length results in
|
||||
do
|
||||
cb <- callbackWrap (callback f :: CCallback)
|
||||
free <- freePtrWrap freePtr
|
||||
userData <- newStablePtr (v, free, cb)
|
||||
let userDataPtr = castStablePtrToPtr userData
|
||||
x <- withCString name (\name -> do
|
||||
withArray params (\params ->
|
||||
withArray results (\results -> do
|
||||
extism_function_new name params nparams results nresults cb userDataPtr free)))
|
||||
let freeFn = extism_function_free x
|
||||
fptr <- Foreign.Concurrent.newForeignPtr x freeFn
|
||||
return $ Function fptr (castPtrToStablePtr userDataPtr)
|
||||
|
||||
@@ -1,35 +1,33 @@
|
||||
import Test.HUnit
|
||||
import Extism
|
||||
import Extism.Manifest
|
||||
import Extism.CurrentPlugin
|
||||
import Extism.HostFunction
|
||||
|
||||
|
||||
unwrap (Right x) = return x
|
||||
unwrap (Left (ExtismError msg)) =
|
||||
assertUnwrap (Right x) = return x
|
||||
assertUnwrap (Left (ExtismError msg)) =
|
||||
assertFailure msg
|
||||
|
||||
defaultManifest = manifest [wasmFile "../../wasm/code.wasm"]
|
||||
hostFunctionManifest = manifest [wasmFile "../../wasm/code-functions.wasm"]
|
||||
|
||||
initPlugin :: Maybe Context -> IO Plugin
|
||||
initPlugin Nothing =
|
||||
Extism.createPluginFromManifest defaultManifest [] False >>= unwrap
|
||||
initPlugin (Just ctx) =
|
||||
Extism.pluginFromManifest ctx defaultManifest [] False >>= unwrap
|
||||
initPlugin :: IO Plugin
|
||||
initPlugin =
|
||||
Extism.pluginFromManifest defaultManifest [] False >>= assertUnwrap
|
||||
|
||||
pluginFunctionExists = do
|
||||
p <- initPlugin Nothing
|
||||
p <- initPlugin
|
||||
exists <- functionExists p "count_vowels"
|
||||
assertBool "function exists" exists
|
||||
exists' <- functionExists p "function_doesnt_exist"
|
||||
assertBool "function doesn't exist" (not exists')
|
||||
|
||||
checkCallResult p = do
|
||||
res <- call p "count_vowels" (toByteString "this is a test") >>= unwrap
|
||||
res <- call p "count_vowels" (toByteString "this is a test") >>= assertUnwrap
|
||||
assertEqual "count vowels output" "{\"count\": 4}" (fromByteString res)
|
||||
|
||||
pluginCall = do
|
||||
p <- initPlugin Nothing
|
||||
p <- initPlugin
|
||||
checkCallResult p
|
||||
|
||||
|
||||
@@ -39,33 +37,25 @@ hello plugin params () = do
|
||||
return [toI64 offs]
|
||||
|
||||
pluginCallHostFunction = do
|
||||
p <- Extism.createPluginFromManifest hostFunctionManifest [] False >>= unwrap
|
||||
res <- call p "count_vowels" (toByteString "this is a test") >>= unwrap
|
||||
p <- Extism.pluginFromManifest hostFunctionManifest [] False >>= assertUnwrap
|
||||
res <- call p "count_vowels" (toByteString "this is a test") >>= assertUnwrap
|
||||
assertEqual "count vowels output" "{\"count\": 999}" (fromByteString res)
|
||||
|
||||
pluginMultiple = do
|
||||
withContext(\ctx -> do
|
||||
p <- initPlugin (Just ctx)
|
||||
p <- initPlugin
|
||||
checkCallResult p
|
||||
q <- initPlugin (Just ctx)
|
||||
r <- initPlugin (Just ctx)
|
||||
q <- initPlugin
|
||||
r <- initPlugin
|
||||
checkCallResult q
|
||||
checkCallResult r)
|
||||
|
||||
pluginUpdate = do
|
||||
withContext (\ctx -> do
|
||||
p <- initPlugin (Just ctx)
|
||||
updateManifest p defaultManifest [] True >>= unwrap
|
||||
checkCallResult p)
|
||||
checkCallResult r
|
||||
|
||||
pluginConfig = do
|
||||
withContext (\ctx -> do
|
||||
p <- initPlugin (Just ctx)
|
||||
b <- setConfig p [("a", Just "1"), ("b", Just "2"), ("c", Just "3"), ("d", Nothing)]
|
||||
assertBool "set config" b)
|
||||
p <- initPlugin
|
||||
b <- setConfig p [("a", Just "1"), ("b", Just "2"), ("c", Just "3"), ("d", Nothing)]
|
||||
assertBool "set config" b
|
||||
|
||||
testSetLogFile = do
|
||||
b <- setLogFile "stderr" Extism.Error
|
||||
b <- setLogFile "stderr" Extism.LogError
|
||||
assertBool "set log file" b
|
||||
|
||||
t name f = TestLabel name (TestCase f)
|
||||
@@ -77,7 +67,6 @@ main = do
|
||||
, t "Plugin.Call" pluginCall
|
||||
, t "Plugin.CallHostFunction" pluginCallHostFunction
|
||||
, t "Plugin.Multiple" pluginMultiple
|
||||
, t "Plugin.Update" pluginUpdate
|
||||
, t "Plugin.Config" pluginConfig
|
||||
, t "SetLogFile" testSetLogFile
|
||||
])
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
package org.extism.sdk;
|
||||
|
||||
import com.sun.jna.Pointer;
|
||||
import org.extism.sdk.manifest.Manifest;
|
||||
|
||||
/**
|
||||
* Extism Context is used to store and manage plugins.
|
||||
*/
|
||||
public class Context implements AutoCloseable {
|
||||
|
||||
/**
|
||||
* Holds a pointer to the native ExtismContext struct.
|
||||
*/
|
||||
private final Pointer contextPointer;
|
||||
|
||||
/**
|
||||
* Creates a new context.
|
||||
* <p>
|
||||
* A Context is used to manage Plugins
|
||||
* and make sure they are cleaned up when you are done with them.
|
||||
*/
|
||||
public Context() {
|
||||
this.contextPointer = LibExtism.INSTANCE.extism_context_new();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new plugin managed by this context.
|
||||
*
|
||||
* @param manifest The manifest for the plugin
|
||||
* @param withWASI Set to true to enable WASI
|
||||
* @param functions List of Host functions
|
||||
* @return the plugin instance
|
||||
*/
|
||||
public Plugin newPlugin(Manifest manifest, boolean withWASI, HostFunction[] functions) {
|
||||
return new Plugin(this, manifest, withWASI, functions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees the context *and* frees all its Plugins. Use {@link #reset()}, if you just want to
|
||||
* free the plugins but keep the context. You should ensure this is called when you are done
|
||||
* with the context.
|
||||
*/
|
||||
public void free() {
|
||||
LibExtism.INSTANCE.extism_context_free(this.contextPointer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the context by freeing all its Plugins. Unlike {@link #free()}, it does not
|
||||
* free the context itself.
|
||||
*/
|
||||
public void reset() {
|
||||
LibExtism.INSTANCE.extism_context_reset(this.contextPointer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version string of the linked Extism Runtime.
|
||||
*
|
||||
* @return the version
|
||||
*/
|
||||
public String getVersion() {
|
||||
return LibExtism.INSTANCE.extism_version();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error associated with a context, if plugin is {@literal null} then the context error will be returned.
|
||||
*
|
||||
* @param plugin
|
||||
* @return the error message
|
||||
*/
|
||||
protected String error(Plugin plugin) {
|
||||
return LibExtism.INSTANCE.extism_error(this.contextPointer, plugin == null ? -1 : plugin.getIndex());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the raw pointer to this context.
|
||||
*
|
||||
* @return the pointer
|
||||
*/
|
||||
public Pointer getPointer() {
|
||||
return this.contextPointer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls {@link #free()} if used in the context of a TWR block.
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
this.free();
|
||||
}
|
||||
}
|
||||
@@ -41,10 +41,8 @@ public class Extism {
|
||||
* @throws ExtismException if the call fails
|
||||
*/
|
||||
public static String invokeFunction(Manifest manifest, String function, String input) throws ExtismException {
|
||||
try (var ctx = new Context()) {
|
||||
try (var plugin = ctx.newPlugin(manifest, false, null)) {
|
||||
return plugin.call(function, input);
|
||||
}
|
||||
try (var plugin = new Plugin(manifest, false, null)) {
|
||||
return plugin.call(function, input);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,23 +86,6 @@ public interface LibExtism extends Library {
|
||||
*/
|
||||
void extism_current_plugin_memory_free(Pointer plugin, long ptr);
|
||||
|
||||
/**
|
||||
* Create a new context
|
||||
*/
|
||||
Pointer extism_context_new();
|
||||
|
||||
/**
|
||||
* Free a context
|
||||
*/
|
||||
void extism_context_free(Pointer contextPointer);
|
||||
|
||||
/**
|
||||
* Remove all plugins from the registry.
|
||||
*
|
||||
* @param contextPointer
|
||||
*/
|
||||
void extism_context_reset(Pointer contextPointer);
|
||||
|
||||
/**
|
||||
* Sets the logger to the given path with the given level of verbosity
|
||||
*
|
||||
@@ -113,26 +96,30 @@ public interface LibExtism extends Library {
|
||||
boolean extism_log_file(String path, String logLevel);
|
||||
|
||||
/**
|
||||
* Returns the error associated with a @{@link Context} or @{@link Plugin}, if {@code pluginId} is {@literal -1} then the context error will be returned
|
||||
* Returns the error associated with a @{@link Plugin}
|
||||
*
|
||||
* @param contextPointer
|
||||
* @param pluginId
|
||||
* @param pluginPointer
|
||||
* @return
|
||||
*/
|
||||
String extism_error(Pointer contextPointer, int pluginId);
|
||||
String extism_plugin_error(Pointer pluginPointer);
|
||||
|
||||
/**
|
||||
* Create a new plugin.
|
||||
*
|
||||
* @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
|
||||
* @param errmsg get the error message if the return value is null
|
||||
* @return id of the plugin or {@literal -1} in case of error
|
||||
*/
|
||||
int extism_plugin_new(Pointer contextPointer, byte[] wasm, long wasmSize, Pointer[] functions, int nFunctions, boolean withWASI);
|
||||
Pointer extism_plugin_new(byte[] wasm, long wasmSize, Pointer[] functions, int nFunctions, boolean withWASI, Pointer[] errmsg);
|
||||
|
||||
/**
|
||||
* Free error message from `extism_plugin_new`
|
||||
*/
|
||||
void extism_plugin_new_error_free(Pointer errmsg);
|
||||
|
||||
/**
|
||||
* Returns the Extism version string
|
||||
@@ -143,68 +130,40 @@ public interface LibExtism extends Library {
|
||||
/**
|
||||
* Calls a function from the @{@link Plugin} at the given {@code pluginIndex}.
|
||||
*
|
||||
* @param contextPointer
|
||||
* @param pluginIndex
|
||||
* @param pluginPointer
|
||||
* @param function_name is the function to call
|
||||
* @param data is the data input data
|
||||
* @param dataLength is the data input data length
|
||||
* @return the result code of the plugin call. {@literal -1} in case of error, {@literal 0} otherwise.
|
||||
*/
|
||||
int extism_plugin_call(Pointer contextPointer, int pluginIndex, String function_name, byte[] data, int dataLength);
|
||||
int extism_plugin_call(Pointer pluginPointer, String function_name, byte[] data, int dataLength);
|
||||
|
||||
/**
|
||||
* Returns the length of a plugin's output data.
|
||||
*
|
||||
* @param contextPointer
|
||||
* @param pluginIndex
|
||||
* Returns
|
||||
* @return the length of the output data in bytes.
|
||||
*/
|
||||
int extism_plugin_output_length(Pointer contextPointer, int pluginIndex);
|
||||
int extism_plugin_output_length(Pointer pluginPointer);
|
||||
|
||||
/**
|
||||
* Returns the plugin's output data.
|
||||
*
|
||||
* @param contextPointer
|
||||
* @param pluginIndex
|
||||
|
||||
* @return
|
||||
*/
|
||||
Pointer extism_plugin_output_data(Pointer contextPointer, int pluginIndex);
|
||||
Pointer extism_plugin_output_data(Pointer pluginPointer);
|
||||
|
||||
/**
|
||||
* Update a plugin, keeping the existing ID.
|
||||
* Similar to {@link #extism_plugin_new(Pointer, byte[], long, Pointer[], int, boolean)} but takes an {@code pluginIndex} argument to specify which plugin to update.
|
||||
* Note: Memory for this plugin will be reset upon update.
|
||||
*
|
||||
* @param contextPointer
|
||||
* @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
|
||||
* Remove a plugin from the
|
||||
*/
|
||||
boolean extism_plugin_update(Pointer contextPointer, int pluginIndex, byte[] wasm, int length, Pointer[] functions, int nFunctions, boolean withWASI);
|
||||
void extism_plugin_free(Pointer pluginPointer);
|
||||
|
||||
/**
|
||||
* Remove a plugin from the registry and free associated memory.
|
||||
*
|
||||
* @param contextPointer
|
||||
* @param pluginIndex
|
||||
*/
|
||||
void extism_plugin_free(Pointer contextPointer, int pluginIndex);
|
||||
|
||||
/**
|
||||
* Update plugin config values, this will merge with the existing values.
|
||||
*
|
||||
* @param contextPointer
|
||||
* @param pluginIndex
|
||||
* Update plugin config values, this
|
||||
* @param json
|
||||
* @param jsonLength
|
||||
* @return {@literal true} if update was successful
|
||||
*/
|
||||
boolean extism_plugin_config(Pointer contextPointer, int pluginIndex, byte[] json, int jsonLength);
|
||||
Pointer extism_plugin_cancel_handle(Pointer contextPointer, int n);
|
||||
boolean extism_plugin_cancel(Pointer contextPointer);
|
||||
boolean extism_plugin_config(Pointer pluginPointer, byte[] json, int jsonLength);
|
||||
Pointer extism_plugin_cancel_handle(Pointer pluginPointer);
|
||||
boolean extism_plugin_cancel(Pointer cancelHandle);
|
||||
void extism_function_set_namespace(Pointer p, String name);
|
||||
int strlen(Pointer s);
|
||||
}
|
||||
|
||||
@@ -13,27 +13,17 @@ import java.util.Objects;
|
||||
public class Plugin implements AutoCloseable {
|
||||
|
||||
/**
|
||||
* Holds the Extism {@link Context} that the plugin belongs to.
|
||||
* Holds the Extism plugin pointer
|
||||
*/
|
||||
private final Context context;
|
||||
private final Pointer pluginPointer;
|
||||
|
||||
/**
|
||||
* Holds the index of the plugin
|
||||
*/
|
||||
private final int index;
|
||||
|
||||
/**
|
||||
* Constructor for a Plugin. Only expose internally. Plugins should be created and
|
||||
* managed from {@link org.extism.sdk.Context}.
|
||||
*
|
||||
* @param context The context to manage the plugin
|
||||
* @param manifestBytes The manifest for the plugin
|
||||
* @param functions The Host functions for th eplugin
|
||||
* @param withWASI Set to true to enable WASI
|
||||
*/
|
||||
public Plugin(Context context, byte[] manifestBytes, boolean withWASI, HostFunction[] functions) {
|
||||
public Plugin(byte[] manifestBytes, boolean withWASI, HostFunction[] functions) {
|
||||
|
||||
Objects.requireNonNull(context, "context");
|
||||
Objects.requireNonNull(manifestBytes, "manifestBytes");
|
||||
|
||||
Pointer[] ptrArr = new Pointer[functions == null ? 0 : functions.length];
|
||||
@@ -43,49 +33,33 @@ public class Plugin implements AutoCloseable {
|
||||
ptrArr[i] = functions[i].pointer;
|
||||
}
|
||||
|
||||
Pointer contextPointer = context.getPointer();
|
||||
|
||||
int index = LibExtism.INSTANCE.extism_plugin_new(contextPointer, manifestBytes, manifestBytes.length,
|
||||
Pointer[] errormsg = new Pointer[1];
|
||||
Pointer p = LibExtism.INSTANCE.extism_plugin_new(manifestBytes, manifestBytes.length,
|
||||
ptrArr,
|
||||
functions == null ? 0 : functions.length,
|
||||
withWASI);
|
||||
if (index == -1) {
|
||||
String error = context.error(this);
|
||||
throw new ExtismException(error);
|
||||
withWASI,
|
||||
errormsg);
|
||||
if (p == null) {
|
||||
int errlen = LibExtism.INSTANCE.strlen(errormsg[0]);
|
||||
byte[] msg = new byte[errlen];
|
||||
errormsg[0].read(0, msg, 0, errlen);
|
||||
LibExtism.INSTANCE.extism_plugin_new_error_free(errormsg[0]);
|
||||
throw new ExtismException(new String(msg));
|
||||
}
|
||||
|
||||
this.index= index;
|
||||
this.context = context;
|
||||
this.pluginPointer = p;
|
||||
}
|
||||
|
||||
public Plugin(Context context, Manifest manifest, boolean withWASI, HostFunction[] functions) {
|
||||
this(context, serialize(manifest), withWASI, functions);
|
||||
}
|
||||
|
||||
|
||||
public Plugin(byte[] manifestBytes, boolean withWASI, HostFunction[] functions) {
|
||||
this(new Context(), manifestBytes, withWASI, functions);
|
||||
}
|
||||
|
||||
|
||||
public Plugin(Manifest manifest, boolean withWASI, HostFunction[] functions) {
|
||||
this(new Context(), serialize(manifest), withWASI, functions);
|
||||
this(serialize(manifest), withWASI, functions);
|
||||
}
|
||||
|
||||
|
||||
private static byte[] serialize(Manifest manifest) {
|
||||
Objects.requireNonNull(manifest, "manifest");
|
||||
return JsonSerde.toJson(manifest).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the internal index pointer to this plugin.
|
||||
*
|
||||
* @return the plugin index
|
||||
*/
|
||||
public int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke a function with the given name and input.
|
||||
*
|
||||
@@ -98,19 +72,19 @@ public class Plugin implements AutoCloseable {
|
||||
|
||||
Objects.requireNonNull(functionName, "functionName");
|
||||
|
||||
Pointer contextPointer = context.getPointer();
|
||||
int inputDataLength = inputData == null ? 0 : inputData.length;
|
||||
int exitCode = LibExtism.INSTANCE.extism_plugin_call(contextPointer, index, functionName, inputData, inputDataLength);
|
||||
int exitCode = LibExtism.INSTANCE.extism_plugin_call(this.pluginPointer, functionName, inputData, inputDataLength);
|
||||
if (exitCode == -1) {
|
||||
String error = context.error(this);
|
||||
String error = this.error();
|
||||
throw new ExtismException(error);
|
||||
}
|
||||
|
||||
int length = LibExtism.INSTANCE.extism_plugin_output_length(contextPointer, index);
|
||||
Pointer output = LibExtism.INSTANCE.extism_plugin_output_data(contextPointer, index);
|
||||
int length = LibExtism.INSTANCE.extism_plugin_output_length(this.pluginPointer);
|
||||
Pointer output = LibExtism.INSTANCE.extism_plugin_output_data(this.pluginPointer);
|
||||
return output.getByteArray(0, length);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Invoke a function with the given name and input.
|
||||
*
|
||||
@@ -126,46 +100,21 @@ public class Plugin implements AutoCloseable {
|
||||
var outputBytes = call(functionName, inputBytes);
|
||||
return new String(outputBytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the plugin code given manifest changes
|
||||
* Get the error associated with a plugin
|
||||
*
|
||||
* @param manifest The manifest for the plugin
|
||||
* @param withWASI Set to true to enable WASI
|
||||
* @return {@literal true} if update was successful
|
||||
* @return the error message
|
||||
*/
|
||||
public boolean update(Manifest manifest, boolean withWASI, HostFunction[] functions) {
|
||||
return update(serialize(manifest), withWASI, functions);
|
||||
protected String error() {
|
||||
return LibExtism.INSTANCE.extism_plugin_error(this.pluginPointer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the plugin code given manifest changes
|
||||
*
|
||||
* @param manifestBytes The manifest for the plugin
|
||||
* @param withWASI Set to true to enable WASI
|
||||
* @return {@literal true} if update was successful
|
||||
*/
|
||||
public boolean update(byte[] manifestBytes, boolean withWASI, HostFunction[] functions) {
|
||||
Objects.requireNonNull(manifestBytes, "manifestBytes");
|
||||
Pointer[] ptrArr = new Pointer[functions == null ? 0 : functions.length];
|
||||
|
||||
if (functions != null)
|
||||
for (int i = 0; i < functions.length; i++) {
|
||||
ptrArr[i] = functions[i].pointer;
|
||||
}
|
||||
|
||||
return LibExtism.INSTANCE.extism_plugin_update(context.getPointer(), index, manifestBytes, manifestBytes.length,
|
||||
ptrArr,
|
||||
functions == null ? 0 : functions.length,
|
||||
withWASI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees a plugin from memory. Plugins will be automatically cleaned up
|
||||
* if you free their parent Context using {@link org.extism.sdk.Context#free() free()} or {@link org.extism.sdk.Context#reset() reset()}
|
||||
* Frees a plugin from memory
|
||||
*/
|
||||
public void free() {
|
||||
LibExtism.INSTANCE.extism_plugin_free(context.getPointer(), index);
|
||||
LibExtism.INSTANCE.extism_plugin_free(this.pluginPointer);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -187,7 +136,7 @@ public class Plugin implements AutoCloseable {
|
||||
*/
|
||||
public boolean updateConfig(byte[] jsonBytes) {
|
||||
Objects.requireNonNull(jsonBytes, "jsonBytes");
|
||||
return LibExtism.INSTANCE.extism_plugin_config(context.getPointer(), index, jsonBytes, jsonBytes.length);
|
||||
return LibExtism.INSTANCE.extism_plugin_config(this.pluginPointer, jsonBytes, jsonBytes.length);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,10 +151,7 @@ public class Plugin implements AutoCloseable {
|
||||
* Return a new `CancelHandle`, which can be used to cancel a running Plugin
|
||||
*/
|
||||
public CancelHandle cancelHandle() {
|
||||
if (this.context.getPointer() == null) {
|
||||
throw new ExtismException("No Context set");
|
||||
}
|
||||
Pointer handle = LibExtism.INSTANCE.extism_plugin_cancel_handle(this.context.getPointer(), this.index);
|
||||
Pointer handle = LibExtism.INSTANCE.extism_plugin_cancel_handle(this.pluginPointer);
|
||||
return new CancelHandle(handle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package org.extism.sdk;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class ContextTests {
|
||||
|
||||
@Test
|
||||
public void shouldReturnVersionString() {
|
||||
try (var ctx = new Context()) {
|
||||
var version = ctx.getVersion();
|
||||
assertThat(version).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAllowResetOnEmptyContext() {
|
||||
try (var ctx = new Context()) {
|
||||
ctx.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,17 +57,6 @@ public class PluginTests {
|
||||
}, "Function not found: unknown");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAllowInvokeFunctionFromFileWasmSourceMultipleTimes() {
|
||||
var wasmSource = CODE.pathWasmSource();
|
||||
var manifest = new Manifest(wasmSource);
|
||||
var output = Extism.invokeFunction(manifest, "count_vowels", "Hello World");
|
||||
assertThat(output).isEqualTo("{\"count\": 3}");
|
||||
|
||||
output = Extism.invokeFunction(manifest, "count_vowels", "Hello World");
|
||||
assertThat(output).isEqualTo("{\"count\": 3}");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAllowInvokeFunctionFromFileWasmSourceApiUsageExample() {
|
||||
|
||||
@@ -77,28 +66,24 @@ public class PluginTests {
|
||||
var functionName = "count_vowels";
|
||||
var input = "Hello World";
|
||||
|
||||
try (var ctx = new Context()) {
|
||||
try (var plugin = ctx.newPlugin(manifest, false, null)) {
|
||||
var output = plugin.call(functionName, input);
|
||||
assertThat(output).isEqualTo("{\"count\": 3}");
|
||||
}
|
||||
try (var plugin = new Plugin(manifest, false, null)) {
|
||||
var output = plugin.call(functionName, input);
|
||||
assertThat(output).isEqualTo("{\"count\": 3}");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAllowInvokeFunctionFromFileWasmSourceMultipleTimesByReusingContext() {
|
||||
public void shouldAllowInvokeFunctionFromFileWasmSourceMultipleTimes() {
|
||||
var manifest = new Manifest(CODE.pathWasmSource());
|
||||
var functionName = "count_vowels";
|
||||
var input = "Hello World";
|
||||
|
||||
try (var ctx = new Context()) {
|
||||
try (var plugin = ctx.newPlugin(manifest, false, null)) {
|
||||
var output = plugin.call(functionName, input);
|
||||
assertThat(output).isEqualTo("{\"count\": 3}");
|
||||
try (var plugin = new Plugin(manifest, false, null)) {
|
||||
var output = plugin.call(functionName, input);
|
||||
assertThat(output).isEqualTo("{\"count\": 3}");
|
||||
|
||||
output = plugin.call(functionName, input);
|
||||
assertThat(output).isEqualTo("{\"count\": 3}");
|
||||
}
|
||||
output = plugin.call(functionName, input);
|
||||
assertThat(output).isEqualTo("{\"count\": 3}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,14 +125,12 @@ public class PluginTests {
|
||||
|
||||
HostFunction[] functions = {helloWorld};
|
||||
|
||||
try (var ctx = new Context()) {
|
||||
Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource()));
|
||||
String functionName = "count_vowels";
|
||||
Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource()));
|
||||
String functionName = "count_vowels";
|
||||
|
||||
try (var plugin = ctx.newPlugin(manifest, true, functions)) {
|
||||
var output = plugin.call(functionName, "this is a test");
|
||||
assertThat(output).isEqualTo("test");
|
||||
}
|
||||
try (var plugin = new Plugin(manifest, true, functions)) {
|
||||
var output = plugin.call(functionName, "this is a test");
|
||||
assertThat(output).isEqualTo("test");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,30 +172,26 @@ public class PluginTests {
|
||||
|
||||
HostFunction[] functions = {f,g};
|
||||
|
||||
try (var ctx = new Context()) {
|
||||
Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource()));
|
||||
String functionName = "count_vowels";
|
||||
Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource()));
|
||||
String functionName = "count_vowels";
|
||||
|
||||
try (var plugin = ctx.newPlugin(manifest, true, functions)) {
|
||||
var output = plugin.call(functionName, "this is a test");
|
||||
assertThat(output).isEqualTo("test");
|
||||
}
|
||||
try (var plugin = new Plugin(manifest, true, functions)) {
|
||||
var output = plugin.call(functionName, "this is a test");
|
||||
assertThat(output).isEqualTo("test");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void shouldFailToInvokeUnknownHostFunction() {
|
||||
try (var ctx = new Context()) {
|
||||
Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource()));
|
||||
String functionName = "count_vowels";
|
||||
Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource()));
|
||||
String functionName = "count_vowels";
|
||||
|
||||
try {
|
||||
var plugin = ctx.newPlugin(manifest, true, null);
|
||||
plugin.call(functionName, "this is a test");
|
||||
} catch (ExtismException e) {
|
||||
assertThat(e.getMessage()).contains("unknown import: `env::hello_world` has not been defined");
|
||||
}
|
||||
try {
|
||||
var plugin = new Plugin(manifest, true, null);
|
||||
plugin.call(functionName, "this is a test");
|
||||
} catch (ExtismException e) {
|
||||
assertThat(e.getMessage()).contains("unknown import: `env::hello_world` has not been defined");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "libextism"
|
||||
version = "0.5.0"
|
||||
version = "1.0.0-alpha.0"
|
||||
edition = "2021"
|
||||
authors = ["The Extism Authors", "oss@extism.org"]
|
||||
license = "BSD-3-Clause"
|
||||
@@ -13,11 +13,11 @@ name = "extism"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
extism-runtime = {path = "../runtime"}
|
||||
extism = {path = "../runtime"}
|
||||
|
||||
[features]
|
||||
default = ["http", "register-http", "register-filesystem"]
|
||||
nn = ["extism-runtime/nn"]
|
||||
register-http = ["extism-runtime/register-http"] # enables wasm to be downloaded using http
|
||||
register-filesystem = ["extism-runtime/register-filesystem"] # enables wasm to be loaded from disk
|
||||
http = ["extism-runtime/http"] # enables extism_http_request
|
||||
nn = ["extism/nn"]
|
||||
register-http = ["extism/register-http"] # enables wasm to be downloaded using http
|
||||
register-filesystem = ["extism/register-filesystem"] # enables wasm to be loaded from disk
|
||||
http = ["extism/http"] # enables extism_http_request
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! This crate is used to generate `libextism` using `extism-runtime`
|
||||
|
||||
pub use extism_runtime::sdk::*;
|
||||
pub use extism::sdk::*;
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "extism-manifest"
|
||||
version = "0.5.0"
|
||||
version = "1.0.0-alpha.0"
|
||||
edition = "2021"
|
||||
authors = ["The Extism Authors", "oss@extism.org"]
|
||||
license = "BSD-3-Clause"
|
||||
|
||||
@@ -240,8 +240,19 @@ impl Manifest {
|
||||
}
|
||||
|
||||
/// Set `config`
|
||||
pub fn with_config(mut self, c: impl Iterator<Item = (String, String)>) -> Self {
|
||||
self.config = c.collect();
|
||||
pub fn with_config(
|
||||
mut self,
|
||||
c: impl Iterator<Item = (impl Into<String>, impl Into<String>)>,
|
||||
) -> Self {
|
||||
for (k, v) in c {
|
||||
self.config.insert(k.into(), v.into());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a single `config` key
|
||||
pub fn with_config_key(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
|
||||
self.config.insert(k.into(), v.into());
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
@@ -37,9 +37,3 @@ async function main() {
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
// or, use a context like this:
|
||||
// let ctx = new Context();
|
||||
// let wasm = readFileSync("../wasm/code.wasm");
|
||||
// let p = ctx.plugin(wasm);
|
||||
// ... where the context can be passed around to various functions etc.
|
||||
|
||||
@@ -6,14 +6,12 @@ var ArrayType = require("ref-array-di")(ref);
|
||||
var StructType = require("ref-struct-di")(ref);
|
||||
var UnionType = require("ref-union-di")(ref);
|
||||
|
||||
const plugin = "void*";
|
||||
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 PtrArray = new ArrayType(function_t);
|
||||
|
||||
let ValUnion = new UnionType({
|
||||
i32: ref.types.uint32,
|
||||
@@ -36,28 +34,29 @@ let Val = new StructType({
|
||||
let ValArray = ArrayType(Val);
|
||||
|
||||
const _functions = {
|
||||
extism_context_new: [context, []],
|
||||
extism_context_free: ["void", [context]],
|
||||
extism_plugin_new: [
|
||||
pluginIndex,
|
||||
[context, "string", "uint64", PtrArray, "uint64", "bool"],
|
||||
plugin,
|
||||
[
|
||||
"string",
|
||||
"uint64",
|
||||
PtrArray,
|
||||
"uint64",
|
||||
"bool",
|
||||
ref.refType(ref.types.char),
|
||||
],
|
||||
],
|
||||
extism_plugin_update: [
|
||||
"bool",
|
||||
[context, pluginIndex, "string", "uint64", PtrArray, "uint64", "bool"],
|
||||
],
|
||||
extism_error: ["string", [context, pluginIndex]],
|
||||
extism_plugin_error: ["string", [plugin]],
|
||||
extism_plugin_call: [
|
||||
"int32",
|
||||
[context, pluginIndex, "string", "string", "uint64"],
|
||||
[plugin, "string", "string", "uint64"],
|
||||
],
|
||||
extism_plugin_output_length: ["uint64", [context, pluginIndex]],
|
||||
extism_plugin_output_data: ["uint8*", [context, pluginIndex]],
|
||||
extism_plugin_output_length: ["uint64", [plugin]],
|
||||
extism_plugin_output_data: ["uint8*", [plugin]],
|
||||
extism_log_file: ["bool", ["string", "char*"]],
|
||||
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_plugin_function_exists: ["bool", [plugin, "string"]],
|
||||
extism_plugin_config: ["void", [plugin, "char*", "uint64"]],
|
||||
extism_plugin_free: ["void", [plugin]],
|
||||
extism_plugin_new_error_free: ["void", ["char*"]],
|
||||
extism_version: ["string", []],
|
||||
extism_function_new: [
|
||||
function_t,
|
||||
@@ -78,7 +77,7 @@ const _functions = {
|
||||
extism_current_plugin_memory_alloc: ["uint64", ["void*", "uint64"]],
|
||||
extism_current_plugin_memory_length: ["uint64", ["void*", "uint64"]],
|
||||
extism_current_plugin_memory_free: ["void", ["void*", "uint64"]],
|
||||
extism_plugin_cancel_handle: ["void*", [context, pluginIndex]],
|
||||
extism_plugin_cancel_handle: ["void*", [plugin]],
|
||||
extism_plugin_cancel: ["bool", ["void*"]],
|
||||
};
|
||||
|
||||
@@ -96,49 +95,35 @@ export enum ValType {
|
||||
}
|
||||
|
||||
interface LibExtism {
|
||||
extism_context_new: () => Buffer;
|
||||
extism_context_free: (ctx: Buffer) => void;
|
||||
extism_plugin_new: (
|
||||
ctx: Buffer,
|
||||
data: string | Buffer,
|
||||
data_len: number,
|
||||
functions: Buffer,
|
||||
nfunctions: number,
|
||||
wasi: boolean,
|
||||
) => number;
|
||||
extism_plugin_update: (
|
||||
ctx: Buffer,
|
||||
plugin_id: number,
|
||||
data: string | Buffer,
|
||||
data_len: number,
|
||||
functions: Buffer,
|
||||
nfunctions: number,
|
||||
wasi: boolean,
|
||||
) => boolean;
|
||||
extism_error: (ctx: Buffer, plugin_id: number) => string;
|
||||
errmsg: Buffer | null,
|
||||
) => Buffer;
|
||||
extism_plugin_error: (plugin: Buffer) => string;
|
||||
extism_plugin_call: (
|
||||
ctx: Buffer,
|
||||
plugin_id: number,
|
||||
plugin: Buffer,
|
||||
func: string,
|
||||
input: string,
|
||||
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_length: (plugin: Buffer) => number;
|
||||
extism_plugin_output_data: (plugin: Buffer) => Uint8Array;
|
||||
extism_log_file: (file: string, level: string) => boolean;
|
||||
extism_plugin_function_exists: (
|
||||
ctx: Buffer,
|
||||
plugin_id: number,
|
||||
plugin: Buffer,
|
||||
func: string,
|
||||
) => boolean;
|
||||
extism_plugin_config: (
|
||||
ctx: Buffer,
|
||||
plugin_id: number,
|
||||
plugin: Buffer,
|
||||
data: string | Buffer,
|
||||
data_len: number,
|
||||
) => void;
|
||||
extism_plugin_free: (ctx: Buffer, plugin_id: number) => void;
|
||||
extism_context_reset: (ctx: Buffer) => void;
|
||||
extism_plugin_free: (plugin: Buffer) => void;
|
||||
extism_plugin_new_error_free: (error: Buffer) => void;
|
||||
extism_version: () => string;
|
||||
extism_function_new: (
|
||||
name: string,
|
||||
@@ -156,7 +141,7 @@ interface LibExtism {
|
||||
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;
|
||||
extism_plugin_cancel_handle: (p: Buffer, n: number) => Buffer;
|
||||
extism_plugin_cancel_handle: (p: Buffer) => Buffer;
|
||||
extism_plugin_cancel: (p: Buffer) => boolean;
|
||||
}
|
||||
|
||||
@@ -206,13 +191,13 @@ export function extismVersion(): string {
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const contextRegistry = new FinalizationRegistry((pointer) => {
|
||||
if (pointer) lib.extism_context_free(pointer);
|
||||
const functionRegistry = new FinalizationRegistry((pointer) => {
|
||||
if (pointer) lib.extism_function_free(pointer);
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
const functionRegistry = new FinalizationRegistry((pointer) => {
|
||||
if (pointer) lib.extism_function_free(pointer);
|
||||
const pluginRegistry = new FinalizationRegistry((handle) => {
|
||||
handle();
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -272,98 +257,9 @@ export type Manifest = {
|
||||
type ManifestData = Manifest | Buffer | string;
|
||||
|
||||
/**
|
||||
* A Context is needed to create plugins. The Context
|
||||
* is where your plugins live. Freeing the context
|
||||
* frees all of the plugins in its scope. We recommand managing
|
||||
* the context with {@link withContext}
|
||||
*
|
||||
* @see {@link withContext}
|
||||
*
|
||||
* @example
|
||||
* Use withContext to ensure your memory is cleaned up
|
||||
* ```
|
||||
* const output = await withContext(async (ctx) => {
|
||||
* const plugin = ctx.plugin(manifest)
|
||||
* return await plugin.call("func", "my-input")
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* You can manage manually if you need a long-lived context
|
||||
* ```
|
||||
* const ctx = Context()
|
||||
* // free all the plugins and reset
|
||||
* ctx.reset()
|
||||
* // free everything
|
||||
* ctx.free()
|
||||
* ```
|
||||
* A memory handle points to a particular offset in memory
|
||||
*/
|
||||
export class Context {
|
||||
pointer: Buffer | null;
|
||||
|
||||
/**
|
||||
* Construct a context
|
||||
*/
|
||||
constructor() {
|
||||
this.pointer = lib.extism_context_new();
|
||||
contextRegistry.register(this, this.pointer, this.pointer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a plugin managed by this context
|
||||
*
|
||||
* @param manifest - The {@link Manifest} describing the plugin code and config
|
||||
* @param wasi - Set to `true` to enable WASI
|
||||
* @param config - Config details for the plugin
|
||||
* @returns A new Plugin scoped to this Context
|
||||
*/
|
||||
plugin(
|
||||
manifest: ManifestData,
|
||||
wasi: boolean = false,
|
||||
functions: HostFunction[] = [],
|
||||
config?: PluginConfig,
|
||||
) {
|
||||
return new Plugin(manifest, wasi, functions, config, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees the context. Should be called after the context is not needed to reclaim the memory.
|
||||
*/
|
||||
free() {
|
||||
contextRegistry.unregister(this.pointer);
|
||||
if (this.pointer) {
|
||||
lib.extism_context_free(this.pointer);
|
||||
this.pointer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the context. This clears all the plugins but keeps the context alive.
|
||||
*/
|
||||
reset() {
|
||||
if (this.pointer) lib.extism_context_reset(this.pointer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a context and gives you a scope to use it. This will ensure the context
|
||||
* and all its plugins are cleaned up for you when you are done.
|
||||
*
|
||||
* @param f - The callback function with the context
|
||||
* @returns Whatever your callback returns
|
||||
*/
|
||||
export async function withContext(f: (ctx: Context) => Promise<any>) {
|
||||
const ctx = new Context();
|
||||
|
||||
try {
|
||||
const x = await f(ctx);
|
||||
ctx.free();
|
||||
return x;
|
||||
} catch (err) {
|
||||
ctx.free();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
type MemoryHandle = number;
|
||||
|
||||
/**
|
||||
* Provides access to the plugin that is currently running from inside a {@link HostFunction}
|
||||
@@ -380,8 +276,11 @@ export class CurrentPlugin {
|
||||
* @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);
|
||||
memory(offset: MemoryHandle): Buffer {
|
||||
const length = lib.extism_current_plugin_memory_length(
|
||||
this.pointer,
|
||||
offset,
|
||||
);
|
||||
return Buffer.from(
|
||||
lib.extism_current_plugin_memory(this.pointer).buffer,
|
||||
offset,
|
||||
@@ -394,7 +293,7 @@ export class CurrentPlugin {
|
||||
* @param n - The number of bytes to allocate
|
||||
* @returns the offset to the newly allocated block
|
||||
*/
|
||||
memoryAlloc(n: number): number {
|
||||
memoryAlloc(n: number): MemoryHandle {
|
||||
return lib.extism_current_plugin_memory_alloc(this.pointer, n);
|
||||
}
|
||||
|
||||
@@ -402,7 +301,7 @@ export class CurrentPlugin {
|
||||
* Free a memory block
|
||||
* @param offset - The offset of the block to free
|
||||
*/
|
||||
memoryFree(offset: number) {
|
||||
memoryFree(offset: MemoryHandle) {
|
||||
return lib.extism_current_plugin_memory_free(this.pointer, offset);
|
||||
}
|
||||
|
||||
@@ -411,7 +310,7 @@ export class CurrentPlugin {
|
||||
* @param offset - The offset of the block
|
||||
* @returns the length of the block specified by `offset`
|
||||
*/
|
||||
memoryLength(offset: number): number {
|
||||
memoryLength(offset: MemoryHandle): number {
|
||||
return lib.extism_current_plugin_memory_length(this.pointer, offset);
|
||||
}
|
||||
|
||||
@@ -421,7 +320,7 @@ export class CurrentPlugin {
|
||||
* @param s - The string to return
|
||||
*/
|
||||
returnString(output: typeof Val, s: string) {
|
||||
var offs = this.memoryAlloc(Buffer.byteLength(s));
|
||||
const offs = this.memoryAlloc(Buffer.byteLength(s));
|
||||
this.memory(offs).write(s);
|
||||
output.v.i64 = offs;
|
||||
}
|
||||
@@ -432,7 +331,7 @@ export class CurrentPlugin {
|
||||
* @param b - The buffer to return
|
||||
*/
|
||||
returnBytes(output: typeof Val, b: Buffer) {
|
||||
var offs = this.memoryAlloc(b.length);
|
||||
const offs = this.memoryAlloc(b.length);
|
||||
this.memory(offs).fill(b);
|
||||
output.v.i64 = offs;
|
||||
}
|
||||
@@ -581,30 +480,24 @@ export class CancelHandle {
|
||||
* A Plugin represents an instance of your WASM program from the given manifest.
|
||||
*/
|
||||
export class Plugin {
|
||||
id: number;
|
||||
ctx: Context;
|
||||
plugin: Buffer | null;
|
||||
functions: typeof PtrArray;
|
||||
token: { id: number; pointer: Buffer };
|
||||
token: { plugin: Buffer | null };
|
||||
|
||||
/**
|
||||
* Constructor for a plugin. @see {@link Context#plugin}.
|
||||
* Constructor for a 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
|
||||
* @param ctx - The context to manage this plugin, or null to use a new context
|
||||
*/
|
||||
constructor(
|
||||
manifest: ManifestData,
|
||||
wasi: boolean = false,
|
||||
functions: HostFunction[] = [],
|
||||
config?: PluginConfig,
|
||||
ctx: Context | null = null,
|
||||
) {
|
||||
if (ctx == null) {
|
||||
ctx = new Context();
|
||||
}
|
||||
let dataRaw: string | Buffer;
|
||||
if (Buffer.isBuffer(manifest) || typeof manifest === "string") {
|
||||
dataRaw = manifest;
|
||||
@@ -613,35 +506,32 @@ export class Plugin {
|
||||
} else {
|
||||
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,
|
||||
const plugin = lib.extism_plugin_new(
|
||||
dataRaw,
|
||||
Buffer.byteLength(dataRaw, "utf-8"),
|
||||
this.functions,
|
||||
functions.length,
|
||||
wasi,
|
||||
null,
|
||||
);
|
||||
if (plugin < 0) {
|
||||
var err = lib.extism_error(ctx.pointer, -1);
|
||||
if (err.length === 0) {
|
||||
throw "extism_context_plugin failed";
|
||||
}
|
||||
throw `Unable to load plugin: ${err.toString()}`;
|
||||
if (ref.address(plugin) === 0) {
|
||||
// TODO: handle error
|
||||
throw Error("Failed to create plugin");
|
||||
}
|
||||
this.id = plugin;
|
||||
this.token = { id: this.id, pointer: ctx.pointer };
|
||||
this.ctx = ctx;
|
||||
this.plugin = plugin;
|
||||
this.token = { plugin: this.plugin };
|
||||
pluginRegistry.register(this, () => {
|
||||
this.free();
|
||||
}, this.token);
|
||||
|
||||
if (config != null) {
|
||||
let s = JSON.stringify(config);
|
||||
const s = JSON.stringify(config);
|
||||
lib.extism_plugin_config(
|
||||
ctx.pointer,
|
||||
this.id,
|
||||
this.plugin,
|
||||
s,
|
||||
Buffer.byteLength(s, "utf-8"),
|
||||
);
|
||||
@@ -652,66 +542,13 @@ export class Plugin {
|
||||
* Return a new `CancelHandle`, which can be used to cancel a running Plugin
|
||||
*/
|
||||
cancelHandle(): CancelHandle {
|
||||
if (!this.ctx.pointer) throw Error("No Context set");
|
||||
let handle = lib.extism_plugin_cancel_handle(this.ctx.pointer, this.id);
|
||||
if (this.plugin === null) {
|
||||
throw Error("Plugin already freed");
|
||||
}
|
||||
const handle = lib.extism_plugin_cancel_handle(this.plugin);
|
||||
return new CancelHandle(handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing plugin with new WASM or manifest
|
||||
*
|
||||
* @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,
|
||||
functions: HostFunction[] = [],
|
||||
config?: PluginConfig,
|
||||
) {
|
||||
let dataRaw: string | Buffer;
|
||||
if (Buffer.isBuffer(manifest) || typeof manifest === "string") {
|
||||
dataRaw = manifest;
|
||||
} else if (typeof manifest === "object" && manifest.wasm) {
|
||||
dataRaw = JSON.stringify(manifest);
|
||||
} else {
|
||||
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"),
|
||||
this.functions,
|
||||
functions.length,
|
||||
wasi,
|
||||
);
|
||||
if (!ok) {
|
||||
var err = lib.extism_error(this.ctx.pointer, -1);
|
||||
if (err.length === 0) {
|
||||
throw "extism_plugin_update failed";
|
||||
}
|
||||
throw `Unable to update plugin: ${err.toString()}`;
|
||||
}
|
||||
|
||||
if (config != null) {
|
||||
let s = JSON.stringify(config);
|
||||
lib.extism_plugin_config(
|
||||
this.ctx.pointer,
|
||||
this.id,
|
||||
s,
|
||||
Buffer.byteLength(s, "utf-8"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a function exists by name
|
||||
*
|
||||
@@ -720,10 +557,11 @@ export class Plugin {
|
||||
*/
|
||||
|
||||
functionExists(functionName: string) {
|
||||
if (!this.ctx.pointer) throw Error("No Context set");
|
||||
if (this.plugin === null) {
|
||||
throw Error("Plugin already freed");
|
||||
}
|
||||
return lib.extism_plugin_function_exists(
|
||||
this.ctx.pointer,
|
||||
this.id,
|
||||
this.plugin,
|
||||
functionName,
|
||||
);
|
||||
}
|
||||
@@ -734,7 +572,7 @@ export class Plugin {
|
||||
* @example
|
||||
* ```
|
||||
* const manifest = { wasm: [{ path: "/tmp/code.wasm" }] }
|
||||
* const plugin = ctx.plugin(manifest)
|
||||
* const plugin = new Plugin(manifest)
|
||||
* const output = await plugin.call("my_function", "some-input")
|
||||
* output.toString()
|
||||
* // => "output from the function"
|
||||
@@ -746,25 +584,27 @@ export class Plugin {
|
||||
*/
|
||||
async call(functionName: string, input: string | Buffer): Promise<Buffer> {
|
||||
return new Promise<Buffer>((resolve, reject) => {
|
||||
if (!this.ctx.pointer) throw Error("No Context set");
|
||||
if (this.plugin === null) {
|
||||
reject("Plugin already freed");
|
||||
return;
|
||||
}
|
||||
var rc = lib.extism_plugin_call(
|
||||
this.ctx.pointer,
|
||||
this.id,
|
||||
this.plugin,
|
||||
functionName,
|
||||
input.toString(),
|
||||
Buffer.byteLength(input, "utf-8"),
|
||||
);
|
||||
if (rc !== 0) {
|
||||
var err = lib.extism_error(this.ctx.pointer, this.id);
|
||||
if (err.length === 0) {
|
||||
reject(`extism_plugin_call: "${functionName}" failed`);
|
||||
var err = lib.extism_plugin_error(this.plugin);
|
||||
if (!err || err.length === 0) {
|
||||
reject(`Plugin error: call to "${functionName}" failed`);
|
||||
}
|
||||
reject(`Plugin error: ${err.toString()}, code: ${rc}`);
|
||||
}
|
||||
|
||||
var out_len = lib.extism_plugin_output_length(this.ctx.pointer, this.id);
|
||||
var out_len = lib.extism_plugin_output_length(this.plugin);
|
||||
var buf = Buffer.from(
|
||||
lib.extism_plugin_output_data(this.ctx.pointer, this.id).buffer,
|
||||
lib.extism_plugin_output_data(this.plugin).buffer,
|
||||
0,
|
||||
out_len,
|
||||
);
|
||||
@@ -776,9 +616,10 @@ export class Plugin {
|
||||
* Free a plugin, this should be called when the plugin is no longer needed
|
||||
*/
|
||||
free() {
|
||||
if (this.ctx.pointer && this.id >= 0) {
|
||||
lib.extism_plugin_free(this.ctx.pointer, this.id);
|
||||
this.id = -1;
|
||||
if (this.plugin !== null) {
|
||||
pluginRegistry.unregister(this.token);
|
||||
lib.extism_plugin_free(this.plugin);
|
||||
this.plugin = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ function manifest(functions: boolean = false): extism.Manifest {
|
||||
__dirname,
|
||||
functions
|
||||
? "/../../wasm/code-functions.wasm"
|
||||
: "/../../wasm/code.wasm"
|
||||
: "/../../wasm/code.wasm",
|
||||
),
|
||||
},
|
||||
],
|
||||
@@ -22,116 +22,60 @@ function wasmBuffer(): Buffer {
|
||||
}
|
||||
|
||||
describe("test extism", () => {
|
||||
test("can create new context", () => {
|
||||
let ctx = new extism.Context();
|
||||
expect(ctx).toBeTruthy();
|
||||
ctx.free();
|
||||
});
|
||||
|
||||
test("can create and call a plugin", async () => {
|
||||
await extism.withContext(async (ctx: extism.Context) => {
|
||||
const plugin = ctx.plugin(manifest());
|
||||
let output = await plugin.call("count_vowels", "this is a test");
|
||||
let result = JSON.parse(output.toString());
|
||||
expect(result["count"]).toBe(4);
|
||||
output = await plugin.call("count_vowels", "this is a test again");
|
||||
result = JSON.parse(output.toString());
|
||||
expect(result["count"]).toBe(7);
|
||||
output = await plugin.call("count_vowels", "this is a test thrice");
|
||||
result = JSON.parse(output.toString());
|
||||
expect(result["count"]).toBe(6);
|
||||
output = await plugin.call("count_vowels", "🌎hello🌎world🌎");
|
||||
result = JSON.parse(output.toString());
|
||||
expect(result["count"]).toBe(3);
|
||||
});
|
||||
const plugin = new extism.Plugin(manifest());
|
||||
let output = await plugin.call("count_vowels", "this is a test");
|
||||
let result = JSON.parse(output.toString());
|
||||
expect(result["count"]).toBe(4);
|
||||
output = await plugin.call("count_vowels", "this is a test again");
|
||||
result = JSON.parse(output.toString());
|
||||
expect(result["count"]).toBe(7);
|
||||
output = await plugin.call("count_vowels", "this is a test thrice");
|
||||
result = JSON.parse(output.toString());
|
||||
expect(result["count"]).toBe(6);
|
||||
output = await plugin.call("count_vowels", "🌎hello🌎world🌎");
|
||||
result = JSON.parse(output.toString());
|
||||
expect(result["count"]).toBe(3);
|
||||
});
|
||||
|
||||
test("can free a plugin", async () => {
|
||||
await extism.withContext(async (ctx: extism.Context) => {
|
||||
const plugin = ctx.plugin(manifest());
|
||||
let output = await plugin.call("count_vowels", "this is a test");
|
||||
plugin.free();
|
||||
await expect(() =>
|
||||
plugin.call("count_vowels", "this is a test")
|
||||
).rejects.toMatch(/Plugin error/);
|
||||
});
|
||||
});
|
||||
|
||||
test("can update the manifest", async () => {
|
||||
await extism.withContext(async (ctx: extism.Context) => {
|
||||
const plugin = ctx.plugin(manifest());
|
||||
let output = await plugin.call("count_vowels", "this is a test");
|
||||
let result = JSON.parse(output.toString());
|
||||
expect(result["count"]).toBe(4);
|
||||
// let's update the plugin with a manifest of raw module bytes
|
||||
plugin.update(wasmBuffer());
|
||||
// can still call it
|
||||
output = await plugin.call("count_vowels", "this is a test");
|
||||
result = JSON.parse(output.toString());
|
||||
expect(result["count"]).toBe(4);
|
||||
});
|
||||
const plugin = new extism.Plugin(manifest());
|
||||
let output = await plugin.call("count_vowels", "this is a test");
|
||||
plugin.free();
|
||||
await expect(() => plugin.call("count_vowels", "this is a test")).rejects
|
||||
.toMatch("Plugin already freed");
|
||||
});
|
||||
|
||||
test("can detect if function exists or not", async () => {
|
||||
await extism.withContext(async (ctx: extism.Context) => {
|
||||
const plugin = ctx.plugin(manifest());
|
||||
expect(plugin.functionExists("count_vowels")).toBe(true);
|
||||
expect(plugin.functionExists("i_dont_extist")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test("withContext returns results", async () => {
|
||||
const count = await extism.withContext(
|
||||
async (ctx: extism.Context): Promise<number> => {
|
||||
const plugin = ctx.plugin(manifest());
|
||||
let output = await plugin.call("count_vowels", "this is a test");
|
||||
let result = JSON.parse(output.toString());
|
||||
return result["count"];
|
||||
}
|
||||
);
|
||||
expect(count).toBe(4);
|
||||
const plugin = new extism.Plugin(manifest());
|
||||
expect(plugin.functionExists("count_vowels")).toBe(true);
|
||||
expect(plugin.functionExists("i_dont_extist")).toBe(false);
|
||||
});
|
||||
|
||||
test("errors when function is not known", async () => {
|
||||
await extism.withContext(async (ctx: extism.Context) => {
|
||||
const plugin = ctx.plugin(manifest());
|
||||
await expect(() =>
|
||||
plugin.call("i_dont_exist", "example-input")
|
||||
).rejects.toMatch(/Plugin error/);
|
||||
});
|
||||
});
|
||||
|
||||
test("can result context", async () => {
|
||||
await extism.withContext(async (ctx: extism.Context) => {
|
||||
const plugin = ctx.plugin(manifest());
|
||||
await plugin.call("count_vowels", "this is a test");
|
||||
ctx.reset();
|
||||
await expect(() =>
|
||||
plugin.call("i_dont_exist", "example-input")
|
||||
).rejects.toMatch(/Plugin error/);
|
||||
});
|
||||
const plugin = new extism.Plugin(manifest());
|
||||
await expect(() => plugin.call("i_dont_exist", "example-input")).rejects
|
||||
.toMatch(/Plugin error/);
|
||||
});
|
||||
|
||||
test("host functions work", async () => {
|
||||
await extism.withContext(async (ctx: extism.Context) => {
|
||||
const plugin = ctx.plugin(manifest(true), true, [
|
||||
new extism.HostFunction(
|
||||
"hello_world",
|
||||
[extism.ValType.I64],
|
||||
[extism.ValType.I64],
|
||||
(plugin: any, params: any, results: any, user_data: string) => {
|
||||
const offs = plugin.memoryAlloc(user_data.length);
|
||||
const mem = plugin.memory(offs);
|
||||
mem.write(user_data);
|
||||
results[0].v.i64 = offs;
|
||||
},
|
||||
"test"
|
||||
),
|
||||
]);
|
||||
const plugin = new extism.Plugin(manifest(true), true, [
|
||||
new extism.HostFunction(
|
||||
"hello_world",
|
||||
[extism.ValType.I64],
|
||||
[extism.ValType.I64],
|
||||
(plugin: any, params: any, results: any, user_data: string) => {
|
||||
const offs = plugin.memoryAlloc(user_data.length);
|
||||
const mem = plugin.memory(offs);
|
||||
mem.write(user_data);
|
||||
results[0].v.i64 = offs;
|
||||
},
|
||||
"test",
|
||||
),
|
||||
]);
|
||||
|
||||
const res = await plugin.call("count_vowels", "aaa");
|
||||
const res = await plugin.call("count_vowels", "aaa");
|
||||
|
||||
expect(res.toString()).toBe("test");
|
||||
});
|
||||
expect(res.toString()).toBe("test");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1 +1 @@
|
||||
version = 0.24.1
|
||||
version = 0.26.0
|
||||
|
||||
@@ -7,6 +7,7 @@ let main file func_name input =
|
||||
let input = if String.equal input "-" then read_stdin () else input in
|
||||
let file = In_channel.with_open_bin file In_channel.input_all in
|
||||
let plugin = Plugin.create file ~wasi:true |> Result.get_ok in
|
||||
print_endline (Plugin.id plugin |> Uuidm.to_string);
|
||||
let res = Plugin.call plugin ~name:func_name input |> Result.get_ok in
|
||||
print_endline res
|
||||
|
||||
|
||||
@@ -45,9 +45,6 @@ let from =
|
||||
open Ctypes
|
||||
|
||||
let fn = Foreign.foreign ~from ~release_runtime_lock:true
|
||||
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 | V128 | FuncRef | ExternRef
|
||||
@@ -94,54 +91,46 @@ module Extism_val = struct
|
||||
let () = seal t
|
||||
end
|
||||
|
||||
let plugin = ptr void
|
||||
|
||||
let extism_plugin_new_error_free =
|
||||
fn "extism_plugin_new_error_free" (ptr char @-> returning void)
|
||||
|
||||
let extism_plugin_new =
|
||||
fn "extism_plugin_new"
|
||||
(context @-> string @-> uint64_t
|
||||
(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
|
||||
@-> ptr (ptr void)
|
||||
@-> uint64_t @-> bool @-> returning bool)
|
||||
@-> uint64_t @-> bool
|
||||
@-> ptr (ptr char)
|
||||
@-> returning plugin)
|
||||
|
||||
let extism_plugin_config =
|
||||
fn "extism_plugin_config"
|
||||
(context @-> int32_t @-> string @-> uint64_t @-> returning bool)
|
||||
fn "extism_plugin_config" (plugin @-> string @-> uint64_t @-> returning bool)
|
||||
|
||||
let extism_plugin_call =
|
||||
fn "extism_plugin_call"
|
||||
(context @-> int32_t @-> string @-> ptr char @-> uint64_t
|
||||
@-> returning int32_t)
|
||||
(plugin @-> string @-> ptr char @-> uint64_t @-> returning int32_t)
|
||||
|
||||
let extism_plugin_call_s =
|
||||
fn "extism_plugin_call"
|
||||
(context @-> int32_t @-> string @-> string @-> uint64_t
|
||||
@-> returning int32_t)
|
||||
(plugin @-> string @-> string @-> uint64_t @-> returning int32_t)
|
||||
|
||||
let extism_error =
|
||||
fn "extism_error" (context @-> int32_t @-> returning string_opt)
|
||||
let extism_error = fn "extism_plugin_error" (plugin @-> returning string_opt)
|
||||
|
||||
let extism_plugin_output_length =
|
||||
fn "extism_plugin_output_length" (context @-> int32_t @-> returning uint64_t)
|
||||
fn "extism_plugin_output_length" (plugin @-> returning uint64_t)
|
||||
|
||||
let extism_plugin_output_data =
|
||||
fn "extism_plugin_output_data" (context @-> int32_t @-> returning (ptr char))
|
||||
fn "extism_plugin_output_data" (plugin @-> returning (ptr char))
|
||||
|
||||
let extism_log_file =
|
||||
fn "extism_log_file" (string @-> string_opt @-> returning bool)
|
||||
|
||||
let extism_version = fn "extism_version" (void @-> returning string)
|
||||
|
||||
let extism_plugin_free =
|
||||
fn "extism_plugin_free" (context @-> int32_t @-> returning void)
|
||||
|
||||
let extism_context_reset = fn "extism_context_reset" (context @-> returning void)
|
||||
let extism_plugin_free = fn "extism_plugin_free" (plugin @-> returning void)
|
||||
|
||||
let extism_plugin_function_exists =
|
||||
fn "extism_plugin_function_exists"
|
||||
(context @-> int32_t @-> string @-> returning bool)
|
||||
fn "extism_plugin_function_exists" (plugin @-> string @-> returning bool)
|
||||
|
||||
let extism_function_type =
|
||||
Foreign.funptr ~runtime_lock:true
|
||||
@@ -179,7 +168,9 @@ let extism_current_plugin_memory_free =
|
||||
(ptr void @-> uint64_t @-> returning void)
|
||||
|
||||
let extism_plugin_cancel_handle =
|
||||
fn "extism_plugin_cancel_handle" (context @-> int32_t @-> returning (ptr void))
|
||||
fn "extism_plugin_cancel_handle" (plugin @-> returning (ptr void))
|
||||
|
||||
let extism_plugin_cancel =
|
||||
fn "extism_plugin_cancel" (ptr void @-> returning bool)
|
||||
|
||||
let extism_plugin_id = fn "extism_plugin_id" (ptr void @-> returning (ptr char))
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
type t = { mutable pointer : unit Ctypes.ptr }
|
||||
|
||||
let create () =
|
||||
let ptr = Bindings.extism_context_new () in
|
||||
let t = { pointer = ptr } in
|
||||
Gc.finalise (fun { pointer } -> Bindings.extism_context_free pointer) t;
|
||||
t
|
||||
|
||||
let free ctx =
|
||||
let () = Bindings.extism_context_free ctx.pointer in
|
||||
ctx.pointer <- Ctypes.null
|
||||
|
||||
let reset ctx = Bindings.extism_context_reset ctx.pointer
|
||||
|
||||
let%test "test context" =
|
||||
let ctx = create () in
|
||||
reset ctx;
|
||||
free ctx;
|
||||
true
|
||||
@@ -1,7 +1,7 @@
|
||||
open Ctypes
|
||||
|
||||
type t = unit ptr
|
||||
type memory_block = { offs : Unsigned.UInt64.t; len : Unsigned.UInt64.t }
|
||||
type memory_handle = { offs : Unsigned.UInt64.t; len : Unsigned.UInt64.t }
|
||||
|
||||
let memory ?(offs = Unsigned.UInt64.zero) t =
|
||||
Bindings.extism_current_plugin_memory t +@ Unsigned.UInt64.to_int offs
|
||||
@@ -17,7 +17,7 @@ let alloc t len =
|
||||
|
||||
let free t { offs; _ } = Bindings.extism_current_plugin_memory_free t offs
|
||||
|
||||
module Memory_block = struct
|
||||
module Memory_handle = struct
|
||||
let of_val t v =
|
||||
match Types.Val.to_i64 v with
|
||||
| None -> None
|
||||
@@ -63,22 +63,22 @@ end
|
||||
|
||||
let return_string t (outputs : Types.Val_array.t) index s =
|
||||
let mem = alloc t (String.length s) in
|
||||
Memory_block.set_string t mem s;
|
||||
Memory_handle.set_string t mem s;
|
||||
Types.Val_array.(
|
||||
outputs.$[index] <- Types.Val.of_i64 (Unsigned.UInt64.to_int64 mem.offs))
|
||||
|
||||
let return_bigstring t (outputs : Types.Val_array.t) index s =
|
||||
let mem = alloc t (Bigstringaf.length s) in
|
||||
Memory_block.set_bigstring t mem s;
|
||||
Memory_handle.set_bigstring t mem s;
|
||||
Types.Val_array.(
|
||||
outputs.$[index] <- Types.Val.of_i64 (Unsigned.UInt64.to_int64 mem.offs))
|
||||
|
||||
let input_string t inputs index =
|
||||
let inp = Types.Val_array.(inputs.$[index]) in
|
||||
let mem = Memory_block.of_val_exn t inp in
|
||||
Memory_block.get_string t mem
|
||||
let mem = Memory_handle.of_val_exn t inp in
|
||||
Memory_handle.get_string t mem
|
||||
|
||||
let input_bigstring t inputs index =
|
||||
let inp = Types.Val_array.(inputs.$[index]) in
|
||||
let mem = Memory_block.of_val_exn t inp in
|
||||
Memory_block.get_bigstring t mem
|
||||
let mem = Memory_handle.of_val_exn t inp in
|
||||
Memory_handle.get_bigstring t mem
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
(public_name extism)
|
||||
(inline_tests
|
||||
(deps test/code.wasm test/code-functions.wasm))
|
||||
(libraries ctypes.foreign bigstringaf extism-manifest)
|
||||
(libraries ctypes.foreign bigstringaf extism-manifest uuidm)
|
||||
(preprocess
|
||||
(pps ppx_yojson_conv ppx_inline_test)))
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
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
|
||||
|
||||
let with_plugin f p =
|
||||
Fun.protect ~finally:(fun () -> Plugin.free p) (fun () -> f p)
|
||||
|
||||
let%test _ = String.length (extism_version ()) > 0
|
||||
|
||||
let set_log_file ?level filename =
|
||||
|
||||
@@ -101,20 +101,20 @@ module Current_plugin : sig
|
||||
type t
|
||||
(** Opaque type, wraps [ExtismCurrentPlugin] *)
|
||||
|
||||
type memory_block = { offs : Unsigned.UInt64.t; len : Unsigned.UInt64.t }
|
||||
type memory_handle = { offs : Unsigned.UInt64.t; len : Unsigned.UInt64.t }
|
||||
(** Represents a block of guest memory *)
|
||||
|
||||
val memory : ?offs:Unsigned.UInt64.t -> t -> Unsigned.uint8 Ctypes.ptr
|
||||
(** Get pointer to entire plugin memory *)
|
||||
|
||||
val find : t -> Unsigned.UInt64.t -> memory_block option
|
||||
(** Find memory block *)
|
||||
val find : t -> Unsigned.UInt64.t -> memory_handle option
|
||||
(** Convert an offset into a {memory_handle} *)
|
||||
|
||||
val alloc : t -> int -> memory_block
|
||||
val alloc : t -> int -> memory_handle
|
||||
(** Allocate a new block of memory *)
|
||||
|
||||
val free : t -> memory_block -> unit
|
||||
(** Free an allocated block of memory *)
|
||||
val free : t -> memory_handle -> unit
|
||||
(** Free allocated memory *)
|
||||
|
||||
val return_string : t -> Val_array.t -> int -> string -> unit
|
||||
val return_bigstring : t -> Val_array.t -> int -> Bigstringaf.t -> unit
|
||||
@@ -122,27 +122,27 @@ module Current_plugin : sig
|
||||
val input_bigstring : t -> Val_array.t -> int -> Bigstringaf.t
|
||||
|
||||
(** Some helpter functions for reading/writing memory *)
|
||||
module Memory_block : sig
|
||||
val to_val : memory_block -> Val.t
|
||||
module Memory_handle : sig
|
||||
val to_val : memory_handle -> Val.t
|
||||
(** Convert memory block to [Val] *)
|
||||
|
||||
val of_val : t -> Val.t -> memory_block option
|
||||
val of_val : t -> Val.t -> memory_handle option
|
||||
(** Convert [Val] to memory block *)
|
||||
|
||||
val of_val_exn : t -> Val.t -> memory_block
|
||||
val of_val_exn : t -> Val.t -> memory_handle
|
||||
(** Convert [Val] to memory block, raises [Invalid_argument] if the value is not a pointer
|
||||
to a valid memory block *)
|
||||
|
||||
val get_string : t -> memory_block -> string
|
||||
val get_string : t -> memory_handle -> string
|
||||
(** Get a string from memory stored at the provided offset *)
|
||||
|
||||
val get_bigstring : t -> memory_block -> Bigstringaf.t
|
||||
val get_bigstring : t -> memory_handle -> Bigstringaf.t
|
||||
(** Get a bigstring from memory stored at the provided offset *)
|
||||
|
||||
val set_string : t -> memory_block -> string -> unit
|
||||
val set_string : t -> memory_handle -> string -> unit
|
||||
(** Store a string into memory at the provided offset *)
|
||||
|
||||
val set_bigstring : t -> memory_block -> Bigstringaf.t -> unit
|
||||
val set_bigstring : t -> memory_handle -> Bigstringaf.t -> unit
|
||||
(** Store a bigstring into memory at the provided offset *)
|
||||
end
|
||||
end
|
||||
@@ -178,25 +178,6 @@ module Function : sig
|
||||
(** Free a list of functions *)
|
||||
end
|
||||
|
||||
(** [Context] is used to group plugins *)
|
||||
module Context : sig
|
||||
type t
|
||||
(** Context type *)
|
||||
|
||||
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 reset : t -> unit
|
||||
(** Reset a context. All plugins will be removed *)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
@@ -208,7 +189,6 @@ module Plugin : sig
|
||||
?config:Manifest.config ->
|
||||
?wasi:bool ->
|
||||
?functions:Function.t list ->
|
||||
?context:Context.t ->
|
||||
string ->
|
||||
(t, Error.t) result
|
||||
(** Make a new plugin from raw WebAssembly or JSON encoded manifest *)
|
||||
@@ -216,23 +196,10 @@ module Plugin : sig
|
||||
val of_manifest :
|
||||
?wasi:bool ->
|
||||
?functions:Function.t list ->
|
||||
?context:Context.t ->
|
||||
Manifest.t ->
|
||||
(t, Error.t) result
|
||||
(** Make a new plugin from a [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 call_bigstring :
|
||||
t -> name:string -> Bigstringaf.t -> (Bigstringaf.t, Error.t) result
|
||||
(** Call a function, uses [Bigstringaf.t] for input/output *)
|
||||
@@ -246,11 +213,15 @@ module Plugin : sig
|
||||
val function_exists : t -> string -> bool
|
||||
(** Check if a function is exported by a plugin *)
|
||||
|
||||
module Cancel_handle: sig
|
||||
module Cancel_handle : sig
|
||||
type t
|
||||
|
||||
val cancel: t -> bool
|
||||
val cancel : t -> bool
|
||||
end
|
||||
|
||||
val cancel_handle: t -> Cancel_handle.t
|
||||
val cancel_handle : t -> Cancel_handle.t
|
||||
|
||||
val id : t -> Uuidm.t
|
||||
end
|
||||
|
||||
val with_plugin : (Plugin.t -> 'a) -> Plugin.t -> 'a
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
module Manifest = Extism_manifest
|
||||
|
||||
type t = { id : int32; ctx : Context.t; mutable functions : Function.t list }
|
||||
|
||||
let with_context f =
|
||||
let ctx = Context.create () in
|
||||
let x =
|
||||
try f ctx
|
||||
with exc ->
|
||||
Context.free ctx;
|
||||
raise exc
|
||||
in
|
||||
Context.free ctx;
|
||||
x
|
||||
type t = {
|
||||
mutable pointer : unit Ctypes.ptr;
|
||||
mutable functions : Function.t list;
|
||||
}
|
||||
|
||||
let set_config plugin = function
|
||||
| None -> true
|
||||
@@ -19,39 +11,56 @@ let set_config plugin = function
|
||||
let config =
|
||||
Extism_manifest.yojson_of_config config |> Yojson.Safe.to_string
|
||||
in
|
||||
Bindings.extism_plugin_config plugin.ctx.pointer plugin.id config
|
||||
Bindings.extism_plugin_config plugin.pointer config
|
||||
(Unsigned.UInt64.of_int (String.length config))
|
||||
|
||||
let free t =
|
||||
if not (Ctypes.is_null t.ctx.pointer) then
|
||||
Bindings.extism_plugin_free t.ctx.pointer t.id
|
||||
if not (Ctypes.is_null t.pointer) then
|
||||
let () = Bindings.extism_plugin_free t.pointer in
|
||||
t.pointer <- Ctypes.null
|
||||
|
||||
let create ?config ?(wasi = false) ?(functions = []) ?context wasm =
|
||||
let ctx = match context with Some c -> c | None -> Context.create () in
|
||||
let strlen ptr =
|
||||
let rec aux ptr len =
|
||||
let c = Ctypes.( !@ ) ptr in
|
||||
if c = char_of_int 0 then len else aux (Ctypes.( +@ ) ptr 1) (len + 1)
|
||||
in
|
||||
aux ptr 0
|
||||
|
||||
let get_errmsg ptr =
|
||||
if Ctypes.is_null ptr then "Call failed"
|
||||
else
|
||||
let length = strlen ptr in
|
||||
let s = Ctypes.string_from_ptr ~length ptr in
|
||||
let () = Bindings.extism_plugin_new_error_free ptr in
|
||||
s
|
||||
|
||||
let create ?config ?(wasi = false) ?(functions = []) 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
|
||||
let errmsg =
|
||||
Ctypes.(allocate (ptr char) (coerce (ptr void) (ptr char) null))
|
||||
in
|
||||
let pointer =
|
||||
Bindings.extism_plugin_new wasm
|
||||
(Unsigned.UInt64.of_int (String.length wasm))
|
||||
(Ctypes.CArray.start arr)
|
||||
(Unsigned.UInt64.of_int n_funcs)
|
||||
wasi
|
||||
wasi errmsg
|
||||
in
|
||||
if id < 0l then
|
||||
match Bindings.extism_error ctx.pointer (-1l) with
|
||||
| None -> Error (`Msg "extism_plugin_call failed")
|
||||
| Some msg -> Error (`Msg msg)
|
||||
if Ctypes.is_null pointer then
|
||||
let s = get_errmsg (Ctypes.( !@ ) errmsg) in
|
||||
Error (`Msg s)
|
||||
else
|
||||
let t = { id; ctx; functions } in
|
||||
let t = { pointer; 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 ?functions ?context manifest =
|
||||
let of_manifest ?wasi ?functions manifest =
|
||||
let data = Manifest.to_json manifest in
|
||||
create ?wasi ?functions ?context data
|
||||
create ?wasi ?functions data
|
||||
|
||||
let%test "free plugin" =
|
||||
let manifest = Manifest.(create [ Wasm.file "test/code.wasm" ]) in
|
||||
@@ -59,54 +68,23 @@ let%test "free plugin" =
|
||||
free plugin;
|
||||
true
|
||||
|
||||
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
|
||||
match Bindings.extism_error ctx.pointer (-1l) with
|
||||
| None -> Error (`Msg "extism_plugin_update failed")
|
||||
| Some msg -> Error (`Msg msg)
|
||||
else if not (set_config plugin config) then
|
||||
Error (`Msg "call to set_config failed")
|
||||
let call' f { pointer; _ } ~name input len =
|
||||
if Ctypes.is_null pointer then Error.throw (`Msg "Plugin already freed")
|
||||
else
|
||||
let () = plugin.functions <- functions in
|
||||
Ok ()
|
||||
|
||||
let update_manifest plugin ?wasi manifest =
|
||||
let data = Manifest.to_json manifest in
|
||||
update plugin ?wasi data
|
||||
|
||||
let%test "update plugin manifest and config" =
|
||||
let manifest = Manifest.(create [ Wasm.file "test/code.wasm" ]) in
|
||||
let config = [ ("a", Some "1") ] in
|
||||
let plugin = of_manifest 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 rc = f ctx.pointer id name input len in
|
||||
if rc <> 0l then
|
||||
match Bindings.extism_error ctx.pointer id with
|
||||
| None -> Error (`Msg "extism_plugin_call failed")
|
||||
| Some msg -> Error (`Msg msg)
|
||||
else
|
||||
let out_len = Bindings.extism_plugin_output_length ctx.pointer id in
|
||||
let ptr = Bindings.extism_plugin_output_data ctx.pointer id in
|
||||
let buf =
|
||||
Ctypes.bigarray_of_ptr Ctypes.array1
|
||||
(Unsigned.UInt64.to_int out_len)
|
||||
Char ptr
|
||||
in
|
||||
Ok buf
|
||||
let rc = f pointer name input len in
|
||||
if rc <> 0l then
|
||||
match Bindings.extism_error pointer with
|
||||
| None -> Error (`Msg "extism_plugin_call failed")
|
||||
| Some msg -> Error (`Msg msg)
|
||||
else
|
||||
let out_len = Bindings.extism_plugin_output_length pointer in
|
||||
let ptr = Bindings.extism_plugin_output_data pointer in
|
||||
let buf =
|
||||
Ctypes.bigarray_of_ptr Ctypes.array1
|
||||
(Unsigned.UInt64.to_int out_len)
|
||||
Char ptr
|
||||
in
|
||||
Ok buf
|
||||
|
||||
let call_bigstring (t : t) ~name input =
|
||||
let len = Unsigned.UInt64.of_int (Bigstringaf.length input) in
|
||||
@@ -150,8 +128,9 @@ let%test "call_functions" =
|
||||
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 function_exists { pointer; _ } name =
|
||||
if Ctypes.is_null pointer then Error.throw (`Msg "Plugin already freed")
|
||||
else Bindings.extism_plugin_function_exists pointer name
|
||||
|
||||
let%test "function exists" =
|
||||
let manifest = Manifest.(create [ Wasm.file "test/code.wasm" ]) in
|
||||
@@ -165,5 +144,13 @@ module Cancel_handle = struct
|
||||
let cancel { inner } = Bindings.extism_plugin_cancel inner
|
||||
end
|
||||
|
||||
let cancel_handle { id; ctx; _ } =
|
||||
Cancel_handle.{ inner = Bindings.extism_plugin_cancel_handle ctx.pointer id }
|
||||
let cancel_handle { pointer; _ } =
|
||||
if Ctypes.is_null pointer then Error.throw (`Msg "Plugin already freed")
|
||||
else Cancel_handle.{ inner = Bindings.extism_plugin_cancel_handle pointer }
|
||||
|
||||
let id { pointer; _ } =
|
||||
if Ctypes.is_null pointer then Error.throw (`Msg "Plugin already freed")
|
||||
else
|
||||
let id = Bindings.extism_plugin_id pointer in
|
||||
let s = Ctypes.string_from_ptr id ~length:16 in
|
||||
Uuidm.unsafe_of_bytes s
|
||||
|
||||
@@ -9,9 +9,3 @@ $output = $plugin->call("count_vowels", "this is an example");
|
||||
$json = json_decode(pack('C*', ...$output));
|
||||
echo "Vowels counted = " . $json->{'count'} . PHP_EOL;
|
||||
|
||||
$wasm = file_get_contents("../../wasm/code.wasm");
|
||||
$ok = $plugin->update($wasm);
|
||||
if ($ok) {
|
||||
$id = $plugin->getId();
|
||||
echo "updated plugin: $id";
|
||||
}
|
||||
@@ -36,40 +36,6 @@ if ($lib == null) {
|
||||
throw new \Exception("Extism: failed to create new runtime instance");
|
||||
}
|
||||
|
||||
class Context
|
||||
{
|
||||
public $pointer;
|
||||
public $lib;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
global $lib;
|
||||
|
||||
if ($lib == null) {
|
||||
$lib = new \ExtismLib(\ExtismLib::SOFILE);
|
||||
}
|
||||
|
||||
$this->pointer = $lib->extism_context_new();
|
||||
$this->lib = $lib;
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
global $lib;
|
||||
|
||||
$lib->extism_context_free($this->pointer);
|
||||
}
|
||||
|
||||
|
||||
public function reset()
|
||||
{
|
||||
global $lib;
|
||||
|
||||
$lib->extism_context_reset($this->pointer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function set_log_file($filename, $level)
|
||||
{
|
||||
global $lib;
|
||||
|
||||
@@ -24,20 +24,22 @@ class CancelHandle
|
||||
class Plugin
|
||||
{
|
||||
private $lib;
|
||||
private $context;
|
||||
|
||||
private $wasi;
|
||||
private $config;
|
||||
|
||||
private $id;
|
||||
private $plugin;
|
||||
|
||||
public function __construct($data, $wasi = false, $config = null, $ctx = null)
|
||||
public function __construct($data, $wasi = false, $config = null)
|
||||
{
|
||||
if ($ctx == null) {
|
||||
$ctx = new Context();
|
||||
}
|
||||
|
||||
$this->lib = $ctx->lib;
|
||||
global $lib;
|
||||
|
||||
if ($lib == null) {
|
||||
$lib = new \ExtismLib(\ExtismLib::SOFILE);
|
||||
}
|
||||
|
||||
$this->lib = $lib;
|
||||
|
||||
$this->wasi = $wasi;
|
||||
$this->config = $config;
|
||||
@@ -50,38 +52,32 @@ class Plugin
|
||||
$data = string_to_bytes($data);
|
||||
}
|
||||
|
||||
$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->toString());
|
||||
// TODO: handle error message
|
||||
$plugin = $this->lib->extism_plugin_new($data, count($data), null, 0, (int)$wasi, null);
|
||||
if ($plugin == null) {
|
||||
throw new \Exception("Extism: unable to load plugin");
|
||||
}
|
||||
$this->id = $id;
|
||||
$this->context = $ctx;
|
||||
$this->plugin = $plugin;
|
||||
|
||||
if ($this->config != null) {
|
||||
$cfg = string_to_bytes(json_encode($config));
|
||||
$this->lib->extism_plugin_config($ctx->pointer, $this->id, $cfg, count($cfg));
|
||||
$this->lib->extism_plugin_config($this->plugin, $cfg, count($cfg));
|
||||
}
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
$this->lib->extism_plugin_free($this->context->pointer, $this->id);
|
||||
$this->id = -1;
|
||||
}
|
||||
|
||||
public function getId() {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
$this->lib->extism_plugin_free($this->plugin);
|
||||
$this->plugin = null;
|
||||
}
|
||||
|
||||
public function functionExists($name)
|
||||
{
|
||||
return $this->lib->extism_plugin_function_exists($this->context->pointer, $this->id, $name);
|
||||
return $this->lib->extism_plugin_function_exists($this->plugin, $name);
|
||||
}
|
||||
|
||||
public function cancelHandle()
|
||||
{
|
||||
return new \CancelHandle($this->lib, $this->lib->extism_plugin_cancel_handle($this->context->pointer, $this->id));
|
||||
return new \CancelHandle($this->lib, $this->lib->extism_plugin_cancel_handle($this->plugin));
|
||||
}
|
||||
|
||||
public function call($name, $input = null)
|
||||
@@ -90,19 +86,19 @@ class Plugin
|
||||
$input = string_to_bytes($input);
|
||||
}
|
||||
|
||||
$rc = $this->lib->extism_plugin_call($this->context->pointer, $this->id, $name, $input, count($input));
|
||||
$rc = $this->lib->extism_plugin_call($this->plugin, $name, $input, count($input));
|
||||
if ($rc != 0) {
|
||||
$msg = "code = " . $rc;
|
||||
$err = $this->lib->extism_error($this->context->pointer, $this->id);
|
||||
$err = $this->lib->extism_error($this->plugin);
|
||||
if ($err) {
|
||||
$msg = $msg . ", error = " . $err->toString();
|
||||
}
|
||||
throw new \Exception("Extism: call to '".$name."' failed with " . $msg);
|
||||
}
|
||||
|
||||
$length = $this->lib->extism_plugin_output_length($this->context->pointer, $this->id);
|
||||
$length = $this->lib->extism_plugin_output_length($this->plugin);
|
||||
|
||||
$buf = $this->lib->extism_plugin_output_data($this->context->pointer, $this->id);
|
||||
$buf = $this->lib->extism_plugin_output_data($this->plugin);
|
||||
|
||||
$output = [];
|
||||
$data = $buf->getData();
|
||||
@@ -112,27 +108,6 @@ class Plugin
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function update($data, $wasi = false, $config = null) {
|
||||
if (gettype($data) == "object" and $data->wasm != null) {
|
||||
$data = json_encode($data);
|
||||
}
|
||||
|
||||
if (gettype($data) == "string") {
|
||||
$data = string_to_bytes($data);
|
||||
}
|
||||
|
||||
$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->toString());
|
||||
}
|
||||
|
||||
if ($config != null) {
|
||||
$config = json_encode($config);
|
||||
$this->lib->extism_plugin_config($this->context->pointer, $this->id, $config, strlen($config));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function string_to_bytes($string) {
|
||||
|
||||
@@ -25,6 +25,7 @@ def count_vowels(data):
|
||||
|
||||
|
||||
def main(args):
|
||||
set_log_file("stderr", "trace")
|
||||
if len(args) > 1:
|
||||
data = args[1].encode()
|
||||
else:
|
||||
@@ -47,6 +48,7 @@ def main(args):
|
||||
)
|
||||
]
|
||||
plugin = Plugin(manifest, wasi=True, functions=functions)
|
||||
print(plugin.id)
|
||||
# Call `count_vowels`
|
||||
wasm_vowel_count = plugin.call("count_vowels", data)
|
||||
print(wasm_vowel_count)
|
||||
|
||||
@@ -2,7 +2,6 @@ from .extism import (
|
||||
Error,
|
||||
Plugin,
|
||||
set_log_file,
|
||||
Context,
|
||||
extism_version,
|
||||
host_fn,
|
||||
Function,
|
||||
|
||||
@@ -4,6 +4,7 @@ from base64 import b64encode
|
||||
from cffi import FFI
|
||||
from typing import Union
|
||||
from enum import Enum
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
@@ -134,70 +135,6 @@ class Memory:
|
||||
return self.length
|
||||
|
||||
|
||||
class Context:
|
||||
"""
|
||||
Context is used to store and manage plugins. You need a context to create
|
||||
or call plugins. The best way to interact with the Context is
|
||||
as a context manager as it can ensure that resources are cleaned up.
|
||||
|
||||
Example
|
||||
-------
|
||||
with Context() as ctx:
|
||||
plugin = ctx.plugin(manifest)
|
||||
print(plugin.call("my_function", "some-input"))
|
||||
|
||||
If you need a long lived context, you can use the constructor and the `del` keyword to free.
|
||||
|
||||
Example
|
||||
-------
|
||||
ctx = Context()
|
||||
del ctx
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.pointer = _lib.extism_context_new()
|
||||
|
||||
def __del__(self):
|
||||
_lib.extism_context_free(self.pointer)
|
||||
self.pointer = _ffi.NULL
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, type, exc, traceback):
|
||||
self.__del__()
|
||||
|
||||
def reset(self):
|
||||
"""Remove all registered plugins"""
|
||||
_lib.extism_context_reset(self.pointer)
|
||||
|
||||
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
|
||||
|
||||
Parameters
|
||||
----------
|
||||
manifest : Union[str, bytes, dict]
|
||||
A manifest dictionary describing the plugin or the raw bytes for a module. See [Extism > Concepts > Manifest](https://extism.org/docs/concepts/manifest/).
|
||||
wasi : bool
|
||||
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(
|
||||
manifest, context=self, wasi=wasi, config=config, functions=functions
|
||||
)
|
||||
|
||||
|
||||
class Function:
|
||||
def __init__(self, name: str, args, returns, f, *user_data):
|
||||
self.pointer = None
|
||||
@@ -250,7 +187,6 @@ class Plugin:
|
||||
def __init__(
|
||||
self,
|
||||
plugin: Union[str, bytes, dict],
|
||||
context=None,
|
||||
wasi=False,
|
||||
config=None,
|
||||
functions=None,
|
||||
@@ -259,87 +195,42 @@ class Plugin:
|
||||
Construct a Plugin
|
||||
"""
|
||||
|
||||
if context is None:
|
||||
context = Context()
|
||||
|
||||
wasm = _wasm(plugin)
|
||||
self.functions = functions
|
||||
|
||||
# Register plugin
|
||||
errmsg = _ffi.new("char**")
|
||||
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
|
||||
wasm, len(wasm), ptr, len(functions), wasi, errmsg
|
||||
)
|
||||
else:
|
||||
self.plugin = _lib.extism_plugin_new(
|
||||
context.pointer, wasm, len(wasm), _ffi.NULL, 0, wasi
|
||||
wasm, len(wasm), _ffi.NULL, 0, wasi, errmsg
|
||||
)
|
||||
|
||||
self.ctx = context
|
||||
|
||||
if self.plugin < 0:
|
||||
error = _lib.extism_error(self.ctx.pointer, -1)
|
||||
if error != _ffi.NULL:
|
||||
raise Error(_ffi.string(error).decode())
|
||||
raise Error("Unable to register plugin")
|
||||
if self.plugin == _ffi.NULL:
|
||||
msg = _ffi.string(errmsg[0])
|
||||
_lib.extism_plugin_new_error_free(errmsg[0])
|
||||
raise Error(msg.decode())
|
||||
|
||||
if config is not None:
|
||||
s = json.dumps(config).encode()
|
||||
_lib.extism_plugin_config(self.ctx.pointer, self.plugin, s, len(s))
|
||||
_lib.extism_plugin_config(self.plugin, s, len(s))
|
||||
|
||||
@property
|
||||
def id(self) -> UUID:
|
||||
b = bytes(_ffi.unpack(_lib.extism_plugin_id(self.plugin), 16))
|
||||
return UUID(bytes=b)
|
||||
|
||||
def cancel_handle(self):
|
||||
return CancelHandle(
|
||||
_lib.extism_plugin_cancel_handle(self.ctx.pointer, self.plugin)
|
||||
)
|
||||
|
||||
def update(
|
||||
self, manifest: Union[str, bytes, dict], wasi=False, config=None, functions=None
|
||||
):
|
||||
"""
|
||||
Update a plugin with a new WASM module or manifest
|
||||
|
||||
Parameters
|
||||
----------
|
||||
plugin : Union[str, bytes, dict]
|
||||
A manifest dictionary describing the plugin or the raw bytes for a module. See [Extism > Concepts > Manifest](https://extism.org/docs/concepts/manifest/).
|
||||
wasi : bool
|
||||
Set to `True` to enable WASI support
|
||||
config : dict
|
||||
The plugin config dictionary
|
||||
"""
|
||||
wasm = _wasm(manifest)
|
||||
if functions is not None:
|
||||
self.functions = functions
|
||||
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:
|
||||
raise Error(_ffi.string(error).decode())
|
||||
raise Error("Unable to update plugin")
|
||||
|
||||
if config is not None:
|
||||
s = json.dumps(config).encode()
|
||||
_lib.extism_plugin_config(self.ctx.pointer, self.plugin, s, len(s))
|
||||
return CancelHandle(_lib.extism_plugin_cancel_handle(self.plugin))
|
||||
|
||||
def _check_error(self, rc):
|
||||
if rc != 0:
|
||||
error = _lib.extism_error(self.ctx.pointer, self.plugin)
|
||||
error = _lib.extism_plugin_error(self.plugin)
|
||||
if error != _ffi.NULL:
|
||||
raise Error(_ffi.string(error).decode())
|
||||
raise Error(f"Error code: {rc}")
|
||||
@@ -357,9 +248,7 @@ class Plugin:
|
||||
-------
|
||||
True if the function exists in the plugin, False otherwise
|
||||
"""
|
||||
return _lib.extism_plugin_function_exists(
|
||||
self.ctx.pointer, self.plugin, name.encode()
|
||||
)
|
||||
return _lib.extism_plugin_function_exists(self.plugin, name.encode())
|
||||
|
||||
def call(self, function_name: str, data: Union[str, bytes], parse=bytes):
|
||||
"""
|
||||
@@ -384,22 +273,20 @@ class Plugin:
|
||||
data = data.encode()
|
||||
self._check_error(
|
||||
_lib.extism_plugin_call(
|
||||
self.ctx.pointer, self.plugin, function_name.encode(), data, len(data)
|
||||
self.plugin, function_name.encode(), data, len(data)
|
||||
)
|
||||
)
|
||||
out_len = _lib.extism_plugin_output_length(self.ctx.pointer, self.plugin)
|
||||
out_buf = _lib.extism_plugin_output_data(self.ctx.pointer, self.plugin)
|
||||
out_len = _lib.extism_plugin_output_length(self.plugin)
|
||||
out_buf = _lib.extism_plugin_output_data(self.plugin)
|
||||
buf = _ffi.buffer(out_buf, out_len)
|
||||
if parse is None:
|
||||
return buf
|
||||
return parse(buf)
|
||||
|
||||
def __del__(self):
|
||||
if not hasattr(self, "ctx"):
|
||||
if not hasattr(self, "pointer"):
|
||||
return
|
||||
if self.ctx.pointer == _ffi.NULL:
|
||||
return
|
||||
_lib.extism_plugin_free(self.ctx.pointer, self.plugin)
|
||||
_lib.extism_plugin_free(self.plugin)
|
||||
self.plugin = -1
|
||||
|
||||
def __enter__(self):
|
||||
|
||||
@@ -9,74 +9,50 @@ from os.path import join, dirname
|
||||
|
||||
|
||||
class TestExtism(unittest.TestCase):
|
||||
def test_context_new(self):
|
||||
ctx = extism.Context()
|
||||
self.assertIsNotNone(ctx)
|
||||
del ctx
|
||||
|
||||
def test_call_plugin(self):
|
||||
with extism.Context() as ctx:
|
||||
plugin = ctx.plugin(self._manifest())
|
||||
j = json.loads(plugin.call("count_vowels", "this is a test"))
|
||||
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"))
|
||||
self.assertEqual(j["count"], 6)
|
||||
j = json.loads(plugin.call("count_vowels", "🌎hello🌎world🌎"))
|
||||
self.assertEqual(j["count"], 3)
|
||||
|
||||
def test_update_plugin_manifest(self):
|
||||
with extism.Context() as ctx:
|
||||
plugin = ctx.plugin(self._manifest())
|
||||
# update with just the raw bytes of the wasm
|
||||
plugin.update(self._count_vowels_wasm())
|
||||
# should still work
|
||||
j = json.loads(plugin.call("count_vowels", "this is a test"))
|
||||
self.assertEqual(j["count"], 4)
|
||||
plugin = extism.Plugin(self._manifest())
|
||||
j = json.loads(plugin.call("count_vowels", "this is a test"))
|
||||
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"))
|
||||
self.assertEqual(j["count"], 6)
|
||||
j = json.loads(plugin.call("count_vowels", "🌎hello🌎world🌎"))
|
||||
self.assertEqual(j["count"], 3)
|
||||
|
||||
def test_function_exists(self):
|
||||
with extism.Context() as ctx:
|
||||
plugin = ctx.plugin(self._manifest())
|
||||
self.assertTrue(plugin.function_exists("count_vowels"))
|
||||
self.assertFalse(plugin.function_exists("i_dont_exist"))
|
||||
plugin = extism.Plugin(self._manifest())
|
||||
self.assertTrue(plugin.function_exists("count_vowels"))
|
||||
self.assertFalse(plugin.function_exists("i_dont_exist"))
|
||||
|
||||
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")
|
||||
)
|
||||
plugin = extism.Plugin(self._manifest())
|
||||
self.assertRaises(
|
||||
extism.Error, lambda: plugin.call("i_dont_exist", "someinput")
|
||||
)
|
||||
|
||||
def test_can_free_plugin(self):
|
||||
with extism.Context() as ctx:
|
||||
plugin = ctx.plugin(self._manifest())
|
||||
del plugin
|
||||
plugin = extism.Plugin(self._manifest())
|
||||
del plugin
|
||||
|
||||
def test_errors_on_bad_manifest(self):
|
||||
with extism.Context() as ctx:
|
||||
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})
|
||||
)
|
||||
self.assertRaises(
|
||||
extism.Error, lambda: extism.Plugin({"invalid_manifest": True})
|
||||
)
|
||||
|
||||
def test_extism_version(self):
|
||||
self.assertIsNotNone(extism.extism_version())
|
||||
|
||||
def test_extism_plugin_timeout(self):
|
||||
with extism.Context() as ctx:
|
||||
plugin = ctx.plugin(self._loop_manifest())
|
||||
start = datetime.now()
|
||||
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",
|
||||
)
|
||||
plugin = extism.Plugin(self._loop_manifest())
|
||||
start = datetime.now()
|
||||
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",
|
||||
)
|
||||
|
||||
def test_extism_host_function(self):
|
||||
@extism.host_fn
|
||||
@@ -86,31 +62,29 @@ class TestExtism(unittest.TestCase):
|
||||
mem[:] = user_data
|
||||
results[0].value = offs.offset
|
||||
|
||||
with extism.Context() as ctx:
|
||||
f = [
|
||||
extism.Function(
|
||||
"hello_world",
|
||||
[extism.ValType.I64],
|
||||
[extism.ValType.I64],
|
||||
hello_world,
|
||||
b"test",
|
||||
)
|
||||
]
|
||||
plugin = ctx.plugin(self._manifest(functions=True), functions=f, wasi=True)
|
||||
res = plugin.call("count_vowels", "aaa")
|
||||
self.assertEqual(res, b"test")
|
||||
f = [
|
||||
extism.Function(
|
||||
"hello_world",
|
||||
[extism.ValType.I64],
|
||||
[extism.ValType.I64],
|
||||
hello_world,
|
||||
b"test",
|
||||
)
|
||||
]
|
||||
plugin = extism.Plugin(self._manifest(functions=True), functions=f, wasi=True)
|
||||
res = plugin.call("count_vowels", "aaa")
|
||||
self.assertEqual(res, b"test")
|
||||
|
||||
def test_extism_plugin_cancel(self):
|
||||
with extism.Context() as ctx:
|
||||
plugin = ctx.plugin(self._loop_manifest())
|
||||
cancel_handle = plugin.cancel_handle()
|
||||
plugin = extism.Plugin(self._loop_manifest())
|
||||
cancel_handle = plugin.cancel_handle()
|
||||
|
||||
def cancel(handle):
|
||||
time.sleep(0.5)
|
||||
handle.cancel()
|
||||
def cancel(handle):
|
||||
time.sleep(0.5)
|
||||
handle.cancel()
|
||||
|
||||
Thread(target=cancel, args=[cancel_handle]).run()
|
||||
self.assertRaises(extism.Error, lambda: plugin.call("infinite_loop", b""))
|
||||
Thread(target=cancel, args=[cancel_handle]).run()
|
||||
self.assertRaises(extism.Error, lambda: plugin.call("infinite_loop", b""))
|
||||
|
||||
def _manifest(self, functions=False):
|
||||
wasm = self._count_vowels_wasm(functions)
|
||||
|
||||
@@ -8,27 +8,19 @@
|
||||
require "extism"
|
||||
require "json"
|
||||
|
||||
Extism.with_context do |ctx|
|
||||
manifest = {
|
||||
:wasm => [{ :path => "../wasm/code.wasm" }],
|
||||
}
|
||||
plugin = ctx.plugin(manifest)
|
||||
res = JSON.parse(plugin.call("count_vowels", "this is a test"))
|
||||
puts res["count"] # => 4
|
||||
end
|
||||
manifest = {
|
||||
:wasm => [{ :path => "../wasm/code.wasm" }],
|
||||
}
|
||||
plugin = Plugin.new(manifest)
|
||||
res = JSON.parse(plugin.call("count_vowels", "this is a test"))
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
There are two primary classes you need to understand:
|
||||
There is just one primary class you need to understand:
|
||||
|
||||
* [Context](Extism/Context.html)
|
||||
* [Plugin](Extism/Plugin.html)
|
||||
|
||||
#### Context
|
||||
|
||||
The [Context](Extism/Context.html) can be thought of as a session. You need a context to interact with the Extism runtime. The context holds your plugins and when you free the context, it frees your plugins. We recommend using the [Extism.with_context](Extism.html#with_context-class_method) method to ensure that your plugins are cleaned up. But if you need a long lived context for any reason, you can use the constructor [Extism::Context.new](Extism/Context.html#initialize-instance_method).
|
||||
|
||||
#### Plugin
|
||||
|
||||
The [Plugin](Extism/Plugin.html) represents an instance of your WASM program from the given manifest.
|
||||
|
||||
@@ -24,98 +24,14 @@ module Extism
|
||||
end
|
||||
|
||||
$PLUGINS = {}
|
||||
$FREE_PLUGIN = proc { |id|
|
||||
x = $PLUGINS[id]
|
||||
$FREE_PLUGIN = proc { |ptr|
|
||||
x = $PLUGINS[ptr]
|
||||
if !x.nil?
|
||||
C.extism_plugin_free(x[:context].pointer, x[:plugin])
|
||||
$PLUGINS.delete(id)
|
||||
C.extism_plugin_free(x[:plugin])
|
||||
$PLUGINS.delete(ptr)
|
||||
end
|
||||
}
|
||||
|
||||
$CONTEXTS = {}
|
||||
$FREE_CONTEXT = proc { |id|
|
||||
x = $CONTEXTS[id]
|
||||
if !x.nil?
|
||||
C.extism_context_free($CONTEXTS[id])
|
||||
$CONTEXTS.delete(id)
|
||||
end
|
||||
}
|
||||
|
||||
# A Context is needed to create plugins. The Context
|
||||
# is where your plugins live. Freeing the context
|
||||
# frees all of the plugins in its scope.
|
||||
#
|
||||
# @example Create and free a context
|
||||
# ctx = Extism::Context.new
|
||||
# plugin = ctx.plugin(my_manifest)
|
||||
# puts plugin.call("my_func", "my-input")
|
||||
# ctx.free # frees any plugins
|
||||
#
|
||||
# @example Use with_context to auto-free
|
||||
# Extism.with_context do |ctx|
|
||||
# plugin = ctx.plugin(my_manifest)
|
||||
# puts plugin.call("my_func", "my-input")
|
||||
# end # frees context after exiting this block
|
||||
#
|
||||
# @attr_reader pointer [FFI::Pointer] Pointer to the Extism context. *Used internally*.
|
||||
class Context
|
||||
attr_reader :pointer
|
||||
|
||||
# Initialize a new context
|
||||
def initialize
|
||||
@pointer = C.extism_context_new()
|
||||
$CONTEXTS[self.object_id] = @pointer
|
||||
ObjectSpace.define_finalizer(self, $FREE_CONTEXT)
|
||||
end
|
||||
|
||||
# Remove all registered plugins in this context
|
||||
# @return [void]
|
||||
def reset
|
||||
C.extism_context_reset(@pointer)
|
||||
end
|
||||
|
||||
# Free the context, this should be called when it is no longer needed
|
||||
# @return [void]
|
||||
def free
|
||||
return if @pointer.nil?
|
||||
|
||||
$CONTEXTS.delete(self.object_id)
|
||||
C.extism_context_free(@pointer)
|
||||
@pointer = nil
|
||||
end
|
||||
|
||||
# Create a new plugin from a WASM module or JSON encoded manifest
|
||||
#
|
||||
# @see Plugin#new
|
||||
# @param wasm [Hash, String] The manifest for the plugin. See https://extism.org/docs/concepts/manifest/.
|
||||
# @param wasi [Boolean] Enable WASI support
|
||||
# @param config [Hash] The plugin config
|
||||
# @return [Plugin]
|
||||
def plugin(wasm, wasi = false, config = nil)
|
||||
Plugin.new(wasm, wasi, config, self)
|
||||
end
|
||||
end
|
||||
|
||||
# A context manager to create contexts and ensure that they get freed.
|
||||
#
|
||||
# @example Use with_context to auto-free
|
||||
# Extism.with_context do |ctx|
|
||||
# plugin = ctx.plugin(my_manifest)
|
||||
# puts plugin.call("my_func", "my-input")
|
||||
# end # frees context after exiting this block
|
||||
#
|
||||
# @yield [ctx] Yields the created Context
|
||||
# @return [Object] returns whatever your block returns
|
||||
def self.with_context(&block)
|
||||
ctx = Context.new
|
||||
begin
|
||||
x = block.call(ctx)
|
||||
return x
|
||||
ensure
|
||||
ctx.free
|
||||
end
|
||||
end
|
||||
|
||||
# A CancelHandle can be used to cancel a running plugin from another thread
|
||||
class CancelHandle
|
||||
def initialize(handle)
|
||||
@@ -135,61 +51,25 @@ module Extism
|
||||
# @param wasm [Hash, String] The manifest or WASM binary. See https://extism.org/docs/concepts/manifest/.
|
||||
# @param wasi [Boolean] Enable WASI support
|
||||
# @param config [Hash] The plugin config
|
||||
# @param context [Context] The context to manager this plugin
|
||||
def initialize(wasm, wasi = false, config = nil, context = nil)
|
||||
if context.nil? then
|
||||
context = Context.new
|
||||
end
|
||||
@context = context
|
||||
def initialize(wasm, wasi = false, config = nil)
|
||||
if wasm.class == Hash
|
||||
wasm = JSON.generate(wasm)
|
||||
end
|
||||
code = FFI::MemoryPointer.new(:char, wasm.bytesize)
|
||||
errmsg = FFI::MemoryPointer.new(:pointer)
|
||||
code.put_bytes(0, wasm)
|
||||
@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?
|
||||
raise Error.new "extism_plugin_new failed"
|
||||
else
|
||||
raise Error.new err
|
||||
end
|
||||
@plugin = C.extism_plugin_new(code, wasm.bytesize, nil, 0, wasi, errmsg)
|
||||
if @plugin.null?
|
||||
err = errmsg.read_pointer.read_string
|
||||
C.extism_plugin_new_error_free errmsg.read_pointer
|
||||
raise Error.new err
|
||||
end
|
||||
$PLUGINS[self.object_id] = { :plugin => @plugin, :context => context }
|
||||
$PLUGINS[self.object_id] = { :plugin => @plugin }
|
||||
ObjectSpace.define_finalizer(self, $FREE_PLUGIN)
|
||||
if config != nil and @plugin >= 0
|
||||
if config != nil and @plugin.null?
|
||||
s = JSON.generate(config)
|
||||
ptr = FFI::MemoryPointer::from_string(s)
|
||||
C.extism_plugin_config(@context.pointer, @plugin, ptr, s.bytesize)
|
||||
end
|
||||
end
|
||||
|
||||
# Update a plugin with new WASM module or manifest
|
||||
#
|
||||
# @param wasm [Hash, String] The manifest or WASM binary. See https://extism.org/docs/concepts/manifest/.
|
||||
# @param wasi [Boolean] Enable WASI support
|
||||
# @param config [Hash] The plugin config
|
||||
# @return [void]
|
||||
def update(wasm, wasi = false, config = nil)
|
||||
if wasm.class == Hash
|
||||
wasm = JSON.generate(wasm)
|
||||
end
|
||||
code = FFI::MemoryPointer.new(:char, wasm.bytesize)
|
||||
code.put_bytes(0, wasm)
|
||||
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?
|
||||
raise Error.new "extism_plugin_update failed"
|
||||
else
|
||||
raise Error.new err
|
||||
end
|
||||
end
|
||||
|
||||
if config != nil
|
||||
s = JSON.generate(config)
|
||||
ptr = FFI::MemoryPointer::from_string(s)
|
||||
C.extism_plugin_config(@context.pointer, @plugin, ptr, s.bytesize)
|
||||
C.extism_plugin_config(@plugin, ptr, s.bytesize)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -198,7 +78,7 @@ module Extism
|
||||
# @param name [String] The name of the function
|
||||
# @return [Boolean] Returns true if function exists
|
||||
def has_function?(name)
|
||||
C.extism_plugin_function_exists(@context.pointer, @plugin, name)
|
||||
C.extism_plugin_function_exists(@plugin, name)
|
||||
end
|
||||
|
||||
# Call a function by name
|
||||
@@ -210,17 +90,17 @@ module Extism
|
||||
# If no block was passed then use Pointer::read_string
|
||||
block ||= ->(buf, len) { buf.read_string(len) }
|
||||
input = FFI::MemoryPointer::from_string(data)
|
||||
rc = C.extism_plugin_call(@context.pointer, @plugin, name, input, data.bytesize)
|
||||
rc = C.extism_plugin_call(@plugin, name, input, data.bytesize)
|
||||
if rc != 0
|
||||
err = C.extism_error(@context.pointer, @plugin)
|
||||
err = C.extism_plugin_error(@plugin)
|
||||
if err&.empty?
|
||||
raise Error.new "extism_call failed"
|
||||
else
|
||||
raise Error.new err
|
||||
end
|
||||
end
|
||||
out_len = C.extism_plugin_output_length(@context.pointer, @plugin)
|
||||
buf = C.extism_plugin_output_data(@context.pointer, @plugin)
|
||||
out_len = C.extism_plugin_output_length(@plugin)
|
||||
buf = C.extism_plugin_output_data(@plugin)
|
||||
block.call(buf, out_len)
|
||||
end
|
||||
|
||||
@@ -228,16 +108,16 @@ module Extism
|
||||
#
|
||||
# @return [void]
|
||||
def free
|
||||
return if @context.pointer.nil?
|
||||
return if @plugin.null?
|
||||
|
||||
$PLUGINS.delete(self.object_id)
|
||||
C.extism_plugin_free(@context.pointer, @plugin)
|
||||
@plugin = -1
|
||||
C.extism_plugin_free(@plugin)
|
||||
@plugin = nil
|
||||
end
|
||||
|
||||
# Get a CancelHandle for a plugin
|
||||
def cancel_handle
|
||||
return CancelHandle.new(C.extism_plugin_cancel_handle(@context.pointer, @plugin))
|
||||
return CancelHandle.new(C.extism_plugin_cancel_handle(@plugin))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -248,20 +128,18 @@ module Extism
|
||||
module C
|
||||
extend FFI::Library
|
||||
ffi_lib "extism"
|
||||
attach_function :extism_context_new, [], :pointer
|
||||
attach_function :extism_context_free, [:pointer], :void
|
||||
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
|
||||
attach_function :extism_plugin_output_length, [:pointer, :int32], :uint64
|
||||
attach_function :extism_plugin_output_data, [:pointer, :int32], :pointer
|
||||
attach_function :extism_plugin_new_error_free, [:pointer], :void
|
||||
attach_function :extism_plugin_new, [:pointer, :uint64, :pointer, :uint64, :bool, :pointer], :pointer
|
||||
attach_function :extism_plugin_error, [:pointer], :string
|
||||
attach_function :extism_plugin_call, [:pointer, :string, :pointer, :uint64], :int32
|
||||
attach_function :extism_plugin_function_exists, [:pointer, :string], :bool
|
||||
attach_function :extism_plugin_output_length, [:pointer], :uint64
|
||||
attach_function :extism_plugin_output_data, [:pointer], :pointer
|
||||
attach_function :extism_log_file, [:string, :pointer], :void
|
||||
attach_function :extism_plugin_free, [:pointer, :int32], :void
|
||||
attach_function :extism_context_reset, [:pointer], :void
|
||||
attach_function :extism_plugin_free, [:pointer], :void
|
||||
attach_function :extism_version, [], :string
|
||||
attach_function :extism_plugin_cancel_handle, [:pointer, :int32], :pointer
|
||||
attach_function :extism_plugin_id, [:pointer], :pointer
|
||||
attach_function :extism_plugin_cancel_handle, [:pointer], :pointer
|
||||
attach_function :extism_plugin_cancel, [:pointer], :bool
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,73 +7,43 @@ class TestExtism < Minitest::Test
|
||||
refute_nil Extism::VERSION
|
||||
end
|
||||
|
||||
def test_create_context
|
||||
refute_nil Extism::Context.new
|
||||
end
|
||||
|
||||
def test_plugin_call
|
||||
Extism.with_context do |ctx|
|
||||
plugin = ctx.plugin(manifest)
|
||||
res = JSON.parse(plugin.call("count_vowels", "this is a test"))
|
||||
assert_equal res["count"], 4
|
||||
res = JSON.parse(plugin.call("count_vowels", "this is a test again"))
|
||||
assert_equal res["count"], 7
|
||||
res = JSON.parse(plugin.call("count_vowels", "this is a test thrice"))
|
||||
assert_equal res["count"], 6
|
||||
res = JSON.parse(plugin.call("count_vowels", "🌎hello🌎world🌎"))
|
||||
assert_equal res["count"], 3
|
||||
end
|
||||
plugin = Extism::Plugin.new(manifest)
|
||||
res = JSON.parse(plugin.call("count_vowels", "this is a test"))
|
||||
assert_equal res["count"], 4
|
||||
res = JSON.parse(plugin.call("count_vowels", "this is a test again"))
|
||||
assert_equal res["count"], 7
|
||||
res = JSON.parse(plugin.call("count_vowels", "this is a test thrice"))
|
||||
assert_equal res["count"], 6
|
||||
res = JSON.parse(plugin.call("count_vowels", "🌎hello🌎world🌎"))
|
||||
assert_equal res["count"], 3
|
||||
end
|
||||
|
||||
def test_can_free_plugin
|
||||
ctx = Extism::Context.new
|
||||
plugin = ctx.plugin(manifest)
|
||||
plugin = Extism::Plugin.new(manifest)
|
||||
_res = plugin.call("count_vowels", "this is a test")
|
||||
plugin.free
|
||||
assert_raises(Extism::Error) do
|
||||
_res = plugin.call("count_vowels", "this is a test")
|
||||
end
|
||||
ctx.free
|
||||
end
|
||||
|
||||
def test_can_update_a_manifest
|
||||
Extism.with_context do |ctx|
|
||||
plugin = ctx.plugin(manifest)
|
||||
# let's load a raw wasm module rather than use a manifest
|
||||
raw_module = IO.read("../wasm/code.wasm")
|
||||
plugin.update(raw_module)
|
||||
# check we can still call it
|
||||
res = JSON.parse(plugin.call("count_vowels", "this is a test"))
|
||||
assert_equal res["count"], 4
|
||||
end
|
||||
end
|
||||
|
||||
def test_errors_on_bad_manifest
|
||||
Extism.with_context do |ctx|
|
||||
assert_raises(Extism::Error) do
|
||||
_plugin = ctx.plugin({ not_a_real_manifest: true })
|
||||
end
|
||||
plugin = ctx.plugin(manifest)
|
||||
assert_raises(Extism::Error) do
|
||||
plugin.update({ not_a_real_manifest: true })
|
||||
end
|
||||
assert_raises(Extism::Error) do
|
||||
_plugin = Extism::Plugin.new({ not_a_real_manifest: true })
|
||||
end
|
||||
end
|
||||
|
||||
def test_has_function
|
||||
Extism.with_context do |ctx|
|
||||
plugin = ctx.plugin(manifest)
|
||||
assert plugin.has_function? "count_vowels"
|
||||
refute plugin.has_function? "i_am_not_a_function"
|
||||
end
|
||||
plugin = Extism::Plugin.new(manifest)
|
||||
assert plugin.has_function? "count_vowels"
|
||||
refute plugin.has_function? "i_am_not_a_function"
|
||||
end
|
||||
|
||||
def test_errors_on_unknown_function
|
||||
Extism.with_context do |ctx|
|
||||
plugin = ctx.plugin(manifest)
|
||||
assert_raises(Extism::Error) do
|
||||
plugin.call("non_existent_function", "input")
|
||||
end
|
||||
plugin = Extism::Plugin.new(manifest)
|
||||
assert_raises(Extism::Error) do
|
||||
plugin.call("non_existent_function", "input")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
[package]
|
||||
name = "extism-runtime"
|
||||
version = "0.5.0"
|
||||
name = "extism"
|
||||
version = "1.0.0-alpha.0"
|
||||
edition = "2021"
|
||||
authors = ["The Extism Authors", "oss@extism.org"]
|
||||
license = "BSD-3-Clause"
|
||||
homepage = "https://extism.org"
|
||||
repository = "https://github.com/extism/extism"
|
||||
description = "Extism runtime component"
|
||||
description = "Extism runtime and Rust SDK"
|
||||
|
||||
[dependencies]
|
||||
wasmtime = ">= 10.0.0, < 12.0.0"
|
||||
wasmtime-wasi = ">= 10.0.0, < 12.0.0"
|
||||
wasmtime-wasi-nn = {version = ">= 10.0.0, < 12.0.0", optional=true}
|
||||
wasmtime = ">= 10.0.0, < 13.0.0"
|
||||
wasmtime-wasi = ">= 10.0.0, < 13.0.0"
|
||||
wasmtime-wasi-nn = {version = ">= 10.0.0, < 13.0.0", optional=true}
|
||||
anyhow = "1"
|
||||
serde = {version = "1", features = ["derive"]}
|
||||
serde_json = "1"
|
||||
@@ -22,7 +22,7 @@ log4rs = "1.1"
|
||||
url = "2"
|
||||
glob = "0.3"
|
||||
ureq = {version = "2.5", optional=true}
|
||||
extism-manifest = { version = "0.5.0", path = "../manifest" }
|
||||
extism-manifest = { version = "1.0.0-alpha.0", path = "../manifest" }
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
libc = "0.2"
|
||||
|
||||
|
||||
@@ -14,11 +14,11 @@ fn main() {
|
||||
.with_pragma_once(true)
|
||||
.with_after_include(fn_macro)
|
||||
.rename_item("Size", "ExtismSize")
|
||||
.rename_item("PluginIndex", "ExtismPlugin")
|
||||
.rename_item("Context", "ExtismContext")
|
||||
.rename_item("ValType", "ExtismValType")
|
||||
.rename_item("ValUnion", "ExtismValUnion")
|
||||
.rename_item("Internal", "ExtismCurrentPlugin")
|
||||
.rename_item("CurrentPlugin", "ExtismCurrentPlugin")
|
||||
.rename_item("Plugin", "ExtismPlugin")
|
||||
.rename_item("Function", "ExtismFunction")
|
||||
.with_style(cbindgen::Style::Type)
|
||||
.generate()
|
||||
{
|
||||
|
||||
107
runtime/extism.h
107
runtime/extism.h
@@ -42,21 +42,24 @@ typedef enum {
|
||||
} ExtismValType;
|
||||
|
||||
/**
|
||||
* A `Context` is used to store and manage plugins
|
||||
* CurrentPlugin stores data that is available to the caller in PDK functions, this should
|
||||
* only be accessed from inside a host function
|
||||
*/
|
||||
typedef struct ExtismContext ExtismContext;
|
||||
typedef struct ExtismCurrentPlugin ExtismCurrentPlugin;
|
||||
|
||||
typedef struct ExtismCancelHandle ExtismCancelHandle;
|
||||
|
||||
/**
|
||||
* Wraps host functions
|
||||
* Wraps raw host functions with some additional metadata and user data
|
||||
*/
|
||||
typedef struct ExtismFunction ExtismFunction;
|
||||
|
||||
/**
|
||||
* Internal stores data that is available to the caller in PDK functions
|
||||
* Plugin contains everything needed to execute a WASM function
|
||||
*/
|
||||
typedef struct ExtismCurrentPlugin ExtismCurrentPlugin;
|
||||
typedef struct ExtismPlugin ExtismPlugin;
|
||||
|
||||
typedef uint64_t ExtismMemoryHandle;
|
||||
|
||||
typedef uint64_t ExtismSize;
|
||||
|
||||
@@ -81,19 +84,17 @@ typedef struct {
|
||||
/**
|
||||
* 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;
|
||||
typedef void (*ExtismFunctionType)(ExtismCurrentPlugin *plugin,
|
||||
const ExtismVal *inputs,
|
||||
ExtismSize n_inputs,
|
||||
ExtismVal *outputs,
|
||||
ExtismSize n_outputs,
|
||||
void *data);
|
||||
|
||||
/**
|
||||
* Create a new context
|
||||
* Get a plugin's ID, the returned bytes are a 16 byte buffer that represent a UUID value
|
||||
*/
|
||||
ExtismContext *extism_context_new(void);
|
||||
|
||||
/**
|
||||
* Free a context
|
||||
*/
|
||||
void extism_context_free(ExtismContext *ctx);
|
||||
const uint8_t *extism_plugin_id(ExtismPlugin *plugin);
|
||||
|
||||
/**
|
||||
* Returns a pointer to the memory of the currently running plugin
|
||||
@@ -105,19 +106,19 @@ 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);
|
||||
ExtismMemoryHandle 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);
|
||||
ExtismSize extism_current_plugin_memory_length(ExtismCurrentPlugin *plugin, ExtismMemoryHandle 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);
|
||||
void extism_current_plugin_memory_free(ExtismCurrentPlugin *plugin, ExtismMemoryHandle ptr);
|
||||
|
||||
/**
|
||||
* Create a new host function
|
||||
@@ -145,16 +146,16 @@ ExtismFunction *extism_function_new(const char *name,
|
||||
void *user_data,
|
||||
void (*free_user_data)(void *_));
|
||||
|
||||
/**
|
||||
* Free `ExtismFunction`
|
||||
*/
|
||||
void extism_function_free(ExtismFunction *f);
|
||||
|
||||
/**
|
||||
* Set the namespace of an `ExtismFunction`
|
||||
*/
|
||||
void extism_function_set_namespace(ExtismFunction *ptr, const char *namespace_);
|
||||
|
||||
/**
|
||||
* Free an `ExtismFunction`
|
||||
*/
|
||||
void extism_function_free(ExtismFunction *ptr);
|
||||
|
||||
/**
|
||||
* Create a new plugin with additional host functions
|
||||
*
|
||||
@@ -164,61 +165,42 @@ void extism_function_free(ExtismFunction *ptr);
|
||||
* `n_functions`: the number of functions provided
|
||||
* `with_wasi`: enables/disables WASI
|
||||
*/
|
||||
ExtismPlugin extism_plugin_new(ExtismContext *ctx,
|
||||
const uint8_t *wasm,
|
||||
ExtismSize wasm_size,
|
||||
const ExtismFunction **functions,
|
||||
ExtismSize n_functions,
|
||||
bool with_wasi);
|
||||
ExtismPlugin *extism_plugin_new(const uint8_t *wasm,
|
||||
ExtismSize wasm_size,
|
||||
const ExtismFunction **functions,
|
||||
ExtismSize n_functions,
|
||||
bool with_wasi,
|
||||
char **errmsg);
|
||||
|
||||
/**
|
||||
* Update a plugin, keeping the existing ID
|
||||
*
|
||||
* Similar to `extism_plugin_new` but takes an `index` argument to specify
|
||||
* which plugin to update
|
||||
*
|
||||
* Memory for this plugin will be reset upon update
|
||||
* Free the error returned by `extism_plugin_new`, errors returned from `extism_plugin_error` don't need to be freed
|
||||
*/
|
||||
bool extism_plugin_update(ExtismContext *ctx,
|
||||
ExtismPlugin index,
|
||||
const uint8_t *wasm,
|
||||
ExtismSize wasm_size,
|
||||
const ExtismFunction **functions,
|
||||
ExtismSize nfunctions,
|
||||
bool with_wasi);
|
||||
void extism_plugin_new_error_free(char *err);
|
||||
|
||||
/**
|
||||
* Remove a plugin from the registry and free associated memory
|
||||
*/
|
||||
void extism_plugin_free(ExtismContext *ctx, ExtismPlugin plugin);
|
||||
void extism_plugin_free(ExtismPlugin *plugin);
|
||||
|
||||
/**
|
||||
* Get plugin ID for cancellation
|
||||
*/
|
||||
const ExtismCancelHandle *extism_plugin_cancel_handle(ExtismContext *ctx, ExtismPlugin plugin);
|
||||
const ExtismCancelHandle *extism_plugin_cancel_handle(const ExtismPlugin *plugin);
|
||||
|
||||
/**
|
||||
* Cancel a running plugin
|
||||
*/
|
||||
bool extism_plugin_cancel(const ExtismCancelHandle *handle);
|
||||
|
||||
/**
|
||||
* Remove all plugins from the registry
|
||||
*/
|
||||
void extism_context_reset(ExtismContext *ctx);
|
||||
|
||||
/**
|
||||
* Update plugin config values, this will merge with the existing values
|
||||
*/
|
||||
bool extism_plugin_config(ExtismContext *ctx,
|
||||
ExtismPlugin plugin,
|
||||
const uint8_t *json,
|
||||
ExtismSize json_size);
|
||||
bool extism_plugin_config(ExtismPlugin *plugin, const uint8_t *json, ExtismSize json_size);
|
||||
|
||||
/**
|
||||
* Returns true if `func_name` exists
|
||||
*/
|
||||
bool extism_plugin_function_exists(ExtismContext *ctx, ExtismPlugin plugin, const char *func_name);
|
||||
bool extism_plugin_function_exists(ExtismPlugin *plugin, const char *func_name);
|
||||
|
||||
/**
|
||||
* Call a function
|
||||
@@ -227,27 +209,30 @@ bool extism_plugin_function_exists(ExtismContext *ctx, ExtismPlugin plugin, cons
|
||||
* `data`: is the input data
|
||||
* `data_len`: is the length of `data`
|
||||
*/
|
||||
int32_t extism_plugin_call(ExtismContext *ctx,
|
||||
ExtismPlugin plugin_id,
|
||||
int32_t extism_plugin_call(ExtismPlugin *plugin,
|
||||
const char *func_name,
|
||||
const uint8_t *data,
|
||||
ExtismSize data_len);
|
||||
|
||||
/**
|
||||
* Get the error associated with a `Context` or `Plugin`, if `plugin` is `-1` then the context
|
||||
* error will be returned
|
||||
* Get the error associated with a `Plugin`
|
||||
*/
|
||||
const char *extism_error(ExtismContext *ctx, ExtismPlugin plugin);
|
||||
const char *extism_error(ExtismPlugin *plugin);
|
||||
|
||||
/**
|
||||
* Get the error associated with a `Plugin`
|
||||
*/
|
||||
const char *extism_plugin_error(ExtismPlugin *plugin);
|
||||
|
||||
/**
|
||||
* Get the length of a plugin's output data
|
||||
*/
|
||||
ExtismSize extism_plugin_output_length(ExtismContext *ctx, ExtismPlugin plugin);
|
||||
ExtismSize extism_plugin_output_length(ExtismPlugin *plugin);
|
||||
|
||||
/**
|
||||
* Get a pointer to the output data
|
||||
*/
|
||||
const uint8_t *extism_plugin_output_data(ExtismContext *ctx, ExtismPlugin plugin);
|
||||
const uint8_t *extism_plugin_output_data(ExtismPlugin *plugin);
|
||||
|
||||
/**
|
||||
* Set log file and level
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
|
||||
use crate::*;
|
||||
|
||||
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>,
|
||||
|
||||
/// Error message
|
||||
pub error: Option<std::ffi::CString>,
|
||||
next_id: std::sync::atomic::AtomicI32,
|
||||
reclaimed_ids: VecDeque<PluginIndex>,
|
||||
|
||||
// Timeout thread
|
||||
pub(crate) epoch_timer_tx: std::sync::mpsc::SyncSender<TimerAction>,
|
||||
}
|
||||
|
||||
impl Default for Context {
|
||||
fn default() -> Self {
|
||||
Context::new()
|
||||
}
|
||||
}
|
||||
|
||||
const START_REUSING_IDS: usize = 25;
|
||||
|
||||
impl Context {
|
||||
pub(crate) fn timer() -> std::sync::MutexGuard<'static, Option<Timer>> {
|
||||
match unsafe { TIMER.lock() } {
|
||||
Ok(x) => x,
|
||||
Err(e) => e.into_inner(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new context
|
||||
pub fn new() -> Context {
|
||||
let timer = &mut *Self::timer();
|
||||
|
||||
let tx = match timer {
|
||||
None => Timer::init(timer),
|
||||
Some(t) => t.tx.clone(),
|
||||
};
|
||||
|
||||
Context {
|
||||
plugins: BTreeMap::new(),
|
||||
error: None,
|
||||
next_id: std::sync::atomic::AtomicI32::new(0),
|
||||
reclaimed_ids: VecDeque::new(),
|
||||
epoch_timer_tx: tx,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the next valid plugin ID
|
||||
pub fn next_id(&mut self) -> Result<PluginIndex, Error> {
|
||||
// Make sure we haven't exhausted all plugin IDs, to reach this it would require the machine
|
||||
// running this code to have a lot of memory - no computer I tested on was able to allocate
|
||||
// the max number of plugins.
|
||||
//
|
||||
// Since `Context::remove` collects IDs that have been removed we will
|
||||
// try to use one of those before returning an error
|
||||
let exhausted = self.next_id.load(std::sync::atomic::Ordering::SeqCst) == PluginIndex::MAX;
|
||||
|
||||
// If there are a significant number of old IDs we can start to re-use them
|
||||
if self.reclaimed_ids.len() >= START_REUSING_IDS || exhausted {
|
||||
if let Some(x) = self.reclaimed_ids.pop_front() {
|
||||
return Ok(x);
|
||||
}
|
||||
|
||||
if exhausted {
|
||||
return Err(anyhow::format_err!(
|
||||
"All plugin descriptors are in use, unable to allocate a new plugin"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self
|
||||
.next_id
|
||||
.fetch_add(1, std::sync::atomic::Ordering::SeqCst))
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, plugin: Plugin) -> PluginIndex {
|
||||
// Generate a new plugin ID
|
||||
let id: i32 = match self.next_id() {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
error!("Error creating Plugin: {:?}", e);
|
||||
self.set_error(e);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
self.plugins.insert(id, plugin);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn new_plugin<'a>(
|
||||
&mut self,
|
||||
data: impl AsRef<[u8]>,
|
||||
imports: impl IntoIterator<Item = &'a Function>,
|
||||
with_wasi: bool,
|
||||
) -> PluginIndex {
|
||||
let plugin = match Plugin::new(data, imports, with_wasi) {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
error!("Error creating Plugin: {:?}", e);
|
||||
self.set_error(e);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
self.insert(plugin)
|
||||
}
|
||||
|
||||
/// Set the context error
|
||||
pub fn set_error(&mut self, e: impl std::fmt::Debug) {
|
||||
trace!("Set context error: {:?}", e);
|
||||
self.error = Some(error_string(e));
|
||||
}
|
||||
|
||||
/// Convenience function to set error and return the value passed as the final parameter
|
||||
pub fn error<T>(&mut self, e: impl std::fmt::Debug, x: T) -> T {
|
||||
self.set_error(e);
|
||||
x
|
||||
}
|
||||
|
||||
/// Get a plugin from the context
|
||||
pub fn plugin(&mut self, id: PluginIndex) -> Option<*mut Plugin> {
|
||||
match self.plugins.get_mut(&id) {
|
||||
Some(x) => Some(x),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn plugin_exists(&mut self, id: PluginIndex) -> bool {
|
||||
self.plugins.contains_key(&id)
|
||||
}
|
||||
|
||||
/// Remove a plugin from the context
|
||||
pub fn remove(&mut self, id: PluginIndex) {
|
||||
if self.plugins.remove(&id).is_some() {
|
||||
// Collect old IDs in case we need to re-use them
|
||||
self.reclaimed_ids.push_back(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
374
runtime/src/current_plugin.rs
Normal file
374
runtime/src/current_plugin.rs
Normal file
@@ -0,0 +1,374 @@
|
||||
use crate::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)]
|
||||
pub struct MemoryHandle(u64, u64);
|
||||
|
||||
impl MemoryHandle {
|
||||
/// Create a new memory handle, this is unsafe because the values are provided by the user
|
||||
/// and may not be a valid handle
|
||||
///
|
||||
/// # Safety
|
||||
/// This function is unsafe because there is no validation that the offset or
|
||||
/// length of the handle is correct
|
||||
pub unsafe fn new(offs: u64, len: u64) -> MemoryHandle {
|
||||
MemoryHandle(offs, len)
|
||||
}
|
||||
|
||||
/// Get the length of the memory block
|
||||
pub fn len(&self) -> usize {
|
||||
self.1 as usize
|
||||
}
|
||||
|
||||
/// Returns `true` when the handle length is 0
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.1 == 0
|
||||
}
|
||||
|
||||
/// Get the offset to this block in Extism memory
|
||||
pub fn offset(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MemoryHandle> for Val {
|
||||
fn from(m: MemoryHandle) -> Self {
|
||||
Val::I64(m.0 as i64)
|
||||
}
|
||||
}
|
||||
|
||||
/// CurrentPlugin stores data that is available to the caller in PDK functions, this should
|
||||
/// only be accessed from inside a host function
|
||||
pub struct CurrentPlugin {
|
||||
/// Plugin variables
|
||||
pub(crate) vars: std::collections::BTreeMap<String, Vec<u8>>,
|
||||
|
||||
/// Extism manifest
|
||||
pub(crate) manifest: extism_manifest::Manifest,
|
||||
pub(crate) store: *mut Store<CurrentPlugin>,
|
||||
pub(crate) linker: *mut wasmtime::Linker<CurrentPlugin>,
|
||||
pub(crate) wasi: Option<Wasi>,
|
||||
pub(crate) http_status: u16,
|
||||
pub(crate) available_pages: Option<u32>,
|
||||
pub(crate) memory_limiter: Option<MemoryLimiter>,
|
||||
}
|
||||
|
||||
unsafe impl Sync for CurrentPlugin {}
|
||||
unsafe impl Send for CurrentPlugin {}
|
||||
|
||||
pub(crate) struct MemoryLimiter {
|
||||
bytes_left: usize,
|
||||
max_bytes: usize,
|
||||
}
|
||||
|
||||
impl MemoryLimiter {
|
||||
pub(crate) fn reset(&mut self) {
|
||||
self.bytes_left = self.max_bytes;
|
||||
}
|
||||
}
|
||||
|
||||
impl wasmtime::ResourceLimiter for MemoryLimiter {
|
||||
fn memory_growing(
|
||||
&mut self,
|
||||
current: usize,
|
||||
desired: usize,
|
||||
maximum: Option<usize>,
|
||||
) -> Result<bool> {
|
||||
if let Some(max) = maximum {
|
||||
if desired > max {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
let d = desired - current;
|
||||
if d > self.bytes_left {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
self.bytes_left -= d;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn table_growing(&mut self, _current: u32, desired: u32, maximum: Option<u32>) -> Result<bool> {
|
||||
if let Some(max) = maximum {
|
||||
return Ok(desired <= max);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl CurrentPlugin {
|
||||
/// Access a plugin's variables
|
||||
pub fn vars(&self) -> &std::collections::BTreeMap<String, Vec<u8>> {
|
||||
&self.vars
|
||||
}
|
||||
|
||||
/// Mutable access to a plugin's variables
|
||||
pub fn vars_mut(&mut self) -> &mut std::collections::BTreeMap<String, Vec<u8>> {
|
||||
&mut self.vars
|
||||
}
|
||||
|
||||
/// Plugin manifest
|
||||
pub fn manifest(&self) -> &Manifest {
|
||||
&self.manifest
|
||||
}
|
||||
|
||||
pub(crate) fn new(
|
||||
manifest: extism_manifest::Manifest,
|
||||
wasi: bool,
|
||||
available_pages: Option<u32>,
|
||||
) -> Result<Self, Error> {
|
||||
let wasi = if wasi {
|
||||
let auth = wasmtime_wasi::ambient_authority();
|
||||
let mut ctx = wasmtime_wasi::WasiCtxBuilder::new();
|
||||
for (k, v) in manifest.config.iter() {
|
||||
ctx = ctx.env(k, v)?;
|
||||
}
|
||||
|
||||
if let Some(a) = &manifest.allowed_paths {
|
||||
for (k, v) in a.iter() {
|
||||
let d = wasmtime_wasi::Dir::open_ambient_dir(k, auth)?;
|
||||
ctx = ctx.preopened_dir(d, v)?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "nn")]
|
||||
let nn = wasmtime_wasi_nn::WasiNnCtx::new()?;
|
||||
|
||||
Some(Wasi {
|
||||
ctx: ctx.build(),
|
||||
#[cfg(feature = "nn")]
|
||||
nn,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let memory_limiter = if let Some(pgs) = available_pages {
|
||||
let n = pgs as usize * 65536;
|
||||
Some(crate::current_plugin::MemoryLimiter {
|
||||
max_bytes: n,
|
||||
bytes_left: n,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(CurrentPlugin {
|
||||
wasi,
|
||||
manifest,
|
||||
http_status: 0,
|
||||
vars: BTreeMap::new(),
|
||||
linker: std::ptr::null_mut(),
|
||||
store: std::ptr::null_mut(),
|
||||
available_pages,
|
||||
memory_limiter,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a pointer to the plugin memory
|
||||
pub fn memory_ptr(&mut self) -> *mut u8 {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
if let Some(mem) = linker.get(&mut store, "env", "memory") {
|
||||
if let Some(mem) = mem.into_memory() {
|
||||
return mem.data_ptr(&mut store);
|
||||
}
|
||||
}
|
||||
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
|
||||
/// Get a slice that contains the entire plugin memory
|
||||
pub fn memory(&mut self) -> &mut [u8] {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
let mem = linker
|
||||
.get(&mut store, "env", "memory")
|
||||
.unwrap()
|
||||
.into_memory()
|
||||
.unwrap();
|
||||
let ptr = mem.data_ptr(&store);
|
||||
if ptr.is_null() {
|
||||
return &mut [];
|
||||
}
|
||||
let size = mem.data_size(&store);
|
||||
unsafe { std::slice::from_raw_parts_mut(ptr, size) }
|
||||
}
|
||||
|
||||
/// Read a section of Extism plugin memory
|
||||
pub fn memory_read(&mut self, handle: MemoryHandle) -> &[u8] {
|
||||
trace!("memory_read: {}, {}", handle.0, handle.1);
|
||||
let offs = handle.0 as usize;
|
||||
let len = handle.1 as usize;
|
||||
let mem = self.memory();
|
||||
&mem[offs..offs + len]
|
||||
}
|
||||
|
||||
/// Read a section of Extism plugin memory and convert to to an `str`
|
||||
pub fn memory_read_str(&mut self, handle: MemoryHandle) -> Result<&str, std::str::Utf8Error> {
|
||||
std::str::from_utf8(self.memory_read(handle))
|
||||
}
|
||||
|
||||
/// Write data to an offset in Extism plugin memory
|
||||
pub fn memory_write(&mut self, handle: MemoryHandle, bytes: impl AsRef<[u8]>) {
|
||||
trace!("memory_write: {}", handle.0);
|
||||
let b = bytes.as_ref();
|
||||
let offs = handle.0 as usize;
|
||||
let len = b.len();
|
||||
self.memory()[offs..offs + len.min(handle.len())].copy_from_slice(bytes.as_ref());
|
||||
}
|
||||
|
||||
/// Allocate a new block of Extism plugin memory
|
||||
pub fn memory_alloc(&mut self, n: Size) -> Result<MemoryHandle, Error> {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
let output = &mut [Val::I64(0)];
|
||||
if let Some(f) = linker.get(&mut store, "env", "extism_alloc") {
|
||||
f.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[Val::I64(n as i64)], output)?;
|
||||
} else {
|
||||
anyhow::bail!("Unable to allocate memory");
|
||||
}
|
||||
let offs = output[0].unwrap_i64() as u64;
|
||||
if offs == 0 {
|
||||
anyhow::bail!("out of memory")
|
||||
}
|
||||
trace!("memory_alloc: {}, {}", offs, n);
|
||||
Ok(MemoryHandle(offs, n))
|
||||
}
|
||||
|
||||
/// Allocate a new block in Extism plugin memory and fill it will the provided bytes
|
||||
pub fn memory_alloc_bytes(&mut self, bytes: impl AsRef<[u8]>) -> Result<MemoryHandle, Error> {
|
||||
let b = bytes.as_ref();
|
||||
let offs = self.memory_alloc(b.len() as Size)?;
|
||||
self.memory_write(offs, b);
|
||||
Ok(offs)
|
||||
}
|
||||
|
||||
/// Free a block of Extism plugin memory
|
||||
pub fn memory_free(&mut self, handle: MemoryHandle) {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
linker
|
||||
.get(&mut store, "env", "extism_free")
|
||||
.unwrap()
|
||||
.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[Val::I64(handle.0 as i64)], &mut [])
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Get a `MemoryHandle` from an offset
|
||||
pub fn memory_handle(&mut self, offs: u64) -> Option<MemoryHandle> {
|
||||
if offs == 0 {
|
||||
return None;
|
||||
}
|
||||
let length = self.memory_length(offs);
|
||||
if length == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(MemoryHandle(offs, length))
|
||||
}
|
||||
|
||||
/// Get a `MemoryHandle` from a `Val` reference - this can be used to convert a host function's
|
||||
/// argument directly to `MemoryHandle`
|
||||
pub fn memory_handle_val(&mut self, offs: &Val) -> Option<MemoryHandle> {
|
||||
let offs = offs.i64()? as u64;
|
||||
let length = self.memory_length(offs);
|
||||
if length == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(MemoryHandle(offs, length))
|
||||
}
|
||||
|
||||
pub(crate) fn memory_length(&mut self, offs: u64) -> u64 {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
let output = &mut [Val::I64(0)];
|
||||
linker
|
||||
.get(&mut store, "env", "extism_length")
|
||||
.unwrap()
|
||||
.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[Val::I64(offs as i64)], output)
|
||||
.unwrap();
|
||||
let len = output[0].unwrap_i64() as u64;
|
||||
trace!("memory_length: {}, {}", offs, len);
|
||||
len
|
||||
}
|
||||
|
||||
/// Clear the current plugin error
|
||||
pub fn clear_error(&mut self) {
|
||||
trace!("CurrentPlugin::clear_error");
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
if let Some(f) = linker.get(&mut store, "env", "extism_error_set") {
|
||||
f.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[Val::I64(0)], &mut [])
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true when the error has been set
|
||||
pub fn has_error(&mut self) -> bool {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
let output = &mut [Val::I64(0)];
|
||||
linker
|
||||
.get(&mut store, "env", "extism_error_get")
|
||||
.unwrap()
|
||||
.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[], output)
|
||||
.unwrap();
|
||||
output[0].unwrap_i64() != 0
|
||||
}
|
||||
|
||||
/// Get the current error message
|
||||
pub fn get_error(&mut self) -> Option<&str> {
|
||||
let (offs, length) = self.get_error_position();
|
||||
if offs == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let data = self.memory_read(MemoryHandle(offs, length));
|
||||
let s = std::str::from_utf8(data);
|
||||
match s {
|
||||
Ok(s) => Some(s),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_error_position(&mut self) -> (u64, u64) {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
let output = &mut [Val::I64(0)];
|
||||
linker
|
||||
.get(&mut store, "env", "extism_error_get")
|
||||
.unwrap()
|
||||
.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[], output)
|
||||
.unwrap();
|
||||
let offs = output[0].unwrap_i64() as u64;
|
||||
let length = self.memory_length(offs);
|
||||
(offs, length)
|
||||
}
|
||||
}
|
||||
|
||||
impl Internal for CurrentPlugin {
|
||||
fn store(&self) -> &Store<CurrentPlugin> {
|
||||
unsafe { &*self.store }
|
||||
}
|
||||
|
||||
fn store_mut(&mut self) -> &mut Store<CurrentPlugin> {
|
||||
unsafe { &mut *self.store }
|
||||
}
|
||||
|
||||
fn linker(&self) -> &Linker<CurrentPlugin> {
|
||||
unsafe { &*self.linker }
|
||||
}
|
||||
|
||||
fn linker_mut(&mut self) -> &mut Linker<CurrentPlugin> {
|
||||
unsafe { &mut *self.linker }
|
||||
}
|
||||
|
||||
fn linker_and_store(&mut self) -> (&mut Linker<CurrentPlugin>, &mut Store<CurrentPlugin>) {
|
||||
unsafe { (&mut *self.linker, &mut *self.store) }
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{Error, Internal};
|
||||
use crate::{CurrentPlugin, Error};
|
||||
|
||||
/// A list of all possible value types in WebAssembly.
|
||||
/// An enumeration of all possible value types in WebAssembly.
|
||||
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub enum ValType {
|
||||
@@ -54,6 +54,8 @@ impl From<ValType> for wasmtime::ValType {
|
||||
|
||||
pub type Val = wasmtime::Val;
|
||||
|
||||
/// UserData is an opaque pointer used to store additional data
|
||||
/// that gets passed into host function callbacks
|
||||
pub struct UserData {
|
||||
ptr: *mut std::ffi::c_void,
|
||||
free: Option<extern "C" fn(_: *mut std::ffi::c_void)>,
|
||||
@@ -66,6 +68,8 @@ extern "C" fn free_any(ptr: *mut std::ffi::c_void) {
|
||||
}
|
||||
|
||||
impl UserData {
|
||||
/// Create a new `UserData` from an existing pointer and free function, this is used
|
||||
/// by the C API to wrap C pointers into user data
|
||||
pub fn new_pointer(
|
||||
ptr: *mut std::ffi::c_void,
|
||||
free: Option<extern "C" fn(_: *mut std::ffi::c_void)>,
|
||||
@@ -77,6 +81,7 @@ impl UserData {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `UserData` with any Rust type
|
||||
pub fn new<T: std::any::Any>(x: T) -> Self {
|
||||
let ptr = Box::into_raw(Box::new(x)) as *mut _;
|
||||
UserData {
|
||||
@@ -86,11 +91,13 @@ impl UserData {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the underlying pointer is `null`
|
||||
pub fn is_null(&self) -> bool {
|
||||
self.ptr.is_null()
|
||||
}
|
||||
|
||||
pub fn as_ptr(&self) -> *mut std::ffi::c_void {
|
||||
/// Get the user data pointer
|
||||
pub(crate) fn as_ptr(&self) -> *mut std::ffi::c_void {
|
||||
self.ptr
|
||||
}
|
||||
|
||||
@@ -102,6 +109,8 @@ impl UserData {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the pointer as an `Any` value - this will only return `Some` if `UserData::new` was used to create the value,
|
||||
/// when `UserData::new_pointer` is used there is no way to know the original type of the pointer
|
||||
pub fn any(&self) -> Option<&dyn std::any::Any> {
|
||||
if !self.is_any || self.is_null() {
|
||||
return None;
|
||||
@@ -110,6 +119,8 @@ impl UserData {
|
||||
unsafe { Some(&*self.ptr) }
|
||||
}
|
||||
|
||||
/// Get the pointer as a mutable `Any` value - this will only return `Some` if `UserData::new` was used to create the value,
|
||||
/// when `UserData::new_pointer` is used there is no way to know the original type of the pointer
|
||||
pub fn any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
|
||||
if !self.is_any || self.is_null() {
|
||||
return None;
|
||||
@@ -146,10 +157,11 @@ impl Drop for UserData {
|
||||
unsafe impl Send for UserData {}
|
||||
unsafe impl Sync for UserData {}
|
||||
|
||||
type FunctionInner = dyn Fn(wasmtime::Caller<Internal>, &[wasmtime::Val], &mut [wasmtime::Val]) -> Result<(), Error>
|
||||
type FunctionInner = dyn Fn(wasmtime::Caller<CurrentPlugin>, &[wasmtime::Val], &mut [wasmtime::Val]) -> Result<(), Error>
|
||||
+ Sync
|
||||
+ Send;
|
||||
|
||||
/// Wraps raw host functions with some additional metadata and user data
|
||||
#[derive(Clone)]
|
||||
pub struct Function {
|
||||
pub(crate) name: String,
|
||||
@@ -160,6 +172,7 @@ pub struct Function {
|
||||
}
|
||||
|
||||
impl Function {
|
||||
/// Create a new host function
|
||||
pub fn new<F>(
|
||||
name: impl Into<String>,
|
||||
args: impl IntoIterator<Item = ValType>,
|
||||
@@ -169,7 +182,7 @@ impl Function {
|
||||
) -> Function
|
||||
where
|
||||
F: 'static
|
||||
+ Fn(&mut Internal, &[Val], &mut [Val], UserData) -> Result<(), Error>
|
||||
+ Fn(&mut CurrentPlugin, &[Val], &mut [Val], UserData) -> Result<(), Error>
|
||||
+ Sync
|
||||
+ Send,
|
||||
{
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::*;
|
||||
|
||||
/// WASI context
|
||||
@@ -12,334 +10,23 @@ pub struct Wasi {
|
||||
pub nn: wasmtime_wasi_nn::WasiNnCtx,
|
||||
}
|
||||
|
||||
/// Internal stores data that is available to the caller in PDK functions
|
||||
pub struct Internal {
|
||||
/// Store
|
||||
pub store: *mut Store<Internal>,
|
||||
|
||||
/// Linker
|
||||
pub linker: *mut wasmtime::Linker<Internal>,
|
||||
|
||||
/// WASI context
|
||||
pub wasi: Option<Wasi>,
|
||||
|
||||
/// Keep track of the status from the last HTTP request
|
||||
pub http_status: u16,
|
||||
|
||||
/// Plugin variables
|
||||
pub vars: BTreeMap<String, Vec<u8>>,
|
||||
|
||||
pub manifest: Manifest,
|
||||
|
||||
pub available_pages: Option<u32>,
|
||||
|
||||
pub(crate) memory_limiter: Option<MemoryLimiter>,
|
||||
}
|
||||
|
||||
/// InternalExt provides a unified way of acessing `memory`, `store` and `internal` values
|
||||
pub trait InternalExt {
|
||||
fn store(&self) -> &Store<Internal>;
|
||||
pub(crate) trait Internal {
|
||||
fn store(&self) -> &Store<CurrentPlugin>;
|
||||
|
||||
fn store_mut(&mut self) -> &mut Store<Internal>;
|
||||
fn store_mut(&mut self) -> &mut Store<CurrentPlugin>;
|
||||
|
||||
fn linker(&self) -> &Linker<Internal>;
|
||||
fn linker(&self) -> &Linker<CurrentPlugin>;
|
||||
|
||||
fn linker_mut(&mut self) -> &mut Linker<Internal>;
|
||||
fn linker_mut(&mut self) -> &mut Linker<CurrentPlugin>;
|
||||
|
||||
fn linker_and_store(&mut self) -> (&mut Linker<Internal>, &mut Store<Internal>);
|
||||
fn linker_and_store(&mut self) -> (&mut Linker<CurrentPlugin>, &mut Store<CurrentPlugin>);
|
||||
|
||||
fn internal(&self) -> &Internal {
|
||||
fn current_plugin(&self) -> &CurrentPlugin {
|
||||
self.store().data()
|
||||
}
|
||||
|
||||
fn internal_mut(&mut self) -> &mut Internal {
|
||||
fn current_plugin_mut(&mut self) -> &mut CurrentPlugin {
|
||||
self.store_mut().data_mut()
|
||||
}
|
||||
|
||||
fn memory_ptr(&mut self) -> *mut u8 {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
if let Some(mem) = linker.get(&mut store, "env", "memory") {
|
||||
if let Some(mem) = mem.into_memory() {
|
||||
return mem.data_ptr(&mut store);
|
||||
}
|
||||
}
|
||||
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
|
||||
fn memory(&mut self) -> &mut [u8] {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
let mem = linker
|
||||
.get(&mut store, "env", "memory")
|
||||
.unwrap()
|
||||
.into_memory()
|
||||
.unwrap();
|
||||
let ptr = mem.data_ptr(&store);
|
||||
if ptr.is_null() {
|
||||
return &mut [];
|
||||
}
|
||||
let size = mem.data_size(&store);
|
||||
unsafe { std::slice::from_raw_parts_mut(ptr, size) }
|
||||
}
|
||||
|
||||
fn memory_read(&mut self, offs: u64, len: Size) -> &[u8] {
|
||||
trace!("memory_read: {}, {}", offs, len);
|
||||
let offs = offs as usize;
|
||||
let len = len as usize;
|
||||
let mem = self.memory();
|
||||
&mem[offs..offs + len]
|
||||
}
|
||||
|
||||
fn memory_read_str(&mut self, offs: u64) -> Result<&str, std::str::Utf8Error> {
|
||||
let len = self.memory_length(offs);
|
||||
std::str::from_utf8(self.memory_read(offs, len))
|
||||
}
|
||||
|
||||
fn memory_write(&mut self, offs: u64, bytes: impl AsRef<[u8]>) {
|
||||
trace!("memory_write: {}", offs);
|
||||
let b = bytes.as_ref();
|
||||
let offs = offs as usize;
|
||||
let len = b.len();
|
||||
self.memory()[offs..offs + len].copy_from_slice(bytes.as_ref());
|
||||
}
|
||||
|
||||
fn memory_alloc(&mut self, n: Size) -> Result<u64, Error> {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
let output = &mut [Val::I64(0)];
|
||||
linker
|
||||
.get(&mut store, "env", "extism_alloc")
|
||||
.unwrap()
|
||||
.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[Val::I64(n as i64)], output)?;
|
||||
let offs = output[0].unwrap_i64() as u64;
|
||||
if offs == 0 {
|
||||
anyhow::bail!("out of memory")
|
||||
}
|
||||
trace!("memory_alloc: {}, {}", offs, n);
|
||||
Ok(offs)
|
||||
}
|
||||
|
||||
fn memory_alloc_bytes(&mut self, bytes: impl AsRef<[u8]>) -> Result<u64, Error> {
|
||||
let b = bytes.as_ref();
|
||||
let offs = self.memory_alloc(b.len() as Size)?;
|
||||
self.memory_write(offs, b);
|
||||
Ok(offs)
|
||||
}
|
||||
|
||||
fn memory_free(&mut self, offs: u64) {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
linker
|
||||
.get(&mut store, "env", "extism_free")
|
||||
.unwrap()
|
||||
.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[Val::I64(offs as i64)], &mut [])
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn memory_length(&mut self, offs: u64) -> u64 {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
let output = &mut [Val::I64(0)];
|
||||
linker
|
||||
.get(&mut store, "env", "extism_length")
|
||||
.unwrap()
|
||||
.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[Val::I64(offs as i64)], output)
|
||||
.unwrap();
|
||||
let len = output[0].unwrap_i64() as u64;
|
||||
trace!("memory_length: {}, {}", offs, len);
|
||||
len
|
||||
}
|
||||
|
||||
// A convenience method to set the plugin error and return a value
|
||||
fn error<E>(&mut self, e: impl std::fmt::Debug, x: E) -> E {
|
||||
let s = format!("{e:?}");
|
||||
debug!("Set error: {:?}", s);
|
||||
if let Ok(offs) = self.memory_alloc_bytes(&s) {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
if let Some(f) = linker.get(&mut store, "env", "extism_error_set") {
|
||||
f.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[Val::I64(offs as i64)], &mut [])
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
x
|
||||
}
|
||||
|
||||
fn clear_error(&mut self) {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
if let Some(f) = linker.get(&mut store, "env", "extism_error_set") {
|
||||
f.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[Val::I64(0)], &mut [])
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn has_error(&mut self) -> bool {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
let output = &mut [Val::I64(0)];
|
||||
linker
|
||||
.get(&mut store, "env", "extism_error_get")
|
||||
.unwrap()
|
||||
.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[], output)
|
||||
.unwrap();
|
||||
output[0].unwrap_i64() != 0
|
||||
}
|
||||
|
||||
fn get_error(&mut self) -> Option<&str> {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
let output = &mut [Val::I64(0)];
|
||||
linker
|
||||
.get(&mut store, "env", "extism_error_get")
|
||||
.unwrap()
|
||||
.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[], output)
|
||||
.unwrap();
|
||||
let offs = output[0].unwrap_i64() as u64;
|
||||
if offs == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let length = self.memory_length(offs);
|
||||
let data = self.memory_read(offs, length);
|
||||
let s = std::str::from_utf8(data);
|
||||
match s {
|
||||
Ok(s) => Some(s),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Internal {
|
||||
pub(crate) fn new(
|
||||
manifest: Manifest,
|
||||
wasi: bool,
|
||||
available_pages: Option<u32>,
|
||||
) -> Result<Self, Error> {
|
||||
let wasi = if wasi {
|
||||
let auth = wasmtime_wasi::ambient_authority();
|
||||
let mut ctx = wasmtime_wasi::WasiCtxBuilder::new();
|
||||
for (k, v) in manifest.as_ref().config.iter() {
|
||||
ctx = ctx.env(k, v)?;
|
||||
}
|
||||
|
||||
if let Some(a) = &manifest.as_ref().allowed_paths {
|
||||
for (k, v) in a.iter() {
|
||||
let d = wasmtime_wasi::Dir::open_ambient_dir(k, auth)?;
|
||||
ctx = ctx.preopened_dir(d, v)?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "nn")]
|
||||
let nn = wasmtime_wasi_nn::WasiNnCtx::new()?;
|
||||
|
||||
Some(Wasi {
|
||||
ctx: ctx.build(),
|
||||
#[cfg(feature = "nn")]
|
||||
nn,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let memory_limiter = if let Some(pgs) = available_pages {
|
||||
let n = pgs as usize * 65536;
|
||||
Some(MemoryLimiter {
|
||||
max_bytes: n,
|
||||
bytes_left: n,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Internal {
|
||||
wasi,
|
||||
manifest,
|
||||
http_status: 0,
|
||||
vars: BTreeMap::new(),
|
||||
linker: std::ptr::null_mut(),
|
||||
store: std::ptr::null_mut(),
|
||||
available_pages,
|
||||
memory_limiter,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn linker(&self) -> &wasmtime::Linker<Internal> {
|
||||
unsafe { &*self.linker }
|
||||
}
|
||||
|
||||
pub fn linker_mut(&mut self) -> &mut wasmtime::Linker<Internal> {
|
||||
unsafe { &mut *self.linker }
|
||||
}
|
||||
}
|
||||
|
||||
impl InternalExt for Internal {
|
||||
fn store(&self) -> &Store<Internal> {
|
||||
unsafe { &*self.store }
|
||||
}
|
||||
|
||||
fn store_mut(&mut self) -> &mut Store<Internal> {
|
||||
unsafe { &mut *self.store }
|
||||
}
|
||||
|
||||
fn linker(&self) -> &Linker<Internal> {
|
||||
unsafe { &*self.linker }
|
||||
}
|
||||
|
||||
fn linker_mut(&mut self) -> &mut Linker<Internal> {
|
||||
unsafe { &mut *self.linker }
|
||||
}
|
||||
|
||||
fn linker_and_store(&mut self) -> (&mut Linker<Internal>, &mut Store<Internal>) {
|
||||
unsafe { (&mut *self.linker, &mut *self.store) }
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct MemoryLimiter {
|
||||
bytes_left: usize,
|
||||
max_bytes: usize,
|
||||
}
|
||||
|
||||
impl MemoryLimiter {
|
||||
pub(crate) fn reset(&mut self) {
|
||||
self.bytes_left = self.max_bytes;
|
||||
}
|
||||
}
|
||||
|
||||
impl wasmtime::ResourceLimiter for MemoryLimiter {
|
||||
fn memory_growing(
|
||||
&mut self,
|
||||
current: usize,
|
||||
desired: usize,
|
||||
maximum: Option<usize>,
|
||||
) -> Result<bool> {
|
||||
if let Some(max) = maximum {
|
||||
if desired > max {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
let d = desired - current;
|
||||
if d > self.bytes_left {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
self.bytes_left -= d;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn table_growing(&mut self, _current: u32, desired: u32, maximum: Option<u32>) -> Result<bool> {
|
||||
if let Some(max) = maximum {
|
||||
return Ok(desired <= max);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,73 @@
|
||||
pub use anyhow::Error;
|
||||
pub(crate) use std::collections::BTreeMap;
|
||||
pub(crate) use wasmtime::*;
|
||||
|
||||
mod context;
|
||||
pub use anyhow::Error;
|
||||
|
||||
mod current_plugin;
|
||||
mod function;
|
||||
mod internal;
|
||||
pub mod manifest;
|
||||
pub(crate) mod manifest;
|
||||
pub(crate) mod pdk;
|
||||
mod plugin;
|
||||
mod plugin_ref;
|
||||
mod plugin_builder;
|
||||
pub mod sdk;
|
||||
mod timer;
|
||||
|
||||
pub use context::Context;
|
||||
pub use current_plugin::{CurrentPlugin, MemoryHandle};
|
||||
pub use extism_manifest::Manifest;
|
||||
pub use function::{Function, UserData, Val, ValType};
|
||||
pub use internal::{Internal, InternalExt, Wasi};
|
||||
pub use manifest::Manifest;
|
||||
pub use plugin::Plugin;
|
||||
pub use plugin_ref::PluginRef;
|
||||
pub use plugin_builder::PluginBuilder;
|
||||
pub use sdk::ExtismCancelHandle as CancelHandle;
|
||||
|
||||
pub(crate) use internal::{Internal, Wasi};
|
||||
pub(crate) use timer::{Timer, TimerAction};
|
||||
|
||||
pub type Size = u64;
|
||||
pub type PluginIndex = i32;
|
||||
|
||||
pub(crate) use log::{debug, error, trace};
|
||||
|
||||
/// Converts any type implementing `std::fmt::Debug` into a suitable CString to use
|
||||
/// as an error message
|
||||
pub(crate) fn error_string(e: impl std::fmt::Debug) -> std::ffi::CString {
|
||||
let x = format!("{:?}", e).into_bytes();
|
||||
let x = if x[0] == b'"' && x[x.len() - 1] == b'"' {
|
||||
x[1..x.len() - 1].to_vec()
|
||||
} else {
|
||||
x
|
||||
};
|
||||
unsafe { std::ffi::CString::from_vec_unchecked(x) }
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub(crate) const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "\0");
|
||||
|
||||
/// Returns a string containing the Extism version of the current runtime, this is the same as the Cargo package
|
||||
/// version
|
||||
pub fn extism_version() -> &'static str {
|
||||
VERSION
|
||||
}
|
||||
|
||||
/// Set the log file Extism will use, this is a global configuration
|
||||
pub fn set_log_file(file: impl AsRef<std::path::Path>, level: log::Level) -> Result<(), Error> {
|
||||
use log4rs::append::console::ConsoleAppender;
|
||||
use log4rs::append::file::FileAppender;
|
||||
use log4rs::config::{Appender, Config, Logger, Root};
|
||||
use log4rs::encode::pattern::PatternEncoder;
|
||||
let encoder = Box::new(PatternEncoder::new("{t} {l} {d} - {m}\n"));
|
||||
let file = file.as_ref();
|
||||
|
||||
let logfile: Box<dyn log4rs::append::Append> = if file == std::path::PathBuf::from("stdout") {
|
||||
let target = log4rs::append::console::Target::Stdout;
|
||||
let console = ConsoleAppender::builder().target(target).encoder(encoder);
|
||||
Box::new(console.build())
|
||||
} else if file == std::path::PathBuf::from("-") || file == std::path::PathBuf::from("stderr") {
|
||||
let target = log4rs::append::console::Target::Stderr;
|
||||
let console = ConsoleAppender::builder().target(target).encoder(encoder);
|
||||
Box::new(console.build())
|
||||
} else {
|
||||
Box::new(FileAppender::builder().encoder(encoder).build(file)?)
|
||||
};
|
||||
|
||||
let config = Config::builder()
|
||||
.appender(Appender::builder().build("logfile", logfile))
|
||||
.logger(
|
||||
Logger::builder()
|
||||
.appender("logfile")
|
||||
.build("extism", level.to_level_filter()),
|
||||
)
|
||||
.build(Root::builder().build(log::LevelFilter::Off))?;
|
||||
|
||||
log4rs::init_config(config)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,11 +6,6 @@ use sha2::Digest;
|
||||
|
||||
use crate::*;
|
||||
|
||||
/// Manifest wraps the manifest exported by `extism_manifest`
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Manifest(extism_manifest::Manifest);
|
||||
|
||||
fn hex(data: &[u8]) -> String {
|
||||
let mut s = String::new();
|
||||
for &byte in data {
|
||||
@@ -166,64 +161,55 @@ fn to_module(engine: &Engine, wasm: &extism_manifest::Wasm) -> Result<(String, M
|
||||
|
||||
const WASM_MAGIC: [u8; 4] = [0x00, 0x61, 0x73, 0x6d];
|
||||
|
||||
impl Manifest {
|
||||
/// Create a new Manifest, returns the manifest and a map of modules
|
||||
pub fn new(engine: &Engine, data: &[u8]) -> Result<(Self, BTreeMap<String, Module>), Error> {
|
||||
let extism_module = Module::new(engine, WASM)?;
|
||||
let has_magic = data.len() >= 4 && data[0..4] == WASM_MAGIC;
|
||||
let is_wast = data.starts_with(b"(module") || data.starts_with(b";;");
|
||||
if !has_magic && !is_wast {
|
||||
if let Ok(s) = std::str::from_utf8(data) {
|
||||
if let Ok(t) = toml::from_str::<Self>(s) {
|
||||
let m = t.modules(engine)?;
|
||||
return Ok((t, m));
|
||||
}
|
||||
pub(crate) fn load(
|
||||
engine: &Engine,
|
||||
data: &[u8],
|
||||
) -> Result<(extism_manifest::Manifest, BTreeMap<String, Module>), Error> {
|
||||
let extism_module = Module::new(engine, WASM)?;
|
||||
let has_magic = data.len() >= 4 && data[0..4] == WASM_MAGIC;
|
||||
let is_wast = data.starts_with(b"(module") || data.starts_with(b";;");
|
||||
if !has_magic && !is_wast {
|
||||
if let Ok(s) = std::str::from_utf8(data) {
|
||||
if let Ok(t) = toml::from_str::<extism_manifest::Manifest>(s) {
|
||||
let m = modules(&t, engine)?;
|
||||
return Ok((t, m));
|
||||
}
|
||||
|
||||
let t = serde_json::from_slice::<Self>(data)?;
|
||||
let mut m = t.modules(engine)?;
|
||||
m.insert("env".to_string(), extism_module);
|
||||
return Ok((t, m));
|
||||
}
|
||||
|
||||
let m = Module::new(engine, data)?;
|
||||
let mut modules = BTreeMap::new();
|
||||
modules.insert("env".to_string(), extism_module);
|
||||
let t = serde_json::from_slice::<extism_manifest::Manifest>(data)?;
|
||||
let mut m = modules(&t, engine)?;
|
||||
m.insert("env".to_string(), extism_module);
|
||||
return Ok((t, m));
|
||||
}
|
||||
|
||||
let m = Module::new(engine, data)?;
|
||||
let mut modules = BTreeMap::new();
|
||||
modules.insert("env".to_string(), extism_module);
|
||||
modules.insert("main".to_string(), m);
|
||||
Ok((Default::default(), modules))
|
||||
}
|
||||
|
||||
pub(crate) fn modules(
|
||||
manifest: &extism_manifest::Manifest,
|
||||
engine: &Engine,
|
||||
) -> Result<BTreeMap<String, Module>, Error> {
|
||||
if manifest.wasm.is_empty() {
|
||||
return Err(anyhow::format_err!("No wasm files specified"));
|
||||
}
|
||||
|
||||
let mut modules = BTreeMap::new();
|
||||
|
||||
// If there's only one module, it should be called `main`
|
||||
if manifest.wasm.len() == 1 {
|
||||
let (_, m) = to_module(engine, &manifest.wasm[0])?;
|
||||
modules.insert("main".to_string(), m);
|
||||
Ok((Manifest::default(), modules))
|
||||
return Ok(modules);
|
||||
}
|
||||
|
||||
fn modules(&self, engine: &Engine) -> Result<BTreeMap<String, Module>, Error> {
|
||||
if self.0.wasm.is_empty() {
|
||||
return Err(anyhow::format_err!("No wasm files specified"));
|
||||
}
|
||||
|
||||
let mut modules = BTreeMap::new();
|
||||
|
||||
// If there's only one module, it should be called `main`
|
||||
if self.0.wasm.len() == 1 {
|
||||
let (_, m) = to_module(engine, &self.0.wasm[0])?;
|
||||
modules.insert("main".to_string(), m);
|
||||
return Ok(modules);
|
||||
}
|
||||
|
||||
for f in &self.0.wasm {
|
||||
let (name, m) = to_module(engine, f)?;
|
||||
modules.insert(name, m);
|
||||
}
|
||||
|
||||
Ok(modules)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<extism_manifest::Manifest> for Manifest {
|
||||
fn as_ref(&self) -> &extism_manifest::Manifest {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsMut<extism_manifest::Manifest> for Manifest {
|
||||
fn as_mut(&mut self) -> &mut extism_manifest::Manifest {
|
||||
&mut self.0
|
||||
for f in &manifest.wasm {
|
||||
let (name, m) = to_module(engine, f)?;
|
||||
modules.insert(name, m);
|
||||
}
|
||||
|
||||
Ok(modules)
|
||||
}
|
||||
|
||||
@@ -22,18 +22,22 @@ macro_rules! args {
|
||||
/// Params: i64 (offset)
|
||||
/// Returns: i64 (offset)
|
||||
pub(crate) fn config_get(
|
||||
mut caller: Caller<Internal>,
|
||||
mut caller: Caller<CurrentPlugin>,
|
||||
input: &[Val],
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let data: &mut CurrentPlugin = caller.data_mut();
|
||||
|
||||
let offset = args!(input, 0, i64) as u64;
|
||||
let key = data.memory_read_str(offset)?;
|
||||
let handle = match data.memory_handle(offset) {
|
||||
Some(h) => h,
|
||||
None => anyhow::bail!("invalid handle offset: {offset}"),
|
||||
};
|
||||
let key = data.memory_read_str(handle)?;
|
||||
let key = unsafe {
|
||||
std::str::from_utf8_unchecked(std::slice::from_raw_parts(key.as_ptr(), key.len()))
|
||||
};
|
||||
let val = data.internal().manifest.as_ref().config.get(key);
|
||||
let val = data.manifest.config.get(key);
|
||||
let ptr = val.map(|x| (x.len(), x.as_ptr()));
|
||||
let mem = match ptr {
|
||||
Some((len, ptr)) => {
|
||||
@@ -45,7 +49,7 @@ pub(crate) fn config_get(
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
output[0] = Val::I64(mem as i64);
|
||||
output[0] = Val::I64(mem.offset() as i64);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -53,18 +57,22 @@ pub(crate) fn config_get(
|
||||
/// Params: i64 (offset)
|
||||
/// Returns: i64 (offset)
|
||||
pub(crate) fn var_get(
|
||||
mut caller: Caller<Internal>,
|
||||
mut caller: Caller<CurrentPlugin>,
|
||||
input: &[Val],
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let data: &mut CurrentPlugin = caller.data_mut();
|
||||
|
||||
let offset = args!(input, 0, i64) as u64;
|
||||
let key = data.memory_read_str(offset)?;
|
||||
let handle = match data.memory_handle(offset) {
|
||||
Some(h) => h,
|
||||
None => anyhow::bail!("invalid handle offset: {offset}"),
|
||||
};
|
||||
let key = data.memory_read_str(handle)?;
|
||||
let key = unsafe {
|
||||
std::str::from_utf8_unchecked(std::slice::from_raw_parts(key.as_ptr(), key.len()))
|
||||
};
|
||||
let val = data.internal().vars.get(key);
|
||||
let val = data.vars.get(key);
|
||||
let ptr = val.map(|x| (x.len(), x.as_ptr()));
|
||||
let mem = match ptr {
|
||||
Some((len, ptr)) => {
|
||||
@@ -76,7 +84,7 @@ pub(crate) fn var_get(
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
output[0] = Val::I64(mem as i64);
|
||||
output[0] = Val::I64(mem.offset() as i64);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -84,11 +92,11 @@ pub(crate) fn var_get(
|
||||
/// Params: i64 (key offset), i64 (value offset)
|
||||
/// Returns: none
|
||||
pub(crate) fn var_set(
|
||||
mut caller: Caller<Internal>,
|
||||
mut caller: Caller<CurrentPlugin>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let data: &mut CurrentPlugin = caller.data_mut();
|
||||
|
||||
let mut size = 0;
|
||||
for v in data.vars.values() {
|
||||
@@ -104,7 +112,11 @@ pub(crate) fn var_set(
|
||||
|
||||
let key_offs = args!(input, 0, i64) as u64;
|
||||
let key = {
|
||||
let key = data.memory_read_str(key_offs)?;
|
||||
let handle = match data.memory_handle(key_offs) {
|
||||
Some(h) => h,
|
||||
None => anyhow::bail!("invalid handle offset: {key_offs}"),
|
||||
};
|
||||
let key = data.memory_read_str(handle)?;
|
||||
let key_len = key.len();
|
||||
let key_ptr = key.as_ptr();
|
||||
unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(key_ptr, key_len)) }
|
||||
@@ -116,8 +128,11 @@ pub(crate) fn var_set(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let vlen = data.memory_length(voffset);
|
||||
let value = data.memory_read(voffset, vlen).to_vec();
|
||||
let handle = match data.memory_handle(voffset) {
|
||||
Some(h) => h,
|
||||
None => anyhow::bail!("invalid handle offset: {key_offs}"),
|
||||
};
|
||||
let value = data.memory_read(handle).to_vec();
|
||||
|
||||
// Insert the value from memory into the `vars` map
|
||||
data.vars.insert(key.to_string(), value);
|
||||
@@ -129,7 +144,7 @@ pub(crate) fn var_set(
|
||||
/// Params: i64 (offset to JSON encoded HttpRequest), i64 (offset to body or 0)
|
||||
/// Returns: i64 (offset)
|
||||
pub(crate) fn http_request(
|
||||
#[allow(unused_mut)] mut caller: Caller<Internal>,
|
||||
#[allow(unused_mut)] mut caller: Caller<CurrentPlugin>,
|
||||
input: &[Val],
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
@@ -145,12 +160,14 @@ pub(crate) fn http_request(
|
||||
#[cfg(feature = "http")]
|
||||
{
|
||||
use std::io::Read;
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let data: &mut CurrentPlugin = caller.data_mut();
|
||||
let http_req_offset = args!(input, 0, i64) as u64;
|
||||
|
||||
let http_req_len = data.memory_length(http_req_offset);
|
||||
let req: extism_manifest::HttpRequest =
|
||||
serde_json::from_slice(data.memory_read(http_req_offset, http_req_len))?;
|
||||
let handle = match data.memory_handle(http_req_offset) {
|
||||
Some(h) => h,
|
||||
None => anyhow::bail!("invalid handle offset: {http_req_offset}"),
|
||||
};
|
||||
let req: extism_manifest::HttpRequest = serde_json::from_slice(data.memory_read(handle))?;
|
||||
|
||||
let body_offset = args!(input, 1, i64) as u64;
|
||||
|
||||
@@ -158,7 +175,7 @@ pub(crate) fn http_request(
|
||||
Ok(u) => u,
|
||||
Err(e) => return Err(Error::msg(format!("Invalid URL: {e:?}"))),
|
||||
};
|
||||
let allowed_hosts = &data.internal().manifest.as_ref().allowed_hosts;
|
||||
let allowed_hosts = &data.manifest.allowed_hosts;
|
||||
let host_str = url.host_str().unwrap_or_default();
|
||||
let host_matches = if let Some(allowed_hosts) = allowed_hosts {
|
||||
allowed_hosts.iter().any(|url| {
|
||||
@@ -187,8 +204,11 @@ pub(crate) fn http_request(
|
||||
}
|
||||
|
||||
let res = if body_offset > 0 {
|
||||
let len = data.memory_length(body_offset);
|
||||
let buf = data.memory_read(body_offset, len);
|
||||
let handle = match data.memory_handle(body_offset) {
|
||||
Some(h) => h,
|
||||
None => anyhow::bail!("invalid handle offset: {http_req_offset}"),
|
||||
};
|
||||
let buf = data.memory_read(handle);
|
||||
r.send_bytes(buf)
|
||||
} else {
|
||||
r.call()
|
||||
@@ -216,7 +236,7 @@ pub(crate) fn http_request(
|
||||
.read_to_end(&mut buf)?;
|
||||
|
||||
let mem = data.memory_alloc_bytes(buf)?;
|
||||
output[0] = Val::I64(mem as i64);
|
||||
output[0] = Val::I64(mem.offset() as i64);
|
||||
} else {
|
||||
output[0] = Val::I64(0);
|
||||
}
|
||||
@@ -229,24 +249,30 @@ pub(crate) fn http_request(
|
||||
/// Params: none
|
||||
/// Returns: i32 (status code)
|
||||
pub(crate) fn http_status_code(
|
||||
mut caller: Caller<Internal>,
|
||||
mut caller: Caller<CurrentPlugin>,
|
||||
_input: &[Val],
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let data: &mut CurrentPlugin = caller.data_mut();
|
||||
output[0] = Val::I32(data.http_status as i32);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn log(
|
||||
level: log::Level,
|
||||
mut caller: Caller<Internal>,
|
||||
mut caller: Caller<CurrentPlugin>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let data: &mut CurrentPlugin = caller.data_mut();
|
||||
let offset = args!(input, 0, i64) as u64;
|
||||
let buf = data.memory_read_str(offset);
|
||||
|
||||
let handle = match data.memory_handle(offset) {
|
||||
Some(h) => h,
|
||||
None => anyhow::bail!("invalid handle offset: {offset}"),
|
||||
};
|
||||
|
||||
let buf = data.memory_read_str(handle);
|
||||
|
||||
match buf {
|
||||
Ok(buf) => log::log!(level, "{}", buf),
|
||||
@@ -259,7 +285,7 @@ pub fn log(
|
||||
/// Params: i64 (offset)
|
||||
/// Returns: none
|
||||
pub(crate) fn log_warn(
|
||||
caller: Caller<Internal>,
|
||||
caller: Caller<CurrentPlugin>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
@@ -270,7 +296,7 @@ pub(crate) fn log_warn(
|
||||
/// Params: i64 (offset)
|
||||
/// Returns: none
|
||||
pub(crate) fn log_info(
|
||||
caller: Caller<Internal>,
|
||||
caller: Caller<CurrentPlugin>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
@@ -281,7 +307,7 @@ pub(crate) fn log_info(
|
||||
/// Params: i64 (offset)
|
||||
/// Returns: none
|
||||
pub(crate) fn log_debug(
|
||||
caller: Caller<Internal>,
|
||||
caller: Caller<CurrentPlugin>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
@@ -292,7 +318,7 @@ pub(crate) fn log_debug(
|
||||
/// Params: i64 (offset)
|
||||
/// Returns: none
|
||||
pub(crate) fn log_error(
|
||||
caller: Caller<Internal>,
|
||||
caller: Caller<CurrentPlugin>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
|
||||
@@ -2,53 +2,86 @@ use std::collections::BTreeMap;
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct Output {
|
||||
pub(crate) offset: u64,
|
||||
pub(crate) length: u64,
|
||||
pub(crate) error_offset: u64,
|
||||
pub(crate) error_length: u64,
|
||||
}
|
||||
|
||||
/// Plugin contains everything needed to execute a WASM function
|
||||
pub struct Plugin {
|
||||
/// A unique ID for each plugin
|
||||
pub id: uuid::Uuid,
|
||||
|
||||
/// Wasmtime linker
|
||||
pub(crate) linker: Linker<CurrentPlugin>,
|
||||
|
||||
/// Wasmtime store
|
||||
pub(crate) store: Store<CurrentPlugin>,
|
||||
|
||||
/// A handle used to cancel execution of a plugin
|
||||
pub(crate) cancel_handle: sdk::ExtismCancelHandle,
|
||||
|
||||
/// All modules that were provided to the linker
|
||||
pub modules: BTreeMap<String, Module>,
|
||||
pub(crate) modules: BTreeMap<String, Module>,
|
||||
|
||||
/// Used to define functions and create new instances
|
||||
pub linker: Linker<Internal>,
|
||||
pub store: Store<Internal>,
|
||||
|
||||
/// Instance provides the ability to call functions in a module
|
||||
pub instance: Option<Instance>,
|
||||
pub instance_pre: InstancePre<Internal>,
|
||||
/// Instance provides the ability to call functions in a module, a `Plugin` is initialized with
|
||||
/// an `instance_pre` but no `instance`. The `instance` will be created during `Plugin::raw_call`
|
||||
pub(crate) instance: std::sync::Arc<std::sync::Mutex<Option<Instance>>>,
|
||||
pub(crate) instance_pre: InstancePre<CurrentPlugin>,
|
||||
|
||||
/// Keep track of the number of times we're instantiated, this exists
|
||||
/// to avoid issues with memory piling up since `Instance`s are only
|
||||
/// actually cleaned up along with a `Store`
|
||||
instantiations: usize,
|
||||
|
||||
/// The ID used to identify this plugin with the `Timer`
|
||||
pub timer_id: uuid::Uuid,
|
||||
|
||||
/// A handle used to cancel execution of a plugin
|
||||
pub(crate) cancel_handle: sdk::ExtismCancelHandle,
|
||||
|
||||
/// Runtime determines any initialization functions needed
|
||||
/// to run a module
|
||||
pub(crate) runtime: Option<Runtime>,
|
||||
pub(crate) runtime: Option<GuestRuntime>,
|
||||
|
||||
/// Keep a reference to the host functions
|
||||
_functions: Vec<Function>,
|
||||
|
||||
/// Communication with the timer thread
|
||||
pub(crate) timer_tx: std::sync::mpsc::Sender<TimerAction>,
|
||||
|
||||
/// Information that gets populated after a call
|
||||
pub(crate) output: Output,
|
||||
|
||||
/// Set to `true` when de-initializarion may have occured (i.e.a call to `_start`),
|
||||
/// in this case we need to re-initialize the entire module.
|
||||
pub(crate) needs_reset: bool,
|
||||
}
|
||||
|
||||
impl InternalExt for Plugin {
|
||||
fn store(&self) -> &Store<Internal> {
|
||||
unsafe impl Send for Plugin {}
|
||||
unsafe impl Sync for Plugin {}
|
||||
|
||||
impl std::fmt::Debug for Plugin {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Plugin({})", self.id)
|
||||
}
|
||||
}
|
||||
|
||||
impl Internal for Plugin {
|
||||
fn store(&self) -> &Store<CurrentPlugin> {
|
||||
&self.store
|
||||
}
|
||||
|
||||
fn store_mut(&mut self) -> &mut Store<Internal> {
|
||||
fn store_mut(&mut self) -> &mut Store<CurrentPlugin> {
|
||||
&mut self.store
|
||||
}
|
||||
|
||||
fn linker(&self) -> &Linker<Internal> {
|
||||
fn linker(&self) -> &Linker<CurrentPlugin> {
|
||||
&self.linker
|
||||
}
|
||||
|
||||
fn linker_mut(&mut self) -> &mut Linker<Internal> {
|
||||
fn linker_mut(&mut self) -> &mut Linker<CurrentPlugin> {
|
||||
&mut self.linker
|
||||
}
|
||||
|
||||
fn linker_and_store(&mut self) -> (&mut Linker<Internal>, &mut Store<Internal>) {
|
||||
fn linker_and_store(&mut self) -> (&mut Linker<CurrentPlugin>, &mut Store<CurrentPlugin>) {
|
||||
(&mut self.linker, &mut self.store)
|
||||
}
|
||||
}
|
||||
@@ -119,11 +152,29 @@ fn calculate_available_memory(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Raise an error when the epoch deadline is encountered - this is used for timeout/cancellation
|
||||
// to stop a plugin that is executing
|
||||
fn deadline_callback(_: StoreContextMut<CurrentPlugin>) -> Result<UpdateDeadline, Error> {
|
||||
Err(Error::msg("timeout"))
|
||||
}
|
||||
|
||||
impl Plugin {
|
||||
/// Create a new plugin from the given WASM code
|
||||
pub fn new<'a>(
|
||||
/// Create a new plugin from the given manifest, and host functions. The `with_wasi` parameter determines
|
||||
/// whether or not the module should be executed with WASI enabled.
|
||||
pub fn new_with_manifest(
|
||||
manifest: &Manifest,
|
||||
functions: impl IntoIterator<Item = Function>,
|
||||
with_wasi: bool,
|
||||
) -> Result<Plugin, Error> {
|
||||
let data = serde_json::to_vec(manifest)?;
|
||||
Self::new(data, functions, with_wasi)
|
||||
}
|
||||
|
||||
/// Create a new plugin from the given WebAssembly module or JSON encoded manifest, and host functions. The `with_wasi`
|
||||
/// parameter determines whether or not the module should be executed with WASI enabled.
|
||||
pub fn new(
|
||||
wasm: impl AsRef<[u8]>,
|
||||
imports: impl IntoIterator<Item = &'a Function>,
|
||||
imports: impl IntoIterator<Item = Function>,
|
||||
with_wasi: bool,
|
||||
) -> Result<Plugin, Error> {
|
||||
// Create a new engine, if the `EXITSM_DEBUG` environment variable is set
|
||||
@@ -135,20 +186,21 @@ impl Plugin {
|
||||
.profiler(profiling_strategy()),
|
||||
)?;
|
||||
let mut imports = imports.into_iter();
|
||||
let (manifest, modules) = Manifest::new(&engine, wasm.as_ref())?;
|
||||
let (manifest, modules) = manifest::load(&engine, wasm.as_ref())?;
|
||||
|
||||
// Calculate how much memory is available based on the value of `max_pages` and the exported
|
||||
// memory of the modules. An error will be returned if a module doesn't have an exported memory
|
||||
// or there is no maximum set for a module's exported memory.
|
||||
let mut available_pages = manifest.as_ref().memory.max_pages;
|
||||
let mut available_pages = manifest.memory.max_pages;
|
||||
calculate_available_memory(&mut available_pages, &modules)?;
|
||||
log::trace!("Available pages: {available_pages:?}");
|
||||
|
||||
let mut store = Store::new(
|
||||
&engine,
|
||||
Internal::new(manifest, with_wasi, available_pages)?,
|
||||
CurrentPlugin::new(manifest, with_wasi, available_pages)?,
|
||||
);
|
||||
store.epoch_deadline_callback(|_internal| Ok(wasmtime::UpdateDeadline::Continue(1)));
|
||||
|
||||
store.set_epoch_deadline(1);
|
||||
|
||||
if available_pages.is_some() {
|
||||
store.limiter(|internal| internal.memory_limiter.as_mut().unwrap());
|
||||
@@ -158,12 +210,12 @@ impl Plugin {
|
||||
|
||||
// If wasi is enabled then add it to the linker
|
||||
if with_wasi {
|
||||
wasmtime_wasi::add_to_linker(&mut linker, |x: &mut Internal| {
|
||||
wasmtime_wasi::add_to_linker(&mut linker, |x: &mut CurrentPlugin| {
|
||||
&mut x.wasi.as_mut().unwrap().ctx
|
||||
})?;
|
||||
|
||||
#[cfg(feature = "nn")]
|
||||
wasmtime_wasi_nn::add_to_linker(&mut linker, |x: &mut Internal| {
|
||||
wasmtime_wasi_nn::add_to_linker(&mut linker, |x: &mut CurrentPlugin| {
|
||||
&mut x.wasi.as_mut().unwrap().nn
|
||||
})?;
|
||||
}
|
||||
@@ -223,31 +275,54 @@ impl Plugin {
|
||||
})?;
|
||||
}
|
||||
|
||||
let instance_pre = linker.instantiate_pre(&main)?;
|
||||
let timer_id = uuid::Uuid::new_v4();
|
||||
let instance_pre = linker.instantiate_pre(main)?;
|
||||
let id = uuid::Uuid::new_v4();
|
||||
let timer_tx = Timer::tx();
|
||||
let mut plugin = Plugin {
|
||||
modules,
|
||||
linker,
|
||||
instance: None,
|
||||
instance: std::sync::Arc::new(std::sync::Mutex::new(None)),
|
||||
instance_pre,
|
||||
store,
|
||||
runtime: None,
|
||||
timer_id,
|
||||
cancel_handle: sdk::ExtismCancelHandle {
|
||||
id: timer_id,
|
||||
epoch_timer_tx: None,
|
||||
},
|
||||
id,
|
||||
timer_tx: timer_tx.clone(),
|
||||
cancel_handle: sdk::ExtismCancelHandle { id, timer_tx },
|
||||
instantiations: 0,
|
||||
output: Output::default(),
|
||||
_functions: imports.collect(),
|
||||
needs_reset: false,
|
||||
};
|
||||
|
||||
plugin.internal_mut().store = &mut plugin.store;
|
||||
plugin.internal_mut().linker = &mut plugin.linker;
|
||||
plugin.current_plugin_mut().store = &mut plugin.store;
|
||||
plugin.current_plugin_mut().linker = &mut plugin.linker;
|
||||
Ok(plugin)
|
||||
}
|
||||
|
||||
pub(crate) fn reset_store(&mut self) -> Result<(), Error> {
|
||||
self.instance = None;
|
||||
if self.instantiations > 5 {
|
||||
// Resets the store and linker to avoid running into Wasmtime memory limits
|
||||
pub(crate) fn reset_store(
|
||||
&mut self,
|
||||
instance_lock: &mut std::sync::MutexGuard<Option<Instance>>,
|
||||
) -> Result<(), Error> {
|
||||
if self.instantiations > 100 {
|
||||
let engine = self.store.engine().clone();
|
||||
let internal = self.current_plugin_mut();
|
||||
self.store = Store::new(
|
||||
&engine,
|
||||
CurrentPlugin::new(
|
||||
internal.manifest.clone(),
|
||||
internal.wasi.is_some(),
|
||||
internal.available_pages,
|
||||
)?,
|
||||
);
|
||||
|
||||
self.store.set_epoch_deadline(1);
|
||||
|
||||
if self.current_plugin().available_pages.is_some() {
|
||||
self.store
|
||||
.limiter(|internal| internal.memory_limiter.as_mut().unwrap());
|
||||
}
|
||||
|
||||
let (main_name, main) = self
|
||||
.modules
|
||||
.get("main")
|
||||
@@ -257,71 +332,73 @@ impl Plugin {
|
||||
(entry.0.as_str(), entry.1)
|
||||
});
|
||||
|
||||
let engine = self.store.engine().clone();
|
||||
let internal = self.internal();
|
||||
self.store = Store::new(
|
||||
&engine,
|
||||
Internal::new(
|
||||
internal.manifest.clone(),
|
||||
internal.wasi.is_some(),
|
||||
internal.available_pages,
|
||||
)?,
|
||||
);
|
||||
self.store
|
||||
.epoch_deadline_callback(|_internal| Ok(UpdateDeadline::Continue(1)));
|
||||
|
||||
if self.internal().available_pages.is_some() {
|
||||
self.store
|
||||
.limiter(|internal| internal.memory_limiter.as_mut().unwrap());
|
||||
}
|
||||
|
||||
for (name, module) in self.modules.iter() {
|
||||
if name != main_name {
|
||||
self.linker.module(&mut self.store, name, module)?;
|
||||
}
|
||||
}
|
||||
self.instantiations = 0;
|
||||
self.instance_pre = self.linker.instantiate_pre(&main)?;
|
||||
self.instance_pre = self.linker.instantiate_pre(main)?;
|
||||
|
||||
let store = &mut self.store as *mut _;
|
||||
let linker = &mut self.linker as *mut _;
|
||||
let internal = self.internal_mut();
|
||||
internal.store = store;
|
||||
internal.linker = linker;
|
||||
let current_plugin = self.current_plugin_mut();
|
||||
current_plugin.store = store;
|
||||
current_plugin.linker = linker;
|
||||
}
|
||||
|
||||
**instance_lock = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn instantiate(&mut self) -> Result<(), Error> {
|
||||
self.instance = Some(self.instance_pre.instantiate(&mut self.store)?);
|
||||
// Instantiate the module. This is done lazily to avoid running any code outside of the `call` function,
|
||||
// since wasmtime may execute a start function (if configured) at instantiation time,
|
||||
pub(crate) fn instantiate(
|
||||
&mut self,
|
||||
instance_lock: &mut std::sync::MutexGuard<Option<Instance>>,
|
||||
) -> Result<(), Error> {
|
||||
if instance_lock.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let instance = self.instance_pre.instantiate(&mut self.store)?;
|
||||
trace!("Plugin::instance is none, instantiating");
|
||||
**instance_lock = Some(instance);
|
||||
self.instantiations += 1;
|
||||
if let Some(limiter) = &mut self.internal_mut().memory_limiter {
|
||||
if let Some(limiter) = &mut self.current_plugin_mut().memory_limiter {
|
||||
limiter.reset();
|
||||
}
|
||||
self.detect_runtime();
|
||||
self.initialize_runtime()?;
|
||||
self.detect_guest_runtime(instance_lock);
|
||||
self.initialize_guest_runtime()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a function by name
|
||||
pub fn get_func(&mut self, function: impl AsRef<str>) -> Option<Func> {
|
||||
if let None = &self.instance {
|
||||
if let Err(e) = self.instantiate() {
|
||||
error!("Unable to instantiate: {e}");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(instance) = &mut self.instance {
|
||||
/// Get an exported function by name
|
||||
pub(crate) fn get_func(
|
||||
&mut self,
|
||||
instance_lock: &mut std::sync::MutexGuard<Option<Instance>>,
|
||||
function: impl AsRef<str>,
|
||||
) -> Option<Func> {
|
||||
if let Some(instance) = &mut **instance_lock {
|
||||
instance.get_func(&mut self.store, function.as_ref())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Store input in memory and initialize `Internal` pointer
|
||||
/// Returns `true` if the given function exists, otherwise `false`
|
||||
pub fn function_exists(&mut self, function: impl AsRef<str>) -> bool {
|
||||
self.modules["main"]
|
||||
.get_export(function.as_ref())
|
||||
.map(|x| x.func().is_some())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
// Store input in memory and re-initialize `Internal` pointer
|
||||
pub(crate) fn set_input(&mut self, input: *const u8, mut len: usize) -> Result<(), Error> {
|
||||
self.output = Output::default();
|
||||
self.clear_error();
|
||||
|
||||
if input.is_null() {
|
||||
len = 0;
|
||||
}
|
||||
@@ -329,9 +406,9 @@ impl Plugin {
|
||||
{
|
||||
let store = &mut self.store as *mut _;
|
||||
let linker = &mut self.linker as *mut _;
|
||||
let internal = self.internal_mut();
|
||||
internal.store = store;
|
||||
internal.linker = linker;
|
||||
let current_plugin = self.current_plugin_mut();
|
||||
current_plugin.store = store;
|
||||
current_plugin.linker = linker;
|
||||
}
|
||||
|
||||
let bytes = unsafe { std::slice::from_raw_parts(input, len) };
|
||||
@@ -343,12 +420,12 @@ impl Plugin {
|
||||
error!("Call to extism_reset failed");
|
||||
}
|
||||
|
||||
let offs = self.memory_alloc_bytes(bytes)?;
|
||||
let handle = self.current_plugin_mut().memory_alloc_bytes(bytes)?;
|
||||
|
||||
if let Some(f) = self.linker.get(&mut self.store, "env", "extism_input_set") {
|
||||
f.into_func().unwrap().call(
|
||||
&mut self.store,
|
||||
&[Val::I64(offs as i64), Val::I64(len as i64)],
|
||||
&[Val::I64(handle.offset() as i64), Val::I64(len as i64)],
|
||||
&mut [],
|
||||
)?;
|
||||
}
|
||||
@@ -358,15 +435,19 @@ impl Plugin {
|
||||
|
||||
/// Determine if wasi is enabled
|
||||
pub fn has_wasi(&self) -> bool {
|
||||
self.internal().wasi.is_some()
|
||||
self.current_plugin().wasi.is_some()
|
||||
}
|
||||
|
||||
fn detect_runtime(&mut self) {
|
||||
// Do a best-effort attempt to detect any guest runtime.
|
||||
fn detect_guest_runtime(
|
||||
&mut self,
|
||||
instance_lock: &mut std::sync::MutexGuard<Option<Instance>>,
|
||||
) {
|
||||
// Check for Haskell runtime initialization functions
|
||||
// Initialize Haskell runtime if `hs_init` is present,
|
||||
// by calling the `hs_init` export
|
||||
if let Some(init) = self.get_func("hs_init") {
|
||||
let reactor_init = if let Some(init) = self.get_func("_initialize") {
|
||||
if let Some(init) = self.get_func(instance_lock, "hs_init") {
|
||||
let reactor_init = if let Some(init) = self.get_func(instance_lock, "_initialize") {
|
||||
if init.typed::<(), ()>(&self.store()).is_err() {
|
||||
trace!(
|
||||
"_initialize function found with type {:?}",
|
||||
@@ -380,13 +461,13 @@ impl Plugin {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.runtime = Some(Runtime::Haskell { init, reactor_init });
|
||||
self.runtime = Some(GuestRuntime::Haskell { init, reactor_init });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for `__wasm_call_ctors` or `_initialize`, this is used by WASI to
|
||||
// initialize certain interfaces.
|
||||
let init = if let Some(init) = self.get_func("__wasm_call_ctors") {
|
||||
let init = if let Some(init) = self.get_func(instance_lock, "__wasm_call_ctors") {
|
||||
if init.typed::<(), ()>(&self.store()).is_err() {
|
||||
trace!(
|
||||
"__wasm_call_ctors function found with type {:?}",
|
||||
@@ -396,7 +477,7 @@ impl Plugin {
|
||||
}
|
||||
trace!("WASI runtime detected");
|
||||
init
|
||||
} else if let Some(init) = self.get_func("_initialize") {
|
||||
} else if let Some(init) = self.get_func(instance_lock, "_initialize") {
|
||||
if init.typed::<(), ()>(&self.store()).is_err() {
|
||||
trace!(
|
||||
"_initialize function found with type {:?}",
|
||||
@@ -410,17 +491,18 @@ impl Plugin {
|
||||
return;
|
||||
};
|
||||
|
||||
self.runtime = Some(Runtime::Wasi { init });
|
||||
self.runtime = Some(GuestRuntime::Wasi { init });
|
||||
|
||||
trace!("No runtime detected");
|
||||
}
|
||||
|
||||
pub(crate) fn initialize_runtime(&mut self) -> Result<(), Error> {
|
||||
// Initialize the guest runtime
|
||||
pub(crate) fn initialize_guest_runtime(&mut self) -> Result<(), Error> {
|
||||
let mut store = &mut self.store;
|
||||
if let Some(runtime) = &self.runtime {
|
||||
trace!("Plugin::initialize_runtime");
|
||||
match runtime {
|
||||
Runtime::Haskell { init, reactor_init } => {
|
||||
GuestRuntime::Haskell { init, reactor_init } => {
|
||||
if let Some(reactor_init) = reactor_init {
|
||||
reactor_init.call(&mut store, &[], &mut [])?;
|
||||
}
|
||||
@@ -432,7 +514,7 @@ impl Plugin {
|
||||
)?;
|
||||
debug!("Initialized Haskell language runtime");
|
||||
}
|
||||
Runtime::Wasi { init } => {
|
||||
GuestRuntime::Wasi { init } => {
|
||||
init.call(&mut store, &[], &mut [])?;
|
||||
debug!("Initialied WASI runtime");
|
||||
}
|
||||
@@ -442,45 +524,207 @@ impl Plugin {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start the timer for a Plugin - this is used for both timeouts
|
||||
/// and cancellation
|
||||
pub(crate) fn start_timer(
|
||||
&mut self,
|
||||
tx: &std::sync::mpsc::SyncSender<TimerAction>,
|
||||
) -> Result<(), Error> {
|
||||
let duration = self
|
||||
.internal()
|
||||
.manifest
|
||||
.as_ref()
|
||||
.timeout_ms
|
||||
.map(std::time::Duration::from_millis);
|
||||
self.cancel_handle.epoch_timer_tx = Some(tx.clone());
|
||||
self.store_mut().set_epoch_deadline(1);
|
||||
self.store
|
||||
.epoch_deadline_callback(|_internal| Err(Error::msg("timeout")));
|
||||
let engine: Engine = self.store().engine().clone();
|
||||
tx.send(TimerAction::Start {
|
||||
id: self.timer_id,
|
||||
duration,
|
||||
engine,
|
||||
})?;
|
||||
Ok(())
|
||||
// Return the position of the output in memory
|
||||
fn output_memory_position(&mut self) -> (u64, u64) {
|
||||
let out = &mut [Val::I64(0)];
|
||||
let out_len = &mut [Val::I64(0)];
|
||||
let mut store = &mut self.store;
|
||||
self.linker
|
||||
.get(&mut store, "env", "extism_output_offset")
|
||||
.unwrap()
|
||||
.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[], out)
|
||||
.unwrap();
|
||||
self.linker
|
||||
.get(&mut store, "env", "extism_output_length")
|
||||
.unwrap()
|
||||
.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[], out_len)
|
||||
.unwrap();
|
||||
|
||||
let offs = out[0].unwrap_i64() as u64;
|
||||
let len = out_len[0].unwrap_i64() as u64;
|
||||
(offs, len)
|
||||
}
|
||||
|
||||
/// Send TimerAction::Stop
|
||||
pub(crate) fn stop_timer(&mut self) -> Result<(), Error> {
|
||||
if let Some(tx) = &self.cancel_handle.epoch_timer_tx {
|
||||
tx.send(TimerAction::Stop { id: self.timer_id })?;
|
||||
// Get the output data after a call has returned
|
||||
fn output(&mut self) -> &[u8] {
|
||||
trace!("Output offset: {}", self.output.offset);
|
||||
let offs = self.output.offset;
|
||||
let len = self.output.length;
|
||||
self.current_plugin_mut()
|
||||
.memory_read(unsafe { MemoryHandle::new(offs, len) })
|
||||
}
|
||||
|
||||
// Cache output memory and error information after call is complete
|
||||
fn get_output_after_call(&mut self) {
|
||||
let (offs, len) = self.output_memory_position();
|
||||
self.output.offset = offs;
|
||||
self.output.length = len;
|
||||
|
||||
let err = self.current_plugin_mut().get_error_position();
|
||||
self.output.error_offset = err.0;
|
||||
self.output.error_length = err.1;
|
||||
}
|
||||
|
||||
// Implements the build of the `call` function, `raw_call` is also used in the SDK
|
||||
// code
|
||||
pub(crate) fn raw_call(
|
||||
&mut self,
|
||||
lock: &mut std::sync::MutexGuard<Option<Instance>>,
|
||||
name: impl AsRef<str>,
|
||||
input: impl AsRef<[u8]>,
|
||||
) -> Result<i32, (Error, i32)> {
|
||||
let name = name.as_ref();
|
||||
let input = input.as_ref();
|
||||
|
||||
if self.needs_reset {
|
||||
if let Err(e) = self.reset_store(lock) {
|
||||
error!("Call to Plugin::reset_store failed: {e:?}");
|
||||
}
|
||||
self.needs_reset = false;
|
||||
}
|
||||
|
||||
self.instantiate(lock).map_err(|e| (e, -1))?;
|
||||
|
||||
self.set_input(input.as_ptr(), input.len())
|
||||
.map_err(|x| (x, -1))?;
|
||||
|
||||
let func = match self.get_func(lock, name) {
|
||||
Some(x) => x,
|
||||
None => return Err((anyhow::anyhow!("Function not found: {name}"), -1)),
|
||||
};
|
||||
|
||||
// Check the number of results, reject functions with more than 1 result
|
||||
let n_results = func.ty(self.store()).results().len();
|
||||
if n_results > 1 {
|
||||
return Err((
|
||||
anyhow::anyhow!("Function {name} has {n_results} results, expected 0 or 1"),
|
||||
-1,
|
||||
));
|
||||
}
|
||||
|
||||
// Start timer
|
||||
self.timer_tx
|
||||
.send(TimerAction::Start {
|
||||
id: self.id,
|
||||
engine: self.store.engine().clone(),
|
||||
duration: self
|
||||
.current_plugin()
|
||||
.manifest
|
||||
.timeout_ms
|
||||
.map(std::time::Duration::from_millis),
|
||||
})
|
||||
.unwrap();
|
||||
self.store.epoch_deadline_callback(deadline_callback);
|
||||
|
||||
// Call the function
|
||||
let mut results = vec![wasmtime::Val::null(); n_results];
|
||||
let res = func.call(self.store_mut(), &[], results.as_mut_slice());
|
||||
|
||||
// Stop timer
|
||||
self.timer_tx
|
||||
.send(TimerAction::Stop { id: self.id })
|
||||
.unwrap();
|
||||
self.store
|
||||
.epoch_deadline_callback(|_internal| Ok(wasmtime::UpdateDeadline::Continue(1)));
|
||||
Ok(())
|
||||
.epoch_deadline_callback(|_| Ok(UpdateDeadline::Continue(1)));
|
||||
|
||||
self.get_output_after_call();
|
||||
|
||||
match res {
|
||||
Ok(()) => {
|
||||
self.needs_reset = name == "_start";
|
||||
}
|
||||
Err(e) => match e.downcast::<wasmtime_wasi::I32Exit>() {
|
||||
Ok(exit) => {
|
||||
trace!("WASI return code: {}", exit.0);
|
||||
if exit.0 != 0 {
|
||||
return Err((Error::msg("WASI return code"), exit.0));
|
||||
}
|
||||
return Ok(0);
|
||||
}
|
||||
Err(e) => {
|
||||
if e.root_cause().to_string() == "timeout" {
|
||||
return Err((Error::msg("timeout"), -1));
|
||||
}
|
||||
|
||||
error!("Call: {e:?}");
|
||||
return Err((e.context("Call failed"), -1));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// If `results` is empty and the return value wasn't a WASI exit code then
|
||||
// the call succeeded
|
||||
if results.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
// Return result to caller
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
/// Call a function by name with the given input, the return value is the output data returned by the plugin.
|
||||
/// This data will be invalidated next time the plugin is called.
|
||||
pub fn call(&mut self, name: impl AsRef<str>, input: impl AsRef<[u8]>) -> Result<&[u8], Error> {
|
||||
let lock = self.instance.clone();
|
||||
let mut lock = lock.lock().unwrap();
|
||||
self.raw_call(&mut lock, name, input)
|
||||
.map(|_| self.output())
|
||||
.map_err(|e| e.0)
|
||||
}
|
||||
|
||||
/// Get a `CancelHandle`, which can be used from another thread to cancel a running plugin
|
||||
pub fn cancel_handle(&self) -> CancelHandle {
|
||||
self.cancel_handle.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn clear_error(&mut self) {
|
||||
trace!("Clearing error on plugin {}", self.id);
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
if let Some(f) = linker.get(&mut store, "env", "extism_error_set") {
|
||||
f.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[Val::I64(0)], &mut [])
|
||||
.unwrap();
|
||||
} else {
|
||||
error!("Plugin::clear_error failed, extism_error_set not found")
|
||||
}
|
||||
}
|
||||
|
||||
// A convenience method to set the plugin error and return a value
|
||||
pub(crate) fn return_error<E>(&mut self, e: impl std::fmt::Debug, x: E) -> E {
|
||||
let s = format!("{e:?}");
|
||||
debug!("Set error: {:?}", s);
|
||||
match self.current_plugin_mut().memory_alloc_bytes(&s) {
|
||||
Ok(handle) => {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
if let Some(f) = linker.get(&mut store, "env", "extism_error_set") {
|
||||
if let Ok(()) = f.into_func().unwrap().call(
|
||||
&mut store,
|
||||
&[Val::I64(handle.offset() as i64)],
|
||||
&mut [],
|
||||
) {
|
||||
self.output.error_offset = handle.offset();
|
||||
self.output.error_length = s.len() as u64;
|
||||
}
|
||||
} else {
|
||||
error!("extism_error_set not found");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Unable to set error: {e:?}")
|
||||
}
|
||||
}
|
||||
x
|
||||
}
|
||||
}
|
||||
|
||||
// Enumerates the supported PDK language runtimes
|
||||
// Enumerates the PDK languages that need some additional initialization
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum Runtime {
|
||||
pub(crate) enum GuestRuntime {
|
||||
Haskell {
|
||||
init: Func,
|
||||
reactor_init: Option<Func>,
|
||||
|
||||
@@ -38,8 +38,27 @@ impl PluginBuilder {
|
||||
}
|
||||
|
||||
/// Add a single host function
|
||||
pub fn with_function(mut self, f: Function) -> Self {
|
||||
self.functions.push(f);
|
||||
pub fn with_function<F>(
|
||||
mut self,
|
||||
name: impl Into<String>,
|
||||
args: impl IntoIterator<Item = ValType>,
|
||||
returns: impl IntoIterator<Item = ValType>,
|
||||
user_data: Option<UserData>,
|
||||
f: F,
|
||||
) -> Self
|
||||
where
|
||||
F: 'static
|
||||
+ Fn(&mut CurrentPlugin, &[Val], &mut [Val], UserData) -> Result<(), Error>
|
||||
+ Sync
|
||||
+ Send,
|
||||
{
|
||||
self.functions.push(Function::new(
|
||||
name,
|
||||
args,
|
||||
returns,
|
||||
user_data.map(UserData::new),
|
||||
f,
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
@@ -49,18 +68,11 @@ impl PluginBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build<'a>(self, context: Option<&'a Context>) -> Result<Plugin<'a>, Error> {
|
||||
match context {
|
||||
Some(context) => match self.source {
|
||||
Source::Manifest(m) => {
|
||||
Plugin::new_with_manifest(context, &m, self.functions, self.wasi)
|
||||
}
|
||||
Source::Data(d) => Plugin::new(context, d, self.functions, self.wasi),
|
||||
},
|
||||
None => match self.source {
|
||||
Source::Manifest(m) => Plugin::create_with_manifest(&m, self.functions, self.wasi),
|
||||
Source::Data(d) => Plugin::create(d, self.functions, self.wasi),
|
||||
},
|
||||
/// Generate a new plugin with the configured settings
|
||||
pub fn build(self) -> Result<Plugin, Error> {
|
||||
match self.source {
|
||||
Source::Manifest(m) => Plugin::new_with_manifest(&m, self.functions, self.wasi),
|
||||
Source::Data(d) => Plugin::new(d, self.functions, self.wasi),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
use crate::*;
|
||||
|
||||
// PluginRef is used to access a plugin from a context-scoped plugin registry
|
||||
pub struct PluginRef<'a> {
|
||||
pub id: PluginIndex,
|
||||
running: bool,
|
||||
pub(crate) epoch_timer_tx: std::sync::mpsc::SyncSender<TimerAction>,
|
||||
plugin: *mut Plugin,
|
||||
_t: std::marker::PhantomData<&'a ()>,
|
||||
}
|
||||
|
||||
impl<'a> PluginRef<'a> {
|
||||
/// Initialize the plugin for a new call
|
||||
pub(crate) fn start_call(mut self, is_start: bool) -> Self {
|
||||
trace!("PluginRef::start_call: {}", self.id,);
|
||||
|
||||
let plugin = unsafe { &mut *self.plugin };
|
||||
if is_start {
|
||||
if let Err(e) = plugin.reset_store() {
|
||||
error!("Call to Plugin::reset_store failed: {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
if plugin.instance.is_none() {
|
||||
trace!("Plugin::instance is none, instantiating");
|
||||
if let Err(e) = plugin.instantiate() {
|
||||
error!("Plugin::instantiate failed: {e:?}");
|
||||
plugin.error(e, ());
|
||||
}
|
||||
}
|
||||
|
||||
self.running = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Create a `PluginRef` from a context
|
||||
///
|
||||
/// - Reinstantiates the plugin if `should_reinstantiate` is set to `true` and WASI is enabled
|
||||
pub fn new(ctx: &'a mut Context, plugin_id: PluginIndex, clear_error: bool) -> Option<Self> {
|
||||
trace!("Loading plugin {plugin_id}");
|
||||
|
||||
let epoch_timer_tx = ctx.epoch_timer_tx.clone();
|
||||
|
||||
let plugin = if let Some(plugin) = ctx.plugin(plugin_id) {
|
||||
plugin
|
||||
} else {
|
||||
error!("Plugin does not exist: {plugin_id}");
|
||||
return ctx.error(format!("Plugin does not exist: {plugin_id}"), None);
|
||||
};
|
||||
|
||||
let plugin = unsafe { &mut *plugin };
|
||||
|
||||
if clear_error {
|
||||
trace!("Clearing context error");
|
||||
ctx.error = None;
|
||||
trace!("Clearing plugin error: {plugin_id}");
|
||||
plugin.clear_error();
|
||||
}
|
||||
|
||||
Some(PluginRef {
|
||||
id: plugin_id,
|
||||
plugin,
|
||||
epoch_timer_tx,
|
||||
_t: std::marker::PhantomData,
|
||||
running: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AsRef<Plugin> for PluginRef<'a> {
|
||||
fn as_ref(&self) -> &Plugin {
|
||||
unsafe { &*self.plugin }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AsMut<Plugin> for PluginRef<'a> {
|
||||
fn as_mut(&mut self) -> &mut Plugin {
|
||||
unsafe { &mut *self.plugin }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Drop for PluginRef<'a> {
|
||||
fn drop(&mut self) {
|
||||
trace!("Dropping PluginRef {}", self.id);
|
||||
if self.running {
|
||||
let plugin = self.as_mut();
|
||||
|
||||
// Stop timer
|
||||
if let Err(e) = plugin.stop_timer() {
|
||||
let id = plugin.timer_id;
|
||||
error!("Failed to stop timeout manager for {id}: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ use std::str::FromStr;
|
||||
|
||||
use crate::*;
|
||||
|
||||
pub type ExtismMemoryHandle = u64;
|
||||
|
||||
/// A union type for host function argument/return values
|
||||
#[repr(C)]
|
||||
pub union ValUnion {
|
||||
@@ -22,18 +24,15 @@ pub struct ExtismVal {
|
||||
v: ValUnion,
|
||||
}
|
||||
|
||||
/// Wraps host functions
|
||||
pub struct ExtismFunction(Function);
|
||||
|
||||
impl From<Function> for ExtismFunction {
|
||||
fn from(x: Function) -> Self {
|
||||
ExtismFunction(x)
|
||||
}
|
||||
#[repr(C)]
|
||||
pub struct ExtismPluginResult {
|
||||
pub plugin: *mut Plugin,
|
||||
pub error: *mut std::ffi::c_char,
|
||||
}
|
||||
|
||||
/// Host function signature
|
||||
pub type ExtismFunctionType = extern "C" fn(
|
||||
plugin: *mut Internal,
|
||||
plugin: *mut CurrentPlugin,
|
||||
inputs: *const ExtismVal,
|
||||
n_inputs: Size,
|
||||
outputs: *mut ExtismVal,
|
||||
@@ -73,27 +72,21 @@ impl From<&wasmtime::Val> for ExtismVal {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new context
|
||||
/// Get a plugin's ID, the returned bytes are a 16 byte buffer that represent a UUID value
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_context_new() -> *mut Context {
|
||||
trace!("Creating new Context");
|
||||
Box::into_raw(Box::new(Context::new()))
|
||||
}
|
||||
|
||||
/// Free a context
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_context_free(ctx: *mut Context) {
|
||||
trace!("Freeing context");
|
||||
if ctx.is_null() {
|
||||
return;
|
||||
pub unsafe extern "C" fn extism_plugin_id(plugin: *mut Plugin) -> *const u8 {
|
||||
if plugin.is_null() {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
drop(Box::from_raw(ctx))
|
||||
|
||||
let plugin = &mut *plugin;
|
||||
plugin.id.as_bytes().as_ptr()
|
||||
}
|
||||
|
||||
/// 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 Internal) -> *mut u8 {
|
||||
pub unsafe extern "C" fn extism_current_plugin_memory(plugin: *mut CurrentPlugin) -> *mut u8 {
|
||||
if plugin.is_null() {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
@@ -105,21 +98,27 @@ pub unsafe extern "C" fn extism_current_plugin_memory(plugin: *mut Internal) ->
|
||||
/// 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 Internal, n: Size) -> u64 {
|
||||
pub unsafe extern "C" fn extism_current_plugin_memory_alloc(
|
||||
plugin: *mut CurrentPlugin,
|
||||
n: Size,
|
||||
) -> ExtismMemoryHandle {
|
||||
if plugin.is_null() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let plugin = &mut *plugin;
|
||||
plugin.memory_alloc(n as u64).unwrap_or_default()
|
||||
match plugin.memory_alloc(n) {
|
||||
Ok(x) => x.offset(),
|
||||
Err(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 Internal,
|
||||
n: Size,
|
||||
plugin: *mut CurrentPlugin,
|
||||
n: ExtismMemoryHandle,
|
||||
) -> Size {
|
||||
if plugin.is_null() {
|
||||
return 0;
|
||||
@@ -132,13 +131,18 @@ pub unsafe extern "C" fn extism_current_plugin_memory_length(
|
||||
/// 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 Internal, ptr: u64) {
|
||||
pub unsafe extern "C" fn extism_current_plugin_memory_free(
|
||||
plugin: *mut CurrentPlugin,
|
||||
ptr: ExtismMemoryHandle,
|
||||
) {
|
||||
if plugin.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
let plugin = &mut *plugin;
|
||||
plugin.memory_free(ptr);
|
||||
if let Some(handle) = plugin.memory_handle(ptr) {
|
||||
plugin.memory_free(handle);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new host function
|
||||
@@ -166,7 +170,7 @@ pub unsafe extern "C" fn extism_function_new(
|
||||
func: ExtismFunctionType,
|
||||
user_data: *mut std::ffi::c_void,
|
||||
free_user_data: Option<extern "C" fn(_: *mut std::ffi::c_void)>,
|
||||
) -> *mut ExtismFunction {
|
||||
) -> *mut Function {
|
||||
let name = match std::ffi::CStr::from_ptr(name).to_str() {
|
||||
Ok(x) => x.to_string(),
|
||||
Err(_) => {
|
||||
@@ -225,24 +229,24 @@ pub unsafe extern "C" fn extism_function_new(
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
Box::into_raw(Box::new(ExtismFunction(f)))
|
||||
Box::into_raw(Box::new(f))
|
||||
}
|
||||
|
||||
/// Free `ExtismFunction`
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_function_free(f: *mut Function) {
|
||||
drop(Box::from_raw(f))
|
||||
}
|
||||
|
||||
/// Set the namespace of an `ExtismFunction`
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_function_set_namespace(
|
||||
ptr: *mut ExtismFunction,
|
||||
ptr: *mut Function,
|
||||
namespace: *const std::ffi::c_char,
|
||||
) {
|
||||
let namespace = std::ffi::CStr::from_ptr(namespace);
|
||||
let f = &mut *ptr;
|
||||
f.0.set_namespace(namespace.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
/// Free an `ExtismFunction`
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_function_free(ptr: *mut ExtismFunction) {
|
||||
drop(Box::from_raw(ptr))
|
||||
f.set_namespace(namespace.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
/// Create a new plugin with additional host functions
|
||||
@@ -254,15 +258,14 @@ pub unsafe extern "C" fn extism_function_free(ptr: *mut ExtismFunction) {
|
||||
/// `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,
|
||||
functions: *mut *const Function,
|
||||
n_functions: Size,
|
||||
with_wasi: bool,
|
||||
) -> PluginIndex {
|
||||
errmsg: *mut *mut std::ffi::c_char,
|
||||
) -> *mut Plugin {
|
||||
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);
|
||||
let mut funcs = vec![];
|
||||
|
||||
@@ -273,100 +276,69 @@ pub unsafe extern "C" fn extism_plugin_new(
|
||||
if f.is_null() {
|
||||
continue;
|
||||
}
|
||||
let f = &*f;
|
||||
funcs.push(&f.0);
|
||||
let f = (*f).clone();
|
||||
funcs.push(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.new_plugin(data, funcs, with_wasi)
|
||||
|
||||
let plugin = Plugin::new(data, funcs, with_wasi);
|
||||
match plugin {
|
||||
Err(e) => {
|
||||
if !errmsg.is_null() {
|
||||
let e =
|
||||
std::ffi::CString::new(format!("Unable to create plugin: {:?}", e)).unwrap();
|
||||
*errmsg = e.into_raw();
|
||||
}
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
Ok(p) => Box::into_raw(Box::new(p)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update a plugin, keeping the existing ID
|
||||
///
|
||||
/// Similar to `extism_plugin_new` but takes an `index` argument to specify
|
||||
/// which plugin to update
|
||||
///
|
||||
/// Memory for this plugin will be reset upon update
|
||||
/// Free the error returned by `extism_plugin_new`, errors returned from `extism_plugin_error` don't need to be freed
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_plugin_update(
|
||||
ctx: *mut Context,
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
pub unsafe extern "C" fn extism_plugin_new_error_free(err: *mut std::ffi::c_char) {
|
||||
if err.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
let plugin = match Plugin::new(data, funcs, with_wasi) {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
error!("Error creating Plugin: {:?}", e);
|
||||
ctx.set_error(e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if !ctx.plugins.contains_key(&index) {
|
||||
ctx.set_error("Plugin index does not exist");
|
||||
return false;
|
||||
}
|
||||
|
||||
ctx.plugins.insert(index, plugin);
|
||||
|
||||
debug!("Plugin updated: {index}");
|
||||
true
|
||||
drop(std::ffi::CString::from_raw(err))
|
||||
}
|
||||
|
||||
/// Remove a plugin from the registry and free associated memory
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_plugin_free(ctx: *mut Context, plugin: PluginIndex) {
|
||||
if plugin < 0 || ctx.is_null() {
|
||||
pub unsafe extern "C" fn extism_plugin_free(plugin: *mut Plugin) {
|
||||
if plugin.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
trace!("Freeing plugin {plugin}");
|
||||
|
||||
let ctx = &mut *ctx;
|
||||
ctx.remove(plugin);
|
||||
let plugin = Box::from_raw(plugin);
|
||||
trace!("Freeing plugin {}", plugin.id);
|
||||
drop(plugin)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ExtismCancelHandle {
|
||||
pub(crate) epoch_timer_tx: Option<std::sync::mpsc::SyncSender<TimerAction>>,
|
||||
pub(crate) timer_tx: std::sync::mpsc::Sender<TimerAction>,
|
||||
pub id: uuid::Uuid,
|
||||
}
|
||||
|
||||
impl ExtismCancelHandle {
|
||||
pub fn cancel(&self) -> Result<(), Error> {
|
||||
self.timer_tx.send(TimerAction::Cancel { id: self.id })?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Get plugin ID for cancellation
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_plugin_cancel_handle(
|
||||
ctx: *mut Context,
|
||||
plugin: PluginIndex,
|
||||
plugin: *const Plugin,
|
||||
) -> *const ExtismCancelHandle {
|
||||
let ctx = &mut *ctx;
|
||||
let mut plugin = match PluginRef::new(ctx, plugin, true) {
|
||||
None => return std::ptr::null_mut(),
|
||||
Some(p) => p,
|
||||
};
|
||||
let plugin = plugin.as_mut();
|
||||
if plugin.is_null() {
|
||||
return std::ptr::null();
|
||||
}
|
||||
let plugin = &*plugin;
|
||||
&plugin.cancel_handle as *const _
|
||||
}
|
||||
|
||||
@@ -374,56 +346,38 @@ pub unsafe extern "C" fn extism_plugin_cancel_handle(
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_plugin_cancel(handle: *const ExtismCancelHandle) -> bool {
|
||||
let handle = &*handle;
|
||||
if let Some(tx) = &handle.epoch_timer_tx {
|
||||
return tx.send(TimerAction::Cancel { id: handle.id }).is_ok();
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Remove all plugins from the registry
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_context_reset(ctx: *mut Context) {
|
||||
let ctx = &mut *ctx;
|
||||
|
||||
trace!(
|
||||
"Resetting context, plugins cleared: {:?}",
|
||||
ctx.plugins.keys().collect::<Vec<&i32>>()
|
||||
);
|
||||
|
||||
ctx.plugins.clear();
|
||||
handle.cancel().is_ok()
|
||||
}
|
||||
|
||||
/// Update plugin config values, this will merge with the existing values
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_plugin_config(
|
||||
ctx: *mut Context,
|
||||
plugin: PluginIndex,
|
||||
plugin: *mut Plugin,
|
||||
json: *const u8,
|
||||
json_size: Size,
|
||||
) -> bool {
|
||||
let ctx = &mut *ctx;
|
||||
let mut plugin_ref = match PluginRef::new(ctx, plugin, true) {
|
||||
None => return false,
|
||||
Some(p) => p,
|
||||
};
|
||||
if plugin.is_null() {
|
||||
return false;
|
||||
}
|
||||
let plugin = &mut *plugin;
|
||||
let _lock = plugin.instance.clone();
|
||||
let _lock = _lock.lock().unwrap();
|
||||
|
||||
trace!(
|
||||
"Call to extism_plugin_config for {} with json pointer {:?}",
|
||||
plugin_ref.id,
|
||||
plugin.id,
|
||||
json
|
||||
);
|
||||
let plugin = plugin_ref.as_mut();
|
||||
|
||||
let data = std::slice::from_raw_parts(json, json_size as usize);
|
||||
let json: std::collections::BTreeMap<String, Option<String>> =
|
||||
match serde_json::from_slice(data) {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
return plugin.error(e, false);
|
||||
return plugin.return_error(e, false);
|
||||
}
|
||||
};
|
||||
|
||||
let wasi = &mut plugin.internal_mut().wasi;
|
||||
let wasi = &mut plugin.current_plugin_mut().wasi;
|
||||
if let Some(Wasi { ctx, .. }) = wasi {
|
||||
for (k, v) in json.iter() {
|
||||
match v {
|
||||
@@ -437,7 +391,7 @@ pub unsafe extern "C" fn extism_plugin_config(
|
||||
}
|
||||
}
|
||||
|
||||
let config = &mut plugin.internal_mut().manifest.as_mut().config;
|
||||
let config = &mut plugin.current_plugin_mut().manifest.config;
|
||||
for (k, v) in json.into_iter() {
|
||||
match v {
|
||||
Some(v) => {
|
||||
@@ -451,21 +405,22 @@ pub unsafe extern "C" fn extism_plugin_config(
|
||||
}
|
||||
}
|
||||
|
||||
plugin.clear_error();
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns true if `func_name` exists
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_plugin_function_exists(
|
||||
ctx: *mut Context,
|
||||
plugin: PluginIndex,
|
||||
plugin: *mut Plugin,
|
||||
func_name: *const c_char,
|
||||
) -> bool {
|
||||
let ctx = &mut *ctx;
|
||||
let mut plugin = match PluginRef::new(ctx, plugin, true) {
|
||||
None => return false,
|
||||
Some(p) => p,
|
||||
};
|
||||
if plugin.is_null() {
|
||||
return false;
|
||||
}
|
||||
let plugin = &mut *plugin;
|
||||
let _lock = plugin.instance.clone();
|
||||
let _lock = _lock.lock().unwrap();
|
||||
|
||||
let name = std::ffi::CStr::from_ptr(func_name);
|
||||
trace!("Call to extism_plugin_function_exists for: {:?}", name);
|
||||
@@ -473,11 +428,12 @@ pub unsafe extern "C" fn extism_plugin_function_exists(
|
||||
let name = match name.to_str() {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
return plugin.as_mut().error(e, false);
|
||||
return plugin.return_error(e, false);
|
||||
}
|
||||
};
|
||||
|
||||
plugin.as_mut().get_func(name).is_some()
|
||||
plugin.clear_error();
|
||||
plugin.function_exists(name)
|
||||
}
|
||||
|
||||
/// Call a function
|
||||
@@ -487,196 +443,90 @@ pub unsafe extern "C" fn extism_plugin_function_exists(
|
||||
/// `data_len`: is the length of `data`
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_plugin_call(
|
||||
ctx: *mut Context,
|
||||
plugin_id: PluginIndex,
|
||||
plugin: *mut Plugin,
|
||||
func_name: *const c_char,
|
||||
data: *const u8,
|
||||
data_len: Size,
|
||||
) -> i32 {
|
||||
let ctx = &mut *ctx;
|
||||
if plugin.is_null() {
|
||||
return -1;
|
||||
}
|
||||
let plugin = &mut *plugin;
|
||||
let lock = plugin.instance.clone();
|
||||
let mut lock = lock.lock().unwrap();
|
||||
|
||||
// Get function name
|
||||
let name = std::ffi::CStr::from_ptr(func_name);
|
||||
let name = match name.to_str() {
|
||||
Ok(name) => name,
|
||||
Err(e) => return ctx.error(e, -1),
|
||||
};
|
||||
let is_start = name == "_start";
|
||||
|
||||
// Get a `PluginRef` and call `init` to set up the plugin input and memory, this is only
|
||||
// needed before a new call
|
||||
let mut plugin_ref = match PluginRef::new(ctx, plugin_id, true) {
|
||||
None => return -1,
|
||||
Some(p) => p.start_call(is_start),
|
||||
};
|
||||
let tx = plugin_ref.epoch_timer_tx.clone();
|
||||
let plugin = plugin_ref.as_mut();
|
||||
|
||||
let func = match plugin.get_func(name) {
|
||||
Some(x) => x,
|
||||
None => return plugin.error(format!("Function not found: {name}"), -1),
|
||||
Err(e) => return plugin.return_error(e, -1),
|
||||
};
|
||||
|
||||
// Check the number of results, reject functions with more than 1 result
|
||||
let n_results = func.ty(plugin.store()).results().len();
|
||||
if n_results > 1 {
|
||||
return plugin.error(
|
||||
format!("Function {name} has {n_results} results, expected 0 or 1"),
|
||||
-1,
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(e) = plugin.set_input(data, data_len as usize) {
|
||||
return plugin.error(e, -1);
|
||||
}
|
||||
|
||||
if plugin.has_error() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Start timer, this will be stopped when PluginRef goes out of scope
|
||||
if let Err(e) = plugin.start_timer(&tx) {
|
||||
return plugin.error(e, -1);
|
||||
}
|
||||
|
||||
debug!("Calling function: {name} in plugin {plugin_id}");
|
||||
|
||||
// Call the function
|
||||
let mut results = vec![wasmtime::Val::null(); n_results];
|
||||
let res = func.call(plugin.store_mut(), &[], results.as_mut_slice());
|
||||
trace!("Calling function {} of plugin {}", name, plugin.id);
|
||||
let input = std::slice::from_raw_parts(data, data_len as usize);
|
||||
let res = plugin.raw_call(&mut lock, name, input);
|
||||
|
||||
match res {
|
||||
Ok(()) => (),
|
||||
Err(e) => {
|
||||
plugin.store.set_epoch_deadline(1);
|
||||
if let Some(exit) = e.downcast_ref::<wasmtime_wasi::I32Exit>() {
|
||||
trace!("WASI return code: {}", exit.0);
|
||||
if exit.0 != 0 {
|
||||
return plugin.error(&e, exit.0);
|
||||
}
|
||||
return exit.0;
|
||||
}
|
||||
|
||||
if e.root_cause().to_string() == "timeout" {
|
||||
return plugin.error("timeout", -1);
|
||||
}
|
||||
|
||||
error!("Call: {e:?}");
|
||||
return plugin.error(e.context("Call failed"), -1);
|
||||
}
|
||||
};
|
||||
|
||||
// If `results` is empty and the return value wasn't a WASI exit code then
|
||||
// the call succeeded
|
||||
if results.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Return result to caller
|
||||
results[0].unwrap_i32()
|
||||
}
|
||||
|
||||
pub fn get_context_error(ctx: &Context) -> *const c_char {
|
||||
match &ctx.error {
|
||||
Some(e) => e.as_ptr() as *const _,
|
||||
None => {
|
||||
trace!("Context error is NULL");
|
||||
std::ptr::null()
|
||||
}
|
||||
Err((e, rc)) => plugin.return_error(e, rc),
|
||||
Ok(x) => x,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the error associated with a `Context` or `Plugin`, if `plugin` is `-1` then the context
|
||||
/// error will be returned
|
||||
/// Get the error associated with a `Plugin`
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_error(ctx: *mut Context, plugin: PluginIndex) -> *const c_char {
|
||||
trace!("Call to extism_error for plugin {plugin}");
|
||||
#[deprecated]
|
||||
pub unsafe extern "C" fn extism_error(plugin: *mut Plugin) -> *const c_char {
|
||||
extism_plugin_error(plugin)
|
||||
}
|
||||
|
||||
let ctx = &mut *ctx;
|
||||
|
||||
if !ctx.plugin_exists(plugin) {
|
||||
return get_context_error(ctx);
|
||||
/// Get the error associated with a `Plugin`
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_plugin_error(plugin: *mut Plugin) -> *const c_char {
|
||||
if plugin.is_null() {
|
||||
return std::ptr::null();
|
||||
}
|
||||
let plugin = &mut *plugin;
|
||||
let _lock = plugin.instance.clone();
|
||||
let _lock = _lock.lock().unwrap();
|
||||
trace!("Call to extism_plugin_error for plugin {}", plugin.id);
|
||||
|
||||
let mut plugin_ref = match PluginRef::new(ctx, plugin, false) {
|
||||
None => return std::ptr::null(),
|
||||
Some(p) => p,
|
||||
};
|
||||
let plugin = plugin_ref.as_mut();
|
||||
let output = &mut [Val::I64(0)];
|
||||
if let Some(f) = plugin
|
||||
.linker
|
||||
.get(&mut plugin.store, "env", "extism_error_get")
|
||||
{
|
||||
f.into_func()
|
||||
.unwrap()
|
||||
.call(&mut plugin.store, &[], output)
|
||||
.unwrap();
|
||||
}
|
||||
if output[0].unwrap_i64() == 0 {
|
||||
if plugin.output.error_offset == 0 {
|
||||
trace!("Error is NULL");
|
||||
return std::ptr::null();
|
||||
}
|
||||
|
||||
plugin.memory_ptr().add(output[0].unwrap_i64() as usize) as *const _
|
||||
plugin
|
||||
.current_plugin_mut()
|
||||
.memory_ptr()
|
||||
.add(plugin.output.error_offset as usize) as *const _
|
||||
}
|
||||
|
||||
/// Get the length of a plugin's output data
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_plugin_output_length(
|
||||
ctx: *mut Context,
|
||||
plugin: PluginIndex,
|
||||
) -> Size {
|
||||
trace!("Call to extism_plugin_output_length for plugin {plugin}");
|
||||
|
||||
let ctx = &mut *ctx;
|
||||
let mut plugin_ref = match PluginRef::new(ctx, plugin, true) {
|
||||
None => return 0,
|
||||
Some(p) => p,
|
||||
};
|
||||
let plugin = plugin_ref.as_mut();
|
||||
let out = &mut [Val::I64(0)];
|
||||
let _ = plugin
|
||||
.linker
|
||||
.get(&mut plugin.store, "env", "extism_output_length")
|
||||
.unwrap()
|
||||
.into_func()
|
||||
.unwrap()
|
||||
.call(&mut plugin.store_mut(), &[], out);
|
||||
let len = out[0].unwrap_i64() as Size;
|
||||
trace!("Output length: {len}");
|
||||
len
|
||||
pub unsafe extern "C" fn extism_plugin_output_length(plugin: *mut Plugin) -> Size {
|
||||
if plugin.is_null() {
|
||||
return 0;
|
||||
}
|
||||
let plugin = &mut *plugin;
|
||||
let _lock = plugin.instance.clone();
|
||||
let _lock = _lock.lock().unwrap();
|
||||
trace!("Output length: {}", plugin.output.length);
|
||||
plugin.output.length
|
||||
}
|
||||
|
||||
/// Get a pointer to the output data
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_plugin_output_data(
|
||||
ctx: *mut Context,
|
||||
plugin: PluginIndex,
|
||||
) -> *const u8 {
|
||||
trace!("Call to extism_plugin_output_data for plugin {plugin}");
|
||||
pub unsafe extern "C" fn extism_plugin_output_data(plugin: *mut Plugin) -> *const u8 {
|
||||
if plugin.is_null() {
|
||||
return std::ptr::null();
|
||||
}
|
||||
let plugin = &mut *plugin;
|
||||
let _lock = plugin.instance.clone();
|
||||
let _lock = _lock.lock().unwrap();
|
||||
trace!("Call to extism_plugin_output_data for plugin {}", plugin.id);
|
||||
|
||||
let ctx = &mut *ctx;
|
||||
let mut plugin_ref = match PluginRef::new(ctx, plugin, true) {
|
||||
None => return std::ptr::null(),
|
||||
Some(p) => p,
|
||||
};
|
||||
let plugin = plugin_ref.as_mut();
|
||||
let ptr = plugin.memory_ptr();
|
||||
let out = &mut [Val::I64(0)];
|
||||
let mut store = &mut *(plugin.store_mut() as *mut Store<_>);
|
||||
plugin
|
||||
.linker
|
||||
.get(&mut store, "env", "extism_output_offset")
|
||||
.unwrap()
|
||||
.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[], out)
|
||||
.unwrap();
|
||||
|
||||
let offs = out[0].unwrap_i64() as usize;
|
||||
trace!("Output offset: {}", offs);
|
||||
ptr.add(offs)
|
||||
let ptr = plugin.current_plugin_mut().memory_ptr();
|
||||
ptr.add(plugin.output.offset as usize)
|
||||
}
|
||||
|
||||
/// Set log file and level
|
||||
@@ -685,11 +535,7 @@ pub unsafe extern "C" fn extism_log_file(
|
||||
filename: *const c_char,
|
||||
log_level: *const c_char,
|
||||
) -> bool {
|
||||
use log::LevelFilter;
|
||||
use log4rs::append::console::ConsoleAppender;
|
||||
use log4rs::append::file::FileAppender;
|
||||
use log4rs::config::{Appender, Config, Logger, Root};
|
||||
use log4rs::encode::pattern::PatternEncoder;
|
||||
use log::Level;
|
||||
|
||||
let file = if !filename.is_null() {
|
||||
let file = std::ffi::CStr::from_ptr(filename);
|
||||
@@ -715,56 +561,16 @@ pub unsafe extern "C" fn extism_log_file(
|
||||
"error"
|
||||
};
|
||||
|
||||
let level = match LevelFilter::from_str(level) {
|
||||
let level = match Level::from_str(&level.to_ascii_lowercase()) {
|
||||
Ok(x) => x,
|
||||
Err(_) => {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let encoder = Box::new(PatternEncoder::new("{t} {l} {d} - {m}\n"));
|
||||
|
||||
let logfile: Box<dyn log4rs::append::Append> =
|
||||
if file == "-" || file == "stdout" || file == "stderr" {
|
||||
let target = if file == "-" || file == "stdout" {
|
||||
log4rs::append::console::Target::Stdout
|
||||
} else {
|
||||
log4rs::append::console::Target::Stderr
|
||||
};
|
||||
let console = ConsoleAppender::builder().target(target).encoder(encoder);
|
||||
Box::new(console.build())
|
||||
} else {
|
||||
match FileAppender::builder().encoder(encoder).build(file) {
|
||||
Ok(x) => Box::new(x),
|
||||
Err(_) => {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let config = match Config::builder()
|
||||
.appender(Appender::builder().build("logfile", logfile))
|
||||
.logger(
|
||||
Logger::builder()
|
||||
.appender("logfile")
|
||||
.build("extism_runtime", level),
|
||||
)
|
||||
.build(Root::builder().build(LevelFilter::Off))
|
||||
{
|
||||
Ok(x) => x,
|
||||
Err(_) => {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if log4rs::init_config(config).is_err() {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
set_log_file(file, level).is_ok()
|
||||
}
|
||||
|
||||
const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "\0");
|
||||
|
||||
/// Get the Extism version string
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_version() -> *const c_char {
|
||||
|
||||
245
runtime/src/tests.rs
Normal file
245
runtime/src/tests.rs
Normal file
@@ -0,0 +1,245 @@
|
||||
use super::*;
|
||||
use std::time::Instant;
|
||||
|
||||
const WASM: &[u8] = include_bytes!("../../wasm/code-functions.wasm");
|
||||
const WASM_LOOP: &[u8] = include_bytes!("../../wasm/loop.wasm");
|
||||
const WASM_GLOBALS: &[u8] = include_bytes!("../../wasm/globals.wasm");
|
||||
|
||||
fn hello_world(
|
||||
plugin: &mut CurrentPlugin,
|
||||
inputs: &[Val],
|
||||
outputs: &mut [Val],
|
||||
_user_data: UserData,
|
||||
) -> Result<(), Error> {
|
||||
let handle = plugin.memory_handle_val(&inputs[0]).unwrap();
|
||||
let input = plugin.memory_read_str(handle).unwrap().to_string();
|
||||
|
||||
let output = plugin.memory_alloc_bytes(&input).unwrap();
|
||||
outputs[0] = output.into();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hello_world_panic(
|
||||
_plugin: &mut CurrentPlugin,
|
||||
_inputs: &[Val],
|
||||
_outputs: &mut [Val],
|
||||
_user_data: UserData,
|
||||
) -> Result<(), Error> {
|
||||
panic!("This should not run");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let wasm_start = Instant::now();
|
||||
assert!(set_log_file("test.log", log::Level::Trace).is_ok());
|
||||
let f = Function::new(
|
||||
"hello_world",
|
||||
[ValType::I64],
|
||||
[ValType::I64],
|
||||
None,
|
||||
hello_world,
|
||||
)
|
||||
.with_namespace("env");
|
||||
let g = Function::new(
|
||||
"hello_world",
|
||||
[ValType::I64],
|
||||
[ValType::I64],
|
||||
None,
|
||||
hello_world_panic,
|
||||
)
|
||||
.with_namespace("test");
|
||||
|
||||
let mut plugin = Plugin::new(WASM, [f, g], true).unwrap();
|
||||
println!("register loaded plugin: {:?}", wasm_start.elapsed());
|
||||
|
||||
let repeat = 1182;
|
||||
let input = "aeiouAEIOU____________________________________&smtms_y?".repeat(repeat);
|
||||
let data = plugin.call("count_vowels", &input).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
data,
|
||||
b"{\"count\": 11820}",
|
||||
"expecting vowel count of {}, input size: {}, output size: {}",
|
||||
10 * repeat,
|
||||
input.len(),
|
||||
data.len()
|
||||
);
|
||||
|
||||
println!(
|
||||
"register plugin + function call: {:?}, sent input size: {} bytes",
|
||||
wasm_start.elapsed(),
|
||||
input.len()
|
||||
);
|
||||
|
||||
println!("--------------");
|
||||
|
||||
let mut test_times = vec![];
|
||||
for _ in 0..100 {
|
||||
let test_start = Instant::now();
|
||||
plugin.call("count_vowels", &input).unwrap();
|
||||
test_times.push(test_start.elapsed());
|
||||
}
|
||||
|
||||
let native_test = || {
|
||||
let native_start = Instant::now();
|
||||
// let native_vowel_count = input
|
||||
// .chars()
|
||||
// .filter(|c| match c {
|
||||
// 'A' | 'E' | 'I' | 'O' | 'U' | 'a' | 'e' | 'i' | 'o' | 'u' => true,
|
||||
// _ => false,
|
||||
// })
|
||||
// .collect::<Vec<_>>()
|
||||
// .len();
|
||||
|
||||
let mut _native_vowel_count = 0;
|
||||
let input: &[u8] = input.as_ref();
|
||||
for i in 0..input.len() {
|
||||
if input[i] == b'A'
|
||||
|| input[i] == b'E'
|
||||
|| input[i] == b'I'
|
||||
|| input[i] == b'O'
|
||||
|| input[i] == b'U'
|
||||
|| input[i] == b'a'
|
||||
|| input[i] == b'e'
|
||||
|| input[i] == b'i'
|
||||
|| input[i] == b'o'
|
||||
|| input[i] == b'u'
|
||||
{
|
||||
_native_vowel_count += 1;
|
||||
}
|
||||
}
|
||||
native_start.elapsed()
|
||||
};
|
||||
|
||||
let native_test_times = (0..100).map(|_| native_test());
|
||||
let native_num_tests = native_test_times.len();
|
||||
|
||||
let native_sum: std::time::Duration = native_test_times
|
||||
.into_iter()
|
||||
.reduce(|accum: std::time::Duration, elapsed| accum + elapsed)
|
||||
.unwrap();
|
||||
let native_avg: std::time::Duration = native_sum / native_num_tests as u32;
|
||||
|
||||
println!(
|
||||
"native function call (avg, N = {}): {:?}",
|
||||
native_num_tests, native_avg
|
||||
);
|
||||
|
||||
let num_tests = test_times.len();
|
||||
let sum: std::time::Duration = test_times
|
||||
.into_iter()
|
||||
.reduce(|accum: std::time::Duration, elapsed| accum + elapsed)
|
||||
.unwrap();
|
||||
let avg: std::time::Duration = sum / num_tests as u32;
|
||||
|
||||
println!("wasm function call (avg, N = {}): {:?}", num_tests, avg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plugin_threads() {
|
||||
let p = std::sync::Arc::new(std::sync::Mutex::new(
|
||||
PluginBuilder::new_with_module(WASM)
|
||||
.with_function(
|
||||
"hello_world",
|
||||
[ValType::I64],
|
||||
[ValType::I64],
|
||||
None,
|
||||
hello_world,
|
||||
)
|
||||
.with_wasi(true)
|
||||
.build()
|
||||
.unwrap(),
|
||||
));
|
||||
|
||||
let mut threads = vec![];
|
||||
for _ in 0..3 {
|
||||
let plugin = p.clone();
|
||||
let a = std::thread::spawn(move || {
|
||||
let mut plugin = plugin.lock().unwrap();
|
||||
for _ in 0..10 {
|
||||
let output = plugin.call("count_vowels", "this is a test aaa").unwrap();
|
||||
assert_eq!(b"{\"count\": 7}", output);
|
||||
}
|
||||
});
|
||||
threads.push(a);
|
||||
}
|
||||
for thread in threads {
|
||||
thread.join().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cancel() {
|
||||
let f = Function::new(
|
||||
"hello_world",
|
||||
[ValType::I64],
|
||||
[ValType::I64],
|
||||
None,
|
||||
hello_world,
|
||||
);
|
||||
|
||||
let mut plugin = Plugin::new(WASM_LOOP, [f], true).unwrap();
|
||||
let handle = plugin.cancel_handle();
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
assert!(handle.cancel().is_ok());
|
||||
});
|
||||
let _output = plugin.call("infinite_loop", "abc123");
|
||||
let end = std::time::Instant::now();
|
||||
let time = end - start;
|
||||
println!("Cancelled plugin ran for {:?}", time);
|
||||
// std::io::stdout().write_all(output).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timeout() {
|
||||
let f = Function::new(
|
||||
"hello_world",
|
||||
[ValType::I64],
|
||||
[ValType::I64],
|
||||
None,
|
||||
hello_world,
|
||||
);
|
||||
|
||||
let manifest = Manifest::new([extism_manifest::Wasm::data(WASM_LOOP)])
|
||||
.with_timeout(std::time::Duration::from_secs(1));
|
||||
let mut plugin = Plugin::new_with_manifest(&manifest, [f], true).unwrap();
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
let _output = plugin.call("infinite_loop", "abc123");
|
||||
let end = std::time::Instant::now();
|
||||
let time = end - start;
|
||||
println!("Timed out plugin ran for {:?}", time);
|
||||
// std::io::stdout().write_all(output).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_instantiations() {
|
||||
let f = Function::new(
|
||||
"hello_world",
|
||||
[ValType::I64],
|
||||
[ValType::I64],
|
||||
None,
|
||||
hello_world,
|
||||
);
|
||||
|
||||
let mut plugin = Plugin::new(WASM, [f], true).unwrap();
|
||||
|
||||
// This is 10,001 because the wasmtime store limit is 10,000 - we want to test
|
||||
// that our reinstantiation process is working and that limit is never hit.
|
||||
for _ in 0..10001 {
|
||||
let _output = plugin.call("count_vowels", "abc123").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_globals() {
|
||||
let mut plugin = Plugin::new(WASM_GLOBALS, [], true).unwrap();
|
||||
for i in 0..1000 {
|
||||
let output = plugin.call("globals", "").unwrap();
|
||||
let count: serde_json::Value = serde_json::from_slice(output).unwrap();
|
||||
assert_eq!(count.get("count").unwrap().as_i64().unwrap(), i);
|
||||
}
|
||||
}
|
||||
@@ -16,18 +16,38 @@ pub(crate) enum TimerAction {
|
||||
}
|
||||
|
||||
pub(crate) struct Timer {
|
||||
pub tx: std::sync::mpsc::SyncSender<TimerAction>,
|
||||
pub tx: std::sync::mpsc::Sender<TimerAction>,
|
||||
pub thread: Option<std::thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "windows"))]
|
||||
extern "C" fn cleanup_timer() {
|
||||
drop(Context::timer().take())
|
||||
let mut timer = match unsafe { TIMER.lock() } {
|
||||
Ok(x) => x,
|
||||
Err(e) => e.into_inner(),
|
||||
};
|
||||
drop(timer.take());
|
||||
}
|
||||
|
||||
static mut TIMER: std::sync::Mutex<Option<Timer>> = std::sync::Mutex::new(None);
|
||||
|
||||
impl Timer {
|
||||
pub fn init(timer: &mut Option<Timer>) -> std::sync::mpsc::SyncSender<TimerAction> {
|
||||
let (tx, rx) = std::sync::mpsc::sync_channel(128);
|
||||
pub(crate) fn tx() -> std::sync::mpsc::Sender<TimerAction> {
|
||||
let mut timer = match unsafe { TIMER.lock() } {
|
||||
Ok(x) => x,
|
||||
Err(e) => e.into_inner(),
|
||||
};
|
||||
|
||||
let timer = &mut *timer;
|
||||
|
||||
match timer {
|
||||
None => Timer::init(timer),
|
||||
Some(t) => t.tx.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(timer: &mut Option<Timer>) -> std::sync::mpsc::Sender<TimerAction> {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let thread = std::thread::spawn(move || {
|
||||
let mut plugins = std::collections::BTreeMap::new();
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
[package]
|
||||
name = "extism"
|
||||
version = "0.5.0"
|
||||
edition = "2021"
|
||||
authors = ["The Extism Authors", "oss@extism.org"]
|
||||
license = "BSD-3-Clause"
|
||||
homepage = "https://extism.org"
|
||||
repository = "https://github.com/extism/extism"
|
||||
description = "Extism Host SDK for Rust"
|
||||
|
||||
[dependencies]
|
||||
extism-manifest = { version = "0.5.0", path = "../manifest" }
|
||||
extism-runtime = { version = "0.5.0", path = "../runtime"}
|
||||
serde_json = "1"
|
||||
log = "0.4"
|
||||
anyhow = "1"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
@@ -1,2 +0,0 @@
|
||||
bindings:
|
||||
bindgen ../runtime/extism.h --allowlist-function extism.* > src/bindings.rs
|
||||
@@ -1,309 +0,0 @@
|
||||
/* automatically generated by rust-bindgen 0.65.1 */
|
||||
|
||||
#[doc = " Signed 32 bit integer."]
|
||||
pub const ExtismValType_I32: ExtismValType = 0;
|
||||
#[doc = " Signed 64 bit integer."]
|
||||
pub const ExtismValType_I64: ExtismValType = 1;
|
||||
#[doc = " Floating point 32 bit integer."]
|
||||
pub const ExtismValType_F32: ExtismValType = 2;
|
||||
#[doc = " Floating point 64 bit integer."]
|
||||
pub const ExtismValType_F64: ExtismValType = 3;
|
||||
#[doc = " A 128 bit number."]
|
||||
pub const ExtismValType_V128: ExtismValType = 4;
|
||||
#[doc = " A reference to a Wasm function."]
|
||||
pub const ExtismValType_FuncRef: ExtismValType = 5;
|
||||
#[doc = " A reference to opaque data in the Wasm instance."]
|
||||
pub const ExtismValType_ExternRef: ExtismValType = 6;
|
||||
#[doc = " A list of all possible value types in WebAssembly."]
|
||||
pub type ExtismValType = ::std::os::raw::c_uint;
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ExtismContext {
|
||||
_unused: [u8; 0],
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ExtismCancelHandle {
|
||||
_unused: [u8; 0],
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ExtismFunction {
|
||||
_unused: [u8; 0],
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ExtismCurrentPlugin {
|
||||
_unused: [u8; 0],
|
||||
}
|
||||
pub type ExtismSize = u64;
|
||||
#[doc = " A union type for host function argument/return values"]
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub union ExtismValUnion {
|
||||
pub i32_: i32,
|
||||
pub i64_: i64,
|
||||
pub f32_: f32,
|
||||
pub f64_: f64,
|
||||
}
|
||||
#[test]
|
||||
fn bindgen_test_layout_ExtismValUnion() {
|
||||
const UNINIT: ::std::mem::MaybeUninit<ExtismValUnion> = ::std::mem::MaybeUninit::uninit();
|
||||
let ptr = UNINIT.as_ptr();
|
||||
assert_eq!(
|
||||
::std::mem::size_of::<ExtismValUnion>(),
|
||||
8usize,
|
||||
concat!("Size of: ", stringify!(ExtismValUnion))
|
||||
);
|
||||
assert_eq!(
|
||||
::std::mem::align_of::<ExtismValUnion>(),
|
||||
8usize,
|
||||
concat!("Alignment of ", stringify!(ExtismValUnion))
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { ::std::ptr::addr_of!((*ptr).i32_) as usize - ptr as usize },
|
||||
0usize,
|
||||
concat!(
|
||||
"Offset of field: ",
|
||||
stringify!(ExtismValUnion),
|
||||
"::",
|
||||
stringify!(i32_)
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { ::std::ptr::addr_of!((*ptr).i64_) as usize - ptr as usize },
|
||||
0usize,
|
||||
concat!(
|
||||
"Offset of field: ",
|
||||
stringify!(ExtismValUnion),
|
||||
"::",
|
||||
stringify!(i64_)
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { ::std::ptr::addr_of!((*ptr).f32_) as usize - ptr as usize },
|
||||
0usize,
|
||||
concat!(
|
||||
"Offset of field: ",
|
||||
stringify!(ExtismValUnion),
|
||||
"::",
|
||||
stringify!(f32_)
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { ::std::ptr::addr_of!((*ptr).f64_) as usize - ptr as usize },
|
||||
0usize,
|
||||
concat!(
|
||||
"Offset of field: ",
|
||||
stringify!(ExtismValUnion),
|
||||
"::",
|
||||
stringify!(f64_)
|
||||
)
|
||||
);
|
||||
}
|
||||
#[doc = " `ExtismVal` holds the type and value of a function argument/return"]
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct ExtismVal {
|
||||
pub t: ExtismValType,
|
||||
pub v: ExtismValUnion,
|
||||
}
|
||||
#[test]
|
||||
fn bindgen_test_layout_ExtismVal() {
|
||||
const UNINIT: ::std::mem::MaybeUninit<ExtismVal> = ::std::mem::MaybeUninit::uninit();
|
||||
let ptr = UNINIT.as_ptr();
|
||||
assert_eq!(
|
||||
::std::mem::size_of::<ExtismVal>(),
|
||||
16usize,
|
||||
concat!("Size of: ", stringify!(ExtismVal))
|
||||
);
|
||||
assert_eq!(
|
||||
::std::mem::align_of::<ExtismVal>(),
|
||||
8usize,
|
||||
concat!("Alignment of ", stringify!(ExtismVal))
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { ::std::ptr::addr_of!((*ptr).t) as usize - ptr as usize },
|
||||
0usize,
|
||||
concat!(
|
||||
"Offset of field: ",
|
||||
stringify!(ExtismVal),
|
||||
"::",
|
||||
stringify!(t)
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { ::std::ptr::addr_of!((*ptr).v) as usize - ptr as usize },
|
||||
8usize,
|
||||
concat!(
|
||||
"Offset of field: ",
|
||||
stringify!(ExtismVal),
|
||||
"::",
|
||||
stringify!(v)
|
||||
)
|
||||
);
|
||||
}
|
||||
#[doc = " Host function signature"]
|
||||
pub type ExtismFunctionType = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
plugin: *mut ExtismCurrentPlugin,
|
||||
inputs: *const ExtismVal,
|
||||
n_inputs: ExtismSize,
|
||||
outputs: *mut ExtismVal,
|
||||
n_outputs: ExtismSize,
|
||||
data: *mut ::std::os::raw::c_void,
|
||||
),
|
||||
>;
|
||||
pub type ExtismPlugin = i32;
|
||||
extern "C" {
|
||||
#[doc = " Create a new context"]
|
||||
pub fn extism_context_new() -> *mut ExtismContext;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Free a context"]
|
||||
pub fn extism_context_free(ctx: *mut ExtismContext);
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Returns a pointer to the memory of the currently running plugin\n NOTE: this should only be called from host functions."]
|
||||
pub fn extism_current_plugin_memory(plugin: *mut ExtismCurrentPlugin) -> *mut u8;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Allocate a memory block in the currently running plugin\n NOTE: this should only be called from host functions."]
|
||||
pub fn extism_current_plugin_memory_alloc(
|
||||
plugin: *mut ExtismCurrentPlugin,
|
||||
n: ExtismSize,
|
||||
) -> u64;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Get the length of an allocated block\n NOTE: this should only be called from host functions."]
|
||||
pub fn extism_current_plugin_memory_length(
|
||||
plugin: *mut ExtismCurrentPlugin,
|
||||
n: ExtismSize,
|
||||
) -> ExtismSize;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Free an allocated memory block\n NOTE: this should only be called from host functions."]
|
||||
pub fn extism_current_plugin_memory_free(plugin: *mut ExtismCurrentPlugin, ptr: u64);
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Create a new host function\n\n Arguments\n - `name`: function name, this should be valid UTF-8\n - `inputs`: argument types\n - `n_inputs`: number of argument types\n - `outputs`: return types\n - `n_outputs`: number of return types\n - `func`: the function to call\n - `user_data`: a pointer that will be passed to the function when it's called\n this value should live as long as the function exists\n - `free_user_data`: a callback to release the `user_data` value when the resulting\n `ExtismFunction` is freed.\n\n Returns a new `ExtismFunction` or `null` if the `name` argument is invalid."]
|
||||
pub fn extism_function_new(
|
||||
name: *const ::std::os::raw::c_char,
|
||||
inputs: *const ExtismValType,
|
||||
n_inputs: ExtismSize,
|
||||
outputs: *const ExtismValType,
|
||||
n_outputs: ExtismSize,
|
||||
func: ExtismFunctionType,
|
||||
user_data: *mut ::std::os::raw::c_void,
|
||||
free_user_data: ::std::option::Option<
|
||||
unsafe extern "C" fn(__: *mut ::std::os::raw::c_void),
|
||||
>,
|
||||
) -> *mut ExtismFunction;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Set the namespace of an `ExtismFunction`"]
|
||||
pub fn extism_function_set_namespace(
|
||||
ptr: *mut ExtismFunction,
|
||||
namespace_: *const ::std::os::raw::c_char,
|
||||
);
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Free an `ExtismFunction`"]
|
||||
pub fn extism_function_free(ptr: *mut ExtismFunction);
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Create a new plugin with additional host functions\n\n `wasm`: is a WASM module (wat or wasm) or a JSON encoded manifest\n `wasm_size`: the length of the `wasm` parameter\n `functions`: an array of `ExtismFunction*`\n `n_functions`: the number of functions provided\n `with_wasi`: enables/disables WASI"]
|
||||
pub fn extism_plugin_new(
|
||||
ctx: *mut ExtismContext,
|
||||
wasm: *const u8,
|
||||
wasm_size: ExtismSize,
|
||||
functions: *mut *const ExtismFunction,
|
||||
n_functions: ExtismSize,
|
||||
with_wasi: bool,
|
||||
) -> ExtismPlugin;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Update a plugin, keeping the existing ID\n\n Similar to `extism_plugin_new` but takes an `index` argument to specify\n which plugin to update\n\n Memory for this plugin will be reset upon update"]
|
||||
pub fn extism_plugin_update(
|
||||
ctx: *mut ExtismContext,
|
||||
index: ExtismPlugin,
|
||||
wasm: *const u8,
|
||||
wasm_size: ExtismSize,
|
||||
functions: *mut *const ExtismFunction,
|
||||
nfunctions: ExtismSize,
|
||||
with_wasi: bool,
|
||||
) -> bool;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Remove a plugin from the registry and free associated memory"]
|
||||
pub fn extism_plugin_free(ctx: *mut ExtismContext, plugin: ExtismPlugin);
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Get plugin ID for cancellation"]
|
||||
pub fn extism_plugin_cancel_handle(
|
||||
ctx: *mut ExtismContext,
|
||||
plugin: ExtismPlugin,
|
||||
) -> *const ExtismCancelHandle;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Cancel a running plugin"]
|
||||
pub fn extism_plugin_cancel(handle: *const ExtismCancelHandle) -> bool;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Remove all plugins from the registry"]
|
||||
pub fn extism_context_reset(ctx: *mut ExtismContext);
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Update plugin config values, this will merge with the existing values"]
|
||||
pub fn extism_plugin_config(
|
||||
ctx: *mut ExtismContext,
|
||||
plugin: ExtismPlugin,
|
||||
json: *const u8,
|
||||
json_size: ExtismSize,
|
||||
) -> bool;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Returns true if `func_name` exists"]
|
||||
pub fn extism_plugin_function_exists(
|
||||
ctx: *mut ExtismContext,
|
||||
plugin: ExtismPlugin,
|
||||
func_name: *const ::std::os::raw::c_char,
|
||||
) -> bool;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Call a function\n\n `func_name`: is the function to call\n `data`: is the input data\n `data_len`: is the length of `data`"]
|
||||
pub fn extism_plugin_call(
|
||||
ctx: *mut ExtismContext,
|
||||
plugin_id: ExtismPlugin,
|
||||
func_name: *const ::std::os::raw::c_char,
|
||||
data: *const u8,
|
||||
data_len: ExtismSize,
|
||||
) -> i32;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Get the error associated with a `Context` or `Plugin`, if `plugin` is `-1` then the context\n error will be returned"]
|
||||
pub fn extism_error(
|
||||
ctx: *mut ExtismContext,
|
||||
plugin: ExtismPlugin,
|
||||
) -> *const ::std::os::raw::c_char;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Get the length of a plugin's output data"]
|
||||
pub fn extism_plugin_output_length(ctx: *mut ExtismContext, plugin: ExtismPlugin)
|
||||
-> ExtismSize;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Get a pointer to the output data"]
|
||||
pub fn extism_plugin_output_data(ctx: *mut ExtismContext, plugin: ExtismPlugin) -> *const u8;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Set log file and level"]
|
||||
pub fn extism_log_file(
|
||||
filename: *const ::std::os::raw::c_char,
|
||||
log_level: *const ::std::os::raw::c_char,
|
||||
) -> bool;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Get the Extism version string"]
|
||||
pub fn extism_version() -> *const ::std::os::raw::c_char;
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
use crate::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Context(pub(crate) std::sync::Arc<std::sync::Mutex<extism_runtime::Context>>);
|
||||
|
||||
impl Default for Context {
|
||||
fn default() -> Context {
|
||||
Context::new()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Sync for Context {}
|
||||
unsafe impl Send for Context {}
|
||||
|
||||
impl Context {
|
||||
/// Create a new context
|
||||
pub fn new() -> Context {
|
||||
Context(std::sync::Arc::new(std::sync::Mutex::new(
|
||||
extism_runtime::Context::new(),
|
||||
)))
|
||||
}
|
||||
|
||||
/// Remove all registered plugins
|
||||
pub fn reset(&mut self) {
|
||||
unsafe { bindings::extism_context_reset(&mut *self.lock()) }
|
||||
}
|
||||
|
||||
pub(crate) fn lock(&self) -> std::sync::MutexGuard<extism_runtime::Context> {
|
||||
match self.0.lock() {
|
||||
Ok(x) => x,
|
||||
Err(x) => x.into_inner(),
|
||||
}
|
||||
}
|
||||
}
|
||||
322
rust/src/lib.rs
322
rust/src/lib.rs
@@ -1,322 +0,0 @@
|
||||
pub use extism_manifest::{self as manifest, Manifest};
|
||||
pub use extism_runtime::{
|
||||
sdk as bindings, Function, Internal as CurrentPlugin, InternalExt, UserData, Val, ValType,
|
||||
};
|
||||
|
||||
mod context;
|
||||
mod plugin;
|
||||
mod plugin_builder;
|
||||
|
||||
pub use context::Context;
|
||||
pub use plugin::{CancelHandle, Plugin};
|
||||
pub use plugin_builder::PluginBuilder;
|
||||
pub type Error = anyhow::Error;
|
||||
|
||||
/// Gets the version of Extism
|
||||
pub fn extism_version() -> String {
|
||||
let err = unsafe { bindings::extism_version() };
|
||||
let buf = unsafe { std::ffi::CStr::from_ptr(err) };
|
||||
return buf.to_str().unwrap().to_string();
|
||||
}
|
||||
|
||||
/// Set the log file and level, this is a global setting
|
||||
pub fn set_log_file(filename: impl AsRef<std::path::Path>, log_level: Option<log::Level>) -> bool {
|
||||
if let Ok(filename) = std::ffi::CString::new(filename.as_ref().to_string_lossy().as_bytes()) {
|
||||
let log_level_s = log_level.map(|x| x.as_str());
|
||||
let log_level_c = log_level_s.map(|x| std::ffi::CString::new(x));
|
||||
if let Some(Ok(log_level_c)) = log_level_c {
|
||||
unsafe {
|
||||
return bindings::extism_log_file(filename.as_ptr(), log_level_c.as_ptr());
|
||||
}
|
||||
} else {
|
||||
unsafe {
|
||||
return bindings::extism_log_file(filename.as_ptr(), std::ptr::null());
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Instant;
|
||||
|
||||
const WASM: &[u8] = include_bytes!("../../wasm/code-functions.wasm");
|
||||
const WASM_LOOP: &[u8] = include_bytes!("../../wasm/loop.wasm");
|
||||
const WASM_GLOBALS: &[u8] = include_bytes!("../../wasm/globals.wasm");
|
||||
|
||||
fn hello_world(
|
||||
plugin: &mut CurrentPlugin,
|
||||
inputs: &[Val],
|
||||
outputs: &mut [Val],
|
||||
_user_data: UserData,
|
||||
) -> Result<(), Error> {
|
||||
let input_offs = inputs[0].unwrap_i64() as u64;
|
||||
let input = plugin.memory_read_str(input_offs).unwrap().to_string();
|
||||
|
||||
let output = plugin.memory_alloc_bytes(&input).unwrap();
|
||||
outputs[0] = Val::I64(output as i64);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hello_world_panic(
|
||||
_plugin: &mut CurrentPlugin,
|
||||
_inputs: &[Val],
|
||||
_outputs: &mut [Val],
|
||||
_user_data: UserData,
|
||||
) -> Result<(), Error> {
|
||||
panic!("This should not run");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let wasm_start = Instant::now();
|
||||
assert!(set_log_file("test.log", Some(log::Level::Trace)));
|
||||
let context = Context::new();
|
||||
let f = Function::new(
|
||||
"hello_world",
|
||||
[ValType::I64],
|
||||
[ValType::I64],
|
||||
None,
|
||||
hello_world,
|
||||
)
|
||||
.with_namespace("env");
|
||||
let g = Function::new(
|
||||
"hello_world",
|
||||
[ValType::I64],
|
||||
[ValType::I64],
|
||||
None,
|
||||
hello_world_panic,
|
||||
)
|
||||
.with_namespace("test");
|
||||
|
||||
let mut plugin = Plugin::new(&context, WASM, [f, g], true).unwrap();
|
||||
println!("register loaded plugin: {:?}", wasm_start.elapsed());
|
||||
|
||||
let repeat = 1182;
|
||||
let input = "aeiouAEIOU____________________________________&smtms_y?".repeat(repeat);
|
||||
let data = plugin.call("count_vowels", &input).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
data,
|
||||
b"{\"count\": 11820}",
|
||||
"expecting vowel count of {}, input size: {}, output size: {}",
|
||||
10 * repeat,
|
||||
input.len(),
|
||||
data.len()
|
||||
);
|
||||
|
||||
println!(
|
||||
"register plugin + function call: {:?}, sent input size: {} bytes",
|
||||
wasm_start.elapsed(),
|
||||
input.len()
|
||||
);
|
||||
|
||||
println!("--------------");
|
||||
|
||||
let test_times = (0..100)
|
||||
.map(|_| {
|
||||
let test_start = Instant::now();
|
||||
plugin.call("count_vowels", &input).unwrap();
|
||||
test_start.elapsed()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let native_test = || {
|
||||
let native_start = Instant::now();
|
||||
// let native_vowel_count = input
|
||||
// .chars()
|
||||
// .filter(|c| match c {
|
||||
// 'A' | 'E' | 'I' | 'O' | 'U' | 'a' | 'e' | 'i' | 'o' | 'u' => true,
|
||||
// _ => false,
|
||||
// })
|
||||
// .collect::<Vec<_>>()
|
||||
// .len();
|
||||
|
||||
let mut _native_vowel_count = 0;
|
||||
let input: &[u8] = input.as_ref();
|
||||
for i in 0..input.len() {
|
||||
if input[i] == b'A'
|
||||
|| input[i] == b'E'
|
||||
|| input[i] == b'I'
|
||||
|| input[i] == b'O'
|
||||
|| input[i] == b'U'
|
||||
|| input[i] == b'a'
|
||||
|| input[i] == b'e'
|
||||
|| input[i] == b'i'
|
||||
|| input[i] == b'o'
|
||||
|| input[i] == b'u'
|
||||
{
|
||||
_native_vowel_count += 1;
|
||||
}
|
||||
}
|
||||
native_start.elapsed()
|
||||
};
|
||||
|
||||
let native_test_times = (0..100).map(|_| native_test());
|
||||
let native_num_tests = native_test_times.len();
|
||||
|
||||
let native_sum: std::time::Duration = native_test_times
|
||||
.into_iter()
|
||||
.reduce(|accum: std::time::Duration, elapsed| accum + elapsed)
|
||||
.unwrap();
|
||||
let native_avg: std::time::Duration = native_sum / native_num_tests as u32;
|
||||
|
||||
println!(
|
||||
"native function call (avg, N = {}): {:?}",
|
||||
native_num_tests, native_avg
|
||||
);
|
||||
|
||||
let num_tests = test_times.len();
|
||||
let sum: std::time::Duration = test_times
|
||||
.into_iter()
|
||||
.reduce(|accum: std::time::Duration, elapsed| accum + elapsed)
|
||||
.unwrap();
|
||||
let avg: std::time::Duration = sum / num_tests as u32;
|
||||
|
||||
println!("wasm function call (avg, N = {}): {:?}", num_tests, avg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_context_threads() {
|
||||
use std::io::Write;
|
||||
std::thread::spawn(|| {
|
||||
let context = Context::new();
|
||||
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();
|
||||
});
|
||||
|
||||
let f = Function::new(
|
||||
"hello_world",
|
||||
[ValType::I64],
|
||||
[ValType::I64],
|
||||
None,
|
||||
hello_world,
|
||||
);
|
||||
|
||||
// One context shared between two threads
|
||||
let context = Context::new();
|
||||
let mut threads = vec![];
|
||||
for _ in 0..3 {
|
||||
let ctx = context.clone();
|
||||
let g = f.clone();
|
||||
let a = std::thread::spawn(move || {
|
||||
let mut plugin = PluginBuilder::new_with_module(WASM)
|
||||
.with_function(g)
|
||||
.with_wasi(true)
|
||||
.build(Some(&ctx))
|
||||
.unwrap();
|
||||
for _ in 0..10 {
|
||||
let output = plugin.call("count_vowels", "this is a test aaa").unwrap();
|
||||
assert_eq!(b"{\"count\": 7}", output);
|
||||
}
|
||||
});
|
||||
threads.push(a);
|
||||
}
|
||||
for thread in threads {
|
||||
thread.join().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plugin_threads() {
|
||||
let f = Function::new(
|
||||
"hello_world",
|
||||
[ValType::I64],
|
||||
[ValType::I64],
|
||||
None,
|
||||
hello_world,
|
||||
);
|
||||
|
||||
let p = std::sync::Arc::new(std::sync::Mutex::new(
|
||||
PluginBuilder::new_with_module(WASM)
|
||||
.with_function(f)
|
||||
.with_wasi(true)
|
||||
.build(None)
|
||||
.unwrap(),
|
||||
));
|
||||
|
||||
let mut threads = vec![];
|
||||
for _ in 0..3 {
|
||||
let plugin = p.clone();
|
||||
let a = std::thread::spawn(move || {
|
||||
let mut plugin = plugin.lock().unwrap();
|
||||
for _ in 0..10 {
|
||||
let output = plugin.call("count_vowels", "this is a test aaa").unwrap();
|
||||
assert_eq!(b"{\"count\": 7}", output);
|
||||
}
|
||||
});
|
||||
threads.push(a);
|
||||
}
|
||||
for thread in threads {
|
||||
thread.join().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cancel() {
|
||||
let f = Function::new(
|
||||
"hello_world",
|
||||
[ValType::I64],
|
||||
[ValType::I64],
|
||||
None,
|
||||
hello_world,
|
||||
);
|
||||
|
||||
let context = Context::new();
|
||||
let mut plugin = Plugin::new(&context, WASM_LOOP, [f], true).unwrap();
|
||||
let handle = plugin.cancel_handle();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
handle.cancel();
|
||||
});
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
let _output = plugin.call("infinite_loop", "abc123");
|
||||
let end = std::time::Instant::now();
|
||||
let time = end - start;
|
||||
println!("Cancelled plugin ran for {:?}", time);
|
||||
// std::io::stdout().write_all(output).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_instantiations() {
|
||||
let f = Function::new(
|
||||
"hello_world",
|
||||
[ValType::I64],
|
||||
[ValType::I64],
|
||||
None,
|
||||
hello_world,
|
||||
);
|
||||
|
||||
let context = Context::new();
|
||||
let mut plugin = Plugin::new(&context, WASM, [f], true).unwrap();
|
||||
|
||||
// This is 10,001 because the wasmtime store limit is 10,000 - we want to test
|
||||
// that our reinstantiation process is working and that limit is never hit.
|
||||
for _ in 0..10001 {
|
||||
let _output = plugin.call("count_vowels", "abc123").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_globals() {
|
||||
let context = Context::new();
|
||||
let mut plugin = Plugin::new(&context, WASM_GLOBALS, [], true).unwrap();
|
||||
for i in 0..1000 {
|
||||
let output = plugin.call("globals", "").unwrap();
|
||||
let count: serde_json::Value = serde_json::from_slice(&output).unwrap();
|
||||
assert_eq!(count.get("count").unwrap().as_i64().unwrap(), i);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,284 +0,0 @@
|
||||
use crate::*;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
enum RefOrOwned<'a, T> {
|
||||
Ref(&'a T),
|
||||
Owned(T),
|
||||
}
|
||||
|
||||
pub struct Plugin<'a> {
|
||||
id: extism_runtime::PluginIndex,
|
||||
context: RefOrOwned<'a, Context>,
|
||||
functions: Vec<Function>,
|
||||
}
|
||||
|
||||
impl<'a, T> From<&'a T> for RefOrOwned<'a, T> {
|
||||
fn from(value: &'a T) -> Self {
|
||||
RefOrOwned::Ref(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for RefOrOwned<'a, T> {
|
||||
fn from(value: T) -> Self {
|
||||
RefOrOwned::Owned(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> AsRef<T> for RefOrOwned<'a, T> {
|
||||
fn as_ref(&self) -> &T {
|
||||
match self {
|
||||
RefOrOwned::Ref(x) => x,
|
||||
RefOrOwned::Owned(x) => x,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CancelHandle(pub(crate) *const extism_runtime::sdk::ExtismCancelHandle);
|
||||
|
||||
unsafe impl Sync for CancelHandle {}
|
||||
unsafe impl Send for CancelHandle {}
|
||||
|
||||
impl CancelHandle {
|
||||
pub fn cancel(&self) -> bool {
|
||||
unsafe { extism_runtime::sdk::extism_plugin_cancel(self.0) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Plugin<'a> {
|
||||
/// Create plugin from a known-good ID
|
||||
///
|
||||
/// # Safety
|
||||
/// This function does not check to ensure the provided ID is valid
|
||||
pub unsafe fn from_id(id: i32, context: &'a Context) -> Plugin<'a> {
|
||||
let context = RefOrOwned::Ref(context);
|
||||
Plugin {
|
||||
id,
|
||||
context,
|
||||
functions: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn context(&self) -> &Context {
|
||||
match &self.context {
|
||||
RefOrOwned::Ref(x) => x,
|
||||
RefOrOwned::Owned(x) => x,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_i32(&self) -> i32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// Create a new plugin from the given manifest in its own context
|
||||
pub fn create_with_manifest(
|
||||
manifest: &Manifest,
|
||||
functions: impl IntoIterator<Item = Function>,
|
||||
wasi: bool,
|
||||
) -> Result<Plugin<'a>, Error> {
|
||||
let data = serde_json::to_vec(manifest)?;
|
||||
Self::create(data, functions, wasi)
|
||||
}
|
||||
|
||||
/// Create a new plugin from a WASM module in its own context
|
||||
pub fn create(
|
||||
data: impl AsRef<[u8]>,
|
||||
functions: impl IntoIterator<Item = Function>,
|
||||
wasi: bool,
|
||||
) -> Result<Plugin<'a>, Error> {
|
||||
let ctx = Context::new();
|
||||
let functions = functions.into_iter().collect();
|
||||
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();
|
||||
return Err(Error::msg(buf));
|
||||
}
|
||||
|
||||
Ok(Plugin {
|
||||
id: plugin,
|
||||
context: ctx.into(),
|
||||
functions,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new plugin from the given manifest
|
||||
pub fn new_with_manifest(
|
||||
ctx: &'a Context,
|
||||
manifest: &Manifest,
|
||||
functions: impl IntoIterator<Item = Function>,
|
||||
wasi: bool,
|
||||
) -> Result<Plugin<'a>, Error> {
|
||||
let data = serde_json::to_vec(manifest)?;
|
||||
Self::new(ctx, data, functions, wasi)
|
||||
}
|
||||
|
||||
/// Create a new plugin from a WASM module
|
||||
pub fn new(
|
||||
ctx: &'a Context,
|
||||
data: impl AsRef<[u8]>,
|
||||
functions: impl IntoIterator<Item = Function>,
|
||||
wasi: bool,
|
||||
) -> Result<Plugin<'a>, Error> {
|
||||
let functions = functions.into_iter().collect();
|
||||
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();
|
||||
return Err(Error::msg(buf));
|
||||
}
|
||||
|
||||
Ok(Plugin {
|
||||
id: plugin,
|
||||
context: ctx.into(),
|
||||
functions,
|
||||
})
|
||||
}
|
||||
|
||||
/// Update a plugin with the given manifest
|
||||
pub fn update_with_manifest(
|
||||
&mut self,
|
||||
manifest: &Manifest,
|
||||
functions: impl IntoIterator<Item = Function>,
|
||||
wasi: bool,
|
||||
) -> 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]>,
|
||||
functions: impl IntoIterator<Item = Function>,
|
||||
wasi: bool,
|
||||
) -> Result<(), Error> {
|
||||
self.functions = functions.into_iter().collect();
|
||||
let functions = self
|
||||
.functions
|
||||
.iter()
|
||||
.map(|x| bindings::ExtismFunction::from(x.clone()));
|
||||
let mut functions = functions
|
||||
.into_iter()
|
||||
.map(|x| &x as *const _)
|
||||
.collect::<Vec<_>>();
|
||||
let b = unsafe {
|
||||
bindings::extism_plugin_update(
|
||||
&mut *self.context.as_ref().lock(),
|
||||
self.id,
|
||||
data.as_ref().as_ptr(),
|
||||
data.as_ref().len() as u64,
|
||||
functions.as_mut_ptr(),
|
||||
functions.len() as u64,
|
||||
wasi,
|
||||
)
|
||||
};
|
||||
if b {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let err = unsafe { bindings::extism_error(&mut *self.context.as_ref().lock(), -1) };
|
||||
if !err.is_null() {
|
||||
let s = unsafe { std::ffi::CStr::from_ptr(err) };
|
||||
return Err(Error::msg(s.to_str().unwrap()));
|
||||
}
|
||||
|
||||
Err(Error::msg("extism_plugin_update failed"))
|
||||
}
|
||||
|
||||
/// Set configuration values
|
||||
pub fn set_config(&mut self, config: &BTreeMap<String, Option<String>>) -> Result<(), Error> {
|
||||
let encoded = serde_json::to_vec(config)?;
|
||||
unsafe {
|
||||
bindings::extism_plugin_config(
|
||||
&mut *self.context.as_ref().lock(),
|
||||
self.id,
|
||||
encoded.as_ptr() as *const _,
|
||||
encoded.len() as u64,
|
||||
)
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set configuration values, builder-style
|
||||
pub fn with_config(mut self, config: &BTreeMap<String, Option<String>>) -> Result<Self, Error> {
|
||||
self.set_config(config)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Returns true if the plugin has a function matching `name`
|
||||
pub fn has_function(&self, name: impl AsRef<str>) -> bool {
|
||||
let name = std::ffi::CString::new(name.as_ref()).expect("Invalid function name");
|
||||
unsafe {
|
||||
bindings::extism_plugin_function_exists(
|
||||
&mut *self.context.as_ref().lock(),
|
||||
self.id,
|
||||
name.as_ptr() as *const _,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancel_handle(&self) -> CancelHandle {
|
||||
let ptr = unsafe {
|
||||
bindings::extism_plugin_cancel_handle(&mut *self.context.as_ref().lock(), self.id)
|
||||
};
|
||||
|
||||
CancelHandle(ptr)
|
||||
}
|
||||
|
||||
/// Call a function with the given input and call a callback with the output, this should be preferred when
|
||||
/// a single plugin may be acessed from multiple threads because the lock on the plugin is held during the
|
||||
/// callback, ensuring the output value is protected from modification.
|
||||
pub fn call_map<'b, T, F: FnOnce(&'b [u8]) -> Result<T, Error>>(
|
||||
&'b mut self,
|
||||
name: impl AsRef<str>,
|
||||
input: impl AsRef<[u8]>,
|
||||
f: F,
|
||||
) -> Result<T, Error> {
|
||||
let context = &mut *self.context.as_ref().lock();
|
||||
let name = std::ffi::CString::new(name.as_ref()).expect("Invalid function name");
|
||||
let rc = unsafe {
|
||||
bindings::extism_plugin_call(
|
||||
context,
|
||||
self.id,
|
||||
name.as_ptr() as *const _,
|
||||
input.as_ref().as_ptr() as *const _,
|
||||
input.as_ref().len() as u64,
|
||||
)
|
||||
};
|
||||
|
||||
if rc != 0 {
|
||||
let err = unsafe { bindings::extism_error(context, self.id) };
|
||||
if !err.is_null() {
|
||||
let s = unsafe { std::ffi::CStr::from_ptr(err) };
|
||||
return Err(Error::msg(s.to_str().unwrap()));
|
||||
}
|
||||
|
||||
return Err(Error::msg("extism_call failed"));
|
||||
}
|
||||
|
||||
let out_len = unsafe { bindings::extism_plugin_output_length(context, self.id) };
|
||||
unsafe {
|
||||
let ptr = bindings::extism_plugin_output_data(context, self.id);
|
||||
f(std::slice::from_raw_parts(ptr, out_len as usize))
|
||||
}
|
||||
}
|
||||
|
||||
/// Call a function with the given input
|
||||
pub fn call<'b>(
|
||||
&'b mut self,
|
||||
name: impl AsRef<str>,
|
||||
input: impl AsRef<[u8]>,
|
||||
) -> Result<&'b [u8], Error> {
|
||||
self.call_map(name, input, |x| Ok(x))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Drop for Plugin<'a> {
|
||||
fn drop(&mut self) {
|
||||
unsafe { bindings::extism_plugin_free(&mut *self.context.as_ref().lock(), self.id) }
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const sdk = @import("extism");
|
||||
const Context = sdk.Context;
|
||||
const Plugin = sdk.Plugin;
|
||||
const CurrentPlugin = sdk.CurrentPlugin;
|
||||
const Function = sdk.Function;
|
||||
@@ -23,8 +22,6 @@ pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
const allocator = gpa.allocator();
|
||||
_ = sdk.setLogFile("extism.log", .Debug);
|
||||
var context = Context.init();
|
||||
defer context.deinit();
|
||||
|
||||
const wasmfile_manifest = manifest.WasmFile{ .path = "../wasm/code-functions.wasm" };
|
||||
const man = .{ .wasm = &[_]manifest.Wasm{.{ .wasm_file = wasmfile_manifest }} };
|
||||
@@ -36,8 +33,7 @@ pub fn main() !void {
|
||||
@constCast(@as(*const anyopaque, @ptrCast("user data"))),
|
||||
);
|
||||
defer f.deinit();
|
||||
var my_plugin = try Plugin.initFromManifest(allocator, &context, man, &[_]Function{f}, true);
|
||||
// var my_plugin = try Plugin.init(allocator, &context, wasm, &[_]Function{f}, true);
|
||||
var my_plugin = try Plugin.initFromManifest(allocator, man, &[_]Function{f}, true);
|
||||
defer my_plugin.deinit();
|
||||
|
||||
var config = std.json.ArrayHashMap([]const u8){};
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
const std = @import("std");
|
||||
const c = @import("ffi.zig");
|
||||
|
||||
const Self = @This();
|
||||
|
||||
mutex: std.Thread.Mutex,
|
||||
ctx: *c.ExtismContext,
|
||||
|
||||
// We have to use this until ziglang/zig#2647 is resolved.
|
||||
error_info: ?[]const u8,
|
||||
|
||||
/// Creates a new context, it should be freed using `deinit`
|
||||
pub fn init() Self {
|
||||
const new_ctx = c.extism_context_new();
|
||||
return .{
|
||||
.mutex = .{},
|
||||
.ctx = new_ctx orelse unreachable,
|
||||
.error_info = null,
|
||||
};
|
||||
}
|
||||
|
||||
// Free a context
|
||||
pub fn deinit(self: Self) void {
|
||||
c.extism_context_free(self.ctx);
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
c.extism_context_reset(self.ctx);
|
||||
}
|
||||
@@ -4,27 +4,28 @@ const c = @import("ffi.zig");
|
||||
c_currplugin: *c.ExtismCurrentPlugin,
|
||||
|
||||
const Self = @This();
|
||||
const MemoryHandle = u64;
|
||||
|
||||
pub fn getCurrentPlugin(ptr: *c.ExtismCurrentPlugin) Self {
|
||||
return .{ .c_currplugin = ptr };
|
||||
}
|
||||
|
||||
pub fn getMemory(self: Self, offset: u64) []const u8 {
|
||||
pub fn getMemory(self: Self, offset: MemoryHandle) []const u8 {
|
||||
const len = c.extism_current_plugin_memory_length(self.c_currplugin, offset);
|
||||
const c_data = c.extism_current_plugin_memory(self.c_currplugin);
|
||||
const data: [*:0]u8 = std.mem.span(c_data);
|
||||
return data[offset .. offset + len];
|
||||
}
|
||||
|
||||
pub fn alloc(self: *Self, n: u64) u64 {
|
||||
pub fn alloc(self: *Self, n: u64) MemoryHandle {
|
||||
return c.extism_current_plugin_memory_alloc(self.c_currplugin, n);
|
||||
}
|
||||
|
||||
pub fn free(self: *Self, offset: u64) void {
|
||||
pub fn free(self: *Self, offset: MemoryHandle) void {
|
||||
c.extism_current_plugin_memory_free(self.c_currplugin, offset);
|
||||
}
|
||||
|
||||
pub fn length(self: *Self, offset: u64) u64 {
|
||||
pub fn length(self: *Self, offset: MemoryHandle) u64 {
|
||||
return c.extism_current_plugin_memory_length(self.c_currplugin, offset);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ const std = @import("std");
|
||||
const testing = std.testing;
|
||||
pub const c = @import("ffi.zig");
|
||||
|
||||
pub const Context = @import("context.zig");
|
||||
pub const Plugin = @import("plugin.zig");
|
||||
pub const CurrentPlugin = @import("current_plugin.zig");
|
||||
pub const CancelHandle = @import("cancel_handle.zig");
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const std = @import("std");
|
||||
const Context = @import("context.zig");
|
||||
const Manifest = @import("manifest.zig").Manifest;
|
||||
const Function = @import("function.zig");
|
||||
const CancelHandle = @import("cancel_handle.zig");
|
||||
@@ -7,18 +6,15 @@ const c = @import("ffi.zig");
|
||||
|
||||
const Self = @This();
|
||||
|
||||
ctx: *Context,
|
||||
owns_context: bool,
|
||||
id: i32,
|
||||
ptr: *c.ExtismPlugin,
|
||||
|
||||
// We have to use this until ziglang/zig#2647 is resolved.
|
||||
error_info: ?[]const u8,
|
||||
|
||||
/// Create a new plugin from a WASM module
|
||||
pub fn init(allocator: std.mem.Allocator, ctx: *Context, data: []const u8, functions: []const Function, wasi: bool) !Self {
|
||||
ctx.mutex.lock();
|
||||
defer ctx.mutex.unlock();
|
||||
var plugin: i32 = -1;
|
||||
pub fn init(allocator: std.mem.Allocator, data: []const u8, functions: []const Function, wasi: bool) !Self {
|
||||
var plugin: ?*c.ExtismPlugin = null;
|
||||
var errmsg: [*c]u8 = null;
|
||||
if (functions.len > 0) {
|
||||
var funcPtrs = try allocator.alloc(?*c.ExtismFunction, functions.len);
|
||||
defer allocator.free(funcPtrs);
|
||||
@@ -27,71 +23,46 @@ pub fn init(allocator: std.mem.Allocator, ctx: *Context, data: []const u8, funct
|
||||
funcPtrs[i] = function.c_func;
|
||||
i += 1;
|
||||
}
|
||||
plugin = c.extism_plugin_new(ctx.ctx, data.ptr, @as(u64, data.len), &funcPtrs[0], functions.len, wasi);
|
||||
plugin = c.extism_plugin_new(data.ptr, @as(u64, data.len), &funcPtrs[0], functions.len, wasi, &errmsg);
|
||||
} else {
|
||||
plugin = c.extism_plugin_new(ctx.ctx, data.ptr, @as(u64, data.len), null, 0, wasi);
|
||||
plugin = c.extism_plugin_new(data.ptr, @as(u64, data.len), null, 0, wasi, &errmsg);
|
||||
}
|
||||
|
||||
if (plugin < 0) {
|
||||
const err_c = c.extism_error(ctx.ctx, @as(i32, -1));
|
||||
const err = std.mem.span(err_c);
|
||||
if (!std.mem.eql(u8, err, "")) {
|
||||
ctx.error_info = err;
|
||||
}
|
||||
ctx.error_info = "Unknown";
|
||||
if (plugin == null) {
|
||||
// TODO: figure out what to do with this error
|
||||
std.debug.print("extism_plugin_new: {s}", .{
|
||||
errmsg,
|
||||
});
|
||||
c.extism_plugin_new_error_free(errmsg);
|
||||
return error.PluginLoadFailed;
|
||||
}
|
||||
return Self{
|
||||
.id = plugin,
|
||||
.ctx = ctx,
|
||||
.ptr = plugin.?,
|
||||
.error_info = null,
|
||||
.owns_context = false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a new plugin from the given manifest
|
||||
pub fn initFromManifest(allocator: std.mem.Allocator, ctx: *Context, manifest: Manifest, functions: []const Function, wasi: bool) !Self {
|
||||
pub fn initFromManifest(allocator: std.mem.Allocator, manifest: Manifest, functions: []const Function, wasi: bool) !Self {
|
||||
const json = try std.json.stringifyAlloc(allocator, manifest, .{ .emit_null_optional_fields = false });
|
||||
defer allocator.free(json);
|
||||
return init(allocator, ctx, json, functions, wasi);
|
||||
}
|
||||
|
||||
/// Create a new plugin from a WASM module in its own context
|
||||
pub fn create(allocator: std.mem.Allocator, data: []const u8, functions: []const Function, wasi: bool) !Self {
|
||||
const ctx = Context.init();
|
||||
var plugin = init(allocator, ctx, data, functions, wasi);
|
||||
plugin.owns_context = true;
|
||||
return plugin;
|
||||
}
|
||||
|
||||
/// Create a new plugin from the given manifest in its own context
|
||||
pub fn createFromManifest(allocator: std.mem.Allocator, manifest: Manifest, functions: []const Function, wasi: bool) !Self {
|
||||
const json = try std.json.stringifyAlloc(allocator, manifest, .{ .emit_null_optional_fields = false });
|
||||
defer allocator.free(json);
|
||||
return create(allocator, json, functions, wasi);
|
||||
return init(allocator, json, functions, wasi);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.ctx.mutex.lock();
|
||||
defer self.ctx.mutex.unlock();
|
||||
c.extism_plugin_free(self.ctx.ctx, self.id);
|
||||
if (self.owns_context) {
|
||||
self.ctx.deinit();
|
||||
}
|
||||
c.extism_plugin_free(self.ptr);
|
||||
}
|
||||
|
||||
pub fn cancelHandle(self: *Self) CancelHandle {
|
||||
const ptr = c.extism_plugin_cancel_handle(self.ctx.ctx, self.id);
|
||||
const ptr = c.extism_plugin_cancel_handle(self.ptr);
|
||||
return .{ .handle = ptr };
|
||||
}
|
||||
|
||||
/// Call a function with the given input
|
||||
pub fn call(self: *Self, function_name: []const u8, input: []const u8) ![]const u8 {
|
||||
self.ctx.mutex.lock();
|
||||
defer self.ctx.mutex.unlock();
|
||||
const res = c.extism_plugin_call(self.ctx.ctx, self.id, function_name.ptr, input.ptr, @as(u64, input.len));
|
||||
const res = c.extism_plugin_call(self.ptr, function_name.ptr, input.ptr, @as(u64, input.len));
|
||||
if (res != 0) {
|
||||
var err_c = c.extism_error(self.ctx.ctx, self.id);
|
||||
var err_c = c.extism_plugin_error(self.ptr);
|
||||
const err = std.mem.span(err_c);
|
||||
|
||||
if (!std.mem.eql(u8, err, "")) {
|
||||
@@ -101,49 +72,23 @@ pub fn call(self: *Self, function_name: []const u8, input: []const u8) ![]const
|
||||
return error.PluginCallFailed;
|
||||
}
|
||||
|
||||
const len = c.extism_plugin_output_length(self.ctx.ctx, self.id);
|
||||
const len = c.extism_plugin_output_length(self.ptr);
|
||||
|
||||
if (len > 0) {
|
||||
const output_data = c.extism_plugin_output_data(self.ctx.ctx, self.id);
|
||||
const output_data = c.extism_plugin_output_data(self.ptr);
|
||||
return output_data[0..len];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/// Update a plugin with the given WASM module
|
||||
pub fn update(self: *Self, 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, data.ptr, @as(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);
|
||||
if (!std.mem.eql(u8, err, "")) {
|
||||
self.error_info = err;
|
||||
}
|
||||
self.error_info = "Unknown";
|
||||
return error.PluginUpdateFailed;
|
||||
}
|
||||
|
||||
/// Update a plugin with the given manifest
|
||||
pub fn updateWithManifest(self: *Self, allocator: std.mem.Allocator, manifest: Manifest, wasi: bool) !void {
|
||||
const json = try std.json.stringifyAlloc(allocator, manifest, .{ .emit_null_optional_fields = false });
|
||||
defer allocator.free(json);
|
||||
return self.update(json, wasi);
|
||||
}
|
||||
/// Set configuration values
|
||||
pub fn setConfig(self: *Self, allocator: std.mem.Allocator, config: std.json.ArrayHashMap([]const u8)) !void {
|
||||
self.ctx.mutex.lock();
|
||||
defer self.ctx.mutex.unlock();
|
||||
const config_json = try std.json.stringifyAlloc(allocator, config, .{ .emit_null_optional_fields = false });
|
||||
defer allocator.free(config_json);
|
||||
_ = c.extism_plugin_config(self.ctx.ctx, self.id, config_json.ptr, @as(u64, config_json.len));
|
||||
_ = c.extism_plugin_config(self.ptr, config_json.ptr, @as(u64, config_json.len));
|
||||
}
|
||||
|
||||
/// Returns true if the plugin has a function matching `function_name`
|
||||
pub fn hasFunction(self: Self, function_name: []const u8) bool {
|
||||
self.ctx.mutex.lock();
|
||||
defer self.ctx.mutex.unlock();
|
||||
const res = c.extism_plugin_function_exists(self.ctx.ctx, self.id, function_name.ptr);
|
||||
const res = c.extism_plugin_function_exists(self.ptr, function_name.ptr);
|
||||
return res;
|
||||
}
|
||||
|
||||
18
zig/test.zig
18
zig/test.zig
@@ -1,7 +1,6 @@
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const sdk = @import("extism");
|
||||
const Context = sdk.Context;
|
||||
const Plugin = sdk.Plugin;
|
||||
const CurrentPlugin = sdk.CurrentPlugin;
|
||||
const Function = sdk.Function;
|
||||
@@ -26,9 +25,6 @@ test "Single threaded tests" {
|
||||
var wasm_start = try std.time.Timer.start();
|
||||
_ = sdk.setLogFile("test.log", .Debug);
|
||||
|
||||
var ctx = Context.init();
|
||||
defer ctx.deinit();
|
||||
|
||||
var f = Function.init(
|
||||
"hello_world",
|
||||
&[_]sdk.c.ExtismValType{sdk.c.I64},
|
||||
@@ -38,7 +34,7 @@ test "Single threaded tests" {
|
||||
);
|
||||
defer f.deinit();
|
||||
|
||||
var plugin = try Plugin.initFromManifest(testing.allocator, &ctx, man, &[_]Function{f}, true);
|
||||
var plugin = try Plugin.initFromManifest(testing.allocator, man, &[_]Function{f}, true);
|
||||
defer plugin.deinit();
|
||||
|
||||
std.debug.print("\nregister loaded plugin: {}\n", .{std.fmt.fmtDuration(wasm_start.read())});
|
||||
@@ -78,8 +74,6 @@ test "Single threaded tests" {
|
||||
test "Multi threaded tests" {
|
||||
const S = struct {
|
||||
fn _test() !void {
|
||||
var ctx = Context.init();
|
||||
defer ctx.deinit();
|
||||
var f = Function.init(
|
||||
"hello_world",
|
||||
&[_]sdk.c.ExtismValType{sdk.c.I64},
|
||||
@@ -88,7 +82,7 @@ test "Multi threaded tests" {
|
||||
@constCast(@as(*const anyopaque, @ptrCast("user data"))),
|
||||
);
|
||||
defer f.deinit();
|
||||
var plugin = try Plugin.initFromManifest(testing.allocator, &ctx, man, &[_]Function{f}, true);
|
||||
var plugin = try Plugin.initFromManifest(testing.allocator, man, &[_]Function{f}, true);
|
||||
defer plugin.deinit();
|
||||
const output = try plugin.call("count_vowels", "this is a test");
|
||||
std.debug.print("{s}\n", .{output});
|
||||
@@ -99,8 +93,6 @@ test "Multi threaded tests" {
|
||||
t1.join();
|
||||
t2.join();
|
||||
_ = sdk.setLogFile("test.log", .Debug);
|
||||
var ctx = Context.init();
|
||||
defer ctx.deinit();
|
||||
|
||||
var f = Function.init(
|
||||
"hello_world",
|
||||
@@ -111,7 +103,7 @@ test "Multi threaded tests" {
|
||||
);
|
||||
defer f.deinit();
|
||||
|
||||
var plugin = try Plugin.initFromManifest(testing.allocator, &ctx, man, &[_]Function{f}, true);
|
||||
var plugin = try Plugin.initFromManifest(testing.allocator, man, &[_]Function{f}, true);
|
||||
defer plugin.deinit();
|
||||
|
||||
const output = try plugin.call("count_vowels", "this is a test");
|
||||
@@ -121,8 +113,6 @@ test "Multi threaded tests" {
|
||||
test "Plugin Cancellation" {
|
||||
const loop_manifest = manifest.WasmFile{ .path = "../wasm/loop.wasm" };
|
||||
const loop_man = .{ .wasm = &[_]manifest.Wasm{.{ .wasm_file = loop_manifest }} };
|
||||
var ctx = Context.init();
|
||||
defer ctx.deinit();
|
||||
_ = sdk.setLogFile("test.log", .Debug);
|
||||
var f = Function.init(
|
||||
"hello_world",
|
||||
@@ -133,7 +123,7 @@ test "Plugin Cancellation" {
|
||||
);
|
||||
defer f.deinit();
|
||||
|
||||
var plugin = try Plugin.initFromManifest(testing.allocator, &ctx, loop_man, &[_]Function{f}, true);
|
||||
var plugin = try Plugin.initFromManifest(testing.allocator, loop_man, &[_]Function{f}, true);
|
||||
defer plugin.deinit();
|
||||
var handle = plugin.cancelHandle();
|
||||
const S = struct {
|
||||
|
||||
Reference in New Issue
Block a user