Compare commits

..

7 Commits

Author SHA1 Message Date
Benjamin Eckel
14d7eae99c ignore already published 2023-09-21 10:15:12 -05:00
Benjamin Eckel
e89ddd5a2a chore: Bump to 0.5.2 2023-09-21 09:41:27 -05:00
zach
93392e0884 fix(stable): improve the way the kernel calculates how many pages to allocate (#471)
Fixes a bug in the kernel memory allocator where the space used by the
`MemoryRoot` wasn't being considered

---------

Co-authored-by: Benjamin Eckel <bhelx@users.noreply.github.com>
2023-09-20 16:02:42 -07:00
Benjamin Eckel
4ebd0eb372 chore: always run the publish of SDK 2023-09-18 19:17:47 -05:00
zach
8feee0c693 cleanup(stable): use wasm-strip on extism-runtime.wasm (#467) 2023-09-18 19:08:27 -05:00
Benjamin Eckel
773ab32a45 chore: Bump to 0.5.1 2023-09-18 18:55:46 -05:00
Benjamin Eckel
6a041d0c39 fix: Fixes rounding issue in kernel num_pages (#466)
There were some scenarios where the kernel was not allocating enough
pages to get data back into the plugin. So some host functions were
failing when the output was right on this boundary.

Related https://github.com/moonrepo/proto/issues/208

We will come back with tests and cherry pick this over to main. For now
we want to get out a 0.5.1 fix
2023-09-18 18:53:30 -05:00
114 changed files with 5471 additions and 3758 deletions

View File

@@ -12,8 +12,9 @@ on:
name: Rust CI
env:
RUNTIME_CRATE: extism
RUNTIME_CRATE: extism-runtime
LIBEXTISM_CRATE: libextism
RUST_SDK_CRATE: extism
jobs:
lib:
@@ -85,9 +86,20 @@ 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,12 +26,14 @@ 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 10
sleep 5
- name: Release Runtime
- name: Release Rust Host SDK
if: always()
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_API_TOKEN }}
run: |
cargo publish --manifest-path runtime/Cargo.toml --no-verify
#cargo publish --manifest-path runtime/Cargo.toml --no-verify
cargo publish --manifest-path rust/Cargo.toml

View File

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

View File

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

View File

@@ -53,29 +53,30 @@ 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);
char *errmsg = NULL;
ExtismPlugin *plugin = extism_plugin_new(
data, len, (const ExtismFunction **)&f, 1, true, &errmsg);
ExtismPlugin plugin =
extism_plugin_new(ctx, data, len, (const ExtismFunction **)&f, 1, true);
free(data);
if (plugin == NULL) {
puts(errmsg);
extism_plugin_new_error_free(errmsg);
if (plugin < 0) {
puts(extism_error(ctx, -1));
exit(1);
}
assert(extism_plugin_call(plugin, "count_vowels", (uint8_t *)argv[1],
assert(extism_plugin_call(ctx, plugin, "count_vowels", (uint8_t *)argv[1],
strlen(argv[1])) == 0);
ExtismSize out_len = extism_plugin_output_length(plugin);
const uint8_t *output = extism_plugin_output_data(plugin);
ExtismSize out_len = extism_plugin_output_length(ctx, plugin);
const uint8_t *output = extism_plugin_output_data(ctx, plugin);
write(STDOUT_FILENO, output, out_len);
write(STDOUT_FILENO, "\n", 1);
extism_plugin_free(plugin);
extism_plugin_free(ctx, plugin);
extism_function_free(f);
extism_context_free(ctx);
return 0;
}

View File

@@ -221,7 +221,6 @@ public:
typedef ExtismValType ValType;
typedef ExtismValUnion ValUnion;
typedef ExtismVal Val;
typedef uint64_t MemoryHandle;
class CurrentPlugin {
ExtismCurrentPlugin *pointer;
@@ -230,18 +229,16 @@ public:
CurrentPlugin(ExtismCurrentPlugin *p) : pointer(p) {}
uint8_t *memory() { return extism_current_plugin_memory(this->pointer); }
uint8_t *memory(MemoryHandle offs) { return this->memory() + offs; }
ExtismSize memoryLength(MemoryHandle offs) {
ExtismSize memory_length(uint64_t offs) {
return extism_current_plugin_memory_length(this->pointer, offs);
}
MemoryHandle alloc(ExtismSize size) {
uint64_t alloc(ExtismSize size) {
return extism_current_plugin_memory_alloc(this->pointer, size);
}
void free(MemoryHandle handle) {
extism_current_plugin_memory_free(this->pointer, handle);
void free(uint64_t offs) {
extism_current_plugin_memory_free(this->pointer, offs);
}
void returnString(Val &output, const std::string &s) {
@@ -259,7 +256,7 @@ public:
return nullptr;
}
if (length != nullptr) {
*length = this->memoryLength(inp.v.i64);
*length = this->memory_length(inp.v.i64);
}
return this->memory() + inp.v.i64;
}
@@ -321,7 +318,7 @@ public:
this->func = std::shared_ptr<ExtismFunction>(ptr, extism_function_free);
}
void setNamespace(std::string s) {
void set_namespace(std::string s) {
extism_function_set_namespace(this->func.get(), s.c_str());
}
@@ -339,51 +336,111 @@ 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::vector<Function> functions = std::vector<Function>(),
std::shared_ptr<ExtismContext> ctx = std::shared_ptr<ExtismContext>(
extism_context_new(), extism_context_free))
: functions(functions) {
std::vector<const ExtismFunction *> ptrs;
for (auto i : this->functions) {
ptrs.push_back(i.get());
}
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->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);
}
this->context = ctx;
}
Plugin(const std::string &str, bool with_wasi = false,
std::vector<Function> functions = {})
: Plugin((const uint8_t *)str.c_str(), str.size(), with_wasi, functions) {
}
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) {}
Plugin(const std::vector<uint8_t> &data, bool with_wasi = false,
std::vector<Function> functions = {})
: Plugin(data.data(), data.size(), with_wasi, functions) {}
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) {}
CancelHandle cancelHandle() {
return CancelHandle(extism_plugin_cancel_handle(this->plugin));
CancelHandle cancel_handle() {
return CancelHandle(
extism_plugin_cancel_handle(this->context.get(), this->id()));
}
#ifndef EXTISM_NO_JSON
// Create a new plugin from Manifest
Plugin(const Manifest &manifest, bool with_wasi = false,
std::vector<Function> functions = {})
: Plugin(manifest.json().c_str(), with_wasi, functions) {}
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
~Plugin() {
extism_plugin_free(this->plugin);
this->plugin = nullptr;
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);
}
}
void config(const Config &data) {
@@ -400,9 +457,10 @@ public:
#endif
void config(const char *json, size_t length) {
bool b = extism_plugin_config(this->plugin, (const uint8_t *)json, length);
bool b = extism_plugin_config(this->context.get(), this->plugin,
(const uint8_t *)json, length);
if (!b) {
const char *err = extism_plugin_error(this->plugin);
const char *err = extism_error(this->context.get(), this->plugin);
throw Error(err == nullptr ? "Unable to update plugin config" : err);
}
}
@@ -414,10 +472,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->plugin, func.c_str(), input, input_length);
int32_t rc = extism_plugin_call(this->context.get(), this->plugin,
func.c_str(), input, input_length);
if (rc != 0) {
const char *error = extism_plugin_error(this->plugin);
const char *error = extism_error(this->context.get(), this->plugin);
if (error == nullptr) {
throw Error("extism_call failed");
}
@@ -425,8 +483,10 @@ public:
throw Error(error);
}
ExtismSize length = extism_plugin_output_length(this->plugin);
const uint8_t *ptr = extism_plugin_output_data(this->plugin);
ExtismSize length =
extism_plugin_output_length(this->context.get(), this->plugin);
const uint8_t *ptr =
extism_plugin_output_data(this->context.get(), this->plugin);
return Buffer(ptr, length);
}
@@ -443,13 +503,56 @@ public:
}
// Returns true if the specified function exists
bool functionExists(const std::string &func) const {
return extism_plugin_function_exists(this->plugin, func.c_str());
bool function_exists(const std::string &func) const {
return extism_plugin_function_exists(this->context.get(), 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 setLogFile(const char *filename, const char *level) {
inline bool set_log_file(const char *filename, const char *level) {
return extism_log_file(filename, level);
}

View File

@@ -1,7 +1,6 @@
#include "../extism.hpp"
#include <fstream>
#include <thread>
#include <gtest/gtest.h>
@@ -16,10 +15,16 @@ 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");
@@ -32,17 +37,19 @@ TEST(Plugin, BadManifest) {
}
TEST(Plugin, Bytes) {
Context context;
auto wasm = read(code.c_str());
ASSERT_NO_THROW(Plugin plugin(wasm));
Plugin plugin(wasm);
ASSERT_NO_THROW(Plugin plugin = context.plugin(wasm));
Plugin plugin = context.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(wasm);
Plugin plugin = context.plugin(wasm);
Config config;
config["abc"] = "123";
@@ -50,11 +57,12 @@ TEST(Plugin, UpdateConfig) {
}
TEST(Plugin, FunctionExists) {
Context context;
auto wasm = read(code.c_str());
Plugin plugin(wasm);
Plugin plugin = context.plugin(wasm);
ASSERT_FALSE(plugin.functionExists("bad_function"));
ASSERT_TRUE(plugin.functionExists("count_vowels"));
ASSERT_FALSE(plugin.function_exists("bad_function"));
ASSERT_TRUE(plugin.function_exists("count_vowels"));
}
TEST(Plugin, HostFunction) {
@@ -77,38 +85,6 @@ 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

@@ -18,7 +18,7 @@
</ItemGroup>
<ItemGroup>
<!-- <PackageReference Include="Extism.runtime.win-x64" Version="0.7.0" /> -->
<PackageReference Include="Extism.runtime.win-x64" Version="0.4.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -4,7 +4,7 @@ using Extism.Sdk.Native;
using System.Runtime.InteropServices;
using System.Text;
Console.WriteLine($"Version: {Plugin.ExtismVersion()}");
var context = new Context();
var userData = Marshal.StringToHGlobalAnsi("Hello again!");
@@ -29,17 +29,8 @@ void HelloWorld(CurrentPlugin plugin, Span<ExtismVal> inputs, Span<ExtismVal> ou
outputs[0].v.i64 = plugin.WriteString(input);
}
var manifest = new Manifest(new PathWasmSource("./code-functions.wasm"))
{
Config = new Dictionary<string, string>
{
{ "my-key", "some cool value" }
},
};
using var plugin = new Plugin(manifest, new[] { helloWorld }, withWasi: true);
Console.WriteLine("Plugin creatd!!!");
var wasm = File.ReadAllBytes("./code-functions.wasm");
using var plugin = context.CreatePlugin(wasm, new[] { helloWorld }, withWasi: true);
var output = Encoding.UTF8.GetString(
plugin.CallFunction("count_vowels", Encoding.UTF8.GetBytes("Hello World!"))

View File

@@ -0,0 +1,191 @@
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

@@ -5,7 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<LangVersion>11</LangVersion>
<LangVersion>10</LangVersion>
</PropertyGroup>
<PropertyGroup>
@@ -24,6 +24,5 @@
<ItemGroup>
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
<PackageReference Include="System.Text.Json" Version="7.0.3" />
</ItemGroup>
</Project>

View File

@@ -97,10 +97,10 @@ public struct ExtismVal
internal static class LibExtism
{
/// <summary>
/// An Extism Plugin
/// A `Context` is used to store and manage plugins.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct ExtismPlugin { }
internal struct ExtismContext { }
/// <summary>
/// Host function signature
@@ -180,86 +180,123 @@ 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>
/// <param name="nFunctions">Number of host functions.</param>
/// <param name="withWasi">Enables/disables WASI.</param>
/// <param name="errmsg"></param>
/// <returns></returns>
[DllImport("extism")]
unsafe internal static extern ExtismPlugin* extism_plugin_new(byte* wasm, ulong wasmSize, IntPtr* functions, ulong nFunctions, [MarshalAs(UnmanagedType.I1)] bool withWasi, out char** errmsg);
unsafe internal static extern int extism_plugin_new(ExtismContext* context, byte* wasm, int wasmSize, IntPtr* functions, int nFunctions, bool withWasi);
/// <summary>
/// Frees a plugin error message.
/// 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="errorMessage"></param>
/// <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 void extism_plugin_new_error_free(IntPtr errorMessage);
unsafe internal static extern bool extism_plugin_update(ExtismContext* context, int plugin, byte* wasm, long wasmSize, Span<IntPtr> functions, long nFunctions, bool withWasi);
/// <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(ExtismPlugin* plugin);
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);
/// <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(ExtismPlugin* plugin, byte* json, int jsonLength);
unsafe internal static extern bool extism_plugin_config(ExtismContext* context, int 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(ExtismPlugin* plugin, string funcName);
unsafe internal static extern bool extism_plugin_function_exists(ExtismContext* context, int 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(ExtismPlugin* plugin, string funcName, byte* data, int dataLen);
unsafe internal static extern int extism_plugin_call(ExtismContext* context, int plugin, string funcName, byte* data, int dataLen);
/// <summary>
/// Get the error associated with a Plugin
/// Get the error associated with a Context or Plugin, if plugin is -1 then the context error will be returned.
/// </summary>
/// <param name="plugin">A plugin pointer</param>
/// <param name="context"></param>
/// <param name="plugin">A plugin pointer, or -1 for the context error.</param>
/// <returns></returns>
[DllImport("extism")]
unsafe internal static extern IntPtr extism_plugin_error(ExtismPlugin* plugin);
unsafe internal static extern IntPtr extism_error(ExtismContext* context, nint 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(ExtismPlugin* plugin);
unsafe internal static extern long extism_plugin_output_length(ExtismContext* context, int 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(ExtismPlugin* plugin);
unsafe internal static extern IntPtr extism_plugin_output_data(ExtismContext* context, int plugin);
/// <summary>
/// Set log file and level.
@@ -271,10 +308,10 @@ internal static class LibExtism
internal static extern bool extism_log_file(string filename, string logLevel);
/// <summary>
/// Get Extism Runtime version.
/// Get the Extism version string.
/// </summary>
/// <returns></returns>
[DllImport("extism")]
[DllImport("extism", EntryPoint = "extism_version")]
internal static extern IntPtr extism_version();
/// <summary>

View File

@@ -1,221 +0,0 @@
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Text;
using System.Xml.Linq;
namespace Extism.Sdk
{
/// <summary>
/// The manifest is a description of your plugin and some of the runtime constraints to apply to it.
/// You can think of it as a blueprint to build your plugin.
/// </summary>
public class Manifest
{
/// <summary>
/// Create an empty manifest.
/// </summary>
public Manifest()
{
AllowedPaths = new Dictionary<string, string>
{
{ "/usr/plugins/1/data", "/data" }, // src, dest
{ "d:/plugins/1/data", "/data" } // src, dest
};
}
/// <summary>
/// Create a manifest from one or more Wasm sources.
/// </summary>
/// <param name="sources"></param>
public Manifest(params WasmSource[] sources)
{
Sources.AddRange(sources);
}
/// <summary>
/// List of Wasm sources. See <see cref="PathWasmSource"/> and <see cref="ByteArrayWasmSource"/>.
/// </summary>
[JsonPropertyName("wasm")]
public List<WasmSource> Sources { get; set; } = new();
/// <summary>
/// Configures memory for the Wasm runtime.
/// Memory is described in units of pages (64KB) and represent contiguous chunks of addressable memory.
/// </summary>
[JsonPropertyName("memory")]
public MemoryOptions? MemoryOptions { get; set; }
/// <summary>
/// List of host names the plugins can access. Example:
/// <code>
/// AllowedHosts = new List&lt;string&gt; {
/// "www.example.com",
/// "api.*.com",
/// "example.*",
/// }
/// </code>
/// </summary>
[JsonPropertyName("allowed_hosts")]
public List<string> AllowedHosts { get; set; } = new();
/// <summary>
/// List of directories that can be accessed by the plugins. Examples:
/// <code>
/// AllowedPaths = new Dictionary&lt;string, string&gt;
/// {
/// { "/usr/plugins/1/data", "/data" }, // src, dest
/// { "d:/plugins/1/data", "/data" } // src, dest
/// };
/// </code>
/// </summary>
[JsonPropertyName("allowed_paths")]
public Dictionary<string, string> AllowedPaths { get; set; } = new();
/// <summary>
/// Configurations available to the plugins. Examples:
/// <code>
/// Config = new Dictionary&lt;string, string&gt;
/// {
/// { "userId", "55" }, // key, value
/// { "mySecret", "super-secret-key" } // key, value
/// };
/// </code>
/// </summary>
[JsonPropertyName("config")]
public Dictionary<string, string> Config { get; set; } = new();
}
/// <summary>
/// Configures memory for the Wasm runtime.
/// Memory is described in units of pages (64KB) and represent contiguous chunks of addressable memory.
/// </summary>
public class MemoryOptions
{
/// <summary>
/// Max number of pages. Each page is 64KB.
/// </summary>
[JsonPropertyName("max")]
public int MaxPages { get; set; }
}
/// <summary>
/// A named Wasm source.
/// </summary>
public abstract class WasmSource
{
/// <summary>
/// Logical name of the Wasm source
/// </summary>
[JsonPropertyName("name")]
public string? Name { get; set; }
/// <summary>
/// Hash of the WASM source
/// </summary>
[JsonPropertyName("hash")]
public string? Hash { get; set; }
}
/// <summary>
/// Wasm Source represented by a file referenced by a path.
/// </summary>
public class PathWasmSource : WasmSource
{
/// <summary>
/// Constructor
/// </summary>
/// <param name="path">path to wasm plugin.</param>
/// <param name="name"></param>
/// <param name="hash"></param>
public PathWasmSource(string path, string? name = null, string? hash = null)
{
Path = System.IO.Path.GetFullPath(path);
Name = name ?? System.IO.Path.GetFileNameWithoutExtension(path);
Hash = hash;
if (Hash is null)
{
using var file = File.OpenRead(Path);
Hash = Helpers.ComputeSha256Hash(file);
}
}
/// <summary>
/// Path to wasm plugin.
/// </summary>
[JsonPropertyName("path")]
public string Path { get; }
}
/// <summary>
/// Wasm Source represented by raw bytes.
/// </summary>
public class ByteArrayWasmSource : WasmSource
{
/// <summary>
/// Constructor
/// </summary>
/// <param name="data">the byte array representing the Wasm code</param>
/// <param name="name"></param>
/// <param name="hash"></param>
public ByteArrayWasmSource(byte[] data, string? name, string? hash = null)
{
Data = data;
Name = name;
Hash = hash;
if (Hash is null)
{
using var memory = new MemoryStream(data);
Hash = Helpers.ComputeSha256Hash(memory);
}
}
/// <summary>
/// The byte array representing the Wasm code
/// </summary>
[JsonPropertyName("data")]
[JsonConverter(typeof(Base64EncodedStringConverter))]
public byte[] Data { get; }
}
static class Helpers
{
public static string ComputeSha256Hash(Stream stream)
{
using (SHA256 sha256 = SHA256.Create())
{
byte[] hashBytes = sha256.ComputeHash(stream);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}
}
}
class Base64EncodedStringConverter : JsonConverter<string>
{
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
Encoding.UTF8.GetString(reader.GetBytesFromBase64());
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) =>
writer.WriteBase64StringValue(Encoding.UTF8.GetBytes(value));
}
class WasmSourceConverter : JsonConverter<WasmSource>
{
public override WasmSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
public override void Write(Utf8JsonWriter writer, WasmSource value, JsonSerializerOptions options)
{
if (value is PathWasmSource path)
JsonSerializer.Serialize(writer, path, typeof(PathWasmSource), options);
else if (value is ByteArrayWasmSource bytes)
JsonSerializer.Serialize(writer, bytes, typeof(ByteArrayWasmSource), options);
else
throw new ArgumentOutOfRangeException(nameof(value), "Unknown Wasm Source");
}
}
}

View File

@@ -1,90 +1,57 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
namespace Extism.Sdk.Native;
/// <summary>
/// Represents a WASM Extism plugin.
/// </summary>
public unsafe class Plugin : IDisposable
public 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 plugin from a Manifest.
/// </summary>
/// <param name="manifest"></param>
/// <param name="functions"></param>
/// <param name="withWasi"></param>
public Plugin(Manifest manifest, HostFunction[] functions, bool withWasi)
{
_functions = functions;
var options = new JsonSerializerOptions
{
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
options.Converters.Add(new WasmSourceConverter());
var json = JsonSerializer.Serialize(manifest, options);
var bytes = Encoding.UTF8.GetBytes(json);
var functionHandles = functions.Select(f => f.NativeHandle).ToArray();
fixed (byte* wasmPtr = bytes)
fixed (IntPtr* functionsPtr = functionHandles)
{
NativeHandle = Initialize(wasmPtr, bytes.Length, functions, withWasi, functionsPtr);
}
}
/// <summary>
/// Create and load a plugin from a byte array.
/// 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 Plugin(ReadOnlySpan<byte> wasm, HostFunction[] functions, bool withWasi)
{
_functions = functions;
var functionHandles = functions.Select(f => f.NativeHandle).ToArray();
fixed (byte* wasmPtr = wasm)
fixed (IntPtr* functionsPtr = functionHandles)
{
NativeHandle = Initialize(wasmPtr, wasm.Length, functions, withWasi, functionsPtr);
}
public static Plugin Create(ReadOnlySpan<byte> wasm, HostFunction[] functions, bool withWasi) {
var context = new Context();
return context.CreatePlugin(wasm, functions, withWasi);
}
private unsafe LibExtism.ExtismPlugin* Initialize(byte* wasmPtr, int wasmLength, HostFunction[] functions, bool withWasi, IntPtr* functionsPtr)
internal Plugin(Context context, HostFunction[] functions, int index)
{
char** errorMsgPtr;
_context = context;
_functions = functions;
Index = index;
}
var handle = LibExtism.extism_plugin_new(wasmPtr, (ulong)wasmLength, functionsPtr, (ulong)functions.Length, withWasi, out errorMsgPtr);
if (handle == null)
/// <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)
{
var msg = "Unable to create plugin";
if (errorMsgPtr is not null)
{
msg = Marshal.PtrToStringAnsi(new IntPtr(errorMsgPtr));
}
throw new ExtismException(msg);
return LibExtism.extism_plugin_update(_context.NativeHandle, Index, wasmPtr, wasm.Length, functions, 0, withWasi);
}
return handle;
}
/// <summary>
@@ -97,7 +64,7 @@ public unsafe class Plugin : IDisposable
fixed (byte* jsonPtr = json)
{
return LibExtism.extism_plugin_config(NativeHandle, jsonPtr, json.Length);
return LibExtism.extism_plugin_config(_context.NativeHandle, Index, jsonPtr, json.Length);
}
}
@@ -108,7 +75,7 @@ public unsafe class Plugin : IDisposable
{
CheckNotDisposed();
return LibExtism.extism_plugin_function_exists(NativeHandle, name);
return LibExtism.extism_plugin_function_exists(_context.NativeHandle, Index, name);
}
/// <summary>
@@ -126,7 +93,7 @@ public unsafe class Plugin : IDisposable
fixed (byte* dataPtr = data)
{
int response = LibExtism.extism_plugin_call(NativeHandle, functionName, dataPtr, data.Length);
int response = LibExtism.extism_plugin_call(_context.NativeHandle, Index, functionName, dataPtr, data.Length);
if (response == 0)
{
return OutputData();
@@ -154,7 +121,7 @@ public unsafe class Plugin : IDisposable
{
CheckNotDisposed();
return (int)LibExtism.extism_plugin_output_length(NativeHandle);
return (int)LibExtism.extism_plugin_output_length(_context.NativeHandle, Index);
}
/// <summary>
@@ -168,7 +135,7 @@ public unsafe class Plugin : IDisposable
unsafe
{
var ptr = LibExtism.extism_plugin_output_data(NativeHandle).ToPointer();
var ptr = LibExtism.extism_plugin_output_data(_context.NativeHandle, Index).ToPointer();
return new Span<byte>(ptr, length);
}
}
@@ -181,7 +148,7 @@ public unsafe class Plugin : IDisposable
{
CheckNotDisposed();
var result = LibExtism.extism_plugin_error(NativeHandle);
var result = LibExtism.extism_error(_context.NativeHandle, Index);
return Marshal.PtrToStringUTF8(result);
}
@@ -230,7 +197,7 @@ public unsafe class Plugin : IDisposable
}
// Free up unmanaged resources
LibExtism.extism_plugin_free(NativeHandle);
LibExtism.extism_plugin_free(_context.NativeHandle, Index);
}
/// <summary>
@@ -240,15 +207,4 @@ public unsafe class Plugin : IDisposable
{
Dispose(false);
}
/// <summary>
/// Get Extism Runtime version.
/// </summary>
/// <returns></returns>
public static string ExtismVersion()
{
var version = LibExtism.extism_version();
return Marshal.PtrToStringAnsi(version);
}
}

View File

@@ -11,11 +11,24 @@ namespace Extism.Sdk.Tests;
public class BasicTests
{
[Fact]
public void CountHelloWorldVowels()
public void CountHelloWorldVowelsWithoutContext()
{
var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
var wasm = File.ReadAllBytes(Path.Combine(binDirectory, "code.wasm"));
using var plugin = new Plugin(wasm, Array.Empty<HostFunction>(), withWasi: true);
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);
var response = plugin.CallFunction("count_vowels", Encoding.UTF8.GetBytes("Hello World"));
Assert.Equal("{\"count\": 3}", Encoding.UTF8.GetString(response));
@@ -24,6 +37,8 @@ public class BasicTests
[Fact]
public void CountVowelsHostFunctions()
{
using var context = new Context();
var userData = Marshal.StringToHGlobalAnsi("Hello again!");
using var helloWorld = new HostFunction(
@@ -36,7 +51,7 @@ public class BasicTests
var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
var wasm = File.ReadAllBytes(Path.Combine(binDirectory, "code-functions.wasm"));
using var plugin = new Plugin(wasm, new[] { helloWorld }, withWasi: true);
using var plugin = context.CreatePlugin(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

@@ -36,7 +36,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Extism.runtime.win-x64" Version="0.7.0" />
<PackageReference Include="Extism.runtime.win-x64" Version="0.4.0" />
</ItemGroup>
</Project>

View File

@@ -29,7 +29,6 @@
(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 -v
mix test
clean:
mix clean

View File

@@ -23,9 +23,12 @@ 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.Plugin.new(manifest, false)
{:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
# {:ok,
# %Extism.Plugin{
# resource: 0,
@@ -35,20 +38,36 @@ 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 primary modules you should learn is:
The two primary modules you should learn are:
* [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.Plugin.new(manifest, false)
{:ok, plugin} = Extism.Context.new_plugin(ctx, 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
# The actual NIF Resource. PluginIndex and the context
handle: nil
]

View File

@@ -0,0 +1,64 @@
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,12 +7,16 @@ defmodule Extism.Native do
otp_app: :extism,
crate: :extism_nif
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 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 set_log_file(_filename, _level), do: error()
def plugin_cancel_handle(_plugin), do: error()
def plugin_cancel_handle(_ctx, _plugin_id), do: error()
def plugin_cancel(_handle), do: error()
defp error, do: :erlang.nif_error(:nif_not_loaded)

View File

@@ -3,25 +3,28 @@ defmodule Extism.Plugin do
A Plugin represents an instance of your WASM program from the given manifest.
"""
defstruct [
# The actual NIF Resource
plugin: nil,
# The actual NIF Resource. PluginIndex and the context
plugin_id: nil,
ctx: nil
]
def wrap_resource(plugin) do
def wrap_resource(ctx, plugin_id) do
%__MODULE__{
plugin: plugin
ctx: ctx,
plugin_id: plugin_id
}
end
@doc """
Creates a new plugin
"""
def new(manifest, wasi \\ false) do
def new(manifest, wasi \\ false, context \\ nil) do
ctx = context || Extism.Context.new()
{:ok, manifest_payload} = JSON.encode(manifest)
case Extism.Native.plugin_new_with_manifest(manifest_payload, wasi) do
case Extism.Native.plugin_new_with_manifest(ctx.ptr, manifest_payload, wasi) do
{:error, err} -> {:error, err}
res -> {:ok, Extism.Plugin.wrap_resource(res)}
res -> {:ok, Extism.Plugin.wrap_resource(ctx, res)}
end
end
@@ -46,24 +49,49 @@ defmodule Extism.Plugin do
"""
def call(plugin, name, input) do
case Extism.Native.plugin_call(plugin.plugin, name, input) do
case Extism.Native.plugin_call(plugin.ctx.ptr, plugin.plugin_id, 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.plugin)
Extism.Native.plugin_free(plugin.ctx.ptr, plugin.plugin_id)
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.plugin, function_name)
Extism.Native.plugin_has_function(plugin.ctx.ptr, plugin.plugin_id, function_name)
end
end
@@ -71,6 +99,6 @@ defimpl Inspect, for: Extim.Plugin do
import Inspect.Algebra
def inspect(dict, opts) do
concat(["#Extism.Plugin<", to_doc(dict.plugin, opts), ">"])
concat(["#Extism.Plugin<", to_doc(dict.plugin_id, opts), ">"])
end
end

View File

@@ -1,6 +1,6 @@
%{
"earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"},
"ex_doc": {:hex, :ex_doc, "0.30.6", "5f8b54854b240a2b55c9734c4b1d0dd7bdd41f71a095d42a70445c03cf05a281", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bd48f2ddacf4e482c727f9293d9498e0881597eae6ddc3d9562bd7923375109f"},
"ex_doc": {:hex, :ex_doc, "0.30.5", "aa6da96a5c23389d7dc7c381eba862710e108cee9cfdc629b7ec021313900e9e", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "88a1e115dcb91cefeef7e22df4a6ebbe4634fbf98b38adcbc25c9607d6d9d8e6"},
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
"json": {:hex, :json, "1.4.1", "8648f04a9439765ad449bc56a3ff7d8b11dd44ff08ffcdefc4329f7c93843dfa", [:mix], [], "hexpm", "9abf218dbe4ea4fcb875e087d5f904ef263d012ee5ed21d46e9dbca63f053d16"},
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},

View File

@@ -14,5 +14,5 @@ crate-type = ["cdylib"]
[dependencies]
rustler = "0.28.0"
extism = {path = "../../../runtime"} # "0.5.0"
extism = "0.5.0"
log = "0.4"

View File

@@ -1,5 +1,6 @@
use extism::Plugin;
use extism::{Context, Plugin};
use rustler::{Atom, Env, ResourceArc, Term};
use std::mem;
use std::path::Path;
use std::str;
use std::str::FromStr;
@@ -13,11 +14,11 @@ mod atoms {
}
}
struct ExtismPlugin {
plugin: RwLock<Option<Plugin>>,
struct ExtismContext {
ctx: RwLock<Context>,
}
unsafe impl Sync for ExtismPlugin {}
unsafe impl Send for ExtismPlugin {}
unsafe impl Sync for ExtismContext {}
unsafe impl Send for ExtismContext {}
struct ExtismCancelHandle {
handle: RwLock<extism::CancelHandle>,
@@ -27,7 +28,7 @@ unsafe impl Sync for ExtismCancelHandle {}
unsafe impl Send for ExtismCancelHandle {}
fn load(env: Env, _: Term) -> bool {
rustler::resource!(ExtismPlugin, env);
rustler::resource!(ExtismContext, env);
rustler::resource!(ExtismCancelHandle, env);
true
}
@@ -36,73 +37,111 @@ fn to_rustler_error(extism_error: extism::Error) -> rustler::Error {
rustler::Error::Term(Box::new(extism_error.to_string()))
}
fn freed_error() -> rustler::Error {
rustler::Error::Term(Box::new("Plugin has already been freed".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)
}
#[rustler::nif]
fn plugin_new_with_manifest(
ctx: ResourceArc<ExtismContext>,
manifest_payload: String,
wasi: bool,
) -> Result<ResourceArc<ExtismPlugin>, rustler::Error> {
let result = match Plugin::new(manifest_payload, [], wasi) {
) -> Result<i32, rustler::Error> {
let context = ctx.ctx.write().unwrap();
let result = match Plugin::new(&context, manifest_payload, [], wasi) {
Err(e) => Err(to_rustler_error(e)),
Ok(plugin) => Ok(ResourceArc::new(ExtismPlugin {
plugin: RwLock::new(Some(plugin)),
})),
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)
}
};
result
}
#[rustler::nif]
fn plugin_call(
plugin: ResourceArc<ExtismPlugin>,
ctx: ResourceArc<ExtismContext>,
plugin_id: i32,
name: String,
input: String,
) -> Result<String, rustler::Error> {
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())
}
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
}
#[rustler::nif]
fn plugin_cancel_handle(
plugin: ResourceArc<ExtismPlugin>,
ctx: ResourceArc<ExtismContext>,
plugin_id: i32,
) -> Result<ResourceArc<ExtismCancelHandle>, rustler::Error> {
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())
}
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),
}))
}
#[rustler::nif]
fn plugin_cancel(handle: ResourceArc<ExtismCancelHandle>) -> bool {
handle.handle.read().unwrap().cancel().is_ok()
handle.handle.read().unwrap().cancel()
}
#[rustler::nif]
fn plugin_free(plugin: ResourceArc<ExtismPlugin>) -> Result<(), rustler::Error> {
let mut plugin = plugin.plugin.write().unwrap();
if let Some(plugin) = plugin.take() {
drop(plugin);
}
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);
Ok(())
}
@@ -114,34 +153,42 @@ fn set_log_file(filename: String, log_level: String) -> Result<Atom, rustler::Er
"{} not a valid log level",
log_level
)))),
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:?}"
)))),
},
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.",
)))
}
}
}
}
#[rustler::nif]
fn plugin_has_function(
plugin: ResourceArc<ExtismPlugin>,
ctx: ResourceArc<ExtismContext>,
plugin_id: i32,
function_name: String,
) -> Result<bool, rustler::Error> {
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())
}
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)
}
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,21 +2,33 @@ defmodule ExtismTest do
use ExUnit.Case
doctest Extism
defp new_plugin() do
test "context create & reset" do
ctx = Extism.Context.new()
path = Path.join([__DIR__, "../../wasm/code.wasm"])
manifest = %{wasm: [%{path: path}]}
{:ok, plugin} = Extism.Plugin.new(manifest, false)
plugin
{: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}
end
test "counts vowels" do
plugin = new_plugin()
{ctx, 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
plugin = new_plugin()
{ctx, 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")
@@ -25,24 +37,37 @@ 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
plugin = new_plugin()
{ctx, 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
{:error, _msg} = Extism.Plugin.new(%{"wasm" => 123}, false)
ctx = Extism.Context.new()
{:error, _msg} = Extism.Context.new_plugin(ctx, %{"wasm" => 123}, false)
Extism.Context.free(ctx)
end
test "errors on unknown function" do
plugin = new_plugin()
{ctx, plugin} = new_plugin()
{:error, _msg} = Extism.Plugin.call(plugin, "unknown", "this is a test")
Extism.Context.free(ctx)
end
test "set_log_file" do
@@ -50,8 +75,9 @@ defmodule ExtismTest do
end
test "has_function" do
plugin = new_plugin()
{ctx, plugin} = new_plugin()
assert Extism.Plugin.has_function(plugin, "count_vowels")
assert !Extism.Plugin.has_function(plugin, "unknown")
Extism.Context.free(ctx)
end
end

215
extism.go
View File

@@ -52,6 +52,11 @@ 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
@@ -76,11 +81,9 @@ type Function struct {
// Free a function
func (f *Function) Free() {
if f.pointer != nil {
C.extism_function_free(f.pointer)
f.pointer = nil
f.userData.Delete()
}
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
@@ -133,32 +136,45 @@ func GetCurrentPlugin(ptr unsafe.Pointer) CurrentPlugin {
}
}
type MemoryHandle = uint
func (p *CurrentPlugin) Memory(offs MemoryHandle) []byte {
func (p *CurrentPlugin) Memory(offs uint) []byte {
length := C.extism_current_plugin_memory_length(p.pointer, C.uint64_t(offs))
data := unsafe.Pointer(C.extism_current_plugin_memory(p.pointer))
return unsafe.Slice((*byte)(unsafe.Add(data, offs)), C.int(length))
}
// Alloc a new memory block of the given length, returning its offset
func (p *CurrentPlugin) Alloc(n uint) MemoryHandle {
func (p *CurrentPlugin) Alloc(n uint) uint {
return uint(C.extism_current_plugin_memory_alloc(p.pointer, C.uint64_t(n)))
}
// Free the memory block specified by the given offset
func (p *CurrentPlugin) Free(offs MemoryHandle) {
func (p *CurrentPlugin) Free(offs uint) {
C.extism_current_plugin_memory_free(p.pointer, C.uint64_t(offs))
}
// Length returns the number of bytes allocated at the specified offset
func (p *CurrentPlugin) Length(offs MemoryHandle) int {
return int(C.extism_current_plugin_memory_length(p.pointer, C.uint64_t(offs)))
func (p *CurrentPlugin) Length(offs uint) uint {
return uint(C.extism_current_plugin_memory_length(p.pointer, C.uint64_t(offs)))
}
// NewContext creates a new context, it should be freed using the `Free` method
func NewContext() Context {
p := C.extism_context_new()
return Context{
pointer: p,
}
}
// Free a context
func (ctx *Context) Free() {
C.extism_context_free(ctx.pointer)
ctx.pointer = nil
}
// Plugin is used to call WASM functions
type Plugin struct {
ptr *C.ExtismPlugin
ctx *Context
id int32
functions []Function
}
@@ -218,99 +234,175 @@ func ExtismVersion() string {
return C.GoString(C.extism_version())
}
func register(data []byte, functions []Function, wasi bool) (Plugin, error) {
func register(ctx *Context, data []byte, functions []Function, wasi bool) (Plugin, error) {
ptr := makePointer(data)
functionPointers := []*C.ExtismFunction{}
for _, f := range functions {
functionPointers = append(functionPointers, f.pointer)
}
plugin := C.int32_t(-1)
if len(functions) == 0 {
plugin = C.extism_plugin_new(
ctx.pointer,
(*C.uchar)(ptr),
C.uint64_t(len(data)),
nil,
0,
C._Bool(wasi))
} else {
plugin = C.extism_plugin_new(
ctx.pointer,
(*C.uchar)(ptr),
C.uint64_t(len(data)),
&functionPointers[0],
C.uint64_t(len(functions)),
C._Bool(wasi),
)
}
if plugin < 0 {
err := C.extism_error(ctx.pointer, C.int32_t(-1))
msg := "Unknown"
if err != nil {
msg = C.GoString(err)
}
return Plugin{id: -1}, errors.New(
fmt.Sprintf("Unable to load plugin: %s", msg),
)
}
return Plugin{id: int32(plugin), ctx: ctx, functions: functions}, nil
}
func update(ctx *Context, plugin int32, data []byte, functions []Function, wasi bool) error {
ptr := makePointer(data)
functionPointers := []*C.ExtismFunction{}
for _, f := range functions {
functionPointers = append(functionPointers, f.pointer)
}
var plugin *C.ExtismPlugin
errmsg := (*C.char)(nil)
if len(functions) == 0 {
plugin = C.extism_plugin_new(
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),
&errmsg)
))
if b {
return nil
}
} else {
plugin = C.extism_plugin_new(
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),
&errmsg,
)
))
if b {
return nil
}
}
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{ptr: plugin, functions: functions}, nil
}
// NewPlugin creates a plugin
func NewPlugin(module io.Reader, functions []Function, wasi bool) (Plugin, error) {
wasm, err := io.ReadAll(module)
err := C.extism_error(ctx.pointer, C.int32_t(-1))
msg := "Unknown"
if err != nil {
return Plugin{}, err
msg = C.GoString(err)
}
return register(wasm, functions, wasi)
return errors.New(
fmt.Sprintf("Unable to load plugin: %s", msg),
)
}
// NewPlugin creates a plugin from a manifest
// NewPlugin creates a plugin in its own context
func NewPlugin(module io.Reader, functions []Function, wasi bool) (Plugin, error) {
ctx := NewContext()
return ctx.Plugin(module, functions, wasi)
}
// NewPlugin creates a plugin in its own context from a manifest
func NewPluginFromManifest(manifest Manifest, functions []Function, wasi bool) (Plugin, error) {
ctx := NewContext()
return ctx.PluginFromManifest(manifest, functions, wasi)
}
// PluginFromManifest creates a plugin from a `Manifest`
func (ctx *Context) PluginFromManifest(manifest Manifest, functions []Function, wasi bool) (Plugin, error) {
data, err := json.Marshal(manifest)
if err != nil {
return Plugin{}, err
return Plugin{id: -1}, err
}
return register(data, functions, wasi)
return register(ctx, data, functions, wasi)
}
// Plugin creates a plugin from a WASM module
func (ctx *Context) Plugin(module io.Reader, functions []Function, wasi bool) (Plugin, error) {
wasm, err := io.ReadAll(module)
if err != nil {
return Plugin{id: -1}, err
}
return register(ctx, wasm, functions, wasi)
}
// Update a plugin with a new WASM module
func (p *Plugin) Update(module io.Reader, functions []Function, wasi bool) error {
wasm, err := io.ReadAll(module)
if err != nil {
return err
}
p.functions = functions
return update(p.ctx, p.id, wasm, functions, wasi)
}
// Update a plugin with a new Manifest
func (p *Plugin) UpdateManifest(manifest Manifest, functions []Function, wasi bool) error {
data, err := json.Marshal(manifest)
if err != nil {
return err
}
p.functions = functions
return update(p.ctx, p.id, data, functions, wasi)
}
// Set configuration values
func (plugin Plugin) SetConfig(data map[string][]byte) error {
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.ptr, (*C.uchar)(ptr), C.uint64_t(len(s)))
C.extism_plugin_config(plugin.ctx.pointer, C.int(plugin.id), (*C.uchar)(ptr), C.uint64_t(len(s)))
return nil
}
// FunctionExists returns true when the named function is present in the plugin
func (plugin Plugin) FunctionExists(functionName string) bool {
if plugin.ptr == nil {
return false
}
name := C.CString(functionName)
b := C.extism_plugin_function_exists(plugin.ptr, name)
b := C.extism_plugin_function_exists(plugin.ctx.pointer, C.int(plugin.id), name)
C.free(unsafe.Pointer(name))
return bool(b)
}
// Call a function by name with the given input, returning the output
func (plugin Plugin) Call(functionName string, input []byte) ([]byte, error) {
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.ptr,
plugin.ctx.pointer,
C.int32_t(plugin.id),
name,
(*C.uchar)(ptr),
C.uint64_t(len(input)),
@@ -318,7 +410,7 @@ func (plugin Plugin) Call(functionName string, input []byte) ([]byte, error) {
C.free(unsafe.Pointer(name))
if rc != 0 {
err := C.extism_plugin_error(plugin.ptr)
err := C.extism_error(plugin.ctx.pointer, C.int32_t(plugin.id))
msg := "<unset by plugin>"
if err != nil {
msg = C.GoString(err)
@@ -329,10 +421,10 @@ func (plugin Plugin) Call(functionName string, input []byte) ([]byte, error) {
)
}
length := C.extism_plugin_output_length(plugin.ptr)
length := C.extism_plugin_output_length(plugin.ctx.pointer, C.int32_t(plugin.id))
if length > 0 {
x := C.extism_plugin_output_data(plugin.ptr)
x := C.extism_plugin_output_data(plugin.ctx.pointer, C.int32_t(plugin.id))
return unsafe.Slice((*byte)(x), C.int(length)), nil
}
@@ -341,11 +433,16 @@ func (plugin Plugin) Call(functionName string, input []byte) ([]byte, error) {
// Free a plugin
func (plugin *Plugin) Free() {
if plugin.ptr == nil {
if plugin.ctx.pointer == nil {
return
}
C.extism_plugin_free(plugin.ptr)
plugin.ptr = nil
C.extism_plugin_free(plugin.ctx.pointer, C.int32_t(plugin.id))
plugin.id = -1
}
// Reset removes all registered plugins in a Context
func (ctx Context) Reset() {
C.extism_context_reset(ctx.pointer)
}
// ValGetI64 returns an I64 from an ExtismVal, it accepts a pointer to a C.ExtismVal
@@ -417,7 +514,7 @@ type CancelHandle struct {
}
func (p *Plugin) CancelHandle() CancelHandle {
pointer := C.extism_plugin_cancel_handle(p.ptr)
pointer := C.extism_plugin_cancel_handle(p.ctx.pointer, C.int(p.id))
return CancelHandle{pointer}
}

View File

@@ -19,7 +19,6 @@ 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,8 +35,16 @@ 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) {
plugin, err := NewPluginFromManifest(manifest(false), []Function{}, false)
ctx := NewContext()
defer ctx.Free()
plugin, err := ctx.PluginFromManifest(manifest(false), []Function{}, false)
if err != nil {
t.Error(err)
}
@@ -53,7 +61,10 @@ func TestCallPlugin(t *testing.T) {
}
func TestFreePlugin(t *testing.T) {
plugin, err := NewPluginFromManifest(manifest(false), []Function{}, false)
ctx := NewContext()
defer ctx.Free()
plugin, err := ctx.PluginFromManifest(manifest(false), []Function{}, false)
if err != nil {
t.Error(err)
}
@@ -69,8 +80,52 @@ 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) {
plugin, err := NewPluginFromManifest(manifest(false), []Function{}, false)
ctx := NewContext()
defer ctx.Free()
plugin, err := ctx.PluginFromManifest(manifest(false), []Function{}, false)
if err != nil {
t.Error(err)
}
@@ -84,7 +139,10 @@ func TestFunctionExists(t *testing.T) {
}
func TestErrorsOnUnknownFunction(t *testing.T) {
plugin, err := NewPluginFromManifest(manifest(false), []Function{}, false)
ctx := NewContext()
defer ctx.Free()
plugin, err := ctx.PluginFromManifest(manifest(false), []Function{}, false)
if err != nil {
t.Error(err)
}
@@ -104,7 +162,10 @@ func TestCancel(t *testing.T) {
},
}
plugin, err := NewPluginFromManifest(manifest, []Function{}, false)
ctx := NewContext()
defer ctx.Free()
plugin, err := ctx.PluginFromManifest(manifest, []Function{}, false)
if err != nil {
t.Error(err)
}

View File

@@ -1,9 +1,13 @@
module Main where
import Extism
import Extism.HostFunction
import Extism.CurrentPlugin
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
@@ -11,11 +15,10 @@ hello plugin params msg = do
return [toI64 offs]
main = do
setLogFile "stdout" LogError
setLogFile "stdout" Error
let m = manifest [wasmFile "../wasm/code-functions.wasm"]
f <- hostFunction "hello_world" [I64] [I64] hello "Hello, again"
plugin <- unwrap <$> pluginFromManifest m [f] True
id <- pluginID plugin
print id
plugin <- unwrap <$> createPluginFromManifest m [f] True
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.HostFunction
exposed-modules: Extism Extism.CurrentPlugin
reexported-modules: Extism.Manifest
hs-source-dirs: src
other-modules: Extism.Bindings
@@ -22,8 +22,7 @@ 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,
uuid >= 1.3 && < 2
extism-manifest >= 0.0.0 && < 0.4.0
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 deriving Eq
data Nullable a = Null | NotNull a
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 deriving (Eq, Show)
newtype Base64 = Base64 B.ByteString
instance JSON Base64 where
showJSON (Base64 bs) = showJSON (BS.unpack $ B64.encode bs)

View File

@@ -9,7 +9,6 @@ newtype Memory = Memory
{
memoryMaxPages :: Nullable Int
}
deriving Eq
instance JSON Memory where
showJSON (Memory max) =
@@ -27,7 +26,6 @@ data HTTPRequest = HTTPRequest
, headers :: Nullable [(String, String)]
, method :: Nullable String
}
deriving Eq
makeKV x =
object [(k, showJSON v) | (k, v) <- x]
@@ -57,7 +55,6 @@ data WasmFile = WasmFile
, fileName :: Nullable String
, fileHash :: Nullable String
}
deriving Eq
instance JSON WasmFile where
showJSON (WasmFile path name hash) =
@@ -83,7 +80,9 @@ data WasmData = WasmData
, dataName :: Nullable String
, dataHash :: Nullable String
}
deriving Eq
instance JSON WasmData where
showJSON (WasmData bytes name hash) =
@@ -111,7 +110,6 @@ data WasmURL = WasmURL
, urlName :: Nullable String
, urlHash :: Nullable String
}
deriving Eq
instance JSON WasmURL where
@@ -129,7 +127,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 deriving Eq
data Wasm = File WasmFile | Data WasmData | URL WasmURL
instance JSON Wasm where
showJSON x =

View File

@@ -1,69 +1,60 @@
module Extism (
module Extism,
module Extism.Manifest,
Function(..),
Plugin(..),
CancelHandle(..),
LogLevel(..),
Error(..),
Result(..),
toByteString,
fromByteString,
extismVersion,
plugin,
pluginFromManifest,
isValid,
setConfig,
setLogFile,
functionExists,
call,
cancelHandle,
cancel,
pluginID,
unwrap
ValType(..),
Val(..)
) 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 qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import Foreign.Marshal.Utils (copyBytes, moveBytes)
import Data.ByteString as B
import Data.ByteString.Internal (c2w, w2c)
import Data.ByteString.Unsafe (unsafeUseAsCString)
import qualified Text.JSON (encode, toJSObject, showJSON)
import Data.Bifunctor (second)
import Text.JSON (encode, toJSObject, showJSON)
import Extism.Manifest (Manifest, toString)
import Extism.Bindings
import qualified Data.UUID (UUID, fromByteString)
-- | Host function, see 'Extism.HostFunction.hostFunction'
data Function = Function (ForeignPtr ExtismFunction) (StablePtr ()) deriving Eq
-- | Context for managing plugins
newtype Context = Context (ForeignPtr ExtismContext)
-- | Host function
data Function = Function (ForeignPtr ExtismFunction) (StablePtr ())
-- | Plugins can be used to call WASM function
newtype Plugin = Plugin (ForeignPtr ExtismPlugin) deriving Eq
data Plugin = Plugin Context Int32 [Function]
-- | 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 = LogError | LogWarn | LogInfo | LogDebug | LogTrace deriving (Show, Eq)
data LogLevel = Error | Warn | Info | Debug | Trace deriving (Show)
-- | Extism error
newtype Error = ExtismError String deriving (Show, Eq)
newtype Error = ExtismError String deriving Show
-- | Result type
type Result a = Either Error a
-- | Helper function to convert a 'String' to a 'ByteString'
toByteString :: String -> B.ByteString
toByteString x = B.pack (map c2w x)
toByteString :: String -> ByteString
toByteString x = B.pack (Prelude.map c2w x)
-- | Helper function to convert a 'ByteString' to a 'String'
fromByteString :: B.ByteString -> String
fromByteString bs = map w2c $ B.unpack bs
fromByteString :: ByteString -> String
fromByteString bs = Prelude.map w2c $ B.unpack bs
-- | Get the Extism version string
extismVersion :: () -> IO String
@@ -71,55 +62,119 @@ 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 :: B.ByteString -> [Function] -> Bool -> IO (Result Plugin)
plugin wasm functions useWasi =
let nfunctions = fromIntegral (length functions) in
plugin :: Context -> B.ByteString -> [Function] -> Bool -> IO (Result Plugin)
plugin c 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
let Context ctx = c in
do
funcs <- Prelude.mapM (\(Function ptr _) -> withForeignPtr ptr (\x -> do return x)) functions
withForeignPtr ctx (\ctx -> do
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
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
-- | Create a 'Plugin' from a 'Manifest'
pluginFromManifest :: Context -> Manifest -> [Function] -> Bool -> IO (Result Plugin)
pluginFromManifest ctx 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 <- mapM (\(Function ptr _) -> withForeignPtr ptr (\x -> do return x)) functions
alloca (\e-> do
let errmsg = (e :: Ptr CString)
p <- unsafeUseAsCString wasm (\s ->
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_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
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 do
ptr <- Foreign.Concurrent.newForeignPtr p (extism_plugin_free p)
return $ Right (Plugin ptr))
else
return (Right (Plugin (Context ctx) id functions)))
-- | Create a 'Plugin' from a 'Manifest'
pluginFromManifest :: Manifest -> [Function] -> Bool -> IO (Result Plugin)
pluginFromManifest manifest functions useWasi =
-- | 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
plugin wasm functions useWasi
update plugin wasm functions useWasi
-- | Check if a 'Plugin' is valid
isValid :: Plugin -> IO Bool
isValid (Plugin p) = withForeignPtr p (\x -> return (x /= nullPtr))
isValid :: Plugin -> Bool
isValid (Plugin _ p _) = p >= 0
-- | Set configuration values for a plugin
setConfig :: Plugin -> [(String, Maybe String)] -> IO Bool
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))
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))
levelStr LogError = "error"
levelStr LogDebug = "debug"
levelStr LogWarn = "warn"
levelStr LogTrace = "trace"
levelStr LogInfo = "info"
levelStr Error = "error"
levelStr Debug = "debug"
levelStr Warn = "warn"
levelStr Trace = "trace"
levelStr Info = "info"
-- | Set the log file and level, this is a global configuration
setLogFile :: String -> LogLevel -> IO Bool
@@ -132,46 +187,43 @@ setLogFile filename level =
-- | Check if a function exists in the given plugin
functionExists :: Plugin -> String -> IO Bool
functionExists (Plugin plugin) name = do
withForeignPtr plugin (\plugin -> do
b <- withCString name (extism_plugin_function_exists plugin)
functionExists (Plugin (Context ctx) plugin _) name = do
withForeignPtr ctx (\ctx -> do
b <- withCString name (extism_plugin_function_exists ctx 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 plugin) name input =
call (Plugin (Context ctx) plugin _) name input =
let length = fromIntegral (B.length input) in
do
withForeignPtr plugin (\plugin -> do
withForeignPtr ctx (\ctx -> do
rc <- withCString name (\name ->
unsafeUseAsCString input (\input ->
extism_plugin_call plugin name (castPtr input) length))
err <- extism_error plugin
extism_plugin_call ctx plugin name (castPtr input) length))
err <- extism_error ctx plugin
if err /= nullPtr
then do e <- peekCString err
return $ Left (ExtismError e)
else if rc == 0
then do
length <- extism_plugin_output_length plugin
ptr <- extism_plugin_output_data plugin
buf <- B.packCStringLen (castPtr ptr, fromIntegral length)
length <- extism_plugin_output_length ctx plugin
ptr <- extism_plugin_output_data ctx plugin
buf <- packCStringLen (castPtr ptr, fromIntegral length)
return $ Right buf
else return $ Left (ExtismError "Call failed"))
-- | 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)
-- | 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)
-- | Create a new 'CancelHandle' that can be used to cancel a running plugin
-- | from another thread.
cancelHandle :: Plugin -> IO CancelHandle
cancelHandle (Plugin plugin) = do
handle <- withForeignPtr plugin extism_plugin_cancel_handle
cancelHandle (Plugin (Context ctx) plugin _) = do
handle <- withForeignPtr ctx (`extism_plugin_cancel_handle` plugin)
return (CancelHandle handle)
-- | Cancel a running plugin using a 'CancelHandle'
@@ -179,16 +231,58 @@ 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)
unwrap (Right x) = x
unwrap (Left (ExtismError msg)) = do
error msg
-- | 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

