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:
zach
2023-08-29 13:57:17 -07:00
committed by GitHub
parent aa073b8acc
commit ddcbeec3de
94 changed files with 2575 additions and 5019 deletions

View File

@@ -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 }}

View File

@@ -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

View File

@@ -1 +1 @@
version = 0.24.1
version = 0.26.0

View File

@@ -2,7 +2,6 @@
members = [
"manifest",
"runtime",
"rust",
"libextism",
]
exclude = ["kernel"]

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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> &params,
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) {

View File

@@ -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!"))

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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));

View File

@@ -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)))

View File

@@ -5,7 +5,7 @@ prepare:
mix compile
test: prepare
mix test
mix test -v
clean:
mix clean

View File

@@ -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")
```

View File

@@ -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
]

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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"

View File

@@ -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,

View File

@@ -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
View File

@@ -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}
}

View File

@@ -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: [

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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 =

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View 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)

View File

@@ -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
])

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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");
}
}

View File

@@ -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

View File

@@ -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]

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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;
}
}
}

View File

@@ -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");
});
});

View File

@@ -1 +1 @@
version = 0.24.1
version = 0.26.0

View File

@@ -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

View File

@@ -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))

View File

@@ -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

View File

@@ -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

View File

@@ -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)))

View File

@@ -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 =

View File

@@ -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

View File

@@ -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

View File

@@ -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";
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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)

View File

@@ -2,7 +2,6 @@ from .extism import (
Error,
Plugin,
set_log_file,
Context,
extism_version,
host_fn,
Function,

View File

@@ -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):

View File

@@ -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)

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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()
{

View File

@@ -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

View File

@@ -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);
}
}
}

View 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) }
}
}

View File

@@ -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,
{

View File

@@ -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)
}
}

View File

@@ -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(())
}

View File

@@ -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)
}

View File

@@ -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> {

View File

@@ -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>,

View File

@@ -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),
}
}
}

View File

@@ -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:?}");
}
}
}
}

View File

@@ -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
View 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);
}
}

View File

@@ -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();

View File

@@ -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"] }

View File

@@ -1,2 +0,0 @@
bindings:
bindgen ../runtime/extism.h --allowlist-function extism.* > src/bindings.rs

View File

@@ -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;
}

View File

@@ -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(),
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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) }
}
}

View File

@@ -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){};

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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");

View File

@@ -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;
}

View File

@@ -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 {