mirror of
https://github.com/extism/extism.git
synced 2026-01-12 07:18:02 -05:00
Compare commits
7 Commits
fix-dotnet
...
throwaway
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14d7eae99c | ||
|
|
e89ddd5a2a | ||
|
|
93392e0884 | ||
|
|
4ebd0eb372 | ||
|
|
8feee0c693 | ||
|
|
773ab32a45 | ||
|
|
6a041d0c39 |
22
.github/workflows/ci-rust.yml
vendored
22
.github/workflows/ci-rust.yml
vendored
@@ -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 }}
|
||||
|
||||
8
.github/workflows/release-rust.yaml
vendored
8
.github/workflows/release-rust.yaml
vendored
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
version = 0.26.0
|
||||
version = 0.24.1
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
members = [
|
||||
"manifest",
|
||||
"runtime",
|
||||
"rust",
|
||||
"libextism",
|
||||
]
|
||||
exclude = ["kernel"]
|
||||
|
||||
25
c/main.c
25
c/main.c
@@ -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;
|
||||
}
|
||||
|
||||
183
cpp/extism.hpp
183
cpp/extism.hpp
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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> ¶ms,
|
||||
std::vector<Val> &results, void *user_data) {
|
||||
auto offs = plugin.alloc(10);
|
||||
memcpy(plugin.memory() + offs, "testing123", 10);
|
||||
results[0].v.i64 = (int64_t)offs;
|
||||
});
|
||||
auto functions = std::vector<Function>{
|
||||
hello_world,
|
||||
};
|
||||
Plugin plugin(wasm, true, functions);
|
||||
|
||||
std::vector<std::thread> threads;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
threads.push_back(std::thread(callThread, &plugin));
|
||||
}
|
||||
|
||||
for (auto &th : threads) {
|
||||
th.join();
|
||||
}
|
||||
}
|
||||
|
||||
}; // namespace
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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!"))
|
||||
|
||||
Binary file not shown.
191
dotnet/src/Extism.Sdk/Context.cs
Normal file
191
dotnet/src/Extism.Sdk/Context.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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<string> {
|
||||
/// "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<string, string>
|
||||
/// {
|
||||
/// { "/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<string, string>
|
||||
/// {
|
||||
/// { "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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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>
|
||||
@@ -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)))
|
||||
|
||||
@@ -5,7 +5,7 @@ prepare:
|
||||
mix compile
|
||||
|
||||
test: prepare
|
||||
mix test -v
|
||||
mix test
|
||||
|
||||
clean:
|
||||
mix clean
|
||||
|
||||
@@ -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")
|
||||
```
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
|
||||
64
elixir/lib/extism/context.ex
Normal file
64
elixir/lib/extism/context.ex
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
215
extism.go
@@ -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}
|
||||
}
|
||||
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
48
haskell/src/Extism/CurrentPlugin.hs
Normal file
48
haskell/src/Extism/CurrentPlugin.hs
Normal 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
])
|
||||
|
||||
90
java/src/main/java/org/extism/sdk/Context.java
Normal file
90
java/src/main/java/org/extism/sdk/Context.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
23
java/src/test/java/org/extism/sdk/ContextTests.java
Normal file
23
java/src/test/java/org/extism/sdk/ContextTests.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
735
node/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1 +1 @@
|
||||
version = 0.26.0
|
||||
version = 0.24.1
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
(executable
|
||||
(name main)
|
||||
(package extism)
|
||||
(public_name extism-call)
|
||||
(public_name extism-run)
|
||||
(libraries extism cmdliner))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
19
ocaml/lib/context.ml
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,6 +2,7 @@ from .extism import (
|
||||
Error,
|
||||
Plugin,
|
||||
set_log_file,
|
||||
Context,
|
||||
extism_version,
|
||||
host_fn,
|
||||
Function,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
13
ruby/Gemfile
13
ruby/Gemfile
@@ -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
|
||||
|
||||
27
ruby/bin/irb
27
ruby/bin/irb
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
27
ruby/bin/ri
27
ruby/bin/ri
@@ -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")
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Extism
|
||||
VERSION = '1.0.0-rc.1'
|
||||
VERSION = '0.5.0'
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
102
runtime/extism.h
102
runtime/extism.h
@@ -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
145
runtime/src/context.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
95
runtime/src/plugin_ref.rs
Normal 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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
17
rust/Cargo.toml
Normal 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
Reference in New Issue
Block a user