View File

@@ -13,7 +13,7 @@ import Foreign.StablePtr
type FreeCallback = Ptr () -> IO ()
newtype ExtismPlugin = ExtismPlugin () deriving Show
newtype ExtismContext = ExtismContext () deriving Show
newtype ExtismFunction = ExtismFunction () deriving Show
newtype ExtismCancelHandle = ExtismCancelHandle () deriving Show
newtype ExtismCurrentPlugin = ExtismCurrentPlugin () deriving Show
@@ -79,19 +79,21 @@ instance Storable ValType where
poke ptr x = do
pokeByteOff ptr 0 (intOfValType x)
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_context_new" extism_context_new :: IO (Ptr ExtismContext)
foreign import ccall safe "extism.h &extism_context_free" extism_context_free :: FunPtr (Ptr ExtismContext -> IO ())
foreign import ccall safe "extism.h extism_plugin_new" extism_plugin_new :: Ptr ExtismContext -> Ptr Word8 -> Word64 -> Ptr (Ptr ExtismFunction) -> Word64 -> CBool -> IO Int32
foreign import ccall safe "extism.h extism_plugin_update" extism_plugin_update :: Ptr ExtismContext -> Int32 -> Ptr Word8 -> Word64 -> Ptr (Ptr ExtismFunction) -> Word64 -> CBool -> IO CBool
foreign import ccall safe "extism.h extism_plugin_call" extism_plugin_call :: Ptr ExtismContext -> Int32 -> CString -> Ptr Word8 -> Word64 -> IO Int32
foreign import ccall safe "extism.h extism_plugin_function_exists" extism_plugin_function_exists :: Ptr ExtismContext -> Int32 -> CString -> IO CBool
foreign import ccall safe "extism.h extism_error" extism_error :: Ptr ExtismContext -> Int32 -> IO CString
foreign import ccall safe "extism.h extism_plugin_output_length" extism_plugin_output_length :: Ptr ExtismContext -> Int32 -> IO Word64
foreign import ccall safe "extism.h extism_plugin_output_data" extism_plugin_output_data :: Ptr ExtismContext -> Int32 -> IO (Ptr Word8)
foreign import ccall safe "extism.h extism_log_file" extism_log_file :: CString -> CString -> IO CBool
foreign import ccall safe "extism.h extism_plugin_config" extism_plugin_config :: Ptr 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_plugin_config" extism_plugin_config :: Ptr ExtismContext -> Int32 -> Ptr Word8 -> Int64 -> IO CBool
foreign import ccall safe "extism.h extism_plugin_free" extism_plugin_free :: Ptr ExtismContext -> Int32 -> IO ()
foreign import ccall safe "extism.h extism_context_reset" extism_context_reset :: Ptr ExtismContext -> IO ()
foreign import ccall safe "extism.h extism_version" extism_version :: IO CString
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_handle" extism_plugin_cancel_handle :: Ptr ExtismContext -> Int32 -> 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

@@ -0,0 +1,48 @@
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

@@ -1,157 +0,0 @@
{-# 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,33 +1,35 @@
import Test.HUnit
import Extism
import Extism.Manifest
import Extism.HostFunction
import Extism.CurrentPlugin
assertUnwrap (Right x) = return x
assertUnwrap (Left (ExtismError msg)) =
unwrap (Right x) = return x
unwrap (Left (ExtismError msg)) =
assertFailure msg
defaultManifest = manifest [wasmFile "../../wasm/code.wasm"]
hostFunctionManifest = manifest [wasmFile "../../wasm/code-functions.wasm"]
initPlugin :: IO Plugin
initPlugin =
Extism.pluginFromManifest defaultManifest [] False >>= assertUnwrap
initPlugin :: Maybe Context -> IO Plugin
initPlugin Nothing =
Extism.createPluginFromManifest defaultManifest [] False >>= unwrap
initPlugin (Just ctx) =
Extism.pluginFromManifest ctx defaultManifest [] False >>= unwrap
pluginFunctionExists = do
p <- initPlugin
p <- initPlugin Nothing
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") >>= assertUnwrap
res <- call p "count_vowels" (toByteString "this is a test") >>= unwrap
assertEqual "count vowels output" "{\"count\": 4}" (fromByteString res)
pluginCall = do
p <- initPlugin
p <- initPlugin Nothing
checkCallResult p
@@ -37,25 +39,33 @@ hello plugin params () = do
return [toI64 offs]
pluginCallHostFunction = do
p <- Extism.pluginFromManifest hostFunctionManifest [] False >>= assertUnwrap
res <- call p "count_vowels" (toByteString "this is a test") >>= assertUnwrap
p <- Extism.createPluginFromManifest hostFunctionManifest [] False >>= unwrap
res <- call p "count_vowels" (toByteString "this is a test") >>= unwrap
assertEqual "count vowels output" "{\"count\": 999}" (fromByteString res)
pluginMultiple = do
p <- initPlugin
withContext(\ctx -> do
p <- initPlugin (Just ctx)
checkCallResult p
q <- initPlugin
r <- initPlugin
q <- initPlugin (Just ctx)
r <- initPlugin (Just ctx)
checkCallResult q
checkCallResult r
checkCallResult r)
pluginUpdate = do
withContext (\ctx -> do
p <- initPlugin (Just ctx)
updateManifest p defaultManifest [] True >>= unwrap
checkCallResult p)
pluginConfig = do
p <- initPlugin
b <- setConfig p [("a", Just "1"), ("b", Just "2"), ("c", Just "3"), ("d", Nothing)]
assertBool "set config" b
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)
testSetLogFile = do
b <- setLogFile "stderr" Extism.LogError
b <- setLogFile "stderr" Extism.Error
assertBool "set log file" b
t name f = TestLabel name (TestCase f)
@@ -67,6 +77,7 @@ 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

@@ -0,0 +1,90 @@
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,8 +41,10 @@ public class Extism {
* @throws ExtismException if the call fails
*/
public static String invokeFunction(Manifest manifest, String function, String input) throws ExtismException {
try (var plugin = new Plugin(manifest, false, null)) {
return plugin.call(function, input);
try (var ctx = new Context()) {
try (var plugin = ctx.newPlugin(manifest, false, null)) {
return plugin.call(function, input);
}
}
}

View File

@@ -86,6 +86,23 @@ 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
*
@@ -96,30 +113,26 @@ public interface LibExtism extends Library {
boolean extism_log_file(String path, String logLevel);
/**
* Returns the error associated with a @{@link Plugin}
* Returns the error associated with a @{@link Context} or @{@link Plugin}, if {@code pluginId} is {@literal -1} then the context error will be returned
*
* @param pluginPointer
* @param contextPointer
* @param pluginId
* @return
*/
String extism_plugin_error(Pointer pluginPointer);
String extism_error(Pointer contextPointer, int pluginId);
/**
* 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
*/
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);
int extism_plugin_new(Pointer contextPointer, byte[] wasm, long wasmSize, Pointer[] functions, int nFunctions, boolean withWASI);
/**
* Returns the Extism version string
@@ -130,40 +143,68 @@ public interface LibExtism extends Library {
/**
* Calls a function from the @{@link Plugin} at the given {@code pluginIndex}.
*
* @param pluginPointer
* @param contextPointer
* @param pluginIndex
* @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 pluginPointer, String function_name, byte[] data, int dataLength);
int extism_plugin_call(Pointer contextPointer, int pluginIndex, String function_name, byte[] data, int dataLength);
/**
* Returns
* Returns the length of a plugin's output data.
*
* @param contextPointer
* @param pluginIndex
* @return the length of the output data in bytes.
*/
int extism_plugin_output_length(Pointer pluginPointer);
int extism_plugin_output_length(Pointer contextPointer, int pluginIndex);
/**
* Returns the plugin's output data.
*
* @param contextPointer
* @param pluginIndex
* @return
*/
Pointer extism_plugin_output_data(Pointer pluginPointer);
Pointer extism_plugin_output_data(Pointer contextPointer, int pluginIndex);
/**
* Remove a plugin from the
* 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
*/
void extism_plugin_free(Pointer pluginPointer);
boolean extism_plugin_update(Pointer contextPointer, int pluginIndex, byte[] wasm, int length, Pointer[] functions, int nFunctions, boolean withWASI);
/**
* Update plugin config values, this
* 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
* @param json
* @param jsonLength
* @return {@literal true} if update was successful
*/
boolean extism_plugin_config(Pointer pluginPointer, byte[] json, int jsonLength);
Pointer extism_plugin_cancel_handle(Pointer pluginPointer);
boolean extism_plugin_cancel(Pointer cancelHandle);
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);
void extism_function_set_namespace(Pointer p, String name);
int strlen(Pointer s);
}

View File

@@ -13,17 +13,27 @@ import java.util.Objects;
public class Plugin implements AutoCloseable {
/**
* Holds the Extism plugin pointer
* Holds the Extism {@link Context} that the plugin belongs to.
*/
private final Pointer pluginPointer;
private final Context context;
/**
* 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(byte[] manifestBytes, boolean withWASI, HostFunction[] functions) {
public Plugin(Context context, byte[] manifestBytes, boolean withWASI, HostFunction[] functions) {
Objects.requireNonNull(context, "context");
Objects.requireNonNull(manifestBytes, "manifestBytes");
Pointer[] ptrArr = new Pointer[functions == null ? 0 : functions.length];
@@ -33,33 +43,49 @@ public class Plugin implements AutoCloseable {
ptrArr[i] = functions[i].pointer;
}
Pointer[] errormsg = new Pointer[1];
Pointer p = LibExtism.INSTANCE.extism_plugin_new(manifestBytes, manifestBytes.length,
Pointer contextPointer = context.getPointer();
int index = LibExtism.INSTANCE.extism_plugin_new(contextPointer, manifestBytes, manifestBytes.length,
ptrArr,
functions == null ? 0 : functions.length,
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));
withWASI);
if (index == -1) {
String error = context.error(this);
throw new ExtismException(error);
}
this.pluginPointer = p;
this.index= index;
this.context = context;
}
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(serialize(manifest), withWASI, functions);
this(new Context(), 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.
*
@@ -72,19 +98,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(this.pluginPointer, functionName, inputData, inputDataLength);
int exitCode = LibExtism.INSTANCE.extism_plugin_call(contextPointer, index, functionName, inputData, inputDataLength);
if (exitCode == -1) {
String error = this.error();
String error = context.error(this);
throw new ExtismException(error);
}
int length = LibExtism.INSTANCE.extism_plugin_output_length(this.pluginPointer);
Pointer output = LibExtism.INSTANCE.extism_plugin_output_data(this.pluginPointer);
int length = LibExtism.INSTANCE.extism_plugin_output_length(contextPointer, index);
Pointer output = LibExtism.INSTANCE.extism_plugin_output_data(contextPointer, index);
return output.getByteArray(0, length);
}
/**
* Invoke a function with the given name and input.
*
@@ -100,21 +126,46 @@ public class Plugin implements AutoCloseable {
var outputBytes = call(functionName, inputBytes);
return new String(outputBytes, StandardCharsets.UTF_8);
}
/**
* Get the error associated with a plugin
* Update the plugin code given manifest changes
*
* @return the error message
* @param manifest The manifest for the plugin
* @param withWASI Set to true to enable WASI
* @return {@literal true} if update was successful
*/
protected String error() {
return LibExtism.INSTANCE.extism_plugin_error(this.pluginPointer);
public boolean update(Manifest manifest, boolean withWASI, HostFunction[] functions) {
return update(serialize(manifest), withWASI, functions);
}
/**
* Frees a plugin from memory
* 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()}
*/
public void free() {
LibExtism.INSTANCE.extism_plugin_free(this.pluginPointer);
LibExtism.INSTANCE.extism_plugin_free(context.getPointer(), index);
}
/**
@@ -136,7 +187,7 @@ public class Plugin implements AutoCloseable {
*/
public boolean updateConfig(byte[] jsonBytes) {
Objects.requireNonNull(jsonBytes, "jsonBytes");
return LibExtism.INSTANCE.extism_plugin_config(this.pluginPointer, jsonBytes, jsonBytes.length);
return LibExtism.INSTANCE.extism_plugin_config(context.getPointer(), index, jsonBytes, jsonBytes.length);
}
/**
@@ -151,7 +202,10 @@ public class Plugin implements AutoCloseable {
* Return a new `CancelHandle`, which can be used to cancel a running Plugin
*/
public CancelHandle cancelHandle() {
Pointer handle = LibExtism.INSTANCE.extism_plugin_cancel_handle(this.pluginPointer);
if (this.context.getPointer() == null) {
throw new ExtismException("No Context set");
}
Pointer handle = LibExtism.INSTANCE.extism_plugin_cancel_handle(this.context.getPointer(), this.index);
return new CancelHandle(handle);
}
}

View File

@@ -0,0 +1,23 @@
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,6 +57,17 @@ 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() {
@@ -66,24 +77,28 @@ public class PluginTests {
var functionName = "count_vowels";
var input = "Hello World";
try (var plugin = new Plugin(manifest, false, null)) {
var output = plugin.call(functionName, input);
assertThat(output).isEqualTo("{\"count\": 3}");
try (var ctx = new Context()) {
try (var plugin = ctx.newPlugin(manifest, false, null)) {
var output = plugin.call(functionName, input);
assertThat(output).isEqualTo("{\"count\": 3}");
}
}
}
@Test
public void shouldAllowInvokeFunctionFromFileWasmSourceMultipleTimes() {
public void shouldAllowInvokeFunctionFromFileWasmSourceMultipleTimesByReusingContext() {
var manifest = new Manifest(CODE.pathWasmSource());
var functionName = "count_vowels";
var input = "Hello World";
try (var plugin = new Plugin(manifest, false, null)) {
var output = plugin.call(functionName, input);
assertThat(output).isEqualTo("{\"count\": 3}");
try (var ctx = new Context()) {
try (var plugin = ctx.newPlugin(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}");
}
}
}
@@ -125,12 +140,14 @@ public class PluginTests {
HostFunction[] functions = {helloWorld};
Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource()));
String functionName = "count_vowels";
try (var ctx = new Context()) {
Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource()));
String functionName = "count_vowels";
try (var plugin = new Plugin(manifest, true, functions)) {
var output = plugin.call(functionName, "this is a test");
assertThat(output).isEqualTo("test");
try (var plugin = ctx.newPlugin(manifest, true, functions)) {
var output = plugin.call(functionName, "this is a test");
assertThat(output).isEqualTo("test");
}
}
}
@@ -172,26 +189,30 @@ public class PluginTests {
HostFunction[] functions = {f,g};
Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource()));
String functionName = "count_vowels";
try (var ctx = new Context()) {
Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource()));
String functionName = "count_vowels";
try (var plugin = new Plugin(manifest, true, functions)) {
var output = plugin.call(functionName, "this is a test");
assertThat(output).isEqualTo("test");
try (var plugin = ctx.newPlugin(manifest, true, functions)) {
var output = plugin.call(functionName, "this is a test");
assertThat(output).isEqualTo("test");
}
}
}
@Test
public void shouldFailToInvokeUnknownHostFunction() {
Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource()));
String functionName = "count_vowels";
try (var ctx = new Context()) {
Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource()));
String functionName = "count_vowels";
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");
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");
}
}
}

View File

@@ -119,9 +119,13 @@ pub struct MemoryBlock {
/// Returns the number of pages needed for the given number of bytes
pub fn num_pages(nbytes: u64) -> usize {
let nbytes = nbytes as f64;
let page = PAGE_SIZE as f64;
((nbytes / page) + 0.5) as usize
let npages = nbytes / PAGE_SIZE as u64;
let remainder = nbytes % PAGE_SIZE as u64;
if remainder != 0 {
(npages + 1) as usize
} else {
npages as usize
}
}
// Get the `MemoryRoot` at the correct offset in memory
@@ -242,13 +246,13 @@ impl MemoryRoot {
let curr = self.blocks.as_ptr() as u64 + self_position;
// Get the number of bytes available
let mem_left = self_length - self_position;
let mem_left = self_length - self_position - core::mem::size_of::<MemoryRoot>() as u64;
// When the allocation is larger than the number of bytes available
// we will need to try to grow the memory
if length >= mem_left {
// Calculate the number of pages needed to cover the remaining bytes
let npages = num_pages(length);
let npages = num_pages(length - mem_left);
let x = core::arch::wasm32::memory_grow(0, npages);
if x == usize::MAX {
return None;

View File

@@ -1,6 +1,6 @@
[package]
name = "libextism"
version = "1.0.0-alpha.0"
version = "0.5.2"
edition = "2021"
authors = ["The Extism Authors", "oss@extism.org"]
license = "BSD-3-Clause"
@@ -13,11 +13,11 @@ name = "extism"
crate-type = ["cdylib"]
[dependencies]
extism = {path = "../runtime"}
extism-runtime = {path = "../runtime"}
[features]
default = ["http", "register-http", "register-filesystem"]
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
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

View File

@@ -1,6 +1,6 @@
//! This crate is used to generate `libextism` using `extism-runtime`
pub use extism::sdk::*;
pub use extism_runtime::sdk::*;
#[cfg(test)]
#[test]

View File

@@ -1,6 +1,6 @@
[package]
name = "extism-manifest"
version = "1.0.0-alpha.0"
version = "0.5.0"
edition = "2021"
authors = ["The Extism Authors", "oss@extism.org"]
license = "BSD-3-Clause"

View File

@@ -240,19 +240,8 @@ impl Manifest {
}
/// Set `config`
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());
pub fn with_config(mut self, c: impl Iterator<Item = (String, String)>) -> Self {
self.config = c.collect();
self
}

View File

@@ -37,3 +37,9 @@ 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.

735
node/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -41,10 +41,10 @@
"@types/jest": "^29.2.0",
"@types/node": "^20.1.0",
"jest": "^29.2.2",
"prettier": "3.0.3",
"prettier": "3.0.2",
"ts-jest": "^29.0.3",
"ts-node": "^10.9.1",
"typedoc": "^0.25.0",
"typedoc": "^0.24.1",
"typescript": "^5.0.4"
}
}

View File

@@ -6,12 +6,14 @@ 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(function_t);
let PtrArray = new ArrayType("void*");
let ValUnion = new UnionType({
i32: ref.types.uint32,
@@ -34,29 +36,28 @@ let Val = new StructType({
let ValArray = ArrayType(Val);
const _functions = {
extism_context_new: [context, []],
extism_context_free: ["void", [context]],
extism_plugin_new: [
plugin,
[
"string",
"uint64",
PtrArray,
"uint64",
"bool",
ref.refType(ref.types.char),
],
pluginIndex,
[context, "string", "uint64", PtrArray, "uint64", "bool"],
],
extism_plugin_error: ["string", [plugin]],
extism_plugin_update: [
"bool",
[context, pluginIndex, "string", "uint64", PtrArray, "uint64", "bool"],
],
extism_error: ["string", [context, pluginIndex]],
extism_plugin_call: [
"int32",
[plugin, "string", "string", "uint64"],
[context, pluginIndex, "string", "string", "uint64"],
],
extism_plugin_output_length: ["uint64", [plugin]],
extism_plugin_output_data: ["uint8*", [plugin]],
extism_plugin_output_length: ["uint64", [context, pluginIndex]],
extism_plugin_output_data: ["uint8*", [context, pluginIndex]],
extism_log_file: ["bool", ["string", "char*"]],
extism_plugin_function_exists: ["bool", [plugin, "string"]],
extism_plugin_config: ["void", [plugin, "char*", "uint64"]],
extism_plugin_free: ["void", [plugin]],
extism_plugin_new_error_free: ["void", ["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_version: ["string", []],
extism_function_new: [
function_t,
@@ -77,7 +78,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*", [plugin]],
extism_plugin_cancel_handle: ["void*", [context, pluginIndex]],
extism_plugin_cancel: ["bool", ["void*"]],
};
@@ -95,35 +96,49 @@ 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,
errmsg: Buffer | null,
) => Buffer;
extism_plugin_error: (plugin: Buffer) => string;
) => 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;
extism_plugin_call: (
plugin: Buffer,
ctx: Buffer,
plugin_id: number,
func: string,
input: string,
input_len: number,
) => number;
extism_plugin_output_length: (plugin: Buffer) => number;
extism_plugin_output_data: (plugin: Buffer) => Uint8Array;
extism_plugin_output_length: (ctx: Buffer, plugin_id: number) => number;
extism_plugin_output_data: (ctx: Buffer, plugin_id: number) => Uint8Array;
extism_log_file: (file: string, level: string) => boolean;
extism_plugin_function_exists: (
plugin: Buffer,
ctx: Buffer,
plugin_id: number,
func: string,
) => boolean;
extism_plugin_config: (
plugin: Buffer,
ctx: Buffer,
plugin_id: number,
data: string | Buffer,
data_len: number,
) => void;
extism_plugin_free: (plugin: Buffer) => void;
extism_plugin_new_error_free: (error: Buffer) => void;
extism_plugin_free: (ctx: Buffer, plugin_id: number) => void;
extism_context_reset: (ctx: Buffer) => void;
extism_version: () => string;
extism_function_new: (
name: string,
@@ -141,7 +156,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) => Buffer;
extism_plugin_cancel_handle: (p: Buffer, n: number) => Buffer;
extism_plugin_cancel: (p: Buffer) => boolean;
}
@@ -191,13 +206,13 @@ export function extismVersion(): string {
}
// @ts-ignore
const functionRegistry = new FinalizationRegistry((pointer) => {
if (pointer) lib.extism_function_free(pointer);
const contextRegistry = new FinalizationRegistry((pointer) => {
if (pointer) lib.extism_context_free(pointer);
});
// @ts-ignore
const pluginRegistry = new FinalizationRegistry((handle) => {
handle();
const functionRegistry = new FinalizationRegistry((pointer) => {
if (pointer) lib.extism_function_free(pointer);
});
/**
@@ -257,9 +272,98 @@ export type Manifest = {
type ManifestData = Manifest | Buffer | string;
/**
* A memory handle points to a particular offset in memory
* 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()
* ```
*/
type MemoryHandle = number;
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;
}
}
/**
* Provides access to the plugin that is currently running from inside a {@link HostFunction}
@@ -276,11 +380,8 @@ export class CurrentPlugin {
* @param offset - The offset in memory
* @returns a pointer to the provided offset
*/
memory(offset: MemoryHandle): Buffer {
const length = lib.extism_current_plugin_memory_length(
this.pointer,
offset,
);
memory(offset: number): Buffer {
let length = lib.extism_current_plugin_memory_length(this.pointer, offset);
return Buffer.from(
lib.extism_current_plugin_memory(this.pointer).buffer,
offset,
@@ -293,7 +394,7 @@ export class CurrentPlugin {
* @param n - The number of bytes to allocate
* @returns the offset to the newly allocated block
*/
memoryAlloc(n: number): MemoryHandle {
memoryAlloc(n: number): number {
return lib.extism_current_plugin_memory_alloc(this.pointer, n);
}
@@ -301,7 +402,7 @@ export class CurrentPlugin {
* Free a memory block
* @param offset - The offset of the block to free
*/
memoryFree(offset: MemoryHandle) {
memoryFree(offset: number) {
return lib.extism_current_plugin_memory_free(this.pointer, offset);
}
@@ -310,7 +411,7 @@ export class CurrentPlugin {
* @param offset - The offset of the block
* @returns the length of the block specified by `offset`
*/
memoryLength(offset: MemoryHandle): number {
memoryLength(offset: number): number {
return lib.extism_current_plugin_memory_length(this.pointer, offset);
}
@@ -320,7 +421,7 @@ export class CurrentPlugin {
* @param s - The string to return
*/
returnString(output: typeof Val, s: string) {
const offs = this.memoryAlloc(Buffer.byteLength(s));
var offs = this.memoryAlloc(Buffer.byteLength(s));
this.memory(offs).write(s);
output.v.i64 = offs;
}
@@ -331,7 +432,7 @@ export class CurrentPlugin {
* @param b - The buffer to return
*/
returnBytes(output: typeof Val, b: Buffer) {
const offs = this.memoryAlloc(b.length);
var offs = this.memoryAlloc(b.length);
this.memory(offs).fill(b);
output.v.i64 = offs;
}
@@ -480,23 +581,95 @@ export class CancelHandle {
* A Plugin represents an instance of your WASM program from the given manifest.
*/
export class Plugin {
plugin: Buffer | null;
id: number;
ctx: Context;
functions: typeof PtrArray;
token: { plugin: Buffer | null };
token: { id: number; pointer: Buffer };
/**
* Constructor for a plugin.
* Constructor for a plugin. @see {@link Context#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;
} else if (typeof manifest === "object" && manifest.wasm) {
dataRaw = JSON.stringify(manifest);
} 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,
dataRaw,
Buffer.byteLength(dataRaw, "utf-8"),
this.functions,
functions.length,
wasi,
);
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()}`;
}
this.id = plugin;
this.token = { id: this.id, pointer: ctx.pointer };
this.ctx = ctx;
if (config != null) {
let s = JSON.stringify(config);
lib.extism_plugin_config(
ctx.pointer,
this.id,
s,
Buffer.byteLength(s, "utf-8"),
);
}
}
/**
* 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);
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") {
@@ -504,51 +677,41 @@ export class Plugin {
} else if (typeof manifest === "object" && manifest.wasm) {
dataRaw = JSON.stringify(manifest);
} else {
throw Error(`Unknown manifest type ${typeof manifest}`);
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 plugin = lib.extism_plugin_new(
const ok = lib.extism_plugin_update(
this.ctx.pointer,
this.id,
dataRaw,
Buffer.byteLength(dataRaw, "utf-8"),
this.functions,
functions.length,
wasi,
null,
);
if (ref.address(plugin) === 0) {
// TODO: handle error
throw Error("Failed to create plugin");
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()}`;
}
this.plugin = plugin;
this.token = { plugin: this.plugin };
pluginRegistry.register(this, () => {
this.free();
}, this.token);
if (config != null) {
const s = JSON.stringify(config);
let s = JSON.stringify(config);
lib.extism_plugin_config(
this.plugin,
this.ctx.pointer,
this.id,
s,
Buffer.byteLength(s, "utf-8"),
);
}
}
/**
* Return a new `CancelHandle`, which can be used to cancel a running Plugin
*/
cancelHandle(): CancelHandle {
if (this.plugin === null) {
throw Error("Plugin already freed");
}
const handle = lib.extism_plugin_cancel_handle(this.plugin);
return new CancelHandle(handle);
}
/**
* Check if a function exists by name
*
@@ -557,11 +720,10 @@ export class Plugin {
*/
functionExists(functionName: string) {
if (this.plugin === null) {
throw Error("Plugin already freed");
}
if (!this.ctx.pointer) throw Error("No Context set");
return lib.extism_plugin_function_exists(
this.plugin,
this.ctx.pointer,
this.id,
functionName,
);
}
@@ -572,7 +734,7 @@ export class Plugin {
* @example
* ```
* const manifest = { wasm: [{ path: "/tmp/code.wasm" }] }
* const plugin = new Plugin(manifest)
* const plugin = ctx.plugin(manifest)
* const output = await plugin.call("my_function", "some-input")
* output.toString()
* // => "output from the function"
@@ -584,27 +746,25 @@ export class Plugin {
*/
async call(functionName: string, input: string | Buffer): Promise<Buffer> {
return new Promise<Buffer>((resolve, reject) => {
if (this.plugin === null) {
reject("Plugin already freed");
return;
}
if (!this.ctx.pointer) throw Error("No Context set");
var rc = lib.extism_plugin_call(
this.plugin,
this.ctx.pointer,
this.id,
functionName,
input.toString(),
Buffer.byteLength(input, "utf-8"),
);
if (rc !== 0) {
var err = lib.extism_plugin_error(this.plugin);
if (!err || err.length === 0) {
reject(`Plugin error: call to "${functionName}" failed`);
var err = lib.extism_error(this.ctx.pointer, this.id);
if (err.length === 0) {
reject(`extism_plugin_call: "${functionName}" failed`);
}
reject(`Plugin error: ${err.toString()}, code: ${rc}`);
}
var out_len = lib.extism_plugin_output_length(this.plugin);
var out_len = lib.extism_plugin_output_length(this.ctx.pointer, this.id);
var buf = Buffer.from(
lib.extism_plugin_output_data(this.plugin).buffer,
lib.extism_plugin_output_data(this.ctx.pointer, this.id).buffer,
0,
out_len,
);
@@ -616,10 +776,9 @@ export class Plugin {
* Free a plugin, this should be called when the plugin is no longer needed
*/
free() {
if (this.plugin !== null) {
pluginRegistry.unregister(this.token);
lib.extism_plugin_free(this.plugin);
this.plugin = null;
if (this.ctx.pointer && this.id >= 0) {
lib.extism_plugin_free(this.ctx.pointer, this.id);
this.id = -1;
}
}
}

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,60 +22,116 @@ 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 () => {
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);
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);
});
});
test("can free a plugin", async () => {
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");
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);
});
});
test("can detect if function exists or not", async () => {
const plugin = new extism.Plugin(manifest());
expect(plugin.functionExists("count_vowels")).toBe(true);
expect(plugin.functionExists("i_dont_extist")).toBe(false);
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);
});
test("errors when function is not known", async () => {
const plugin = new extism.Plugin(manifest());
await expect(() => plugin.call("i_dont_exist", "example-input")).rejects
.toMatch(/Plugin error/);
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/);
});
});
test("host functions work", async () => {
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",
),
]);
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 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.26.0
version = 0.24.1

View File

@@ -1,8 +1,6 @@
VERSION?=0.4.0
TAG?=0.5.0
PREFIX?=$$HOME/.local
build:
dune build
@@ -16,6 +14,3 @@ prepare:
publish:
opam publish -v $(VERSION) https://github.com/extism/extism/archive/refs/tags/v$(TAG).tar.gz ..
install-cli: build
install ../_build/default/ocaml/bin/main.exe "$(PREFIX)/bin/extism-call"

View File

@@ -1,5 +1,5 @@
(executable
(name main)
(package extism)
(public_name extism-call)
(public_name extism-run)
(libraries extism cmdliner))

View File

@@ -3,132 +3,25 @@ open Cmdliner
let read_stdin () = In_channel.input_all stdin
let split_allowed_paths =
List.filter_map (fun path ->
let s = String.split_on_char ':' path in
match s with
| [] -> None
| [ p ] -> Some (p, p)
| p :: tl -> Some (p, String.concat ":" tl))
let split_config =
List.filter_map (fun path ->
let s = String.split_on_char '=' path in
match s with
| [] -> None
| [ p ] -> Some (p, None)
| p :: tl -> Some (p, Some (String.concat "=" tl)))
let main file func_name input loop timeout_ms allowed_paths allowed_hosts config
memory_max log_level log_file wasi =
let main file func_name input =
let input = if String.equal input "-" then read_stdin () else input in
let allowed_paths = split_allowed_paths allowed_paths in
let config = split_config config in
let memory = Manifest.{ max_pages = memory_max } in
let manifest =
try
let m = Manifest.of_file file in
{
m with
timeout_ms = Some timeout_ms;
allowed_hosts = Some allowed_hosts;
allowed_paths = Some allowed_paths;
config = Some config;
memory = Some memory;
}
with _ ->
Manifest.create ~timeout_ms ~allowed_hosts ~allowed_paths ~config ~memory
[ Manifest.(Wasm.File { path = file; hash = None; name = None }) ]
in
let () =
match (log_level, log_file) with
| None, _ -> ()
| Some level, Some file -> assert (set_log_file ~level file)
| Some level, None -> assert (set_log_file ~level "stderr")
in
let plugin =
match Plugin.of_manifest manifest ~wasi with
| Ok x -> x
| Error (`Msg e) ->
Printf.eprintf "ERROR Unable to load plugin: %s" e;
exit 1
in
for _ = 0 to loop do
match Plugin.call plugin ~name:func_name input with
| Ok res -> print_endline res
| Error (`Msg e) ->
Printf.eprintf "ERROR Unable to call function: %s" e;
exit 2
done
let file = In_channel.with_open_bin file In_channel.input_all in
let plugin = Plugin.create file ~wasi:true |> Result.get_ok in
let res = Plugin.call plugin ~name:func_name input |> Result.get_ok in
print_endline res
let file =
let doc = "The Wasm module or Extism manifest path." in
Arg.(required & pos 0 (some file) None & info [] ~docv:"FILE" ~doc)
let doc = "The WASM module or Extism manifest path." in
Arg.(value & pos 0 file "" & info [] ~docv:"FILE" ~doc)
let func_name =
let doc = "The function to run." in
Arg.(required & pos 1 (some string) None & info [] ~docv:"NAME" ~doc)
Arg.(value & pos 1 string "_start" & info [] ~docv:"NAME" ~doc)
let input =
let doc = "Input data." in
Arg.(value & opt string "" & info [ "input"; "i" ] ~docv:"INPUT" ~doc)
let loop =
let doc = "Number of times to call the plugin." in
Arg.(value & opt int 0 & info [ "loop" ] ~docv:"TIMES" ~doc)
let memory_max =
let doc = "Max number of memory pages." in
Arg.(value & opt (some int) None & info [ "memory-max" ] ~docv:"PAGES" ~doc)
let timeout =
let doc = "Plugin timeout in milliseconds." in
Arg.(value & opt int 30000 & info [ "timeout"; "t" ] ~docv:"MILLIS" ~doc)
let allowed_paths =
let doc = "Allowed paths." in
Arg.(
value & opt_all string []
& info [ "allow-path" ] ~docv:"LOCAL_PATH[:PLUGIN_PATH]" ~doc)
let allowed_hosts =
let doc = "Allowed hosts for HTTP requests." in
Arg.(value & opt_all string [] & info [ "allow-host" ] ~docv:"HOST" ~doc)
let config =
let doc = "Plugin config." in
Arg.(value & opt_all string [] & info [ "config" ] ~docv:"KEY=VALUE" ~doc)
let log_file =
let doc = "File to write logs to." in
Arg.(
value & opt (some string) None & info [ "log-file" ] ~docv:"FILENAME" ~doc)
let log_level_enum =
Arg.enum
[
("warn", `Warn);
("info", `Info);
("debug", `Debug);
("error", `Error);
("trace", `Trace);
]
let log_level =
let doc = "Log level." in
Arg.(
value
& opt (some log_level_enum) None
& info [ "log-level" ] ~docv:"LEVEL" ~doc)
let wasi =
let doc = "Enable WASI." in
Arg.(value & flag & info [ "wasi" ] ~doc)
let main_t =
Term.(
const main $ file $ func_name $ input $ loop $ timeout $ allowed_paths
$ allowed_hosts $ config $ memory_max $ log_level $ log_file $ wasi)
let main_t = Term.(const main $ file $ func_name $ input)
let cmd = Cmd.v (Cmd.info "extism-run") main_t
let () = exit (Cmd.eval cmd)

View File

@@ -45,6 +45,9 @@ 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
@@ -91,46 +94,54 @@ 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"
(string @-> uint64_t
(context @-> string @-> uint64_t
@-> ptr (ptr void)
@-> uint64_t @-> bool
@-> ptr (ptr char)
@-> returning plugin)
@-> 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)
let extism_plugin_config =
fn "extism_plugin_config" (plugin @-> string @-> uint64_t @-> returning bool)
fn "extism_plugin_config"
(context @-> int32_t @-> string @-> uint64_t @-> returning bool)
let extism_plugin_call =
fn "extism_plugin_call"
(plugin @-> string @-> ptr char @-> uint64_t @-> returning int32_t)
(context @-> int32_t @-> string @-> ptr char @-> uint64_t
@-> returning int32_t)
let extism_plugin_call_s =
fn "extism_plugin_call"
(plugin @-> string @-> string @-> uint64_t @-> returning int32_t)
(context @-> int32_t @-> string @-> string @-> uint64_t
@-> returning int32_t)
let extism_error = fn "extism_plugin_error" (plugin @-> returning string_opt)
let extism_error =
fn "extism_error" (context @-> int32_t @-> returning string_opt)
let extism_plugin_output_length =
fn "extism_plugin_output_length" (plugin @-> returning uint64_t)
fn "extism_plugin_output_length" (context @-> int32_t @-> returning uint64_t)
let extism_plugin_output_data =
fn "extism_plugin_output_data" (plugin @-> returning (ptr char))
fn "extism_plugin_output_data" (context @-> int32_t @-> 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" (plugin @-> returning void)
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_function_exists =
fn "extism_plugin_function_exists" (plugin @-> string @-> returning bool)
fn "extism_plugin_function_exists"
(context @-> int32_t @-> string @-> returning bool)
let extism_function_type =
Foreign.funptr ~runtime_lock:true
@@ -168,9 +179,7 @@ let extism_current_plugin_memory_free =
(ptr void @-> uint64_t @-> returning void)
let extism_plugin_cancel_handle =
fn "extism_plugin_cancel_handle" (plugin @-> returning (ptr void))
fn "extism_plugin_cancel_handle" (context @-> int32_t @-> 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))

19
ocaml/lib/context.ml Normal file
View File

@@ -0,0 +1,19 @@
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_handle = { offs : Unsigned.UInt64.t; len : Unsigned.UInt64.t }
type memory_block = { 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_handle = struct
module Memory_block = 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_handle.set_string t mem s;
Memory_block.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_handle.set_bigstring t mem s;
Memory_block.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_handle.of_val_exn t inp in
Memory_handle.get_string t mem
let mem = Memory_block.of_val_exn t inp in
Memory_block.get_string t mem
let input_bigstring t inputs index =
let inp = Types.Val_array.(inputs.$[index]) in
let mem = Memory_handle.of_val_exn t inp in
Memory_handle.get_bigstring t mem
let mem = Memory_block.of_val_exn t inp in
Memory_block.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 uuidm)
(libraries ctypes.foreign bigstringaf extism-manifest)
(preprocess
(pps ppx_yojson_conv ppx_inline_test)))

View File

@@ -1,15 +1,14 @@
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_handle = { offs : Unsigned.UInt64.t; len : Unsigned.UInt64.t }
type memory_block = { 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_handle option
(** Convert an offset into a {memory_handle} *)
val find : t -> Unsigned.UInt64.t -> memory_block option
(** Find memory block *)
val alloc : t -> int -> memory_handle
val alloc : t -> int -> memory_block
(** Allocate a new block of memory *)
val free : t -> memory_handle -> unit
(** Free allocated memory *)
val free : t -> memory_block -> unit
(** Free an allocated block of 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_handle : sig
val to_val : memory_handle -> Val.t
module Memory_block : sig
val to_val : memory_block -> Val.t
(** Convert memory block to [Val] *)
val of_val : t -> Val.t -> memory_handle option
val of_val : t -> Val.t -> memory_block option
(** Convert [Val] to memory block *)
val of_val_exn : t -> Val.t -> memory_handle
val of_val_exn : t -> Val.t -> memory_block
(** 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_handle -> string
val get_string : t -> memory_block -> string
(** Get a string from memory stored at the provided offset *)
val get_bigstring : t -> memory_handle -> Bigstringaf.t
val get_bigstring : t -> memory_block -> Bigstringaf.t
(** Get a bigstring from memory stored at the provided offset *)
val set_string : t -> memory_handle -> string -> unit
val set_string : t -> memory_block -> string -> unit
(** Store a string into memory at the provided offset *)
val set_bigstring : t -> memory_handle -> Bigstringaf.t -> unit
val set_bigstring : t -> memory_block -> Bigstringaf.t -> unit
(** Store a bigstring into memory at the provided offset *)
end
end
@@ -178,6 +178,25 @@ 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
@@ -189,6 +208,7 @@ 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 *)
@@ -196,10 +216,23 @@ 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 *)
@@ -213,15 +246,11 @@ 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 id : t -> Uuidm.t
val cancel_handle: t -> Cancel_handle.t
end
val with_plugin : (Plugin.t -> 'a) -> Plugin.t -> 'a

View File

@@ -1,9 +1,17 @@
module Manifest = Extism_manifest
type t = {
mutable pointer : unit Ctypes.ptr;
mutable functions : Function.t list;
}
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
let set_config plugin = function
| None -> true
@@ -11,56 +19,39 @@ let set_config plugin = function
let config =
Extism_manifest.yojson_of_config config |> Yojson.Safe.to_string
in
Bindings.extism_plugin_config plugin.pointer config
Bindings.extism_plugin_config plugin.ctx.pointer plugin.id config
(Unsigned.UInt64.of_int (String.length config))
let free t =
if not (Ctypes.is_null t.pointer) then
let () = Bindings.extism_plugin_free t.pointer in
t.pointer <- Ctypes.null
if not (Ctypes.is_null t.ctx.pointer) then
Bindings.extism_plugin_free t.ctx.pointer t.id
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 create ?config ?(wasi = false) ?(functions = []) ?context wasm =
let ctx = match context with Some c -> c | None -> Context.create () 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 errmsg =
Ctypes.(allocate (ptr char) (coerce (ptr void) (ptr char) null))
in
let pointer =
Bindings.extism_plugin_new wasm
let id =
Bindings.extism_plugin_new ctx.Context.pointer wasm
(Unsigned.UInt64.of_int (String.length wasm))
(Ctypes.CArray.start arr)
(Unsigned.UInt64.of_int n_funcs)
wasi errmsg
wasi
in
if Ctypes.is_null pointer then
let s = get_errmsg (Ctypes.( !@ ) errmsg) in
Error (`Msg s)
if id < 0l then
match Bindings.extism_error ctx.pointer (-1l) with
| None -> Error (`Msg "extism_plugin_call failed")
| Some msg -> Error (`Msg msg)
else
let t = { pointer; functions } in
let t = { id; ctx; functions } in
if not (set_config t config) then Error (`Msg "call to set_config failed")
else
let () = Gc.finalise free t in
Ok t
let of_manifest ?wasi ?functions manifest =
let of_manifest ?wasi ?functions ?context manifest =
let data = Manifest.to_json manifest in
create ?wasi ?functions data
create ?wasi ?functions ?context data
let%test "free plugin" =
let manifest = Manifest.(create [ Wasm.file "test/code.wasm" ]) in
@@ -68,23 +59,54 @@ let%test "free plugin" =
free plugin;
true
let call' f { pointer; _ } ~name input len =
if Ctypes.is_null pointer then Error.throw (`Msg "Plugin already freed")
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")
else
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 () = 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 call_bigstring (t : t) ~name input =
let len = Unsigned.UInt64.of_int (Bigstringaf.length input) in
@@ -128,9 +150,8 @@ let%test "call_functions" =
call plugin ~name:"count_vowels" "this is a test"
|> Error.unwrap = "{\"count\": 4}"
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 function_exists { id; ctx; _ } name =
Bindings.extism_plugin_function_exists ctx.pointer id name
let%test "function exists" =
let manifest = Manifest.(create [ Wasm.file "test/code.wasm" ]) in
@@ -144,13 +165,5 @@ module Cancel_handle = struct
let cancel { inner } = Bindings.extism_plugin_cancel inner
end
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
let cancel_handle { id; ctx; _ } =
Cancel_handle.{ inner = Bindings.extism_plugin_cancel_handle ctx.pointer id }

View File

@@ -9,3 +9,9 @@ $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,6 +36,40 @@ 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,22 +24,20 @@ class CancelHandle
class Plugin
{
private $lib;
private $context;
private $wasi;
private $config;
private $plugin;
private $id;
public function __construct($data, $wasi = false, $config = null)
public function __construct($data, $wasi = false, $config = null, $ctx = null)
{
global $lib;
if ($lib == null) {
$lib = new \ExtismLib(\ExtismLib::SOFILE);
if ($ctx == null) {
$ctx = new Context();
}
$this->lib = $lib;
$this->lib = $ctx->lib;
$this->wasi = $wasi;
$this->config = $config;
@@ -52,32 +50,38 @@ class Plugin
$data = string_to_bytes($data);
}
// 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");
$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());
}
$this->plugin = $plugin;
$this->id = $id;
$this->context = $ctx;
if ($this->config != null) {
$cfg = string_to_bytes(json_encode($config));
$this->lib->extism_plugin_config($this->plugin, $cfg, count($cfg));
$this->lib->extism_plugin_config($ctx->pointer, $this->id, $cfg, count($cfg));
}
}
public function __destruct() {
$this->lib->extism_plugin_free($this->plugin);
$this->plugin = null;
}
$this->lib->extism_plugin_free($this->context->pointer, $this->id);
$this->id = -1;
}
public function getId() {
return $this->id;
}
public function functionExists($name)
{
return $this->lib->extism_plugin_function_exists($this->plugin, $name);
return $this->lib->extism_plugin_function_exists($this->context->pointer, $this->id, $name);
}
public function cancelHandle()
{
return new \CancelHandle($this->lib, $this->lib->extism_plugin_cancel_handle($this->plugin));
return new \CancelHandle($this->lib, $this->lib->extism_plugin_cancel_handle($this->context->pointer, $this->id));
}
public function call($name, $input = null)
@@ -86,19 +90,19 @@ class Plugin
$input = string_to_bytes($input);
}
$rc = $this->lib->extism_plugin_call($this->plugin, $name, $input, count($input));
$rc = $this->lib->extism_plugin_call($this->context->pointer, $this->id, $name, $input, count($input));
if ($rc != 0) {
$msg = "code = " . $rc;
$err = $this->lib->extism_error($this->plugin);
$err = $this->lib->extism_error($this->context->pointer, $this->id);
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->plugin);
$length = $this->lib->extism_plugin_output_length($this->context->pointer, $this->id);
$buf = $this->lib->extism_plugin_output_data($this->plugin);
$buf = $this->lib->extism_plugin_output_data($this->context->pointer, $this->id);
$output = [];
$data = $buf->getData();
@@ -108,6 +112,27 @@ 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,7 +25,6 @@ def count_vowels(data):
def main(args):
set_log_file("stderr", "trace")
if len(args) > 1:
data = args[1].encode()
else:
@@ -48,7 +47,6 @@ 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,6 +2,7 @@ from .extism import (
Error,
Plugin,
set_log_file,
Context,
extism_version,
host_fn,
Function,

View File

@@ -4,7 +4,6 @@ from base64 import b64encode
from cffi import FFI
from typing import Union
from enum import Enum
from uuid import UUID
class Error(Exception):
@@ -135,6 +134,70 @@ 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
@@ -187,6 +250,7 @@ class Plugin:
def __init__(
self,
plugin: Union[str, bytes, dict],
context=None,
wasi=False,
config=None,
functions=None,
@@ -195,42 +259,87 @@ 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(
wasm, len(wasm), ptr, len(functions), wasi, errmsg
context.pointer, wasm, len(wasm), ptr, len(functions), wasi
)
else:
self.plugin = _lib.extism_plugin_new(
wasm, len(wasm), _ffi.NULL, 0, wasi, errmsg
context.pointer, wasm, len(wasm), _ffi.NULL, 0, wasi
)
if self.plugin == _ffi.NULL:
msg = _ffi.string(errmsg[0])
_lib.extism_plugin_new_error_free(errmsg[0])
raise Error(msg.decode())
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 config is not None:
s = json.dumps(config).encode()
_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)
_lib.extism_plugin_config(self.ctx.pointer, self.plugin, s, len(s))
def cancel_handle(self):
return CancelHandle(_lib.extism_plugin_cancel_handle(self.plugin))
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))
def _check_error(self, rc):
if rc != 0:
error = _lib.extism_plugin_error(self.plugin)
error = _lib.extism_error(self.ctx.pointer, self.plugin)
if error != _ffi.NULL:
raise Error(_ffi.string(error).decode())
raise Error(f"Error code: {rc}")
@@ -248,7 +357,9 @@ class Plugin:
-------
True if the function exists in the plugin, False otherwise
"""
return _lib.extism_plugin_function_exists(self.plugin, name.encode())
return _lib.extism_plugin_function_exists(
self.ctx.pointer, self.plugin, name.encode()
)
def call(self, function_name: str, data: Union[str, bytes], parse=bytes):
"""
@@ -273,20 +384,22 @@ class Plugin:
data = data.encode()
self._check_error(
_lib.extism_plugin_call(
self.plugin, function_name.encode(), data, len(data)
self.ctx.pointer, self.plugin, function_name.encode(), data, len(data)
)
)
out_len = _lib.extism_plugin_output_length(self.plugin)
out_buf = _lib.extism_plugin_output_data(self.plugin)
out_len = _lib.extism_plugin_output_length(self.ctx.pointer, self.plugin)
out_buf = _lib.extism_plugin_output_data(self.ctx.pointer, 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, "pointer"):
if not hasattr(self, "ctx"):
return
_lib.extism_plugin_free(self.plugin)
if self.ctx.pointer == _ffi.NULL:
return
_lib.extism_plugin_free(self.ctx.pointer, self.plugin)
self.plugin = -1
def __enter__(self):

View File

@@ -9,50 +9,74 @@ 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):
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)
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)
def test_function_exists(self):
plugin = extism.Plugin(self._manifest())
self.assertTrue(plugin.function_exists("count_vowels"))
self.assertFalse(plugin.function_exists("i_dont_exist"))
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"))
def test_errors_on_unknown_function(self):
plugin = extism.Plugin(self._manifest())
self.assertRaises(
extism.Error, lambda: plugin.call("i_dont_exist", "someinput")
)
with extism.Context() as ctx:
plugin = ctx.plugin(self._manifest())
self.assertRaises(
extism.Error, lambda: plugin.call("i_dont_exist", "someinput")
)
def test_can_free_plugin(self):
plugin = extism.Plugin(self._manifest())
del plugin
with extism.Context() as ctx:
plugin = ctx.plugin(self._manifest())
del plugin
def test_errors_on_bad_manifest(self):
self.assertRaises(
extism.Error, lambda: extism.Plugin({"invalid_manifest": True})
)
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})
)
def test_extism_version(self):
self.assertIsNotNone(extism.extism_version())
def test_extism_plugin_timeout(self):
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",
)
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",
)
def test_extism_host_function(self):
@extism.host_fn
@@ -62,29 +86,31 @@ class TestExtism(unittest.TestCase):
mem[:] = user_data
results[0].value = offs.offset
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")
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")
def test_extism_plugin_cancel(self):
plugin = extism.Plugin(self._loop_manifest())
cancel_handle = plugin.cancel_handle()
with extism.Context() as ctx:
plugin = ctx.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,19 +8,27 @@
require "extism"
require "json"
manifest = {
:wasm => [{ :path => "../wasm/code.wasm" }],
}
plugin = Plugin.new(manifest)
res = JSON.parse(plugin.call("count_vowels", "this is a test"))
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
```
### API
There is just one primary class you need to understand:
There are two primary classes 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

@@ -1,16 +1,15 @@
# frozen_string_literal: true
source 'https://rubygems.org'
source "https://rubygems.org"
# Specify your gem's dependencies in extism.gemspec
gemspec
gem 'ffi', '~> 1.15.5'
gem 'rake', '~> 13.0'
gem "rake", "~> 13.0"
gem "ffi", "~> 1.15.5"
group :development do
gem 'debug'
gem 'minitest', '~> 5.20.0'
gem 'rufo', '~> 0.13.0'
gem 'yard', '~> 0.9.28'
gem "yard", "~> 0.9.28"
gem "rufo", "~> 0.13.0"
gem "minitest", "~> 5.19.0"
end

View File

@@ -1,27 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'irb' is installed as part of a gem, and
# this file is here to facilitate running it.
#
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
bundle_binstub = File.expand_path("bundle", __dir__)
if File.file?(bundle_binstub)
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
load(bundle_binstub)
else
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
end
end
require "rubygems"
require "bundler/setup"
load Gem.bin_path("irb", "irb")

View File

@@ -1,27 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'rdbg' is installed as part of a gem, and
# this file is here to facilitate running it.
#
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
bundle_binstub = File.expand_path("bundle", __dir__)
if File.file?(bundle_binstub)
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
load(bundle_binstub)
else
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
end
end
require "rubygems"
require "bundler/setup"
load Gem.bin_path("debug", "rdbg")

View File

@@ -1,27 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'rdoc' is installed as part of a gem, and
# this file is here to facilitate running it.
#
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
bundle_binstub = File.expand_path("bundle", __dir__)
if File.file?(bundle_binstub)
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
load(bundle_binstub)
else
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
end
end
require "rubygems"
require "bundler/setup"
load Gem.bin_path("rdoc", "rdoc")

View File

@@ -1,27 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'ri' is installed as part of a gem, and
# this file is here to facilitate running it.
#
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
bundle_binstub = File.expand_path("bundle", __dir__)
if File.file?(bundle_binstub)
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
load(bundle_binstub)
else
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
end
end
require "rubygems"
require "bundler/setup"
load Gem.bin_path("rdoc", "ri")

View File

@@ -1,6 +1,6 @@
require 'ffi'
require 'json'
require_relative './extism/version'
require "ffi"
require "json"
require_relative "./extism/version"
module Extism
class Error < StandardError
@@ -17,18 +17,105 @@ module Extism
# @param name [String] The path to the logfile
# @param level [String] The log level. One of {"debug", "error", "info", "trace" }
def self.set_log_file(name, level = nil)
if level
level = FFI::MemoryPointer::from_string(level)
end
C.extism_log_file(name, level)
end
$PLUGINS = {}
$FREE_PLUGIN = proc { |ptr|
x = $PLUGINS[ptr]
unless x.nil?
C.extism_plugin_free(x[:plugin])
$PLUGINS.delete(ptr)
$FREE_PLUGIN = proc { |id|
x = $PLUGINS[id]
if !x.nil?
C.extism_plugin_free(x[:context].pointer, x[:plugin])
$PLUGINS.delete(id)
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)
@@ -37,7 +124,7 @@ module Extism
# Cancel the plugin used to generate the handle
def cancel
C.extism_plugin_cancel(@handle)
return C.extism_plugin_cancel(@handle)
end
end
@@ -48,26 +135,62 @@ 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
def initialize(wasm, functions = [], wasi = false, config = nil)
wasm = JSON.generate(wasm) if wasm.instance_of?(Hash)
code = FFI::MemoryPointer.new(:char, wasm.bytesize)
errmsg = FFI::MemoryPointer.new(:pointer)
code.put_bytes(0, wasm)
funcs_ptr = FFI::MemoryPointer.new(C::ExtismFunction)
funcs_ptr.write_array_of_pointer(functions.map { |f| f.pointer })
@plugin = C.extism_plugin_new(code, wasm.bytesize, funcs_ptr, functions.length, wasi, errmsg)
if @plugin.null?
err = errmsg.read_pointer.read_string
C.extism_plugin_new_error_free errmsg.read_pointer
raise Error, err
# @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
$PLUGINS[object_id] = { plugin: @plugin }
@context = context
if wasm.class == Hash
wasm = JSON.generate(wasm)
end
code = FFI::MemoryPointer.new(:char, wasm.bytesize)
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
end
$PLUGINS[self.object_id] = { :plugin => @plugin, :context => context }
ObjectSpace.define_finalizer(self, $FREE_PLUGIN)
return unless !config.nil? and @plugin.null?
if config != nil and @plugin >= 0
s = JSON.generate(config)
ptr = FFI::MemoryPointer::from_string(s)
C.extism_plugin_config(@context.pointer, @plugin, ptr, s.bytesize)
end
end
s = JSON.generate(config)
ptr = FFI::MemoryPointer.from_string(s)
C.extism_plugin_config(@plugin, ptr, s.bytesize)
# 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)
end
end
# Check if a function exists
@@ -75,7 +198,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(@plugin, name)
C.extism_plugin_function_exists(@context.pointer, @plugin, name)
end
# Call a function by name
@@ -86,18 +209,18 @@ module Extism
def call(name, data, &block)
# 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(@plugin, name, input, data.bytesize)
input = FFI::MemoryPointer::from_string(data)
rc = C.extism_plugin_call(@context.pointer, @plugin, name, input, data.bytesize)
if rc != 0
err = C.extism_plugin_error(@plugin)
raise Error, 'extism_call failed' if err&.empty?
raise Error, err
err = C.extism_error(@context.pointer, @plugin)
if err&.empty?
raise Error.new "extism_call failed"
else
raise Error.new err
end
end
out_len = C.extism_plugin_output_length(@plugin)
buf = C.extism_plugin_output_data(@plugin)
out_len = C.extism_plugin_output_length(@context.pointer, @plugin)
buf = C.extism_plugin_output_data(@context.pointer, @plugin)
block.call(buf, out_len)
end
@@ -105,211 +228,40 @@ module Extism
#
# @return [void]
def free
return if @plugin.null?
return if @context.pointer.nil?
$PLUGINS.delete(object_id)
C.extism_plugin_free(@plugin)
@plugin = nil
$PLUGINS.delete(self.object_id)
C.extism_plugin_free(@context.pointer, @plugin)
@plugin = -1
end
# Get a CancelHandle for a plugin
def cancel_handle
CancelHandle.new(C.extism_plugin_cancel_handle(@plugin))
return CancelHandle.new(C.extism_plugin_cancel_handle(@context.pointer, @plugin))
end
end
Memory = Struct.new(:offset, :len)
class CurrentPlugin
def initialize(ptr)
@ptr = ptr
end
def alloc(amount)
offset = C.extism_current_plugin_memory_alloc(@ptr, amount)
Memory.new(offset, amount)
end
def free(memory)
C.extism_current_plugin_memory_free(@ptr, memory.offset)
end
def memory_at_offset(offset)
len = C.extism_current_plugin_memory_length(@ptr, offset)
Memory.new(offset, len)
end
def input_as_bytes(input)
# TODO: should assert that this is an int input
mem = memory_at_offset(input.value)
memory_ptr(mem).read_bytes(mem.len)
end
def return_bytes(output, bytes)
mem = alloc(bytes.length)
memory_ptr(mem).put_bytes(0, bytes)
output.value = mem.offset
end
def return_string(output, string)
return_bytes(output, string)
end
private
def memory_ptr(mem)
plugin_ptr = C.extism_current_plugin_memory(@ptr)
FFI::Pointer.new(plugin_ptr.address + mem.offset)
end
end
module ValType
I32 = 0
I64 = 1
F32 = 2
F64 = 3
V128 = 4
FUNC_REF = 5
EXTERN_REF = 6
end
class Val
def initialize(ptr)
@c_val = C::ExtismVal.new(ptr)
end
def type
case @c_val[:t]
when :I32
:i32
when :I64
:i64
when :F32
:f32
when :F64
:f64
else
raise "Unsupported wasm value type #{type}"
end
end
def value
@c_val[:v][type]
end
def value=(val)
@c_val[:v][type] = val
end
end
class Function
def initialize(name, args, returns, func_proc, user_data)
@name = name
@args = args
@returns = returns
@func = func_proc
@user_data = user_data
end
def pointer
return @pointer if @pointer
free = proc { puts 'freeing ' }
args = C.from_int_array(@args)
returns = C.from_int_array(@returns)
@pointer = C.extism_function_new(@name, args, @args.length, returns, @returns.length, c_func, free, nil)
end
private
def c_func
@c_func ||= proc do |plugin_ptr, inputs_ptr, inputs_size, outputs_ptr, outputs_size, _data_ptr|
current_plugin = CurrentPlugin.new(plugin_ptr)
val_struct_size = C::ExtismVal.size
inputs = (0...inputs_size).map do |i|
Val.new(inputs_ptr + i * val_struct_size)
end
outputs = (0...outputs_size).map do |i|
Val.new(outputs_ptr + i * val_struct_size)
end
@func.call(current_plugin, inputs, outputs, @user_data)
end
end
end
private
# Private module used to interface with the Extism runtime.
# *Warning*: Do not use or rely on this directly.
module C
extend FFI::Library
ffi_lib 'extism'
def self.from_int_array(ruby_array)
ptr = FFI::MemoryPointer.new(:int, ruby_array.length)
ptr.write_array_of_int(ruby_array)
ptr
end
typedef :uint64, :ExtismMemoryHandle
typedef :uint64, :ExtismSize
enum :ExtismValType, %i[I32 I64 F32 F64 V128 FuncRef ExternRef]
class ExtismValUnion < FFI::Union
layout :i32, :int32,
:i64, :int64,
:f32, :float,
:f64, :double
end
class ExtismVal < FFI::Struct
layout :t, :ExtismValType,
:v, ExtismValUnion
end
class ExtismFunction < FFI::Struct
layout :name, :string,
:inputs, :pointer,
:n_inputs, :uint64,
:outputs, :pointer,
:n_outputs, :uint64,
:data, :pointer
end
callback :ExtismFunctionType, [
:pointer, # plugin
:pointer, # inputs
:ExtismSize, # n_inputs
:pointer, # outputs
:ExtismSize, # n_outputs
:pointer # user_data
], :void
callback :ExtismFreeFunctionType, [], :void
attach_function :extism_plugin_id, [:pointer], :pointer
attach_function :extism_current_plugin_memory, [:pointer], :pointer
attach_function :extism_current_plugin_memory_alloc, %i[pointer ExtismSize], :ExtismMemoryHandle
attach_function :extism_current_plugin_memory_length, %i[pointer ExtismMemoryHandle], :ExtismSize
attach_function :extism_current_plugin_memory_free, %i[pointer ExtismMemoryHandle], :void
attach_function :extism_function_new,
%i[string pointer ExtismSize pointer ExtismSize ExtismFunctionType ExtismFreeFunctionType pointer], :pointer
attach_function :extism_function_free, [:pointer], :void
attach_function :extism_function_set_namespace, %i[pointer string], :void
attach_function :extism_plugin_new, %i[pointer ExtismSize pointer ExtismSize bool pointer], :pointer
attach_function :extism_plugin_new_error_free, [:pointer], :void
attach_function :extism_plugin_free, [:pointer], :void
attach_function :extism_plugin_cancel_handle, [:pointer], :pointer
attach_function :extism_plugin_cancel, [:pointer], :bool
attach_function :extism_plugin_config, %i[pointer pointer ExtismSize], :bool
attach_function :extism_plugin_function_exists, %i[pointer string], :bool
attach_function :extism_plugin_call, %i[pointer string pointer ExtismSize], :int32
attach_function :extism_error, [:pointer], :string
attach_function :extism_plugin_error, [:pointer], :string
attach_function :extism_plugin_output_length, [:pointer], :ExtismSize
attach_function :extism_plugin_output_data, [:pointer], :pointer
attach_function :extism_log_file, %i[string string], :bool
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_log_file, [:string, :pointer], :void
attach_function :extism_plugin_free, [:pointer, :int32], :void
attach_function :extism_context_reset, [:pointer], :void
attach_function :extism_version, [], :string
attach_function :extism_plugin_cancel_handle, [:pointer, :int32], :pointer
attach_function :extism_plugin_cancel, [:pointer], :bool
end
end

View File

@@ -1,5 +1,5 @@
# frozen_string_literal: true
module Extism
VERSION = '1.0.0-rc.1'
VERSION = '0.5.0'
end

View File

@@ -1,83 +1,91 @@
# frozen_string_literal: true
require 'test_helper'
require "test_helper"
class TestExtism < Minitest::Test
def test_that_it_has_a_version_number
refute_nil Extism::VERSION
end
def test_create_context
refute_nil Extism::Context.new
end
def test_plugin_call
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
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
end
def test_can_free_plugin
plugin = Extism::Plugin.new(manifest)
_res = plugin.call('count_vowels', 'this is a test')
ctx = Extism::Context.new
plugin = ctx.plugin(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')
_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
assert_raises(Extism::Error) do
_plugin = Extism::Plugin.new({ not_a_real_manifest: true })
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
end
end
def test_has_function
plugin = Extism::Plugin.new(manifest)
assert plugin.has_function? 'count_vowels'
refute plugin.has_function? 'i_am_not_a_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
end
def test_errors_on_unknown_function
plugin = Extism::Plugin.new(manifest)
assert_raises(Extism::Error) do
plugin.call('non_existent_function', 'input')
Extism.with_context do |ctx|
plugin = ctx.plugin(manifest)
assert_raises(Extism::Error) do
plugin.call("non_existent_function", "input")
end
end
end
def test_host_functions
Extism.set_log_file('stdout', 'info')
func = proc do |current_plugin, inputs, outputs, user_data|
input = current_plugin.input_as_bytes(inputs.first)
current_plugin.return_string(outputs.first, "#{input} #{user_data}")
end
f = Extism::Function.new('transform_string', [Extism::ValType::I64], [Extism::ValType::I64], func, 'My User Data')
plugin = Extism::Plugin.new(host_manifest, [f], true)
result = plugin.call('reflect_string', 'Hello, World!')
assert_equal result, 'Hello, World! My User Data'
end
private
def manifest
{
wasm: [
{
path: File.join(__dir__, '../../wasm/code.wasm')
}
]
}
end
def host_manifest
{
wasm: [
{
path: File.join(__dir__, '../../wasm/kitchensink.wasm')
}
]
path: File.join(__dir__, "../../wasm/code.wasm"),
},
],
}
end
end

View File

@@ -1,17 +1,17 @@
[package]
name = "extism"
version = "1.0.0-alpha.0"
name = "extism-runtime"
version = "0.5.2"
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 and Rust SDK"
description = "Extism runtime component"
[dependencies]
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}
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}
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 = "1.0.0-alpha.0", path = "../manifest" }
extism-manifest = { version = "0.5.0", path = "../manifest" }
uuid = { version = "1", features = ["v4"] }
libc = "0.2"
@@ -34,4 +34,4 @@ register-filesystem = [] # enables wasm to be loaded from disk
http = ["ureq"] # enables extism_http_request
[build-dependencies]
cbindgen = "0.25"
cbindgen = "0.24"

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("CurrentPlugin", "ExtismCurrentPlugin")
.rename_item("Plugin", "ExtismPlugin")
.rename_item("Function", "ExtismFunction")
.rename_item("Internal", "ExtismCurrentPlugin")
.with_style(cbindgen::Style::Type)
.generate()
{

View File

@@ -8,7 +8,7 @@
/**
* An enumeration of all possible value types in WebAssembly.
* A list of all possible value types in WebAssembly.
*/
typedef enum {
/**
@@ -42,24 +42,21 @@ typedef enum {
} ExtismValType;
/**
* CurrentPlugin stores data that is available to the caller in PDK functions, this should
* only be accessed from inside a host function
* A `Context` is used to store and manage plugins
*/
typedef struct ExtismCurrentPlugin ExtismCurrentPlugin;
typedef struct ExtismContext ExtismContext;
typedef struct ExtismCancelHandle ExtismCancelHandle;
/**
* Wraps raw host functions with some additional metadata and user data
* Wraps host functions
*/
typedef struct ExtismFunction ExtismFunction;
/**
* Plugin contains everything needed to execute a WASM function
* Internal stores data that is available to the caller in PDK functions
*/
typedef struct ExtismPlugin ExtismPlugin;
typedef uint64_t ExtismMemoryHandle;
typedef struct ExtismCurrentPlugin ExtismCurrentPlugin;
typedef uint64_t ExtismSize;
@@ -91,10 +88,17 @@ typedef void (*ExtismFunctionType)(ExtismCurrentPlugin *plugin,
ExtismSize n_outputs,
void *data);
typedef int32_t ExtismPlugin;
/**
* Get a plugin's ID, the returned bytes are a 16 byte buffer that represent a UUID value
* Create a new context
*/
const uint8_t *extism_plugin_id(ExtismPlugin *plugin);
ExtismContext *extism_context_new(void);
/**
* Free a context
*/
void extism_context_free(ExtismContext *ctx);
/**
* Returns a pointer to the memory of the currently running plugin
@@ -106,19 +110,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.
*/
ExtismMemoryHandle extism_current_plugin_memory_alloc(ExtismCurrentPlugin *plugin, ExtismSize n);
uint64_t extism_current_plugin_memory_alloc(ExtismCurrentPlugin *plugin, ExtismSize n);
/**
* Get the length of an allocated block
* NOTE: this should only be called from host functions.
*/
ExtismSize extism_current_plugin_memory_length(ExtismCurrentPlugin *plugin, ExtismMemoryHandle n);
ExtismSize extism_current_plugin_memory_length(ExtismCurrentPlugin *plugin, ExtismSize n);
/**
* Free an allocated memory block
* NOTE: this should only be called from host functions.
*/
void extism_current_plugin_memory_free(ExtismCurrentPlugin *plugin, ExtismMemoryHandle ptr);
void extism_current_plugin_memory_free(ExtismCurrentPlugin *plugin, uint64_t ptr);
/**
* Create a new host function
@@ -146,16 +150,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
*
@@ -165,42 +169,61 @@ void extism_function_set_namespace(ExtismFunction *ptr, const char *namespace_);
* `n_functions`: the number of functions provided
* `with_wasi`: enables/disables WASI
*/
ExtismPlugin *extism_plugin_new(const uint8_t *wasm,
ExtismSize wasm_size,
const ExtismFunction **functions,
ExtismSize n_functions,
bool with_wasi,
char **errmsg);
ExtismPlugin extism_plugin_new(ExtismContext *ctx,
const uint8_t *wasm,
ExtismSize wasm_size,
const ExtismFunction **functions,
ExtismSize n_functions,
bool with_wasi);
/**
* Free the error returned by `extism_plugin_new`, errors returned from `extism_plugin_error` don't need to be freed
* 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
*/
void extism_plugin_new_error_free(char *err);
bool extism_plugin_update(ExtismContext *ctx,
ExtismPlugin index,
const uint8_t *wasm,
ExtismSize wasm_size,
const ExtismFunction **functions,
ExtismSize nfunctions,
bool with_wasi);
/**
* Remove a plugin from the registry and free associated memory
*/
void extism_plugin_free(ExtismPlugin *plugin);
void extism_plugin_free(ExtismContext *ctx, ExtismPlugin plugin);
/**
* Get plugin ID for cancellation
*/
const ExtismCancelHandle *extism_plugin_cancel_handle(const ExtismPlugin *plugin);
const ExtismCancelHandle *extism_plugin_cancel_handle(ExtismContext *ctx, 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(ExtismPlugin *plugin, const uint8_t *json, ExtismSize json_size);
bool extism_plugin_config(ExtismContext *ctx,
ExtismPlugin plugin,
const uint8_t *json,
ExtismSize json_size);
/**
* Returns true if `func_name` exists
*/
bool extism_plugin_function_exists(ExtismPlugin *plugin, const char *func_name);
bool extism_plugin_function_exists(ExtismContext *ctx, ExtismPlugin plugin, const char *func_name);
/**
* Call a function
@@ -209,30 +232,27 @@ bool extism_plugin_function_exists(ExtismPlugin *plugin, const char *func_name);
* `data`: is the input data
* `data_len`: is the length of `data`
*/
int32_t extism_plugin_call(ExtismPlugin *plugin,
int32_t extism_plugin_call(ExtismContext *ctx,
ExtismPlugin plugin_id,
const char *func_name,
const uint8_t *data,
ExtismSize data_len);
/**
* Get the error associated with a `Plugin`
* Get the error associated with a `Context` or `Plugin`, if `plugin` is `-1` then the context
* error will be returned
*/
const char *extism_error(ExtismPlugin *plugin);
/**
* Get the error associated with a `Plugin`
*/
const char *extism_plugin_error(ExtismPlugin *plugin);
const char *extism_error(ExtismContext *ctx, ExtismPlugin plugin);
/**
* Get the length of a plugin's output data
*/
ExtismSize extism_plugin_output_length(ExtismPlugin *plugin);
ExtismSize extism_plugin_output_length(ExtismContext *ctx, ExtismPlugin plugin);
/**
* Get a pointer to the output data
*/
const uint8_t *extism_plugin_output_data(ExtismPlugin *plugin);
const uint8_t *extism_plugin_output_data(ExtismContext *ctx, ExtismPlugin plugin);
/**
* Set log file and level

145
runtime/src/context.rs Normal file
View File

@@ -0,0 +1,145 @@
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

@@ -1,379 +0,0 @@
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)?;
}
}
// Enable WASI output, typically used for debugging purposes
if std::env::var("EXTISM_ENABLE_WASI_OUTPUT").is_ok() {
ctx = ctx.inherit_stdout().inherit_stderr();
}
#[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) }
}
}

Binary file not shown.

View File

@@ -1,6 +1,6 @@
use crate::{CurrentPlugin, Error};
use crate::{Error, Internal};
/// An enumeration of all possible value types in WebAssembly.
/// A list of all possible value types in WebAssembly.
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
#[repr(C)]
pub enum ValType {
@@ -54,8 +54,6 @@ 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)>,
@@ -68,8 +66,6 @@ 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)>,
@@ -81,7 +77,6 @@ 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 {
@@ -91,13 +86,11 @@ impl UserData {
}
}
/// Returns `true` if the underlying pointer is `null`
pub fn is_null(&self) -> bool {
self.ptr.is_null()
}
/// Get the user data pointer
pub(crate) fn as_ptr(&self) -> *mut std::ffi::c_void {
pub fn as_ptr(&self) -> *mut std::ffi::c_void {
self.ptr
}
@@ -109,8 +102,6 @@ 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;
@@ -119,8 +110,6 @@ 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;
@@ -157,11 +146,10 @@ impl Drop for UserData {
unsafe impl Send for UserData {}
unsafe impl Sync for UserData {}
type FunctionInner = dyn Fn(wasmtime::Caller<CurrentPlugin>, &[wasmtime::Val], &mut [wasmtime::Val]) -> Result<(), Error>
type FunctionInner = dyn Fn(wasmtime::Caller<Internal>, &[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,
@@ -172,7 +160,6 @@ pub struct Function {
}
impl Function {
/// Create a new host function
pub fn new<F>(
name: impl Into<String>,
args: impl IntoIterator<Item = ValType>,
@@ -182,7 +169,7 @@ impl Function {
) -> Function
where
F: 'static
+ Fn(&mut CurrentPlugin, &[Val], &mut [Val], UserData) -> Result<(), Error>
+ Fn(&mut Internal, &[Val], &mut [Val], UserData) -> Result<(), Error>
+ Sync
+ Send,
{

View File

@@ -1,3 +1,5 @@
use std::collections::BTreeMap;
use crate::*;
/// WASI context
@@ -10,23 +12,334 @@ 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(crate) trait Internal {
fn store(&self) -> &Store<CurrentPlugin>;
pub trait InternalExt {
fn store(&self) -> &Store<Internal>;
fn store_mut(&mut self) -> &mut Store<CurrentPlugin>;
fn store_mut(&mut self) -> &mut Store<Internal>;
fn linker(&self) -> &Linker<CurrentPlugin>;
fn linker(&self) -> &Linker<Internal>;
fn linker_mut(&mut self) -> &mut Linker<CurrentPlugin>;
fn linker_mut(&mut self) -> &mut Linker<Internal>;
fn linker_and_store(&mut self) -> (&mut Linker<CurrentPlugin>, &mut Store<CurrentPlugin>);
fn linker_and_store(&mut self) -> (&mut Linker<Internal>, &mut Store<Internal>);
fn current_plugin(&self) -> &CurrentPlugin {
fn internal(&self) -> &Internal {
self.store().data()
}
fn current_plugin_mut(&mut self) -> &mut CurrentPlugin {
fn internal_mut(&mut self) -> &mut Internal {
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,73 +1,37 @@
pub(crate) use std::collections::BTreeMap;
pub use anyhow::Error;
pub(crate) use wasmtime::*;
pub use anyhow::Error;
mod current_plugin;
mod context;
mod function;
mod internal;
pub(crate) mod manifest;
pub mod manifest;
pub(crate) mod pdk;
mod plugin;
mod plugin_builder;
mod plugin_ref;
pub mod sdk;
mod timer;
pub use current_plugin::{CurrentPlugin, MemoryHandle};
pub use extism_manifest::Manifest;
pub use context::Context;
pub use function::{Function, UserData, Val, ValType};
pub use internal::{Internal, InternalExt, Wasi};
pub use manifest::Manifest;
pub use plugin::Plugin;
pub use plugin_builder::PluginBuilder;
pub use sdk::ExtismCancelHandle as CancelHandle;
pub(crate) use internal::{Internal, Wasi};
pub use plugin_ref::PluginRef;
pub(crate) use timer::{Timer, TimerAction};
pub type Size = u64;
pub type PluginIndex = i32;
pub(crate) use log::{debug, error, trace};
#[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())
/// 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 {
Box::new(FileAppender::builder().encoder(encoder).build(file)?)
x
};
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(())
unsafe { std::ffi::CString::from_vec_unchecked(x) }
}

View File

@@ -6,6 +6,11 @@ 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 {
@@ -161,56 +166,64 @@ fn to_module(engine: &Engine, wasm: &extism_manifest::Wasm) -> Result<(String, M
const WASM_MAGIC: [u8; 4] = [0x00, 0x61, 0x73, 0x6d];
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 mut m = modules(&t, engine)?;
m.insert("env".to_string(), extism_module);
return Ok((t, m));
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));
}
}
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 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])?;
let m = Module::new(engine, data)?;
let mut modules = BTreeMap::new();
modules.insert("env".to_string(), extism_module);
modules.insert("main".to_string(), m);
return Ok(modules);
Ok((Manifest::default(), modules))
}
for f in &manifest.wasm {
let (name, m) = to_module(engine, f)?;
modules.insert(name, m);
}
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"));
}
Ok(modules)
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
}
}

View File

@@ -22,22 +22,18 @@ macro_rules! args {
/// Params: i64 (offset)
/// Returns: i64 (offset)
pub(crate) fn config_get(
mut caller: Caller<CurrentPlugin>,
mut caller: Caller<Internal>,
input: &[Val],
output: &mut [Val],
) -> Result<(), Error> {
let data: &mut CurrentPlugin = caller.data_mut();
let data: &mut Internal = caller.data_mut();
let offset = args!(input, 0, i64) as u64;
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 = data.memory_read_str(offset)?;
let key = unsafe {
std::str::from_utf8_unchecked(std::slice::from_raw_parts(key.as_ptr(), key.len()))
};
let val = data.manifest.config.get(key);
let val = data.internal().manifest.as_ref().config.get(key);
let ptr = val.map(|x| (x.len(), x.as_ptr()));
let mem = match ptr {
Some((len, ptr)) => {
@@ -49,7 +45,7 @@ pub(crate) fn config_get(
return Ok(());
}
};
output[0] = Val::I64(mem.offset() as i64);
output[0] = Val::I64(mem as i64);
Ok(())
}
@@ -57,22 +53,18 @@ pub(crate) fn config_get(
/// Params: i64 (offset)
/// Returns: i64 (offset)
pub(crate) fn var_get(
mut caller: Caller<CurrentPlugin>,
mut caller: Caller<Internal>,
input: &[Val],
output: &mut [Val],
) -> Result<(), Error> {
let data: &mut CurrentPlugin = caller.data_mut();
let data: &mut Internal = caller.data_mut();
let offset = args!(input, 0, i64) as u64;
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 = data.memory_read_str(offset)?;
let key = unsafe {
std::str::from_utf8_unchecked(std::slice::from_raw_parts(key.as_ptr(), key.len()))
};
let val = data.vars.get(key);
let val = data.internal().vars.get(key);
let ptr = val.map(|x| (x.len(), x.as_ptr()));
let mem = match ptr {
Some((len, ptr)) => {
@@ -84,7 +76,7 @@ pub(crate) fn var_get(
return Ok(());
}
};
output[0] = Val::I64(mem.offset() as i64);
output[0] = Val::I64(mem as i64);
Ok(())
}
@@ -92,11 +84,11 @@ pub(crate) fn var_get(
/// Params: i64 (key offset), i64 (value offset)
/// Returns: none
pub(crate) fn var_set(
mut caller: Caller<CurrentPlugin>,
mut caller: Caller<Internal>,
input: &[Val],
_output: &mut [Val],
) -> Result<(), Error> {
let data: &mut CurrentPlugin = caller.data_mut();
let data: &mut Internal = caller.data_mut();
let mut size = 0;
for v in data.vars.values() {
@@ -112,11 +104,7 @@ pub(crate) fn var_set(
let key_offs = args!(input, 0, i64) as u64;
let key = {
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 = data.memory_read_str(key_offs)?;
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)) }
@@ -128,11 +116,8 @@ pub(crate) fn var_set(
return Ok(());
}
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();
let vlen = data.memory_length(voffset);
let value = data.memory_read(voffset, vlen).to_vec();
// Insert the value from memory into the `vars` map
data.vars.insert(key.to_string(), value);
@@ -144,7 +129,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<CurrentPlugin>,
#[allow(unused_mut)] mut caller: Caller<Internal>,
input: &[Val],
output: &mut [Val],
) -> Result<(), Error> {
@@ -160,14 +145,12 @@ pub(crate) fn http_request(
#[cfg(feature = "http")]
{
use std::io::Read;
let data: &mut CurrentPlugin = caller.data_mut();
let data: &mut Internal = caller.data_mut();
let http_req_offset = args!(input, 0, i64) as u64;
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 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 body_offset = args!(input, 1, i64) as u64;
@@ -175,7 +158,7 @@ pub(crate) fn http_request(
Ok(u) => u,
Err(e) => return Err(Error::msg(format!("Invalid URL: {e:?}"))),
};
let allowed_hosts = &data.manifest.allowed_hosts;
let allowed_hosts = &data.internal().manifest.as_ref().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| {
@@ -204,11 +187,8 @@ pub(crate) fn http_request(
}
let res = if body_offset > 0 {
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);
let len = data.memory_length(body_offset);
let buf = data.memory_read(body_offset, len);
r.send_bytes(buf)
} else {
r.call()
@@ -236,7 +216,7 @@ pub(crate) fn http_request(
.read_to_end(&mut buf)?;
let mem = data.memory_alloc_bytes(buf)?;
output[0] = Val::I64(mem.offset() as i64);
output[0] = Val::I64(mem as i64);
} else {
output[0] = Val::I64(0);
}
@@ -249,30 +229,24 @@ pub(crate) fn http_request(
/// Params: none
/// Returns: i32 (status code)
pub(crate) fn http_status_code(
mut caller: Caller<CurrentPlugin>,
mut caller: Caller<Internal>,
_input: &[Val],
output: &mut [Val],
) -> Result<(), Error> {
let data: &mut CurrentPlugin = caller.data_mut();
let data: &mut Internal = caller.data_mut();
output[0] = Val::I32(data.http_status as i32);
Ok(())
}
pub fn log(
level: log::Level,
mut caller: Caller<CurrentPlugin>,
mut caller: Caller<Internal>,
input: &[Val],
_output: &mut [Val],
) -> Result<(), Error> {
let data: &mut CurrentPlugin = caller.data_mut();
let data: &mut Internal = caller.data_mut();
let offset = args!(input, 0, i64) as u64;
let handle = match data.memory_handle(offset) {
Some(h) => h,
None => anyhow::bail!("invalid handle offset: {offset}"),
};
let buf = data.memory_read_str(handle);
let buf = data.memory_read_str(offset);
match buf {
Ok(buf) => log::log!(level, "{}", buf),
@@ -285,7 +259,7 @@ pub fn log(
/// Params: i64 (offset)
/// Returns: none
pub(crate) fn log_warn(
caller: Caller<CurrentPlugin>,
caller: Caller<Internal>,
input: &[Val],
_output: &mut [Val],
) -> Result<(), Error> {
@@ -296,7 +270,7 @@ pub(crate) fn log_warn(
/// Params: i64 (offset)
/// Returns: none
pub(crate) fn log_info(
caller: Caller<CurrentPlugin>,
caller: Caller<Internal>,
input: &[Val],
_output: &mut [Val],
) -> Result<(), Error> {
@@ -307,7 +281,7 @@ pub(crate) fn log_info(
/// Params: i64 (offset)
/// Returns: none
pub(crate) fn log_debug(
caller: Caller<CurrentPlugin>,
caller: Caller<Internal>,
input: &[Val],
_output: &mut [Val],
) -> Result<(), Error> {
@@ -318,7 +292,7 @@ pub(crate) fn log_debug(
/// Params: i64 (offset)
/// Returns: none
pub(crate) fn log_error(
caller: Caller<CurrentPlugin>,
caller: Caller<Internal>,
input: &[Val],
_output: &mut [Val],
) -> Result<(), Error> {

View File

@@ -2,86 +2,53 @@ 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(crate) modules: BTreeMap<String, Module>,
pub modules: BTreeMap<String, Module>,
/// 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>,
/// 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>,
/// 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<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,
pub(crate) runtime: Option<Runtime>,
}
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> {
impl InternalExt for Plugin {
fn store(&self) -> &Store<Internal> {
&self.store
}
fn store_mut(&mut self) -> &mut Store<CurrentPlugin> {
fn store_mut(&mut self) -> &mut Store<Internal> {
&mut self.store
}
fn linker(&self) -> &Linker<CurrentPlugin> {
fn linker(&self) -> &Linker<Internal> {
&self.linker
}
fn linker_mut(&mut self) -> &mut Linker<CurrentPlugin> {
fn linker_mut(&mut self) -> &mut Linker<Internal> {
&mut self.linker
}
fn linker_and_store(&mut self) -> (&mut Linker<CurrentPlugin>, &mut Store<CurrentPlugin>) {
fn linker_and_store(&mut self) -> (&mut Linker<Internal>, &mut Store<Internal>) {
(&mut self.linker, &mut self.store)
}
}
@@ -152,32 +119,14 @@ 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 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(
/// Create a new plugin from the given WASM code
pub fn new<'a>(
wasm: impl AsRef<[u8]>,
imports: impl IntoIterator<Item = Function>,
imports: impl IntoIterator<Item = &'a Function>,
with_wasi: bool,
) -> Result<Plugin, Error> {
// Create a new engine, if the `EXTISM_DEBUG` environment variable is set
// Create a new engine, if the `EXITSM_DEBUG` environment variable is set
// then we enable debug info
let engine = Engine::new(
Config::new()
@@ -186,21 +135,20 @@ impl Plugin {
.profiler(profiling_strategy()),
)?;
let mut imports = imports.into_iter();
let (manifest, modules) = manifest::load(&engine, wasm.as_ref())?;
let (manifest, modules) = Manifest::new(&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.memory.max_pages;
let mut available_pages = manifest.as_ref().memory.max_pages;
calculate_available_memory(&mut available_pages, &modules)?;
log::trace!("Available pages: {available_pages:?}");
let mut store = Store::new(
&engine,
CurrentPlugin::new(manifest, with_wasi, available_pages)?,
Internal::new(manifest, with_wasi, available_pages)?,
);
store.set_epoch_deadline(1);
store.epoch_deadline_callback(|_internal| Ok(wasmtime::UpdateDeadline::Continue(1)));
if available_pages.is_some() {
store.limiter(|internal| internal.memory_limiter.as_mut().unwrap());
@@ -210,12 +158,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 CurrentPlugin| {
wasmtime_wasi::add_to_linker(&mut linker, |x: &mut Internal| {
&mut x.wasi.as_mut().unwrap().ctx
})?;
#[cfg(feature = "nn")]
wasmtime_wasi_nn::add_to_linker(&mut linker, |x: &mut CurrentPlugin| {
wasmtime_wasi_nn::add_to_linker(&mut linker, |x: &mut Internal| {
&mut x.wasi.as_mut().unwrap().nn
})?;
}
@@ -275,54 +223,31 @@ impl Plugin {
})?;
}
let instance_pre = linker.instantiate_pre(main)?;
let id = uuid::Uuid::new_v4();
let timer_tx = Timer::tx();
let instance_pre = linker.instantiate_pre(&main)?;
let timer_id = uuid::Uuid::new_v4();
let mut plugin = Plugin {
modules,
linker,
instance: std::sync::Arc::new(std::sync::Mutex::new(None)),
instance: None,
instance_pre,
store,
runtime: None,
id,
timer_tx: timer_tx.clone(),
cancel_handle: sdk::ExtismCancelHandle { id, timer_tx },
timer_id,
cancel_handle: sdk::ExtismCancelHandle {
id: timer_id,
epoch_timer_tx: None,
},
instantiations: 0,
output: Output::default(),
_functions: imports.collect(),
needs_reset: false,
};
plugin.current_plugin_mut().store = &mut plugin.store;
plugin.current_plugin_mut().linker = &mut plugin.linker;
plugin.internal_mut().store = &mut plugin.store;
plugin.internal_mut().linker = &mut plugin.linker;
Ok(plugin)
}
// 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());
}
pub(crate) fn reset_store(&mut self) -> Result<(), Error> {
self.instance = None;
if self.instantiations > 5 {
let (main_name, main) = self
.modules
.get("main")
@@ -332,73 +257,71 @@ 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 current_plugin = self.current_plugin_mut();
current_plugin.store = store;
current_plugin.linker = linker;
let internal = self.internal_mut();
internal.store = store;
internal.linker = linker;
}
**instance_lock = None;
Ok(())
}
// 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);
pub(crate) fn instantiate(&mut self) -> Result<(), Error> {
self.instance = Some(self.instance_pre.instantiate(&mut self.store)?);
self.instantiations += 1;
if let Some(limiter) = &mut self.current_plugin_mut().memory_limiter {
if let Some(limiter) = &mut self.internal_mut().memory_limiter {
limiter.reset();
}
self.detect_guest_runtime(instance_lock);
self.initialize_guest_runtime()?;
self.detect_runtime();
self.initialize_runtime()?;
Ok(())
}
/// 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 {
/// 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 {
instance.get_func(&mut self.store, function.as_ref())
} else {
None
}
}
/// 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
/// Store input in memory and 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;
}
@@ -406,9 +329,9 @@ impl Plugin {
{
let store = &mut self.store as *mut _;
let linker = &mut self.linker as *mut _;
let current_plugin = self.current_plugin_mut();
current_plugin.store = store;
current_plugin.linker = linker;
let internal = self.internal_mut();
internal.store = store;
internal.linker = linker;
}
let bytes = unsafe { std::slice::from_raw_parts(input, len) };
@@ -420,12 +343,12 @@ impl Plugin {
error!("Call to extism_reset failed");
}
let handle = self.current_plugin_mut().memory_alloc_bytes(bytes)?;
let offs = self.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(handle.offset() as i64), Val::I64(len as i64)],
&[Val::I64(offs as i64), Val::I64(len as i64)],
&mut [],
)?;
}
@@ -435,19 +358,15 @@ impl Plugin {
/// Determine if wasi is enabled
pub fn has_wasi(&self) -> bool {
self.current_plugin().wasi.is_some()
self.internal().wasi.is_some()
}
// Do a best-effort attempt to detect any guest runtime.
fn detect_guest_runtime(
&mut self,
instance_lock: &mut std::sync::MutexGuard<Option<Instance>>,
) {
fn detect_runtime(&mut self) {
// 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(instance_lock, "hs_init") {
let reactor_init = if let Some(init) = self.get_func(instance_lock, "_initialize") {
if let Some(init) = self.get_func("hs_init") {
let reactor_init = if let Some(init) = self.get_func("_initialize") {
if init.typed::<(), ()>(&self.store()).is_err() {
trace!(
"_initialize function found with type {:?}",
@@ -461,13 +380,13 @@ impl Plugin {
} else {
None
};
self.runtime = Some(GuestRuntime::Haskell { init, reactor_init });
self.runtime = Some(Runtime::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(instance_lock, "__wasm_call_ctors") {
let init = if let Some(init) = self.get_func("__wasm_call_ctors") {
if init.typed::<(), ()>(&self.store()).is_err() {
trace!(
"__wasm_call_ctors function found with type {:?}",
@@ -477,7 +396,7 @@ impl Plugin {
}
trace!("WASI runtime detected");
init
} else if let Some(init) = self.get_func(instance_lock, "_initialize") {
} else if let Some(init) = self.get_func("_initialize") {
if init.typed::<(), ()>(&self.store()).is_err() {
trace!(
"_initialize function found with type {:?}",
@@ -491,18 +410,17 @@ impl Plugin {
return;
};
self.runtime = Some(GuestRuntime::Wasi { init });
self.runtime = Some(Runtime::Wasi { init });
trace!("No runtime detected");
}
// Initialize the guest runtime
pub(crate) fn initialize_guest_runtime(&mut self) -> Result<(), Error> {
pub(crate) fn initialize_runtime(&mut self) -> Result<(), Error> {
let mut store = &mut self.store;
if let Some(runtime) = &self.runtime {
trace!("Plugin::initialize_runtime");
match runtime {
GuestRuntime::Haskell { init, reactor_init } => {
Runtime::Haskell { init, reactor_init } => {
if let Some(reactor_init) = reactor_init {
reactor_init.call(&mut store, &[], &mut [])?;
}
@@ -514,7 +432,7 @@ impl Plugin {
)?;
debug!("Initialized Haskell language runtime");
}
GuestRuntime::Wasi { init } => {
Runtime::Wasi { init } => {
init.call(&mut store, &[], &mut [])?;
debug!("Initialied WASI runtime");
}
@@ -524,207 +442,45 @@ impl Plugin {
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)
}
// 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(
/// Start the timer for a Plugin - this is used for both timeouts
/// and cancellation
pub(crate) fn start_timer(
&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();
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(|_| Ok(UpdateDeadline::Continue(1)));
.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(())
}
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);
/// 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 })?;
}
// 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
self.store
.epoch_deadline_callback(|_internal| Ok(wasmtime::UpdateDeadline::Continue(1)));
Ok(())
}
}
// Enumerates the PDK languages that need some additional initialization
// Enumerates the supported PDK language runtimes
#[derive(Clone)]
pub(crate) enum GuestRuntime {
pub(crate) enum Runtime {
Haskell {
init: Func,
reactor_init: Option<Func>,

95
runtime/src/plugin_ref.rs Normal file
View File

@@ -0,0 +1,95 @@
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,8 +5,6 @@ use std::str::FromStr;
use crate::*;
pub type ExtismMemoryHandle = u64;
/// A union type for host function argument/return values
#[repr(C)]
pub union ValUnion {
@@ -24,15 +22,18 @@ pub struct ExtismVal {
v: ValUnion,
}
#[repr(C)]
pub struct ExtismPluginResult {
pub plugin: *mut Plugin,
pub error: *mut std::ffi::c_char,
/// Wraps host functions
pub struct ExtismFunction(Function);
impl From<Function> for ExtismFunction {
fn from(x: Function) -> Self {
ExtismFunction(x)
}
}
/// Host function signature
pub type ExtismFunctionType = extern "C" fn(
plugin: *mut CurrentPlugin,
plugin: *mut Internal,
inputs: *const ExtismVal,
n_inputs: Size,
outputs: *mut ExtismVal,
@@ -72,21 +73,27 @@ impl From<&wasmtime::Val> for ExtismVal {
}
}
/// Get a plugin's ID, the returned bytes are a 16 byte buffer that represent a UUID value
/// Create a new context
#[no_mangle]
pub unsafe extern "C" fn extism_plugin_id(plugin: *mut Plugin) -> *const u8 {
if plugin.is_null() {
return std::ptr::null_mut();
}
pub unsafe extern "C" fn extism_context_new() -> *mut Context {
trace!("Creating new Context");
Box::into_raw(Box::new(Context::new()))
}
let plugin = &mut *plugin;
plugin.id.as_bytes().as_ptr()
/// Free a context
#[no_mangle]
pub unsafe extern "C" fn extism_context_free(ctx: *mut Context) {
trace!("Freeing context");
if ctx.is_null() {
return;
}
drop(Box::from_raw(ctx))
}
/// 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 CurrentPlugin) -> *mut u8 {
pub unsafe extern "C" fn extism_current_plugin_memory(plugin: *mut Internal) -> *mut u8 {
if plugin.is_null() {
return std::ptr::null_mut();
}
@@ -98,27 +105,21 @@ pub unsafe extern "C" fn extism_current_plugin_memory(plugin: *mut CurrentPlugin
/// 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 CurrentPlugin,
n: Size,
) -> ExtismMemoryHandle {
pub unsafe extern "C" fn extism_current_plugin_memory_alloc(plugin: *mut Internal, n: Size) -> u64 {
if plugin.is_null() {
return 0;
}
let plugin = &mut *plugin;
match plugin.memory_alloc(n) {
Ok(x) => x.offset(),
Err(_) => 0,
}
plugin.memory_alloc(n as u64).unwrap_or_default()
}
/// 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 CurrentPlugin,
n: ExtismMemoryHandle,
plugin: *mut Internal,
n: Size,
) -> Size {
if plugin.is_null() {
return 0;
@@ -131,18 +132,13 @@ 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 CurrentPlugin,
ptr: ExtismMemoryHandle,
) {
pub unsafe extern "C" fn extism_current_plugin_memory_free(plugin: *mut Internal, ptr: u64) {
if plugin.is_null() {
return;
}
let plugin = &mut *plugin;
if let Some(handle) = plugin.memory_handle(ptr) {
plugin.memory_free(handle);
}
plugin.memory_free(ptr);
}
/// Create a new host function
@@ -170,7 +166,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 Function {
) -> *mut ExtismFunction {
let name = match std::ffi::CStr::from_ptr(name).to_str() {
Ok(x) => x.to_string(),
Err(_) => {
@@ -229,24 +225,24 @@ pub unsafe extern "C" fn extism_function_new(
Ok(())
},
);
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))
Box::into_raw(Box::new(ExtismFunction(f)))
}
/// Set the namespace of an `ExtismFunction`
#[no_mangle]
pub unsafe extern "C" fn extism_function_set_namespace(
ptr: *mut Function,
ptr: *mut ExtismFunction,
namespace: *const std::ffi::c_char,
) {
let namespace = std::ffi::CStr::from_ptr(namespace);
let f = &mut *ptr;
f.set_namespace(namespace.to_string_lossy().to_string());
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))
}
/// Create a new plugin with additional host functions
@@ -258,14 +254,15 @@ pub unsafe extern "C" fn extism_function_set_namespace(
/// `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 Function,
functions: *mut *const ExtismFunction,
n_functions: Size,
with_wasi: bool,
errmsg: *mut *mut std::ffi::c_char,
) -> *mut Plugin {
) -> PluginIndex {
trace!("Call to extism_plugin_new with wasm pointer {:?}", wasm);
let ctx = &mut *ctx;
let data = std::slice::from_raw_parts(wasm, wasm_size as usize);
let mut funcs = vec![];
@@ -276,69 +273,100 @@ pub unsafe extern "C" fn extism_plugin_new(
if f.is_null() {
continue;
}
let f = (*f).clone();
funcs.push(f);
let f = &*f;
funcs.push(&f.0);
}
}
}
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)),
}
ctx.new_plugin(data, funcs, with_wasi)
}
/// Free the error returned by `extism_plugin_new`, errors returned from `extism_plugin_error` don't need to be freed
/// 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
#[no_mangle]
pub unsafe extern "C" fn extism_plugin_new_error_free(err: *mut std::ffi::c_char) {
if err.is_null() {
return;
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);
}
}
}
drop(std::ffi::CString::from_raw(err))
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
}
/// Remove a plugin from the registry and free associated memory
#[no_mangle]
pub unsafe extern "C" fn extism_plugin_free(plugin: *mut Plugin) {
if plugin.is_null() {
pub unsafe extern "C" fn extism_plugin_free(ctx: *mut Context, plugin: PluginIndex) {
if plugin < 0 || ctx.is_null() {
return;
}
let plugin = Box::from_raw(plugin);
trace!("Freeing plugin {}", plugin.id);
drop(plugin)
trace!("Freeing plugin {plugin}");
let ctx = &mut *ctx;
ctx.remove(plugin);
}
#[derive(Clone)]
pub struct ExtismCancelHandle {
pub(crate) timer_tx: std::sync::mpsc::Sender<TimerAction>,
pub(crate) epoch_timer_tx: Option<std::sync::mpsc::SyncSender<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(
plugin: *const Plugin,
ctx: *mut Context,
plugin: PluginIndex,
) -> *const ExtismCancelHandle {
if plugin.is_null() {
return std::ptr::null();
}
let plugin = &*plugin;
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();
&plugin.cancel_handle as *const _
}
@@ -346,38 +374,56 @@ 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;
handle.cancel().is_ok()
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();
}
/// Update plugin config values, this will merge with the existing values
#[no_mangle]
pub unsafe extern "C" fn extism_plugin_config(
plugin: *mut Plugin,
ctx: *mut Context,
plugin: PluginIndex,
json: *const u8,
json_size: Size,
) -> bool {
if plugin.is_null() {
return false;
}
let plugin = &mut *plugin;
let _lock = plugin.instance.clone();
let _lock = _lock.lock().unwrap();
let ctx = &mut *ctx;
let mut plugin_ref = match PluginRef::new(ctx, plugin, true) {
None => return false,
Some(p) => p,
};
trace!(
"Call to extism_plugin_config for {} with json pointer {:?}",
plugin.id,
plugin_ref.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.return_error(e, false);
return plugin.error(e, false);
}
};
let wasi = &mut plugin.current_plugin_mut().wasi;
let wasi = &mut plugin.internal_mut().wasi;
if let Some(Wasi { ctx, .. }) = wasi {
for (k, v) in json.iter() {
match v {
@@ -391,7 +437,7 @@ pub unsafe extern "C" fn extism_plugin_config(
}
}
let config = &mut plugin.current_plugin_mut().manifest.config;
let config = &mut plugin.internal_mut().manifest.as_mut().config;
for (k, v) in json.into_iter() {
match v {
Some(v) => {
@@ -405,22 +451,21 @@ 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(
plugin: *mut Plugin,
ctx: *mut Context,
plugin: PluginIndex,
func_name: *const c_char,
) -> bool {
if plugin.is_null() {
return false;
}
let plugin = &mut *plugin;
let _lock = plugin.instance.clone();
let _lock = _lock.lock().unwrap();
let ctx = &mut *ctx;
let mut plugin = match PluginRef::new(ctx, plugin, true) {
None => return false,
Some(p) => p,
};
let name = std::ffi::CStr::from_ptr(func_name);
trace!("Call to extism_plugin_function_exists for: {:?}", name);
@@ -428,12 +473,11 @@ pub unsafe extern "C" fn extism_plugin_function_exists(
let name = match name.to_str() {
Ok(x) => x,
Err(e) => {
return plugin.return_error(e, false);
return plugin.as_mut().error(e, false);
}
};
plugin.clear_error();
plugin.function_exists(name)
plugin.as_mut().get_func(name).is_some()
}
/// Call a function
@@ -443,90 +487,196 @@ 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(
plugin: *mut Plugin,
ctx: *mut Context,
plugin_id: PluginIndex,
func_name: *const c_char,
data: *const u8,
data_len: Size,
) -> i32 {
if plugin.is_null() {
return -1;
}
let plugin = &mut *plugin;
let lock = plugin.instance.clone();
let mut lock = lock.lock().unwrap();
let ctx = &mut *ctx;
// Get function name
let name = std::ffi::CStr::from_ptr(func_name);
let name = match name.to_str() {
Ok(name) => name,
Err(e) => return plugin.return_error(e, -1),
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),
};
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);
// 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());
match res {
Err((e, rc)) => plugin.return_error(e, rc),
Ok(x) => x,
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()
}
}
}
/// Get the error associated with a `Plugin`
/// Get the error associated with a `Context` or `Plugin`, if `plugin` is `-1` then the context
/// error will be returned
#[no_mangle]
#[deprecated]
pub unsafe extern "C" fn extism_error(plugin: *mut Plugin) -> *const c_char {
extism_plugin_error(plugin)
}
pub unsafe extern "C" fn extism_error(ctx: *mut Context, plugin: PluginIndex) -> *const c_char {
trace!("Call to extism_error for plugin {plugin}");
/// 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 ctx = &mut *ctx;
if !ctx.plugin_exists(plugin) {
return get_context_error(ctx);
}
let plugin = &mut *plugin;
let _lock = plugin.instance.clone();
let _lock = _lock.lock().unwrap();
trace!("Call to extism_plugin_error for plugin {}", plugin.id);
if plugin.output.error_offset == 0 {
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 {
trace!("Error is NULL");
return std::ptr::null();
}
plugin
.current_plugin_mut()
.memory_ptr()
.add(plugin.output.error_offset as usize) as *const _
plugin.memory_ptr().add(output[0].unwrap_i64() as usize) as *const _
}
/// Get the length of a plugin's output data
#[no_mangle]
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
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
}
/// Get a pointer to the output data
#[no_mangle]
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);
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}");
let ptr = plugin.current_plugin_mut().memory_ptr();
ptr.add(plugin.output.offset as usize)
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)
}
/// Set log file and level
@@ -535,7 +685,11 @@ pub unsafe extern "C" fn extism_log_file(
filename: *const c_char,
log_level: *const c_char,
) -> bool {
use log::Level;
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;
let file = if !filename.is_null() {
let file = std::ffi::CStr::from_ptr(filename);
@@ -561,16 +715,56 @@ pub unsafe extern "C" fn extism_log_file(
"error"
};
let level = match Level::from_str(&level.to_ascii_lowercase()) {
let level = match LevelFilter::from_str(level) {
Ok(x) => x,
Err(_) => {
return false;
}
};
set_log_file(file, level).is_ok()
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
}
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 {

View File

@@ -1,266 +0,0 @@
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);
}
}
#[test]
fn test_toml_manifest() {
let f = Function::new(
"hello_world",
[ValType::I64],
[ValType::I64],
None,
hello_world,
);
let manifest = Manifest::new([extism_manifest::Wasm::data(WASM)])
.with_timeout(std::time::Duration::from_secs(1));
let manifest_toml = toml::to_string_pretty(&manifest).unwrap();
let mut plugin = Plugin::new(manifest_toml.as_bytes(), [f], true).unwrap();
let output = plugin.call("count_vowels", "abc123").unwrap();
let count: serde_json::Value = serde_json::from_slice(output).unwrap();
assert_eq!(count.get("count").unwrap().as_i64().unwrap(), 1);
}

View File

@@ -16,38 +16,18 @@ pub(crate) enum TimerAction {
}
pub(crate) struct Timer {
pub tx: std::sync::mpsc::Sender<TimerAction>,
pub tx: std::sync::mpsc::SyncSender<TimerAction>,
pub thread: Option<std::thread::JoinHandle<()>>,
}
#[cfg(not(target_family = "windows"))]
extern "C" fn cleanup_timer() {
let mut timer = match unsafe { TIMER.lock() } {
Ok(x) => x,
Err(e) => e.into_inner(),
};
drop(timer.take());
drop(Context::timer().take())
}
static mut TIMER: std::sync::Mutex<Option<Timer>> = std::sync::Mutex::new(None);
impl Timer {
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();
pub fn init(timer: &mut Option<Timer>) -> std::sync::mpsc::SyncSender<TimerAction> {
let (tx, rx) = std::sync::mpsc::sync_channel(128);
let thread = std::thread::spawn(move || {
let mut plugins = std::collections::BTreeMap::new();

17
rust/Cargo.toml Normal file
View File

@@ -0,0 +1,17 @@
[package]
name = "extism"
version = "0.5.1"
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.2", path = "../runtime"}
serde_json = "1"
log = "0.4"
anyhow = "1"
uuid = { version = "1", features = ["v4"] }

Some files were not shown because too many files have changed in this diff Show More