Compare commits

..

1 Commits

Author SHA1 Message Date
Steve Manuel
ce67aecf61 ci: add multiple version test strategy for java sdk 2023-01-10 09:14:37 -07:00
190 changed files with 8835 additions and 14382 deletions

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
os: [ubuntu-latest, macos-latest, windows-latest]
rust:
- stable
steps:

View File

@@ -34,6 +34,6 @@ jobs:
- name: Test Go Host SDK
run: |
go version
LD_LIBRARY_PATH=/usr/local/lib go test
cd go
LD_LIBRARY_PATH=/usr/local/lib go run main.go | grep "Hello from Go!"
LD_LIBRARY_PATH=/usr/local/lib go run main.go
LD_LIBRARY_PATH=/usr/local/lib go test

View File

@@ -18,7 +18,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
version: [11, 17]
version: [8, 11, 17]
rust:
- stable
steps:
@@ -33,5 +33,10 @@ jobs:
- name: Test Java
run: |
cd java
cat pom.xml | sed 's/<java.version>17/<java.version>${{ matrix.version }}/' > updated.xml
mv updated.xml pom.xml
mvn --batch-mode -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn verify
#- name: Examine logs
# if: success() || failure()
# run: |
# cat /tmp/extism.log

View File

@@ -12,8 +12,9 @@ on:
name: Rust CI
env:
RUNTIME_CRATE: extism
RUNTIME_CRATE: extism-runtime
LIBEXTISM_CRATE: libextism
RUST_SDK_CRATE: extism
jobs:
lib:
@@ -85,9 +86,20 @@ jobs:
- name: Lint
run: cargo clippy --release --all-features --no-deps -p ${{ env.RUNTIME_CRATE }}
- name: Test
run: cargo test --release -p ${{ env.RUNTIME_CRATE }}
- name: Test all features
run: cargo test --all-features --release -p ${{ env.RUNTIME_CRATE }}
- name: Test no features
run: cargo test --no-default-features --release -p ${{ env.RUNTIME_CRATE }}
rust:
name: Rust
needs: lib
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
rust:
- stable
steps:
- name: Checkout sources
uses: actions/checkout@v3
- uses: ./.github/actions/extism
- name: Test Rust Host SDK
run: LD_LIBRARY_PATH=/usr/local/lib cargo test --release -p ${{ env.RUST_SDK_CRATE }}

View File

@@ -18,7 +18,6 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
zig_version: ["master"] # eventually use multiple versions once stable
rust:
- stable
steps:
@@ -27,8 +26,6 @@ jobs:
- uses: ./.github/actions/extism
- name: Setup Zig env
uses: goto-bus-stop/setup-zig@v2
with:
version: ${{ matrix.zig_version }}
- name: Test Zig Host SDK
run: |

View File

@@ -1,7 +1,7 @@
on:
workflow_dispatch:
name: Release Haskell SDK
name: Release Rust SDK
jobs:
release-sdks:
@@ -10,6 +10,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: cachix/haskell-release-action@v1
with:
- hackage-token: "${{ secrets.HACKAGE_TOKEN }}"

View File

@@ -19,19 +19,17 @@ jobs:
override: true
target: ${{ matrix.target }}
- name: Release Rust Manifest Crate
- name: Release Rust Host SDK
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_API_TOKEN }}
run: |
# 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
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_API_TOKEN }}
run: |
cargo publish --manifest-path runtime/Cargo.toml --no-verify
cargo publish --manifest-path rust/Cargo.toml

6
.gitignore vendored
View File

@@ -30,15 +30,9 @@ rust/test.log
duniverse
_build
php/Extism.php
python/docs
dist-newstyle
.stack-work
vendor
zig/zig-*
zig/example-out/
zig/*.log
java/*.iml
java/*.log
java/.idea
java/.DS_Store

View File

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

View File

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

View File

@@ -18,23 +18,21 @@ else
FEATURE_FLAGS=--features $(FEATURES)
endif
build:
cargo build --release $(FEATURE_FLAGS) --manifest-path libextism/Cargo.toml
.PHONY: kernel
kernel:
cd kernel && bash build.sh
.PHONY: build
lint:
cargo clippy --release --no-deps --manifest-path runtime/Cargo.toml
build:
cargo build --release $(FEATURE_FLAGS) --manifest-path libextism/Cargo.toml
debug:
RUSTFLAGS=-g $(MAKE) build
install:
mkdir -p $(DEST)/lib $(DEST)/include
install runtime/extism.h $(DEST)/include/extism.h
install target/release/libextism.$(SOEXT) $(DEST)/lib/libextism.$(SOEXT)
install runtime/extism.h $(DEST)/include
install target/release/libextism.$(SOEXT) $(DEST)/lib
uninstall:
rm -f $(DEST)/include/extism.h $(DEST)/lib/libextism.$(SOEXT)

View File

@@ -1,8 +1,6 @@
### _Welcome!_
**Please note:** This project still under active development and APIs may change until we hit v1.0.
If you're interested in working on or building with Extism, please join our [Discord](https://discord.gg/cx3usBCWnc) and let us know - we are happy to help get you started.
**Please note:** this project still under active development. It's usable, but expect some rough edges while work is underway. If you're interested in working on or building with Extism, please join our [Discord](https://discord.gg/cx3usBCWnc) and let us know - we are happy to help get you started.
[![Discord](https://img.shields.io/discord/1011124058408112148?color=%23404eed&label=Community%20Chat&logo=Discord&logoColor=%23404eed)](https://discord.gg/cx3usBCWnc)
@@ -64,4 +62,5 @@ Extism is an open-source product from the team at:
</p>
_Reach out and tell us what you're building! We'd love to help._

View File

@@ -103,11 +103,7 @@
}
async loadFunctions(url) {
let helloWorld = function(index){
console.log("Hello, " + this.allocator.getString(index));
return index;
};
let plugin = await this.extismContext.newPlugin({ "wasm": [ { "path": url } ] }, {"hello_world": helloWorld});
let plugin = await this.extismContext.newPlugin({ "wasm": [ { "path": url } ] })
let functions = Object.keys(await plugin.getExports())
console.log("funcs ", functions)
this.setState({functions})
@@ -139,13 +135,7 @@
async handleOnRun(e) {
e && e.preventDefault && e.preventDefault();
let helloWorld = function(index){
console.log("Hello, " + this.allocator.getString(index));
return index;
};
let plugin = await this.extismContext.newPlugin({ "wasm": [ { "path": this.state.url } ] }, {
"hello_world": helloWorld
});
let plugin = await this.extismContext.newPlugin({ "wasm": [ { "path": this.state.url } ] })
let result = await plugin.call(this.state.func_name, this.state.input)
let output = result
this.setState({output})

View File

@@ -1,5 +1,5 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
testEnvironment: 'node',
};

5145
browser/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@extism/runtime-browser",
"version": "0.3.0",
"version": "0.2.2",
"description": "Extism runtime in the browser",
"scripts": {
"build": "node build.js && tsc --emitDeclarationOnly --outDir dist",
@@ -23,9 +23,7 @@
"devDependencies": {
"@types/jest": "^29.2.2",
"esbuild": "^0.15.13",
"esbuild-jest": "^0.5.0",
"jest": "^29.2.2",
"jest-environment-jsdom": "^29.3.1",
"prettier": "^2.7.1",
"ts-jest": "^29.0.3",
"tslint": "^6.1.3",
@@ -34,6 +32,6 @@
"typescript": "^4.8.4"
},
"dependencies": {
"@bjorn3/browser_wasi_shim": "^0.2.7"
"@bjorn3/browser_wasi_shim": "^0.2.1"
}
}

View File

@@ -1,5 +1,5 @@
import { Manifest, PluginConfig, ManifestWasmFile, ManifestWasmData } from './manifest';
import { ExtismPlugin } from './plugin';
import ExtismPlugin from './plugin';
/**
* Can be a {@link Manifest} or just the raw bytes of the WASM module as an ArrayBuffer.
@@ -20,7 +20,7 @@ export default class ExtismContext {
* @param config - Config details for the plugin
* @returns A new Plugin scoped to this Context
*/
async newPlugin(manifest: ManifestData, functions: Record<string, any> = {}, config?: PluginConfig) {
async newPlugin(manifest: ManifestData, config?: PluginConfig) {
let moduleData: ArrayBuffer | null = null;
if (manifest instanceof ArrayBuffer) {
moduleData = manifest;
@@ -40,6 +40,6 @@ export default class ExtismContext {
throw Error(`Unsure how to interpret manifest ${manifest}`);
}
return new ExtismPlugin(moduleData, functions, config);
return new ExtismPlugin(moduleData, config);
}
}

View File

@@ -8,17 +8,16 @@ function parse(bytes: Uint8Array): any {
describe('', () => {
it('can load and call a plugin', async () => {
// const data = fs.readFileSync(path.join(__dirname, '..', 'data', 'code.wasm'));
// const ctx = new ExtismContext();
// const plugin = await ctx.newPlugin({ wasm: [{ data: data }] });
// const functions = await plugin.getExports();
// expect(Object.keys(functions).filter((x) => !x.startsWith('__') && x !== 'memory')).toEqual(['count_vowels']);
// let output = await plugin.call('count_vowels', 'this is a test');
// expect(parse(output)).toEqual({ count: 4 });
// output = await plugin.call('count_vowels', 'this is a test again');
// expect(parse(output)).toEqual({ count: 7 });
// output = await plugin.call('count_vowels', 'this is a test thrice');
// expect(parse(output)).toEqual({ count: 6 });
expect(true).toEqual(true);
const data = fs.readFileSync(path.join(__dirname, '..', 'data', 'code.wasm'));
const ctx = new ExtismContext();
const plugin = await ctx.newPlugin({ wasm: [{ data: data }] });
const functions = await plugin.getExports();
expect(Object.keys(functions).filter((x) => !x.startsWith('__') && x !== 'memory')).toEqual(['count_vowels']);
let output = await plugin.call('count_vowels', 'this is a test');
expect(parse(output)).toEqual({ count: 4 });
output = await plugin.call('count_vowels', 'this is a test again');
expect(parse(output)).toEqual({ count: 7 });
output = await plugin.call('count_vowels', 'this is a test thrice');
expect(parse(output)).toEqual({ count: 6 });
});
});

View File

@@ -1,4 +1,3 @@
import ExtismContext from './context';
import { ExtismFunction, ExtismPlugin } from './plugin';
export { ExtismContext, ExtismFunction, ExtismPlugin };
export { ExtismContext };

View File

@@ -1,10 +1,9 @@
import Allocator from './allocator';
import { PluginConfig } from './manifest';
import { WASI, Fd } from '@bjorn3/browser_wasi_shim';
//@ts-ignore TODO add types to this library
import { WASI, File } from "@bjorn3/browser_wasi_shim";
export type ExtismFunction = any;
export class ExtismPlugin {
export default class ExtismPlugin {
moduleData: ArrayBuffer;
allocator: Allocator;
config?: PluginConfig;
@@ -12,16 +11,14 @@ export class ExtismPlugin {
input: Uint8Array;
output: Uint8Array;
module?: WebAssembly.WebAssemblyInstantiatedSource;
functions: Record<string, ExtismFunction>;
constructor(moduleData: ArrayBuffer, functions: Record<string, ExtismFunction> = {}, config?: PluginConfig) {
constructor(moduleData: ArrayBuffer, config?: PluginConfig) {
this.moduleData = moduleData;
this.allocator = new Allocator(1024 * 1024);
this.config = config;
this.vars = {};
this.input = new Uint8Array();
this.output = new Uint8Array();
this.functions = functions;
}
async getExports(): Promise<WebAssembly.Exports> {
@@ -68,31 +65,23 @@ export class ExtismPlugin {
const environment = this.makeEnv();
const args: Array<string> = [];
const envVars: Array<string> = [];
let fds: Fd[] = [
// new XtermStdio(term), // stdin
// new XtermStdio(term), // stdout
// new XtermStdio(term), // stderr
let fds = [
new File([]), // stdin
new File([]), // stdout
new File([]), // stderr
];
let wasi = new WASI(args, envVars, fds);
let env = {
wasi_snapshot_preview1: wasi.wasiImport,
env: environment,
env: environment
};
this.module = await WebAssembly.instantiate(this.moduleData, env);
// normally we would call wasi.start here but it doesn't respect when there is
// no _start function
//@ts-ignore
wasi.inst = this.module.instance;
if (this.module.instance.exports._start) {
//@ts-ignore
this.module.instance.exports._start();
}
return this.module;
}
makeEnv(): any {
const plugin = this;
var env: any = {
return {
extism_alloc(n: bigint): bigint {
return plugin.allocator.alloc(n);
},
@@ -191,13 +180,5 @@ export class ExtismPlugin {
console.error(s);
},
};
for (const [name, func] of Object.entries(this.functions)) {
env[name] = function () {
return func.apply(plugin, arguments);
};
}
return env;
}
}

View File

@@ -1,13 +1,13 @@
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"strict": true,
"skipLibCheck": true,
"allowJs": true
},
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"strict": true,
"skipLibCheck": true,
"allowJs": true
},
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}

View File

@@ -1,2 +1,2 @@
build:
clang -g -o main main.c -lextism -L .
clang -o main main.c -lextism -L .

View File

@@ -9,21 +9,6 @@
#include <sys/stat.h>
#include <unistd.h>
void hello_world(ExtismCurrentPlugin *plugin, const ExtismVal *inputs,
uint64_t n_inputs, ExtismVal *outputs, uint64_t n_outputs,
void *data) {
puts("Hello from C!");
puts(data);
ExtismSize ptr_offs = inputs[0].v.i64;
uint8_t *buf = extism_current_plugin_memory(plugin) + ptr_offs;
uint64_t length = extism_current_plugin_memory_length(plugin, ptr_offs);
fwrite(buf, length, 1, stdout);
fputc('\n', stdout);
outputs[0].v.i64 = inputs[0].v.i64;
}
uint8_t *read_file(const char *filename, size_t *len) {
FILE *fp = fopen(filename, "rb");
@@ -53,29 +38,24 @@ int main(int argc, char *argv[]) {
exit(1);
}
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);
ExtismContext *ctx = extism_context_new();
char *errmsg = NULL;
ExtismPlugin *plugin = extism_plugin_new(
data, len, (const ExtismFunction **)&f, 1, true, &errmsg);
size_t len = 0;
uint8_t *data = read_file("../wasm/code.wasm", &len);
ExtismPlugin plugin = extism_plugin_new(ctx, data, len, false);
free(data);
if (plugin == NULL) {
puts(errmsg);
extism_plugin_new_error_free(errmsg);
if (plugin < 0) {
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_context_free(ctx);
return 0;
}

View File

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

View File

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

View File

@@ -1,10 +1,7 @@
#pragma once
#include <cstring>
#include <functional>
#include <map>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
@@ -23,62 +20,27 @@ extern "C" {
namespace extism {
typedef std::map<std::string, std::string> Config;
template <typename T> class ManifestKey {
bool is_set = false;
public:
T value;
ManifestKey(T x, bool is_set = false) : is_set(is_set) { value = x; }
void set(T x) {
value = x;
is_set = true;
}
bool empty() const { return is_set == false; }
};
class Wasm {
std::string _path;
std::string _url;
// TODO: add base64 encoded raw data
ManifestKey<std::string> _hash =
ManifestKey<std::string>(std::string(), false);
public:
// Create Wasm pointing to a path
static Wasm path(std::string s, std::string hash = std::string()) {
Wasm w;
w._path = s;
if (!hash.empty()) {
w._hash.set(hash);
}
return w;
}
// Create Wasm pointing to a URL
static Wasm url(std::string s, std::string hash = std::string()) {
Wasm w;
w._url = s;
if (!hash.empty()) {
w._hash.set(hash);
}
return w;
}
std::string path;
std::string url;
// TODO: add base64 encoded raw data
std::string hash;
#ifndef EXTISM_NO_JSON
Json::Value json() const {
Json::Value doc;
if (!this->_path.empty()) {
doc["path"] = this->_path;
} else if (!this->_url.empty()) {
doc["url"] = this->_url;
if (!this->path.empty()) {
doc["path"] = this->path;
}
if (!this->_hash.empty()) {
doc["hash"] = this->_hash.value;
if (!this->url.empty()) {
doc["url"] = this->url;
}
if (!this->hash.empty()) {
doc["hash"] = this->hash;
}
return doc;
@@ -90,23 +52,18 @@ class Manifest {
public:
Config config;
std::vector<Wasm> wasm;
ManifestKey<std::vector<std::string>> allowed_hosts;
ManifestKey<std::map<std::string, std::string>> allowed_paths;
ManifestKey<uint64_t> timeout_ms;
std::vector<std::string> allowed_hosts;
std::map<std::string, std::string> allowed_paths;
uint64_t timeout_ms;
// Empty manifest
Manifest()
: timeout_ms(0, false), allowed_hosts(std::vector<std::string>(), false),
allowed_paths(std::map<std::string, std::string>(), false) {}
Manifest() : timeout_ms(30000) {}
// Create manifest with a single Wasm from a path
static Manifest path(std::string s, std::string hash = std::string()) {
Manifest m;
m.add_wasm_path(s, hash);
return m;
}
// Create manifest with a single Wasm from a URL
static Manifest url(std::string s, std::string hash = std::string()) {
Manifest m;
m.add_wasm_url(s, hash);
@@ -135,7 +92,7 @@ public:
if (!this->allowed_hosts.empty()) {
Json::Value h;
for (auto s : this->allowed_hosts.value) {
for (auto s : this->allowed_hosts) {
h.append(s);
}
doc["allowed_hosts"] = h;
@@ -143,63 +100,54 @@ public:
if (!this->allowed_paths.empty()) {
Json::Value h;
for (auto k : this->allowed_paths.value) {
for (auto k : this->allowed_paths) {
h[k.first] = k.second;
}
doc["allowed_paths"] = h;
}
if (!this->timeout_ms.empty()) {
doc["timeout_ms"] = Json::Value(this->timeout_ms.value);
}
doc["timeout_ms"] = Json::Value(this->timeout_ms);
Json::FastWriter writer;
return writer.write(doc);
}
#endif
// Add Wasm from path
void add_wasm_path(std::string s, std::string hash = std::string()) {
Wasm w = Wasm::path(s, hash);
Wasm w;
w.path = s;
w.hash = hash;
this->wasm.push_back(w);
}
// Add Wasm from URL
void add_wasm_url(std::string u, std::string hash = std::string()) {
Wasm w = Wasm::url(u, hash);
Wasm w;
w.url = u;
w.hash = hash;
this->wasm.push_back(w);
}
// Add host to allowed hosts
void allow_host(std::string host) {
if (this->allowed_hosts.empty()) {
this->allowed_hosts.set(std::vector<std::string>{});
}
this->allowed_hosts.value.push_back(host);
}
void allow_host(std::string host) { this->allowed_hosts.push_back(host); }
// Add path to allowed paths
void allow_path(std::string src, std::string dest = std::string()) {
if (this->allowed_paths.empty()) {
this->allowed_paths.set(std::map<std::string, std::string>{});
}
if (dest.empty()) {
dest = src;
}
this->allowed_paths.value[src] = dest;
this->allowed_paths[src] = dest;
}
// Set timeout
void set_timeout_ms(uint64_t ms) { this->timeout_ms = ms; }
// Set config key/value
void set_config(std::string k, std::string v) { this->config[k] = v; }
};
class Error : public std::runtime_error {
class Error : public std::exception {
private:
std::string message;
public:
Error(std::string msg) : std::runtime_error(msg) {}
Error(std::string msg) : message(msg) {}
const char *what() { return message.c_str(); }
};
class Buffer {
@@ -218,172 +166,63 @@ public:
}
};
typedef ExtismValType ValType;
typedef ExtismValUnion ValUnion;
typedef ExtismVal Val;
typedef uint64_t MemoryHandle;
class CurrentPlugin {
ExtismCurrentPlugin *pointer;
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) {
return extism_current_plugin_memory_length(this->pointer, offs);
}
MemoryHandle 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 returnString(Val &output, const std::string &s) {
this->returnBytes(output, (const uint8_t *)s.c_str(), s.size());
}
void returnBytes(Val &output, const uint8_t *bytes, size_t len) {
auto offs = this->alloc(len);
memcpy(this->memory() + offs, bytes, len);
output.v.i64 = offs;
}
uint8_t *inputBytes(Val &inp, size_t *length = nullptr) {
if (inp.t != ValType::I64) {
return nullptr;
}
if (length != nullptr) {
*length = this->memoryLength(inp.v.i64);
}
return this->memory() + inp.v.i64;
}
std::string inputString(Val &inp) {
size_t length = 0;
char *buf = (char *)this->inputBytes(inp, &length);
return std::string(buf, length);
}
};
typedef std::function<void(CurrentPlugin, const std::vector<Val> &,
std::vector<Val> &, void *user_data)>
FunctionType;
struct UserData {
FunctionType func;
void *user_data = NULL;
std::function<void(void *)> free_user_data;
};
static void function_callback(ExtismCurrentPlugin *plugin,
const ExtismVal *inputs, ExtismSize n_inputs,
ExtismVal *outputs, ExtismSize n_outputs,
void *user_data) {
UserData *data = (UserData *)user_data;
const std::vector<Val> inp(inputs, inputs + n_inputs);
std::vector<Val> outp(outputs, outputs + n_outputs);
data->func(CurrentPlugin(plugin), inp, outp, data->user_data);
for (ExtismSize i = 0; i < n_outputs; i++) {
outputs[i] = outp[i];
}
}
static void free_user_data(void *user_data) {
UserData *data = (UserData *)user_data;
if (data->user_data != NULL && data->free_user_data != NULL) {
data->free_user_data(data->user_data);
}
}
class Function {
std::shared_ptr<ExtismFunction> func;
std::string name;
UserData user_data;
public:
Function(std::string name, const std::vector<ValType> inputs,
const std::vector<ValType> outputs, FunctionType f,
void *user_data = NULL, std::function<void(void *)> free = nullptr)
: name(name) {
this->user_data.func = f;
this->user_data.user_data = user_data;
this->user_data.free_user_data = free;
auto ptr = extism_function_new(
this->name.c_str(), inputs.data(), inputs.size(), outputs.data(),
outputs.size(), function_callback, &this->user_data, free_user_data);
this->func = std::shared_ptr<ExtismFunction>(ptr, extism_function_free);
}
void setNamespace(std::string s) {
extism_function_set_namespace(this->func.get(), s.c_str());
}
Function(const Function &f) { this->func = f.func; }
ExtismFunction *get() { return this->func.get(); }
};
class CancelHandle {
const ExtismCancelHandle *handle;
public:
CancelHandle(const ExtismCancelHandle *x) : handle(x){};
bool cancel() { return extism_plugin_cancel(this->handle); }
};
class Plugin {
std::vector<Function> functions;
std::shared_ptr<ExtismContext> context;
ExtismPlugin plugin;
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>())
: functions(functions) {
std::vector<const ExtismFunction *> ptrs;
for (auto i : this->functions) {
ptrs.push_back(i.get());
Plugin(std::shared_ptr<ExtismContext> ctx, const uint8_t *wasm,
ExtismSize length, bool with_wasi = false) {
this->plugin = extism_plugin_new(ctx.get(), wasm, length, with_wasi);
if (this->plugin < 0) {
const char *err = extism_error(ctx.get(), -1);
throw Error(err == nullptr ? "Unable to load plugin" : err);
}
char *errmsg = nullptr;
this->plugin = extism_plugin_new(wasm, length, ptrs.data(), ptrs.size(),
with_wasi, &errmsg);
if (this->plugin == nullptr) {
std::string s(errmsg);
extism_plugin_new_error_free(errmsg);
throw Error(s);
}
}
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) {
}
Plugin(const std::vector<uint8_t> &data, bool with_wasi = false,
std::vector<Function> functions = {})
: Plugin(data.data(), data.size(), with_wasi, functions) {}
CancelHandle cancelHandle() {
return CancelHandle(extism_plugin_cancel_handle(this->plugin));
this->context = ctx;
}
#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) {}
Plugin(std::shared_ptr<ExtismContext> ctx, const Manifest &manifest,
bool with_wasi = false) {
auto buffer = manifest.json();
this->plugin = extism_plugin_new(ctx.get(), (const uint8_t *)buffer.c_str(),
buffer.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) {
bool b = extism_plugin_update(this->context.get(), this->plugin, wasm,
length, 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) {
auto buffer = manifest.json();
bool b = extism_plugin_update(this->context.get(), this->plugin,
(const uint8_t *)buffer.c_str(),
buffer.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 +239,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);
}
}
@@ -411,13 +251,12 @@ public:
this->config(json.c_str(), json.size());
}
// Call a plugin
Buffer call(const std::string &func, const uint8_t *input,
ExtismSize input_length) const {
int32_t rc =
extism_plugin_call(this->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,34 +264,64 @@ 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);
}
// Call a plugin function with std::vector<uint8_t> input
Buffer call(const std::string &func,
const std::vector<uint8_t> &input) const {
return this->call(func, input.data(), input.size());
}
// Call a plugin function with string input
Buffer call(const std::string &func,
const std::string &input = std::string()) const {
Buffer call(const std::string &func, const std::string &input) const {
return this->call(func, (const uint8_t *)input.c_str(), input.size());
}
// 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());
}
};
// Set global log file for plugins
inline bool setLogFile(const char *filename, const char *level) {
class Context {
public:
std::shared_ptr<ExtismContext> pointer;
Context() {
this->pointer = std::shared_ptr<ExtismContext>(extism_context_new(),
extism_context_free);
}
Plugin plugin(const uint8_t *wasm, size_t length,
bool with_wasi = false) const {
return Plugin(this->pointer, wasm, length, with_wasi);
}
Plugin plugin(const std::string &str, bool with_wasi = false) const {
return Plugin(this->pointer, (const uint8_t *)str.c_str(), str.size(),
with_wasi);
}
Plugin plugin(const std::vector<uint8_t> &data,
bool with_wasi = false) const {
return Plugin(this->pointer, data.data(), data.size(), with_wasi);
}
#ifndef EXTISM_NO_JSON
Plugin plugin(const Manifest &manifest, bool with_wasi = false) const {
return Plugin(this->pointer, manifest, with_wasi);
}
#endif
void reset() { extism_context_reset(this->pointer.get()); }
};
inline bool set_log_file(const char *filename, const char *level) {
return extism_log_file(filename, level);
}
// Get libextism version
inline std::string version() { return std::string(extism_version()); }
} // namespace extism

View File

@@ -1,7 +1,6 @@
#include "../extism.hpp"
#include <fstream>
#include <thread>
#include <gtest/gtest.h>
@@ -11,38 +10,46 @@ std::vector<uint8_t> read(const char *filename) {
std::istreambuf_iterator<char>());
}
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);
Context context;
Manifest manifest = Manifest::path("code.wasm");
manifest.set_config("a", "1");
Plugin plugin(manifest);
ASSERT_NO_THROW(Plugin plugin = context.plugin(manifest));
Plugin plugin = context.plugin(manifest);
Buffer buf = plugin.call("count_vowels", "this is a test");
ASSERT_EQ((std::string)buf, "{\"count\": 4}");
}
TEST(Plugin, BadManifest) {
Context context;
Manifest manifest;
ASSERT_THROW(Plugin plugin(manifest), Error);
ASSERT_THROW(Plugin plugin = context.plugin(manifest), Error);
}
TEST(Plugin, Bytes) {
auto wasm = read(code.c_str());
ASSERT_NO_THROW(Plugin plugin(wasm));
Plugin plugin(wasm);
Context context;
auto wasm = read("code.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) {
auto wasm = read(code.c_str());
Plugin plugin(wasm);
Context context;
auto wasm = read("code.wasm");
Plugin plugin = context.plugin(wasm);
Config config;
config["abc"] = "123";
@@ -50,63 +57,12 @@ TEST(Plugin, UpdateConfig) {
}
TEST(Plugin, FunctionExists) {
auto wasm = read(code.c_str());
Plugin plugin(wasm);
Context context;
auto wasm = read("code.wasm");
Plugin plugin = context.plugin(wasm);
ASSERT_FALSE(plugin.functionExists("bad_function"));
ASSERT_TRUE(plugin.functionExists("count_vowels"));
}
TEST(Plugin, HostFunction) {
auto wasm = read("../../wasm/code-functions.wasm");
auto t = std::vector<ValType>{ValType::I64};
Function hello_world =
Function("hello_world", t, t,
[](CurrentPlugin plugin, const std::vector<Val> &params,
std::vector<Val> &results, void *user_data) {
auto offs = plugin.alloc(4);
memcpy(plugin.memory() + offs, "test", 4);
results[0].v.i64 = (int64_t)offs;
});
auto functions = std::vector<Function>{
hello_world,
};
Plugin plugin(wasm, true, functions);
auto buf = plugin.call("count_vowels", "aaa");
ASSERT_EQ(buf.length, 4);
ASSERT_EQ((std::string)buf, "test");
}
void callThread(Plugin *plugin) {
auto buf = plugin->call("count_vowels", "aaa").string();
ASSERT_EQ(buf.size(), 10);
ASSERT_EQ(buf, "testing123");
}
TEST(Plugin, MultipleThreads) {
auto wasm = read("../../wasm/code-functions.wasm");
auto t = std::vector<ValType>{ValType::I64};
Function hello_world =
Function("hello_world", t, t,
[](CurrentPlugin plugin, const std::vector<Val> &params,
std::vector<Val> &results, void *user_data) {
auto offs = plugin.alloc(10);
memcpy(plugin.memory() + offs, "testing123", 10);
results[0].v.i64 = (int64_t)offs;
});
auto functions = std::vector<Function>{
hello_world,
};
Plugin plugin(wasm, true, functions);
std::vector<std::thread> threads;
for (int i = 0; i < 3; i++) {
threads.push_back(std::thread(callThread, &plugin));
}
for (auto &th : threads) {
th.join();
}
ASSERT_FALSE(plugin.function_exists("bad_function"));
ASSERT_TRUE(plugin.function_exists("count_vowels"));
}
}; // namespace

View File

@@ -1,24 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<NoBuild>true</NoBuild>
<IncludeBuildOutput>false</IncludeBuildOutput>
</PropertyGroup>
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<NoBuild>true</NoBuild>
<IncludeBuildOutput>false</IncludeBuildOutput>
</PropertyGroup>
<PropertyGroup>
<PackageId>Extism.runtime.win-x64</PackageId>
<Version>0.7.0</Version>
<Authors>Extism Contributors</Authors>
<Description>Internal implementation package for Extism to work on Windows x64</Description>
<Tags>extism, wasm, plugin</Tags>
<PackageLicenseExpression>BSD-3-Clause</PackageLicenseExpression>
</PropertyGroup>
<PropertyGroup>
<PackageId>Extism.runtime.win-x64</PackageId>
<Version>0.2.0</Version>
<Authors>Extism Contributors</Authors>
<Description>Internal implementation package for Extism to work on Windows x64</Description>
<Tags>extism, wasm, plugin</Tags>
<PackageLicenseExpression>BSD-3-Clause</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<Content Include="runtimes/win-x64.dll"
CopyToOutputDirectory="Always"
Pack="true"
PackagePath="runtimes\win-x64\native\extism.dll" />
</ItemGroup>
<ItemGroup>
<Content Include="runtimes/win-x64.dll"
CopyToOutputDirectory="Always"
Pack="true"
PackagePath="runtimes\win-x64\native\extism.dll" />
</ItemGroup>
</Project>

View File

@@ -1,29 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Include="..\..\..\wasm\code.wasm" Link="code.wasm">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="..\..\..\wasm\code-functions.wasm" Link="code-functions.wasm">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<None Include="..\..\..\wasm\code.wasm" Link="code.wasm">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<!-- <PackageReference Include="Extism.runtime.win-x64" Version="0.7.0" /> -->
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Extism.Sdk\Extism.Sdk.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Extism.Sdk\Extism.Sdk.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@@ -1,49 +1,11 @@
using Extism.Sdk;
using Extism.Sdk.Native;
using System.Runtime.InteropServices;
using System.Text;
Console.WriteLine($"Version: {Plugin.ExtismVersion()}");
var userData = Marshal.StringToHGlobalAnsi("Hello again!");
using var helloWorld = new HostFunction(
"hello_world",
"env",
new[] { ExtismValType.I64 },
new[] { ExtismValType.I64 },
userData,
HelloWorld);
void HelloWorld(CurrentPlugin plugin, Span<ExtismVal> inputs, Span<ExtismVal> outputs, nint data)
{
Console.WriteLine("Hello from .NET!");
var text = Marshal.PtrToStringAnsi(data);
Console.WriteLine(text);
var input = plugin.ReadString(new nint(inputs[0].v.i64));
Console.WriteLine($"Input: {input}");
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 context = new Context();
var wasm = await File.ReadAllBytesAsync("./code.wasm");
using var plugin = context.CreatePlugin(wasm, withWasi: true);
var output = Encoding.UTF8.GetString(
plugin.CallFunction("count_vowels", Encoding.UTF8.GetBytes("Hello World!"))
);
Console.WriteLine($"Output: {output}");
Console.WriteLine(output); // prints {"count": 3}

View File

@@ -0,0 +1,184 @@
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 class Context : IDisposable
{
private const int DisposedMarker = 1;
private int _disposed;
/// <summary>
/// Initialize a new Extism Context.
/// </summary>
public Context()
{
NativeHandle = LibExtism.extism_context_new();
}
/// <summary>
/// Native pointer to the Extism Context.
/// </summary>
internal IntPtr 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="withWasi">Enable/Disable WASI.</param>
public Plugin CreatePlugin(ReadOnlySpan<byte> wasm, bool withWasi)
{
CheckNotDisposed();
unsafe
{
fixed (byte* wasmPtr = wasm)
{
var plugin = LibExtism.extism_plugin_new(NativeHandle, wasmPtr, wasm.Length, withWasi);
return new Plugin(this, plugin);
}
}
}
/// <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
}
// 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);
}
}
/// <summary>
/// Extism Log Levels
/// </summary>
public enum LogLevel
{
/// <summary>
/// Designates very serious errors.
/// </summary>
Error,
/// <summary>
/// Designates hazardous situations.
/// </summary>
Warning,
/// <summary>
/// Designates useful information.
/// </summary>
Info,
/// <summary>
/// Designates lower priority information.
/// </summary>
Debug,
/// <summary>
/// Designates very low priority, often extremely verbose, information.
/// </summary>
Trace
}

View File

@@ -1,138 +0,0 @@
using Extism.Sdk.Native;
using System.Text;
namespace Extism.Sdk
{
/// <summary>
/// Represents the current plugin. Can only be used within <see cref="HostFunction"/>s.
/// </summary>
public class CurrentPlugin
{
internal CurrentPlugin(nint nativeHandle)
{
NativeHandle = nativeHandle;
}
internal nint NativeHandle { get; }
/// <summary>
/// Returns a pointer to the memory of the currently running plugin.
/// NOTE: this should only be called from host functions.
/// </summary>
/// <returns></returns>
public nint GetMemory()
{
return LibExtism.extism_current_plugin_memory(NativeHandle);
}
/// <summary>
/// Reads a string from a memory block using UTF8.
/// </summary>
/// <param name="pointer"></param>
/// <returns></returns>
public string ReadString(nint pointer)
{
return ReadString(pointer, Encoding.UTF8);
}
/// <summary>
/// Reads a string form a memory block.
/// </summary>
/// <param name="pointer"></param>
/// <param name="encoding"></param>
/// <returns></returns>
public string ReadString(nint pointer, Encoding encoding)
{
var buffer = ReadBytes(pointer);
return encoding.GetString(buffer);
}
/// <summary>
/// Returns a span of bytes for a given block.
/// </summary>
/// <param name="pointer"></param>
/// <returns></returns>
public unsafe Span<byte> ReadBytes(nint pointer)
{
var mem = GetMemory();
var length = (int)BlockLength(pointer);
var ptr = (byte*)mem + pointer;
return new Span<byte>(ptr, length);
}
/// <summary>
/// Writes a string into the current plugin memory using UTF-8 encoding and returns the pointer of the block.
/// </summary>
/// <param name="value"></param>
public nint WriteString(string value)
=> WriteString(value, Encoding.UTF8);
/// <summary>
/// Writes a string into the current plugin memory and returns the pointer of the block.
/// </summary>
/// <param name="value"></param>
/// <param name="encoding"></param>
public nint WriteString(string value, Encoding encoding)
{
var bytes = encoding.GetBytes(value);
var pointer = AllocateBlock(bytes.Length);
WriteBytes(pointer, bytes);
return pointer;
}
/// <summary>
/// Writes a byte array into a block of memory.
/// </summary>
/// <param name="pointer"></param>
/// <param name="bytes"></param>
public unsafe void WriteBytes(nint pointer, Span<byte> bytes)
{
var length = BlockLength(pointer);
if (length < bytes.Length)
{
throw new InvalidOperationException("Destination block length is less than source block length.");
}
var mem = GetMemory();
var ptr = (void*)(mem + pointer);
var destination = new Span<byte>(ptr, bytes.Length);
bytes.CopyTo(destination);
}
/// <summary>
/// Frees a block of memory belonging to the current plugin.
/// </summary>
/// <param name="pointer"></param>
public void FreeBlock(nint pointer)
{
LibExtism.extism_current_plugin_memory_free(NativeHandle, pointer);
}
/// <summary>
/// Allocate a memory block in the currently running plugin.
///
/// </summary>
/// <param name="length"></param>
/// <returns></returns>
public nint AllocateBlock(long length)
{
return LibExtism.extism_current_plugin_memory_alloc(NativeHandle, length);
}
/// <summary>
/// Get the length of an allocated block.
/// NOTE: this should only be called from host functions.
/// </summary>
/// <param name="pointer"></param>
/// <returns></returns>
public long BlockLength(nint pointer)
{
return LibExtism.extism_current_plugin_memory_length(NativeHandle, pointer);
}
}
}

View File

@@ -28,7 +28,7 @@ public class ExtismException : Exception
/// with a specified error message and a reference to the inner exception
/// that is the cause of this exception.
/// </summary>
/// <param name="message">The message that describes the error.</param>
/// <param name="message">The message that describes the error .</param>
/// <param name="innerException">
/// The exception that is the cause of the current exception, or a null reference
/// (Nothing in Visual Basic) if no inner exception is specified.

View File

@@ -1,29 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<LangVersion>11</LangVersion>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<LangVersion>10</LangVersion>
</PropertyGroup>
<PropertyGroup>
<PackageId>Extism.Sdk</PackageId>
<Version>0.7.0</Version>
<Authors>Extism Contributors</Authors>
<Description>Extism SDK that allows hosting Extism plugins in .NET apps.</Description>
<Tags>extism, wasm, plugin</Tags>
<PackageLicenseExpression>BSD-3-Clause</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<PropertyGroup>
<PackageId>Extism.Sdk</PackageId>
<Version>0.2.0</Version>
<Authors>Extism Contributors</Authors>
<Description>Extism SDK that allows hosting Extism plugins in .NET apps.</Description>
<Tags>extism, wasm, plugin</Tags>
<PackageLicenseExpression>BSD-3-Clause</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
<PackageReference Include="System.Text.Json" Version="7.0.3" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
</ItemGroup>
</Project>

View File

@@ -1,146 +0,0 @@
using Extism.Sdk.Native;
using System.Diagnostics.CodeAnalysis;
namespace Extism.Sdk
{
/// <summary>
/// A host function signature.
/// </summary>
/// <param name="plugin">Plugin Index</param>
/// <param name="inputs">Input parameters</param>
/// <param name="outputs">Output parameters, the host function can change this.</param>
/// <param name="userData">A data passed in during Host Function creation.</param>
public delegate void ExtismFunction(CurrentPlugin plugin, Span<ExtismVal> inputs, Span<ExtismVal> outputs, IntPtr userData);
/// <summary>
/// A function provided by the host that plugins can call.
/// </summary>
public class HostFunction : IDisposable
{
private const int DisposedMarker = 1;
private int _disposed;
/// <summary>
/// Registers a Host Function.
/// </summary>
/// <param name="functionName">The literal name of the function, how it would be called from a <see cref="Plugin"/>.</param>
/// <param name="inputTypes">The types of the input arguments/parameters the <see cref="Plugin"/> caller will provide.</param>
/// <param name="outputTypes">The types of the output returned from the host function to the <see cref="Plugin"/>.</param>
/// <param name="userData">An opaque pointer to an object from the host, accessible to the <see cref="Plugin"/>.
/// NOTE: it is the shared responsibility of the host and <see cref="Plugin"/> to cast/dereference this value properly.</param>
/// <param name="hostFunction"></param>
public HostFunction(
string functionName,
Span<ExtismValType> inputTypes,
Span<ExtismValType> outputTypes,
IntPtr userData,
ExtismFunction hostFunction) :
this(functionName, "", inputTypes, outputTypes, userData, hostFunction)
{
}
/// <summary>
/// Registers a Host Function.
/// </summary>
/// <param name="functionName">The literal name of the function, how it would be called from a <see cref="Plugin"/>.</param>
/// <param name="namespace">Function namespace.</param>
/// <param name="inputTypes">The types of the input arguments/parameters the <see cref="Plugin"/> caller will provide.</param>
/// <param name="outputTypes">The types of the output returned from the host function to the <see cref="Plugin"/>.</param>
/// <param name="userData">An opaque pointer to an object from the host, accessible to the <see cref="Plugin"/>.
/// NOTE: it is the shared responsibility of the host and <see cref="Plugin"/> to cast/dereference this value properly.</param>
/// <param name="hostFunction"></param>
unsafe public HostFunction(
string functionName,
string @namespace,
Span<ExtismValType> inputTypes,
Span<ExtismValType> outputTypes,
IntPtr userData,
ExtismFunction hostFunction)
{
fixed (ExtismValType* inputs = inputTypes)
fixed (ExtismValType* outputs = outputTypes)
{
NativeHandle = LibExtism.extism_function_new(functionName, inputs, inputTypes.Length, outputs, outputTypes.Length, CallbackImpl, userData, IntPtr.Zero);
}
if (!string.IsNullOrEmpty(@namespace))
{
LibExtism.extism_function_set_namespace(NativeHandle, @namespace);
}
void CallbackImpl(
nint plugin,
ExtismVal* inputsPtr,
uint n_inputs,
ExtismVal* outputsPtr,
uint n_outputs,
IntPtr data)
{
var outputs = new Span<ExtismVal>(outputsPtr, (int)n_outputs);
var inputs = new Span<ExtismVal>(inputsPtr, (int)n_inputs);
hostFunction(new CurrentPlugin(plugin), inputs, outputs, data);
}
}
internal IntPtr NativeHandle { get; }
/// <summary>
/// Frees all resources held by this Host Function.
/// </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 Host Function 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(HostFunction));
}
/// <summary>
/// Frees all resources held by this Host Function.
/// </summary>
unsafe protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// Free up any managed resources here
}
// Free up unmanaged resources
LibExtism.extism_function_free(NativeHandle);
}
/// <summary>
/// Destructs the current Host Function and frees all resources used by it.
/// </summary>
~HostFunction()
{
Dispose(false);
}
}
}

View File

@@ -2,264 +2,124 @@ using System.Runtime.InteropServices;
namespace Extism.Sdk.Native;
/// <summary>
/// A union type for host function argument/return values.
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public struct ExtismValUnion
{
/// <summary>
/// Set this for 32 bit integers
/// </summary>
[FieldOffset(0)]
public int i32;
/// <summary>
/// Set this for 64 bit integers
/// </summary>
[FieldOffset(0)]
public long i64;
/// <summary>
/// Set this for 32 bit floats
/// </summary>
[FieldOffset(0)]
public float f32;
/// <summary>
/// Set this for 64 bit floats
/// </summary>
[FieldOffset(0)]
public double f64;
}
/// <summary>
/// Represents Wasm data types that Extism can understand
/// </summary>
public enum ExtismValType : byte
{
/// <summary>
/// Signed 32 bit integer. Equivalent of <see cref="int"/> or <see cref="uint"/>
/// </summary>
I32,
/// <summary>
/// Signed 64 bit integer. Equivalent of <see cref="long"/> or <see cref="ulong"/>
/// </summary>
I64,
/// <summary>
/// Floating point 32 bit integer. Equivalent of <see cref="float"/>
/// </summary>
F32,
/// <summary>
/// Floating point 64 bit integer. Equivalent of <see cref="double"/>
/// </summary>
F64,
/// <summary>
/// A 128 bit number.
/// </summary>
V128,
/// <summary>
/// A reference to opaque data in the Wasm instance.
/// </summary>
FuncRef,
/// <summary>
/// A reference to opaque data in the Wasm instance.
/// </summary>
ExternRef
}
/// <summary>
/// `ExtismVal` holds the type and value of a function argument/return
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct ExtismVal
{
/// <summary>
/// The type for the argument
/// </summary>
public ExtismValType t;
/// <summary>
/// The value for the argument
/// </summary>
public ExtismValUnion v;
}
/// <summary>
/// Functions exposed by the native Extism library.
/// </summary>
internal static class LibExtism
{
/// <summary>
/// An Extism Plugin
/// Create a new context.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct ExtismPlugin { }
/// <returns>A pointer to the newly created context.</returns>
[DllImport("extism")]
public static extern IntPtr extism_context_new();
/// <summary>
/// Host function signature
/// Remove a context from the registry and free associated memory.
/// </summary>
/// <param name="plugin"></param>
/// <param name="inputs"></param>
/// <param name="n_inputs"></param>
/// <param name="outputs"></param>
/// <param name="n_outputs"></param>
/// <param name="data"></param>
unsafe internal delegate void InternalExtismFunction(nint plugin, ExtismVal* inputs, uint n_inputs, ExtismVal* outputs, uint n_outputs, IntPtr data);
/// <summary>
/// Returns a pointer to the memory of the currently running plugin.
/// NOTE: this should only be called from host functions.
/// </summary>
/// <param name="plugin"></param>
/// <returns></returns>
[DllImport("extism", EntryPoint = "extism_current_plugin_memory")]
internal static extern IntPtr extism_current_plugin_memory(nint plugin);
/// <summary>
/// Allocate a memory block in the currently running plugin
/// </summary>
/// <param name="plugin"></param>
/// <param name="n"></param>
/// <returns></returns>
[DllImport("extism", EntryPoint = "extism_current_plugin_memory_alloc")]
internal static extern IntPtr extism_current_plugin_memory_alloc(nint plugin, long n);
/// <summary>
/// Get the length of an allocated block.
/// NOTE: this should only be called from host functions.
/// </summary>
/// <param name="plugin"></param>
/// <param name="n"></param>
/// <returns></returns>
[DllImport("extism", EntryPoint = "extism_current_plugin_memory_length")]
internal static extern long extism_current_plugin_memory_length(nint plugin, long n);
/// <summary>
/// Get the length of an allocated block.
/// NOTE: this should only be called from host functions.
/// </summary>
/// <param name="plugin"></param>
/// <param name="ptr"></param>
[DllImport("extism", EntryPoint = "extism_current_plugin_memory_free")]
internal static extern void extism_current_plugin_memory_free(nint plugin, IntPtr ptr);
/// <summary>
/// Create a new host function.
/// </summary>
/// <param name="name">function name, this should be valid UTF-8</param>
/// <param name="inputs">argument types</param>
/// <param name="nInputs">number of argument types</param>
/// <param name="outputs">return types</param>
/// <param name="nOutputs">number of return types</param>
/// <param name="func">the function to call</param>
/// <param name="userData">a pointer that will be passed to the function when it's called this value should live as long as the function exists</param>
/// <param name="freeUserData">a callback to release the `user_data` value when the resulting `ExtismFunction` is freed.</param>
/// <returns></returns>
[DllImport("extism", EntryPoint = "extism_function_new")]
unsafe internal static extern IntPtr extism_function_new(string name, ExtismValType* inputs, long nInputs, ExtismValType* outputs, long nOutputs, InternalExtismFunction func, IntPtr userData, IntPtr freeUserData);
/// <summary>
/// Set the namespace of an <see cref="ExtismFunction"/>
/// </summary>
/// <param name="ptr"></param>
/// <param name="namespace"></param>
[DllImport("extism", EntryPoint = "extism_function_set_namespace")]
internal static extern void extism_function_set_namespace(IntPtr ptr, string @namespace);
/// <summary>
/// Free an <see cref="ExtismFunction"/>
/// </summary>
/// <param name="ptr"></param>
[DllImport("extism", EntryPoint = "extism_function_free")]
internal static extern void extism_function_free(IntPtr ptr);
/// <param name="context"></param>
[DllImport("extism")]
public static extern void extism_context_free(IntPtr 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 public static extern IntPtr extism_plugin_new(IntPtr context, byte* wasm, int wasmSize, 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="wasmLength">The length of the `wasm` parameter.</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 public static extern bool extism_plugin_update(IntPtr context, IntPtr plugin, byte* wasm, int wasmLength, 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);
public static extern void extism_plugin_free(IntPtr context, IntPtr plugin);
/// <summary>
/// Remove all plugins from the registry.
/// </summary>
/// <param name="context"></param>
[DllImport("extism")]
public static extern void extism_context_reset(IntPtr 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 public static extern bool extism_plugin_config(IntPtr context, IntPtr 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);
public static extern bool extism_plugin_function_exists(IntPtr context, IntPtr 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 public static extern int extism_plugin_call(IntPtr context, IntPtr 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);
public static extern IntPtr extism_error(IntPtr 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);
public static extern long extism_plugin_output_length(IntPtr context, IntPtr 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);
public static extern IntPtr extism_plugin_output_data(IntPtr context, IntPtr plugin);
/// <summary>
/// Set log file and level.
@@ -268,43 +128,43 @@ internal static class LibExtism
/// <param name="logLevel"></param>
/// <returns></returns>
[DllImport("extism")]
internal static extern bool extism_log_file(string filename, string logLevel);
public 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")]
internal static extern IntPtr extism_version();
[DllImport("extism", EntryPoint = "extism_version")]
public static extern IntPtr extism_version();
/// <summary>
/// Extism Log Levels
/// </summary>
internal static class LogLevels
public static class LogLevels
{
/// <summary>
/// Designates very serious errors.
/// </summary>
internal const string Error = "Error";
public const string Error = "Error";
/// <summary>
/// Designates hazardous situations.
/// </summary>
internal const string Warn = "Warn";
public const string Warn = "Warn";
/// <summary>
/// Designates useful information.
/// </summary>
internal const string Info = "Info";
public const string Info = "Info";
/// <summary>
/// Designates lower priority information.
/// </summary>
internal const string Debug = "Debug";
public const string Debug = "Debug";
/// <summary>
/// Designates very low priority, often extremely verbose, information.
/// </summary>
internal const string Trace = "Trace";
public const string Trace = "Trace";
}
}
}

View File

@@ -1,32 +0,0 @@
namespace Extism.Sdk.Native;
/// <summary>
/// Extism Log Levels
/// </summary>
public enum LogLevel
{
/// <summary>
/// Designates very serious errors.
/// </summary>
Error,
/// <summary>
/// Designates hazardous situations.
/// </summary>
Warning,
/// <summary>
/// Designates useful information.
/// </summary>
Info,
/// <summary>
/// Designates lower priority information.
/// </summary>
Debug,
/// <summary>
/// Designates very low priority, often extremely verbose, information.
/// </summary>
Trace
}

View File

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

View File

@@ -1,92 +1,44 @@
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 HostFunction[] _functions;
private readonly Context _context;
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)
internal Plugin(Context context, IntPtr handle)
{
_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);
}
_context = context;
NativeHandle = handle;
}
/// <summary>
/// Create and load a plugin from a byte array.
/// A pointer to the native Plugin struct.
/// </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>
internal IntPtr NativeHandle { 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>
public Plugin(ReadOnlySpan<byte> wasm, HostFunction[] functions, bool withWasi)
unsafe public bool Update(ReadOnlySpan<byte> wasm, bool withWasi)
{
_functions = functions;
CheckNotDisposed();
var functionHandles = functions.Select(f => f.NativeHandle).ToArray();
fixed (byte* wasmPtr = wasm)
fixed (IntPtr* functionsPtr = functionHandles)
{
NativeHandle = Initialize(wasmPtr, wasm.Length, functions, withWasi, functionsPtr);
return LibExtism.extism_plugin_update(_context.NativeHandle, NativeHandle, wasmPtr, wasm.Length, withWasi);
}
}
private unsafe LibExtism.ExtismPlugin* Initialize(byte* wasmPtr, int wasmLength, HostFunction[] functions, bool withWasi, IntPtr* functionsPtr)
{
char** errorMsgPtr;
var handle = LibExtism.extism_plugin_new(wasmPtr, (ulong)wasmLength, functionsPtr, (ulong)functions.Length, withWasi, out errorMsgPtr);
if (handle == null)
{
var msg = "Unable to create plugin";
if (errorMsgPtr is not null)
{
msg = Marshal.PtrToStringAnsi(new IntPtr(errorMsgPtr));
}
throw new ExtismException(msg);
}
return handle;
}
/// <summary>
/// Update plugin config values, this will merge with the existing values.
/// </summary>
@@ -97,18 +49,18 @@ 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, NativeHandle, jsonPtr, json.Length);
}
}
/// <summary>
/// Checks if a specific function exists in the current plugin.
/// </summary>
unsafe public bool FunctionExists(string name)
public bool FunctionExists(string name)
{
CheckNotDisposed();
return LibExtism.extism_plugin_function_exists(NativeHandle, name);
return LibExtism.extism_plugin_function_exists(_context.NativeHandle, NativeHandle, name);
}
/// <summary>
@@ -126,20 +78,14 @@ public unsafe class Plugin : IDisposable
fixed (byte* dataPtr = data)
{
int response = LibExtism.extism_plugin_call(NativeHandle, functionName, dataPtr, data.Length);
if (response == 0)
{
int response = LibExtism.extism_plugin_call(_context.NativeHandle, NativeHandle, functionName, dataPtr, data.Length);
if (response == 0) {
return OutputData();
}
else
{
} else {
var errorMsg = GetError();
if (errorMsg != null)
{
if (errorMsg != null) {
throw new ExtismException(errorMsg);
}
else
{
} else {
throw new ExtismException("Call to Extism failed");
}
}
@@ -150,11 +96,11 @@ public unsafe class Plugin : IDisposable
/// Get the length of a plugin's output data.
/// </summary>
/// <returns></returns>
unsafe internal int OutputLength()
internal int OutputLength()
{
CheckNotDisposed();
return (int)LibExtism.extism_plugin_output_length(NativeHandle);
return (int)LibExtism.extism_plugin_output_length(_context.NativeHandle, NativeHandle);
}
/// <summary>
@@ -168,7 +114,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, NativeHandle).ToPointer();
return new Span<byte>(ptr, length);
}
}
@@ -177,11 +123,11 @@ public unsafe class Plugin : IDisposable
/// Get the error associated with the current plugin.
/// </summary>
/// <returns></returns>
unsafe internal string? GetError()
internal string? GetError()
{
CheckNotDisposed();
var result = LibExtism.extism_plugin_error(NativeHandle);
var result = LibExtism.extism_error(_context.NativeHandle, NativeHandle);
return Marshal.PtrToStringUTF8(result);
}
@@ -222,7 +168,7 @@ public unsafe class Plugin : IDisposable
/// <summary>
/// Frees all resources held by this Plugin.
/// </summary>
unsafe protected virtual void Dispose(bool disposing)
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
@@ -230,7 +176,7 @@ public unsafe class Plugin : IDisposable
}
// Free up unmanaged resources
LibExtism.extism_plugin_free(NativeHandle);
LibExtism.extism_plugin_free(_context.NativeHandle, NativeHandle);
}
/// <summary>
@@ -240,15 +186,4 @@ public unsafe class Plugin : IDisposable
{
Dispose(false);
}
/// <summary>
/// Get Extism Runtime version.
/// </summary>
/// <returns></returns>
public static string ExtismVersion()
{
var version = LibExtism.extism_version();
return Marshal.PtrToStringAnsi(version);
}
}
}

View File

@@ -1,7 +1,6 @@
using Extism.Sdk.Native;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using Xunit;
@@ -13,46 +12,13 @@ public class BasicTests
[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 = new Plugin(wasm, Array.Empty<HostFunction>(), withWasi: true);
using var plugin = context.CreatePlugin(wasm, withWasi: true);
var response = plugin.CallFunction("count_vowels", Encoding.UTF8.GetBytes("Hello World"));
Assert.Equal("{\"count\": 3}", Encoding.UTF8.GetString(response));
}
[Fact]
public void CountVowelsHostFunctions()
{
var userData = Marshal.StringToHGlobalAnsi("Hello again!");
using var helloWorld = new HostFunction(
"hello_world",
"env",
new[] { ExtismValType.I64 },
new[] { ExtismValType.I64 },
userData,
HelloWorld);
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);
var response = plugin.CallFunction("count_vowels", Encoding.UTF8.GetBytes("Hello World"));
Assert.Equal("{\"count\": 3}", Encoding.UTF8.GetString(response));
void HelloWorld(CurrentPlugin plugin, Span<ExtismVal> inputs, Span<ExtismVal> outputs, nint data)
{
Console.WriteLine("Hello from .NET!");
var text = Marshal.PtrToStringAnsi(data);
Console.WriteLine(text);
var input = plugin.ReadString(new nint(inputs[0].v.i64));
Console.WriteLine($"Input: {input}");
var output = new string(input); // clone the string
outputs[0].v.i64 = plugin.WriteString(output);
}
}
}
}

View File

@@ -25,9 +25,6 @@
<None Include="..\..\..\wasm\code.wasm" Link="code.wasm">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="..\..\..\wasm\code-functions.wasm" Link="code-functions.wasm">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
@@ -35,8 +32,4 @@
<ProjectReference Include="..\..\src\Extism.Sdk\Extism.Sdk.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Extism.runtime.win-x64" Version="0.7.0" />
</ItemGroup>
</Project>

View File

@@ -21,15 +21,13 @@
(description "Bindings to Extism, the universal plugin system")
(depends
(ocaml (>= 4.14.1))
dune
(ctypes (>= 0.18.0))
(dune (>= 3.2))
(ctypes-foreign (>= 0.18.0))
(bigstringaf (>= 0.9.0))
(ppx_yojson_conv (>= v0.15.0))
(extism-manifest (= :version))
(ppx_inline_test (>= v0.15.0))
(ppx_yojson_conv (>= 0.15.0))
extism-manifest
(ppx_inline_test (>= 0.15.0))
(cmdliner (>= 1.1.1))
(uuidm (>= 0.9.0))
)
(tags
(topics wasm plugin)))
@@ -37,12 +35,11 @@
(package
(name extism-manifest)
(synopsis "Extism manifest bindings")
(description "Bindings to the Extism manifest format")
(description "Bindings to Extism, the universal plugin system")
(depends
(ocaml (>= 4.14.1))
dune
(ppx_yojson_conv (>= v0.15.0))
(ppx_inline_test (>= v0.15.0))
(dune (>= 3.2))
(ppx_yojson_conv (>= 0.15.0))
(base64 (>= 3.5.0))
)
(tags

View File

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

View File

@@ -23,9 +23,12 @@ end
### Example
```elixir
# Create a context for which plugins can be allocated and cleaned
ctx = Extism.Context.new()
# point to some wasm code, this is the count_vowels example that ships with extism
manifest = %{ wasm: [ %{ path: "/Users/ben/code/extism/wasm/code.wasm" } ]}
{:ok, plugin} = Extism.Plugin.new(manifest, false)
{:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
# {:ok,
# %Extism.Plugin{
# resource: 0,
@@ -35,20 +38,36 @@ manifest = %{ wasm: [ %{ path: "/Users/ben/code/extism/wasm/code.wasm" } ]}
# {:ok, "{\"count\": 4}"}
{:ok, result} = JSON.decode(output)
# {:ok, %{"count" => 4}}
# free up the context and any plugins we allocated
Extism.Context.free(ctx)
```
### Modules
The primary modules you should learn is:
The two primary modules you should learn are:
* [Extism.Context](Extism.Context.html)
* [Extism.Plugin](Extism.Plugin.html)
#### Context
The [Context](Extism.Context.html) can be thought of as a session. You need a context to interact with the Extism runtime. The context holds your plugins and when you free the context, it frees your plugins. It's important to free up your context and plugins when you are done with them.
```elixir
ctx = Extism.Context.new()
# frees all the plugins
Extism.Context.reset(ctx)
# frees the context and all its plugins
Extism.Context.free(ctx)
```
#### Plugin
The [Plugin](Extism.Plugin.html) represents an instance of your WASM program from the given manifest.
The key method to know here is [Extism.Plugin#call](Extism.Plugin.html#call/3) which takes a function name to invoke and some input data, and returns the results from the plugin.
```elixir
{:ok, plugin} = Extism.Plugin.new(manifest, false)
{:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
{:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
```

View File

@@ -1,31 +0,0 @@
defmodule Extism.CancelHandle do
@moduledoc """
A CancelHandle is a handle generated by a plugin that allows it to be cancelled from another
thread while running.
"""
defstruct [
# The actual NIF Resource
handle: nil
]
def wrap_resource(handle) do
%__MODULE__{
handle: handle
}
end
@doc """
Cancel plugin execution
"""
def cancel(handle) do
Extism.Native.plugin_cancel(handle.handle)
end
end
defimpl Inspect, for: Extim.CancelHandle do
import Inspect.Algebra
def inspect(dict, opts) do
concat(["#Extism.CancelHandle<", to_doc(dict.handle, opts), ">"])
end
end

View File

@@ -0,0 +1,64 @@
defmodule Extism.Context do
@moduledoc """
A Context is needed to create plugins. The Context is where your plugins
live. Freeing the context frees all of the plugins in its scope.
"""
defstruct [
# The actual NIF Resource. A pointer in this case
ptr: nil
]
def wrap_resource(ptr) do
%__MODULE__{
ptr: ptr
}
end
@doc """
Creates a new context.
"""
def new() do
ptr = Extism.Native.context_new()
Extism.Context.wrap_resource(ptr)
end
@doc """
Resets the context. This has the effect of freeing all the plugins created so far.
"""
def reset(ctx) do
Extism.Native.context_reset(ctx.ptr)
end
@doc """
Frees the context from memory and all of its plugins.
"""
def free(ctx) do
Extism.Native.context_free(ctx.ptr)
end
@doc """
Create a new plugin from a WASM module or manifest
## Examples:
iex> ctx = Extism.Context.new()
iex> manifest = %{ wasm: [ %{ path: "/Users/ben/code/extism/wasm/code.wasm" } ]}
iex> {:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
## Parameters
- ctx: The Context to manage this plugin
- manifest: The String or Map of the WASM module or [manifest](https://extism.org/docs/concepts/manifest)
- wasi: A bool you set to true if you want WASI support
"""
def new_plugin(ctx, manifest, wasi \\ false) do
{:ok, manifest_payload} = JSON.encode(manifest)
case Extism.Native.plugin_new_with_manifest(ctx.ptr, manifest_payload, wasi) do
{:error, err} -> {:error, err}
res -> {:ok, Extism.Plugin.wrap_resource(ctx, res)}
end
end
end

View File

@@ -7,13 +7,15 @@ 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), do: error()
defp error, do: :erlang.nif_error(:nif_not_loaded)
end

View File

@@ -3,34 +3,24 @@ 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
{:ok, manifest_payload} = JSON.encode(manifest)
case Extism.Native.plugin_new_with_manifest(manifest_payload, wasi) do
{:error, err} -> {:error, err}
res -> {:ok, Extism.Plugin.wrap_resource(res)}
end
end
@doc """
Call a plugin's function by name
## Examples
iex> {:ok, plugin} = Extism.Plugin.new(manifest, false)
iex> {:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
iex> {:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
# {:ok, "{\"count\": 4}"}
@@ -46,24 +36,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 +86,6 @@ defimpl Inspect, for: Extim.Plugin do
import Inspect.Algebra
def inspect(dict, opts) do
concat(["#Extism.Plugin<", to_doc(dict.plugin, opts), ">"])
concat(["#Extism.Plugin<", to_doc(dict.plugin_id, opts), ">"])
end
end

View File

@@ -4,7 +4,7 @@ defmodule Extism.MixProject do
def project do
[
app: :extism,
version: "0.5.0",
version: "0.1.0",
elixir: "~> 1.12",
start_permanent: Mix.env() == :prod,
deps: deps(),
@@ -23,7 +23,7 @@ defmodule Extism.MixProject do
defp deps do
[
{:rustler, "~> 0.29.1"},
{:rustler, "~> 0.26.0"},
{:json, "~> 1.4"},
{:ex_doc, "~> 0.21", only: :dev, runtime: false}
]

View File

@@ -1,12 +1,12 @@
%{
"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"},
"earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"},
"ex_doc": {:hex, :ex_doc, "0.29.1", "b1c652fa5f92ee9cf15c75271168027f92039b3877094290a75abcaac82a9f77", [:mix], [{:earmark_parser, "~> 1.4.19", [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", "b7745fa6374a36daf484e2a2012274950e084815b936b1319aeebcf7809574f6"},
"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"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
"rustler": {:hex, :rustler, "0.29.1", "880f20ae3027bd7945def6cea767f5257bc926f33ff50c0d5d5a5315883c084d", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "109497d701861bfcd26eb8f5801fe327a8eef304f56a5b63ef61151ff44ac9b6"},
"toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
"rustler": {:hex, :rustler, "0.26.0", "06a2773d453ee3e9109efda643cf2ae633dedea709e2455ac42b83637c9249bf", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "42961e9d2083d004d5a53e111ad1f0c347efd9a05cb2eb2ffa1d037cdc74db91"},
"toml": {:hex, :toml, "0.6.2", "38f445df384a17e5d382befe30e3489112a48d3ba4c459e543f748c2f25dd4d1", [:mix], [], "hexpm", "d013e45126d74c0c26a38d31f5e8e9b83ea19fc752470feb9a86071ca5a672fa"},
}

View File

@@ -1,6 +1,6 @@
[package]
name = "extism_nif"
version = "0.3.0"
version = "0.1.0"
edition = "2021"
authors = ["Benjamin Eckel <bhelx@simst.im>"]
@@ -9,10 +9,7 @@ name = "extism_nif"
path = "src/lib.rs"
crate-type = ["cdylib"]
# need this to be here and be empty
[workspace]
[dependencies]
rustler = "0.28.0"
extism = {path = "../../../runtime"} # "0.5.0"
rustler = "0.26.0"
extism = { version = "0.1.0", path = "../../../rust" }
log = "0.4"

View File

@@ -1,5 +1,6 @@
use extism::Plugin;
use extism::{Context, Plugin};
use rustler::{Atom, Env, ResourceArc, Term};
use std::mem;
use std::path::Path;
use std::str;
use std::str::FromStr;
@@ -13,96 +14,114 @@ mod atoms {
}
}
struct ExtismPlugin {
plugin: RwLock<Option<Plugin>>,
}
unsafe impl Sync for ExtismPlugin {}
unsafe impl Send for ExtismPlugin {}
struct ExtismCancelHandle {
handle: RwLock<extism::CancelHandle>,
struct ExtismContext {
ctx: RwLock<Context>,
}
unsafe impl Sync for ExtismCancelHandle {}
unsafe impl Send for ExtismCancelHandle {}
unsafe impl Sync for ExtismContext {}
unsafe impl Send for ExtismContext {}
fn load(env: Env, _: Term) -> bool {
rustler::resource!(ExtismPlugin, env);
rustler::resource!(ExtismCancelHandle, env);
rustler::resource!(ExtismContext, env);
true
}
fn to_rustler_error(extism_error: extism::Error) -> rustler::Error {
rustler::Error::Term(Box::new(extism_error.to_string()))
match extism_error {
extism::Error::UnableToLoadPlugin(msg) => rustler::Error::Term(Box::new(msg)),
extism::Error::Message(msg) => rustler::Error::Term(Box::new(msg)),
extism::Error::Json(json_err) => rustler::Error::Term(Box::new(json_err.to_string())),
extism::Error::Runtime(e) => rustler::Error::Term(Box::new(e.to_string())),
}
}
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_cancel_handle(
plugin: ResourceArc<ExtismPlugin>,
) -> 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())
}
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: ResourceArc<ExtismCancelHandle>) -> bool {
handle.handle.read().unwrap().cancel().is_ok()
}
#[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,37 +133,43 @@ 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,
plugin_free,
set_log_file,
],

View File

@@ -2,21 +2,33 @@ defmodule ExtismTest do
use ExUnit.Case
doctest Extism
defp new_plugin() do
test "context create & reset" do
ctx = Extism.Context.new()
path = Path.join([__DIR__, "../../wasm/code.wasm"])
manifest = %{wasm: [%{path: path}]}
{:ok, plugin} = Extism.Plugin.new(manifest, false)
plugin
{:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
Extism.Context.reset(ctx)
# we should expect an error after resetting context
{:error, _err} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
end
defp new_plugin() do
ctx = Extism.Context.new()
path = Path.join([__DIR__, "../../wasm/code.wasm"])
manifest = %{wasm: [%{path: path}]}
{:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
{ctx, plugin}
end
test "counts vowels" do
plugin = new_plugin()
{ctx, plugin} = new_plugin()
{:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
assert JSON.decode(output) == {:ok, %{"count" => 4}}
Extism.Context.free(ctx)
end
test "can make multiple calls on a plugin" do
plugin = new_plugin()
{ctx, plugin} = new_plugin()
{:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
assert JSON.decode(output) == {:ok, %{"count" => 4}}
{:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "this is a test again")
@@ -25,24 +37,37 @@ defmodule ExtismTest do
assert JSON.decode(output) == {:ok, %{"count" => 6}}
{:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "🌎hello🌎world🌎")
assert JSON.decode(output) == {:ok, %{"count" => 3}}
Extism.Context.free(ctx)
end
test "can free a plugin" do
plugin = new_plugin()
{ctx, plugin} = new_plugin()
{:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
assert JSON.decode(output) == {:ok, %{"count" => 4}}
Extism.Plugin.free(plugin)
# Expect an error when calling a plugin that was freed
{:error, _err} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
Extism.Context.free(ctx)
end
test "can update manifest" do
{ctx, plugin} = new_plugin()
path = Path.join([__DIR__, "../../wasm/code.wasm"])
manifest = %{wasm: [%{path: path}]}
assert Extism.Plugin.update(plugin, manifest, true) == :ok
Extism.Context.free(ctx)
end
test "errors on bad manifest" do
{:error, _msg} = Extism.Plugin.new(%{"wasm" => 123}, false)
ctx = Extism.Context.new()
{:error, _msg} = Extism.Context.new_plugin(ctx, %{"wasm" => 123}, false)
Extism.Context.free(ctx)
end
test "errors on unknown function" do
plugin = new_plugin()
{ctx, plugin} = new_plugin()
{:error, _msg} = Extism.Plugin.call(plugin, "unknown", "this is a test")
Extism.Context.free(ctx)
end
test "set_log_file" do
@@ -50,8 +75,9 @@ defmodule ExtismTest do
end
test "has_function" do
plugin = new_plugin()
{ctx, plugin} = new_plugin()
assert Extism.Plugin.has_function(plugin, "count_vowels")
assert !Extism.Plugin.has_function(plugin, "unknown")
Extism.Context.free(ctx)
end
end

View File

@@ -1,7 +1,7 @@
# This file is generated by dune, edit dune-project instead
opam-version: "2.0"
synopsis: "Extism manifest bindings"
description: "Bindings to the Extism manifest format"
description: "Bindings to Extism, the universal plugin system"
maintainer: ["Extism Authors <oss@extism.org>"]
authors: ["Extism Authors <oss@extism.org>"]
license: "BSD-3-Clause"
@@ -11,9 +11,8 @@ doc: "https://github.com/extism/extism"
bug-reports: "https://github.com/extism/extism/issues"
depends: [
"ocaml" {>= "4.14.1"}
"dune" {>= "3.2"}
"ppx_yojson_conv" {>= "v0.15.0"}
"ppx_inline_test" {>= "v0.15.0"}
"dune" {>= "3.2" & >= "3.2"}
"ppx_yojson_conv" {>= "0.15.0"}
"base64" {>= "3.5.0"}
"odoc" {with-doc}
]

371
extism.go
View File

@@ -5,161 +5,39 @@ import (
"errors"
"fmt"
"io"
"runtime/cgo"
"unsafe"
)
/*
#cgo CFLAGS: -I/usr/local/include
#cgo LDFLAGS: -L/usr/local/lib -lextism
#cgo pkg-config: libextism.pc
#include <extism.h>
#include <stdlib.h>
int64_t extism_val_i64(ExtismValUnion* x){
return x->i64;
}
int32_t extism_val_i32(ExtismValUnion* x){
return x->i32;
}
float extism_val_f32(ExtismValUnion* x){
return x->f32;
}
double extism_val_f64(ExtismValUnion* x){
return x->f64;
}
void extism_val_set_i64(ExtismValUnion* x, int64_t i){
x->i64 = i;
}
void extism_val_set_i32(ExtismValUnion* x, int32_t i){
x->i32 = i;
}
void extism_val_set_f32(ExtismValUnion* x, float f){
x->f32 = f;
}
void extism_val_set_f64(ExtismValUnion* x, double f){
x->f64 = f;
}
*/
import "C"
type ValType = C.ExtismValType
type Val = C.ExtismVal
type Size = C.ExtismSize
var (
I32 ValType = C.I32
I64 ValType = C.I64
F32 ValType = C.F32
F64 ValType = C.F64
V128 ValType = C.V128
FuncRef ValType = C.FuncRef
ExternRef ValType = C.ExternRef
)
// Function is used to define host functions
type Function struct {
pointer *C.ExtismFunction
userData cgo.Handle
// Context is used to manage Plugins
type Context struct {
pointer *C.ExtismContext
}
// Free a function
func (f *Function) Free() {
if f.pointer != nil {
C.extism_function_free(f.pointer)
f.pointer = nil
f.userData.Delete()
// 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,
}
}
// NewFunction creates a new host function with the given name, input/outputs and optional user data, which can be an
// arbitrary `interface{}`
func NewFunction(name string, inputs []ValType, outputs []ValType, f unsafe.Pointer, userData interface{}) Function {
var function Function
function.userData = cgo.NewHandle(userData)
cname := C.CString(name)
ptr := unsafe.Pointer(function.userData)
var inputsPtr *C.ExtismValType = nil
if len(inputs) > 0 {
inputsPtr = (*C.ExtismValType)(&inputs[0])
}
var outputsPtr *C.ExtismValType = nil
if len(outputs) > 0 {
outputsPtr = (*C.ExtismValType)(&outputs[0])
}
function.pointer = C.extism_function_new(
cname,
inputsPtr,
C.uint64_t(len(inputs)),
outputsPtr,
C.uint64_t(len(outputs)),
(*[0]byte)(f),
ptr,
nil,
)
C.free(unsafe.Pointer(cname))
return function
}
func (f *Function) SetNamespace(s string) {
cstr := C.CString(s)
defer C.free(unsafe.Pointer(cstr))
C.extism_function_set_namespace(f.pointer, cstr)
}
func (f Function) WithNamespace(s string) Function {
f.SetNamespace(s)
return f
}
type CurrentPlugin struct {
pointer *C.ExtismCurrentPlugin
}
func GetCurrentPlugin(ptr unsafe.Pointer) CurrentPlugin {
return CurrentPlugin{
pointer: (*C.ExtismCurrentPlugin)(ptr),
}
}
type MemoryHandle = uint
func (p *CurrentPlugin) Memory(offs MemoryHandle) []byte {
length := C.extism_current_plugin_memory_length(p.pointer, C.uint64_t(offs))
data := unsafe.Pointer(C.extism_current_plugin_memory(p.pointer))
return unsafe.Slice((*byte)(unsafe.Add(data, offs)), C.int(length))
}
// Alloc a new memory block of the given length, returning its offset
func (p *CurrentPlugin) Alloc(n uint) MemoryHandle {
return uint(C.extism_current_plugin_memory_alloc(p.pointer, C.uint64_t(n)))
}
// Free the memory block specified by the given offset
func (p *CurrentPlugin) Free(offs MemoryHandle) {
C.extism_current_plugin_memory_free(p.pointer, C.uint64_t(offs))
}
// Length returns the number of bytes allocated at the specified offset
func (p *CurrentPlugin) Length(offs MemoryHandle) int {
return int(C.extism_current_plugin_memory_length(p.pointer, C.uint64_t(offs)))
// 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
functions []Function
ctx *Context
id int32
}
type WasmData struct {
@@ -218,99 +96,121 @@ 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, wasi bool) (Plugin, error) {
ptr := makePointer(data)
functionPointers := []*C.ExtismFunction{}
for _, f := range functions {
functionPointers = append(functionPointers, f.pointer)
}
plugin := C.extism_plugin_new(
ctx.pointer,
(*C.uchar)(ptr),
C.uint64_t(len(data)),
C._Bool(wasi),
)
var plugin *C.ExtismPlugin
errmsg := (*C.char)(nil)
if len(functions) == 0 {
plugin = C.extism_plugin_new(
(*C.uchar)(ptr),
C.uint64_t(len(data)),
nil,
0,
C._Bool(wasi),
&errmsg)
} else {
plugin = C.extism_plugin_new(
(*C.uchar)(ptr),
C.uint64_t(len(data)),
&functionPointers[0],
C.uint64_t(len(functions)),
C._Bool(wasi),
&errmsg,
)
}
if plugin < 0 {
err := C.extism_error(ctx.pointer, C.int32_t(-1))
msg := "Unknown"
if err != nil {
msg = C.GoString(err)
}
if plugin == nil {
msg := C.GoString(errmsg)
C.extism_plugin_new_error_free(errmsg)
return Plugin{}, errors.New(
return Plugin{id: -1}, errors.New(
fmt.Sprintf("Unable to load plugin: %s", msg),
)
}
return Plugin{ptr: plugin, functions: functions}, nil
return Plugin{id: int32(plugin), ctx: ctx}, nil
}
// NewPlugin creates a plugin
func NewPlugin(module io.Reader, functions []Function, wasi bool) (Plugin, error) {
wasm, err := io.ReadAll(module)
if err != nil {
return Plugin{}, err
func update(ctx *Context, plugin int32, data []byte, wasi bool) error {
ptr := makePointer(data)
b := bool(C.extism_plugin_update(
ctx.pointer,
C.int32_t(plugin),
(*C.uchar)(ptr),
C.uint64_t(len(data)),
C._Bool(wasi),
))
if b {
return nil
}
return register(wasm, functions, wasi)
err := C.extism_error(ctx.pointer, C.int32_t(-1))
msg := "Unknown"
if err != nil {
msg = C.GoString(err)
}
return errors.New(
fmt.Sprintf("Unable to load plugin: %s", msg),
)
}
// NewPlugin creates a plugin from a manifest
func NewPluginFromManifest(manifest Manifest, functions []Function, wasi bool) (Plugin, error) {
// PluginFromManifest creates a plugin from a `Manifest`
func (ctx *Context) PluginFromManifest(manifest Manifest, 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, wasi)
}
// Plugin creates a plugin from a WASM module
func (ctx *Context) Plugin(module io.Reader, wasi bool) (Plugin, error) {
wasm, err := io.ReadAll(module)
if err != nil {
return Plugin{id: -1}, err
}
return register(ctx, wasm, wasi)
}
// Update a plugin with a new WASM module
func (p *Plugin) Update(module io.Reader, wasi bool) error {
wasm, err := io.ReadAll(module)
if err != nil {
return err
}
return update(p.ctx, p.id, wasm, wasi)
}
// Update a plugin with a new Manifest
func (p *Plugin) UpdateManifest(manifest Manifest, wasi bool) error {
data, err := json.Marshal(manifest)
if err != nil {
return err
}
return update(p.ctx, p.id, data, 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 +218,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,11 +229,12 @@ 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)
return unsafe.Slice((*byte)(x), C.int(length)), nil
x := C.extism_plugin_output_data(plugin.ctx.pointer, C.int32_t(plugin.id))
y := (*[]byte)(unsafe.Pointer(&x))
return []byte((*y)[0:length]), nil
}
return []byte{}, nil
@@ -341,86 +242,14 @@ 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
}
// ValGetI64 returns an I64 from an ExtismVal, it accepts a pointer to a C.ExtismVal
func ValGetI64(v unsafe.Pointer) int64 {
return int64(C.extism_val_i64(&(*Val)(v).v))
}
// ValGetUInt returns a uint from an ExtismVal, it accepts a pointer to a C.ExtismVal
func ValGetUInt(v unsafe.Pointer) uint {
return uint(C.extism_val_i64(&(*Val)(v).v))
}
// ValGetI32 returns an int32 from an ExtismVal, it accepts a pointer to a C.ExtismVal
func ValGetI32(v unsafe.Pointer) int32 {
return int32(C.extism_val_i32(&(*Val)(v).v))
}
// ValGetF32 returns a float32 from an ExtismVal, it accepts a pointer to a C.ExtismVal
func ValGetF32(v unsafe.Pointer) float32 {
return float32(C.extism_val_f32(&(*Val)(v).v))
}
// ValGetF32 returns a float64 from an ExtismVal, it accepts a pointer to a C.ExtismVal
func ValGetF64(v unsafe.Pointer) float64 {
return float64(C.extism_val_i64(&(*Val)(v).v))
}
// ValSetI64 stores an int64 in an ExtismVal, it accepts a pointer to a C.ExtismVal and the new value
func ValSetI64(v unsafe.Pointer, i int64) {
C.extism_val_set_i64(&(*Val)(v).v, C.int64_t(i))
}
// ValSetI32 stores an int32 in an ExtismVal, it accepts a pointer to a C.ExtismVal and the new value
func ValSetI32(v unsafe.Pointer, i int32) {
C.extism_val_set_i32(&(*Val)(v).v, C.int32_t(i))
}
// ValSetF32 stores a float32 in an ExtismVal, it accepts a pointer to a C.ExtismVal and the new value
func ValSetF32(v unsafe.Pointer, i float32) {
C.extism_val_set_f32(&(*Val)(v).v, C.float(i))
}
// ValSetF64 stores a float64 in an ExtismVal, it accepts a pointer to a C.ExtismVal and the new value
func ValSetF64(v unsafe.Pointer, f float64) {
C.extism_val_set_f64(&(*Val)(v).v, C.double(f))
}
func (p *CurrentPlugin) ReturnBytes(v unsafe.Pointer, b []byte) {
mem := p.Alloc(uint(len(b)))
ptr := p.Memory(mem)
copy(ptr, b)
ValSetI64(v, int64(mem))
}
func (p *CurrentPlugin) ReturnString(v unsafe.Pointer, s string) {
p.ReturnBytes(v, []byte(s))
}
func (p *CurrentPlugin) InputBytes(v unsafe.Pointer) []byte {
return p.Memory(ValGetUInt(v))
}
func (p *CurrentPlugin) InputString(v unsafe.Pointer) string {
return string(p.InputBytes(v))
}
type CancelHandle struct {
pointer *C.ExtismCancelHandle
}
func (p *Plugin) CancelHandle() CancelHandle {
pointer := C.extism_plugin_cancel_handle(p.ptr)
return CancelHandle{pointer}
}
func (c *CancelHandle) Cancel() bool {
return bool(C.extism_plugin_cancel(c.pointer))
// Reset removes all registered plugins in a Context
func (ctx Context) Reset() {
C.extism_context_reset(ctx.pointer)
}

View File

@@ -11,15 +11,13 @@ doc: "https://github.com/extism/extism"
bug-reports: "https://github.com/extism/extism/issues"
depends: [
"ocaml" {>= "4.14.1"}
"dune" {>= "3.2"}
"ctypes" {>= "0.18.0"}
"dune" {>= "3.2" & >= "3.2"}
"ctypes-foreign" {>= "0.18.0"}
"bigstringaf" {>= "0.9.0"}
"ppx_yojson_conv" {>= "v0.15.0"}
"extism-manifest" {= version}
"ppx_inline_test" {>= "v0.15.0"}
"ppx_yojson_conv" {>= "0.15.0"}
"extism-manifest"
"ppx_inline_test" {>= "0.15.0"}
"cmdliner" {>= "1.1.1"}
"uuidm" {>= "0.9.0"}
"odoc" {with-doc}
]
build: [
@@ -37,5 +35,3 @@ build: [
]
]
dev-repo: "git+https://github.com/extism/extism.git"
build-env: [EXTISM_TEST_NO_LIB = ""]
post-messages: ["See https://extism.org/docs/install/ for information about installing libextism"]

View File

@@ -1,2 +0,0 @@
build-env: [EXTISM_TEST_NO_LIB = ""]
post-messages: ["See https://extism.org/docs/install/ for information about installing libextism"]

View File

@@ -4,19 +4,13 @@ import (
"encoding/json"
"fmt"
"testing"
"time"
)
func manifest(functions bool) Manifest {
path := "./wasm/code.wasm"
if functions {
path = "./wasm/code-functions.wasm"
}
func manifest() Manifest {
return Manifest{
Wasm: []Wasm{
WasmFile{
Path: path,
Path: "./wasm/code.wasm",
},
},
}
@@ -35,8 +29,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)
if err != nil {
t.Error(err)
}
@@ -53,7 +55,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)
if err != nil {
t.Error(err)
}
@@ -69,8 +74,52 @@ func TestFreePlugin(t *testing.T) {
}
}
func TestContextReset(t *testing.T) {
ctx := NewContext()
defer ctx.Free()
plugin, err := ctx.PluginFromManifest(manifest(), 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)
if err != nil {
t.Error(err)
}
if err := expectVowelCount(plugin, "this is a test", 4); err != nil {
t.Error(err)
}
plugin.UpdateManifest(manifest(), 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)
if err != nil {
t.Error(err)
}
@@ -84,7 +133,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)
if err != nil {
t.Error(err)
}
@@ -94,30 +146,3 @@ func TestErrorsOnUnknownFunction(t *testing.T) {
t.Fatal("Was expecting call to unknown function to fail")
}
}
func TestCancel(t *testing.T) {
manifest := Manifest{
Wasm: []Wasm{
WasmFile{
Path: "./wasm/loop.wasm",
},
},
}
plugin, err := NewPluginFromManifest(manifest, []Function{}, false)
if err != nil {
t.Error(err)
}
cancelHandle := plugin.CancelHandle()
go func(handle CancelHandle) {
time.Sleep(time.Second * 1)
handle.Cancel()
}(cancelHandle)
_, err = plugin.Call("infinite_loop", []byte(""))
if err == nil {
t.Fail()
}
}

View File

@@ -4,38 +4,17 @@ import (
"encoding/json"
"fmt"
"os"
"runtime/cgo"
"unsafe"
"github.com/extism/extism"
)
/*
#include <extism.h>
EXTISM_GO_FUNCTION(hello_world);
*/
import "C"
//export hello_world
func hello_world(plugin unsafe.Pointer, inputs *C.ExtismVal, nInputs C.ExtismSize, outputs *C.ExtismVal, nOutputs C.ExtismSize, userData uintptr) {
fmt.Println("Hello from Go!")
s := cgo.Handle(userData)
fmt.Println(s.Value().(string))
inputSlice := unsafe.Slice(inputs, nInputs)
outputSlice := unsafe.Slice(outputs, nOutputs)
// Get memory pointed to by first element of input slice
p := extism.GetCurrentPlugin(plugin)
str := p.InputString(unsafe.Pointer(&inputSlice[0]))
fmt.Println(str)
outputSlice[0] = inputSlice[0]
}
func main() {
version := extism.ExtismVersion()
fmt.Println("Extism Version: ", version)
ctx := extism.NewContext()
defer ctx.Free() // this will free the context and all associated plugins
// set some input data to provide to the plugin module
var data []byte
if len(os.Args) > 1 {
@@ -43,10 +22,9 @@ func main() {
} else {
data = []byte("testing from go -> wasm shared memory...")
}
manifest := extism.Manifest{Wasm: []extism.Wasm{extism.WasmFile{Path: "../wasm/code-functions.wasm"}}}
f := extism.NewFunction("hello_world", []extism.ValType{extism.I64}, []extism.ValType{extism.I64}, C.hello_world, "Hello again!")
defer f.Free()
plugin, err := extism.NewPluginFromManifest(manifest, []extism.Function{f}, true)
manifest := extism.Manifest{Wasm: []extism.Wasm{extism.WasmFile{Path: "../wasm/code.wasm"}}}
plugin, err := ctx.PluginFromManifest(manifest, false)
if err != nil {
fmt.Println(err)
os.Exit(1)

View File

@@ -1,5 +1,5 @@
# Revision history for extism
## 0.2.0.0 -- 2023-01-16
## 0.1.0.0 -- YYYY-mm-dd
* First version. Released on an unsuspecting world.

View File

@@ -1,21 +1,16 @@
module Main where
import Extism
import Extism.HostFunction
import Extism.Manifest(manifest, wasmFile)
hello plugin params msg = do
putStrLn "Hello from Haskell!"
putStrLn msg
offs <- allocBytes plugin (toByteString "{\"count\": 999}")
return [toI64 offs]
unwrap (Right x) = x
unwrap (Left (ExtismError msg)) = do
error msg
main = do
setLogFile "stdout" LogError
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
res <- unwrap <$> call plugin "count_vowels" (toByteString "this is a test")
putStrLn (fromByteString res)
let m = manifest [wasmFile "../wasm/code.wasm"]
context <- Extism.newContext
plugin <- unwrap <$> Extism.pluginFromManifest context m False
res <- unwrap <$> Extism.call plugin "count_vowels" (Extism.toByteString "this is a test")
putStrLn (Extism.fromByteString res)
Extism.free plugin

View File

@@ -13,9 +13,6 @@ clean:
cabal clean
publish: clean prepare
cabal v2-haddock --haddock-for-hackage ./manifest/extism-manifest.cabal
cabal v2-haddock --haddock-for-hackage
cabal sdist ./manifest/extism-manifest.cabal
cabal sdist
# TODO: upload

View File

@@ -1,6 +1,6 @@
cabal-version: 3.0
name: extism
version: 0.5.0
version: 0.0.1
license: BSD-3-Clause
maintainer: oss@extism.org
author: Extism authors
@@ -8,10 +8,10 @@ bug-reports: https://github.com/extism/extism
synopsis: Extism bindings
description: Bindings to Extism, the universal plugin system
category: Plugins, WebAssembly
extra-doc-files: CHANGELOG.md
extra-source-files: CHANGELOG.md
library
exposed-modules: Extism Extism.HostFunction
exposed-modules: Extism
reexported-modules: Extism.Manifest
hs-source-dirs: src
other-modules: Extism.Bindings
@@ -19,11 +19,10 @@ library
extra-libraries: extism
extra-lib-dirs: /usr/local/lib
build-depends:
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
base >= 4.16.1 && < 4.18.0,
bytestring >= 0.11.3 && < 0.12,
json >= 0.10 && < 0.11,
extism-manifest >= 0.0.0 && < 0.1.0
test-suite extism-example
type: exitcode-stdio-1.0

View File

@@ -9,23 +9,21 @@ 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
isNull _ = False
filterNulls obj = [(a, b) | (a, b) <- obj, not (isNull b)]
object x = makeObj $ filterNulls x
objectWithNulls = makeObj
nonNull = NotNull
objectWithNulls x = makeObj x
nonNull x = NotNull x
null' = Null
(.=) a b = (a, showJSON b)
toNullable (Just x) = NotNull x
toNullable Nothing = Null
fromNullable (NotNull x) = Just x
fromNullable Null = Nothing
fromNotNull (NotNull x) = x
fromNotNull Null = error "Value is Null"
mapNullable f Null = Null
mapNullable f (NotNull x) = NotNull (f x)
@@ -40,7 +38,7 @@ find :: JSON a => String -> JSValue -> Nullable a
find k obj = obj .? k
update :: JSON a => String -> a -> JSValue -> JSValue
update k v (JSObject obj) = object $ fromJSObject obj ++ [k .= v]
update k v (JSObject obj) = object $ (fromJSObject obj) ++ [k .= v]
instance JSON a => JSON (Nullable a) where
showJSON (NotNull x) = showJSON x
@@ -49,7 +47,7 @@ instance JSON a => JSON (Nullable a) where
readJSON x = readJSON x
newtype Base64 = Base64 B.ByteString deriving (Eq, Show)
newtype Base64 = Base64 B.ByteString
instance JSON Base64 where
showJSON (Base64 bs) = showJSON (BS.unpack $ B64.encode bs)

View File

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

View File

@@ -1,6 +1,6 @@
cabal-version: 3.0
name: extism-manifest
version: 0.3.0
version: 0.0.1
license: BSD-3-Clause
maintainer: oss@extism.org
author: Extism authors
@@ -8,14 +8,14 @@ bug-reports: https://github.com/extism/extism
synopsis: Extism manifest bindings
description: Bindings to Extism WebAssembly manifest
category: Plugins, WebAssembly
extra-doc-files: CHANGELOG.md
extra-source-files: CHANGELOG.md
library
exposed-modules: Extism.Manifest Extism.JSON
hs-source-dirs: .
default-language: Haskell2010
build-depends:
base >= 4.16.1 && < 5,
bytestring >= 0.11.3 && <= 0.12,
json >= 0.10 && <= 0.11,
base >= 4.16.1 && < 4.18.0,
bytestring >= 0.11.3 && < 0.12,
json >= 0.10 && < 0.11,
base64-bytestring >= 1.2.1 && < 1.3,

View File

@@ -1,69 +1,40 @@
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
) where
module Extism (module Extism, module Extism.Manifest) 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 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)
-- | Plugins can be used to call WASM function
newtype Plugin = Plugin (ForeignPtr ExtismPlugin) deriving Eq
-- | Cancellation handle for Plugins
newtype CancelHandle = CancelHandle (Ptr ExtismCancelHandle)
data Plugin = Plugin Context Int32
-- | 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 +42,93 @@ 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 <- 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
-- | 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 -> Bool -> IO (Result Plugin)
plugin c wasm useWasi =
let length = fromIntegral (B.length wasm) in
let wasi = fromInteger (if useWasi then 1 else 0) in
let Context ctx = c in
do
withForeignPtr ctx (\ctx -> do
p <- unsafeUseAsCString wasm (\s ->
extism_plugin_new ctx (castPtr s) length wasi)
if p < 0 then do
err <- extism_error ctx (-1)
e <- peekCString err
return $ Left (ExtismError e)
else
return $ Right (Plugin c p))
-- | Create a 'Plugin' from a 'Manifest'
pluginFromManifest :: Context -> Manifest -> Bool -> IO (Result Plugin)
pluginFromManifest ctx manifest useWasi =
let wasm = toByteString $ toString manifest in
plugin ctx wasm useWasi
-- | Update a 'Plugin' with a new WASM module
update :: Plugin -> B.ByteString -> Bool -> IO (Result ())
update (Plugin (Context ctx) id) wasm useWasi =
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 ->
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
withForeignPtr ctx (\ctx -> do
b <- unsafeUseAsCString wasm (\s ->
extism_plugin_update ctx id (castPtr s) length 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 ()))
-- | 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 -> Bool -> IO (Result ())
updateManifest plugin manifest useWasi =
let wasm = toByteString $ toString manifest in
plugin wasm functions useWasi
update plugin wasm 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,63 +141,34 @@ 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)
-- | 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
return (CancelHandle handle)
-- | Cancel a running plugin using a 'CancelHandle'
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
-- | 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)

View File

@@ -7,114 +7,20 @@ import Foreign.Ptr
import Foreign.C.String
import Data.Int
import Data.Word
import Foreign.Storable
import Foreign.Marshal.Array
import Foreign.StablePtr
type FreeCallback = Ptr () -> IO ()
newtype ExtismContext = ExtismContext () deriving Show
newtype ExtismPlugin = ExtismPlugin () deriving Show
newtype ExtismFunction = ExtismFunction () deriving Show
newtype ExtismCancelHandle = ExtismCancelHandle () deriving Show
newtype ExtismCurrentPlugin = ExtismCurrentPlugin () deriving Show
data ValType = I32 | I64 | F32 | F64 | V128 | FuncRef | ExternRef deriving (Show, Eq)
data Val = ValI32 Int32 | ValI64 Int64 | ValF32 Float | ValF64 Double deriving (Show, Eq)
typeOfVal (ValI32 _) = I32
typeOfVal (ValI64 _) = I64
typeOfVal (ValF32 _) = F32
typeOfVal (ValF64 _) = F64
type CCallback = Ptr ExtismCurrentPlugin -> Ptr Val -> Word64 -> Ptr Val -> Word64 -> Ptr () -> IO ()
_32Bit = sizeOf (undefined :: Int) == 4
instance Storable Val where
sizeOf _ =
if _32Bit then 12 else 16
alignment _ = 1
peek ptr = do
let offs = if _32Bit then 4 else 8
t <- valTypeOfInt <$> peekByteOff ptr 0
case t of
I32 -> ValI32 <$> peekByteOff ptr offs
I64 -> ValI64 <$> peekByteOff ptr offs
F32 -> ValF32 <$> peekByteOff ptr offs
F64 -> ValF64 <$> peekByteOff ptr offs
poke ptr x = do
let offs = if _32Bit then 4 else 8
pokeByteOff ptr 0 (typeOfVal x)
case x of
ValI32 x -> pokeByteOff ptr offs x
ValI64 x -> pokeByteOff ptr offs x
ValF32 x -> pokeByteOff ptr offs x
ValF64 x -> pokeByteOff ptr offs x
intOfValType :: ValType -> CInt
intOfValType I32 = 0
intOfValType I64 = 1
intOfValType F32 = 2
intOfValType F64 = 3
intOfValType V128 = 4
intOfValType FuncRef = 5
intOfValType ExternRef = 6
valTypeOfInt :: CInt -> ValType
valTypeOfInt 0 = I32
valTypeOfInt 1 = I64
valTypeOfInt 2 = F32
valTypeOfInt 3 = F64
valTypeOfInt 4 = V128
valTypeOfInt 5 = FuncRef
valTypeOfInt 6 = ExternRef
valTypeOfInt _ = error "Invalid ValType"
instance Storable ValType where
sizeOf _ = 4
alignment _ = 1
peek ptr = do
x <- peekByteOff ptr 0
return $ valTypeOfInt (x :: CInt)
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_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_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" 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)
foreign import ccall safe "extism.h extism_function_free" extism_function_free :: Ptr ExtismFunction -> IO ()
foreign import ccall safe "extism.h extism_current_plugin_memory" extism_current_plugin_memory :: Ptr ExtismCurrentPlugin -> IO (Ptr Word8)
foreign import ccall safe "extism.h extism_current_plugin_memory_alloc" extism_current_plugin_memory_alloc :: Ptr ExtismCurrentPlugin -> Word64 -> IO Word64
foreign import ccall safe "extism.h extism_current_plugin_memory_length" extism_current_plugin_memory_length :: Ptr ExtismCurrentPlugin -> Word64 -> IO Word64
foreign import ccall safe "extism.h extism_current_plugin_memory_free" extism_current_plugin_memory_free :: Ptr ExtismCurrentPlugin -> Word64 -> IO ()
freePtr ptr = do
let s = castPtrToStablePtr ptr
(a, b, c) <- deRefStablePtr s
freeHaskellFunPtr b
freeHaskellFunPtr c
freeStablePtr s
foreign import ccall "wrapper" freePtrWrap :: FreeCallback -> IO (FunPtr FreeCallback)
foreign import ccall "wrapper" callbackWrap :: CCallback -> IO (FunPtr CCallback)
callback :: (Ptr ExtismCurrentPlugin -> [Val] -> a -> IO [Val]) -> (Ptr ExtismCurrentPlugin -> Ptr Val -> Word64 -> Ptr Val -> Word64 -> Ptr () -> IO ())
callback f plugin params nparams results nresults ptr = do
p <- peekArray (fromIntegral nparams) params
(userData, _, _) <- deRefStablePtr (castPtrToStablePtr ptr)
res <- f plugin p userData
pokeArray results res
foreign import ccall unsafe "extism.h extism_context_new" extism_context_new :: IO (Ptr ExtismContext)
foreign import ccall unsafe "extism.h &extism_context_free" extism_context_free :: FunPtr (Ptr ExtismContext -> IO ())
foreign import ccall unsafe "extism.h extism_plugin_new" extism_plugin_new :: Ptr ExtismContext -> Ptr Word8 -> Word64 -> CBool -> IO Int32
foreign import ccall unsafe "extism.h extism_plugin_update" extism_plugin_update :: Ptr ExtismContext -> Int32 -> Ptr Word8 -> Word64 -> CBool -> IO CBool
foreign import ccall unsafe "extism.h extism_plugin_call" extism_plugin_call :: Ptr ExtismContext -> Int32 -> CString -> Ptr Word8 -> Word64 -> IO Int32
foreign import ccall unsafe "extism.h extism_plugin_function_exists" extism_plugin_function_exists :: Ptr ExtismContext -> Int32 -> CString -> IO CBool
foreign import ccall unsafe "extism.h extism_error" extism_error :: Ptr ExtismContext -> Int32 -> IO CString
foreign import ccall unsafe "extism.h extism_plugin_output_length" extism_plugin_output_length :: Ptr ExtismContext -> Int32 -> IO Word64
foreign import ccall unsafe "extism.h extism_plugin_output_data" extism_plugin_output_data :: Ptr ExtismContext -> Int32 -> IO (Ptr Word8)
foreign import ccall unsafe "extism.h extism_log_file" extism_log_file :: CString -> CString -> IO CBool
foreign import ccall unsafe "extism.h extism_plugin_config" extism_plugin_config :: Ptr ExtismContext -> Int32 -> Ptr Word8 -> Int64 -> IO CBool
foreign import ccall unsafe "extism.h extism_plugin_free" extism_plugin_free :: Ptr ExtismContext -> Int32 -> IO ()
foreign import ccall unsafe "extism.h extism_context_reset" extism_context_reset :: Ptr ExtismContext -> IO ()
foreign import ccall unsafe "extism.h extism_version" extism_version :: IO CString

View File

@@ -1,157 +0,0 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Extism.HostFunction(
CurrentPlugin(..),
ValType(..),
Val(..),
MemoryHandle,
memoryAlloc,
memoryLength,
memoryFree,
memory,
memoryOffset,
memoryBytes,
memoryString,
allocBytes,
allocString,
toI32,
toI64,
toF32,
toF64,
fromI32,
fromI64,
fromF32,
fromF64,
hostFunction
) where
import Extism
import Extism.Bindings
import Data.Word
import qualified Data.ByteString as B
import Foreign.Ptr
import Foreign.ForeignPtr
import Foreign.C.String
import Foreign.StablePtr
import Foreign.Concurrent
import Foreign.Marshal.Array
import qualified Data.ByteString.Internal as BS (c2w)
-- | Access the plugin that is currently executing from inside a host function
type CurrentPlugin = Ptr ExtismCurrentPlugin
-- | A memory handle represents an allocated block of Extism memory
newtype MemoryHandle = MemoryHandle Word64 deriving (Num, Enum, Eq, Ord, Real, Integral, Show)
-- | Allocate a new handle of the given size
memoryAlloc :: CurrentPlugin -> Word64 -> IO MemoryHandle
memoryAlloc p n = MemoryHandle <$> extism_current_plugin_memory_alloc p n
-- | Get the length of a handle, returns 0 if the handle is invalid
memoryLength :: CurrentPlugin -> MemoryHandle -> IO Word64
memoryLength p (MemoryHandle offs) = extism_current_plugin_memory_length p offs
-- | Free allocated memory
memoryFree :: CurrentPlugin -> MemoryHandle -> IO ()
memoryFree p (MemoryHandle offs) = extism_current_plugin_memory_free p offs
-- | Access a pointer to the entire memory region
memory :: CurrentPlugin -> IO (Ptr Word8)
memory = extism_current_plugin_memory
-- | Access the pointer for the given 'MemoryHandle'
memoryOffset :: CurrentPlugin -> MemoryHandle -> IO (Ptr Word8)
memoryOffset plugin (MemoryHandle offs) = do
x <- extism_current_plugin_memory plugin
return $ plusPtr x (fromIntegral offs)
-- | Access the data associated with a handle as a 'ByteString'
memoryBytes :: CurrentPlugin -> MemoryHandle -> IO B.ByteString
memoryBytes plugin offs = do
ptr <- memoryOffset plugin offs
len <- memoryLength plugin offs
arr <- peekArray (fromIntegral len) ptr
return $ B.pack arr
-- | Access the data associated with a handle as a 'String'
memoryString :: CurrentPlugin -> MemoryHandle -> IO String
memoryString plugin offs = do
ptr <- memoryOffset plugin offs
len <- memoryLength plugin offs
arr <- peekArray (fromIntegral len) ptr
return $ fromByteString $ B.pack arr
-- | Allocate memory and copy an existing 'ByteString' into it
allocBytes :: CurrentPlugin -> B.ByteString -> IO MemoryHandle
allocBytes plugin s = do
let length = B.length s
offs <- memoryAlloc plugin (fromIntegral length)
ptr <- memoryOffset plugin offs
pokeArray ptr (B.unpack s)
return offs
-- | Allocate memory and copy an existing 'String' into it
allocString :: CurrentPlugin -> String -> IO MemoryHandle
allocString plugin s = do
let length = Prelude.length s
offs <- memoryAlloc plugin (fromIntegral length)
ptr <- memoryOffset plugin offs
pokeArray ptr (Prelude.map BS.c2w s)
return offs
-- | Create a new I32 'Val'
toI32 :: Integral a => a -> Val
toI32 x = ValI32 (fromIntegral x)
-- | Create a new I64 'Val'
toI64 :: Integral a => a -> Val
toI64 x = ValI64 (fromIntegral x)
-- | Create a new F32 'Val'
toF32 :: Float -> Val
toF32 = ValF32
-- | Create a new F64 'Val'
toF64 :: Double -> Val
toF64 = ValF64
-- | Get I32 'Val'
fromI32 :: Integral a => Val -> Maybe a
fromI32 (ValI32 x) = Just (fromIntegral x)
fromI32 _ = Nothing
-- | Get I64 'Val'
fromI64 :: Integral a => Val -> Maybe a
fromI64 (ValI64 x) = Just (fromIntegral x)
fromI64 _ = Nothing
-- | Get F32 'Val'
fromF32 :: Val -> Maybe Float
fromF32 (ValF32 x) = Just x
fromF32 _ = Nothing
-- | Get F64 'Val'
fromF64 :: Val -> Maybe Double
fromF64 (ValF64 x) = Just x
fromF64 _ = Nothing
-- | Create a new 'Function' that can be called from a 'Plugin'
hostFunction :: String -> [ValType] -> [ValType] -> (CurrentPlugin -> [Val] -> a -> IO [Val]) -> a -> IO Function
hostFunction name params results f v =
let nparams = fromIntegral $ length params in
let nresults = fromIntegral $ length results in
do
cb <- callbackWrap (callback f :: CCallback)
free <- freePtrWrap freePtr
userData <- newStablePtr (v, free, cb)
let userDataPtr = castStablePtrToPtr userData
x <- withCString name (\name -> do
withArray params (\params ->
withArray results (\results -> do
extism_function_new name params nparams results nresults cb userDataPtr free)))
let freeFn = extism_function_free x
fptr <- Foreign.Concurrent.newForeignPtr x freeFn
return $ Function fptr (castPtrToStablePtr userDataPtr)

View File

@@ -1,61 +1,58 @@
import Test.HUnit
import Extism
import Extism.Manifest
import Extism.HostFunction
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"]
defaultManifest = manifest [wasmFile "test/code.wasm"]
initPlugin :: IO Plugin
initPlugin =
Extism.pluginFromManifest defaultManifest [] False >>= assertUnwrap
initPlugin :: Context -> IO Plugin
initPlugin context =
Extism.pluginFromManifest context defaultManifest False >>= unwrap
pluginFunctionExists = do
p <- initPlugin
exists <- functionExists p "count_vowels"
assertBool "function exists" exists
exists' <- functionExists p "function_doesnt_exist"
assertBool "function doesn't exist" (not exists')
withContext (\ctx -> do
p <- initPlugin ctx
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
assertEqual "count vowels output" "{\"count\": 4}" (fromByteString res)
res <- call p "count_vowels" (toByteString "this is a test") >>= unwrap
assertEqual "count vowels output" "{\"count\": 4}" (fromByteString res)
pluginCall = do
p <- initPlugin
checkCallResult p
hello plugin params () = do
putStrLn "Hello from Haskell!"
offs <- allocBytes plugin (toByteString "{\"count\": 999}")
return [toI64 offs]
pluginCallHostFunction = do
p <- Extism.pluginFromManifest hostFunctionManifest [] False >>= assertUnwrap
res <- call p "count_vowels" (toByteString "this is a test") >>= assertUnwrap
assertEqual "count vowels output" "{\"count\": 999}" (fromByteString res)
withContext (\ctx -> do
p <- initPlugin ctx
checkCallResult p)
pluginMultiple = do
p <- initPlugin
withContext (\ctx -> do
p <- initPlugin ctx
checkCallResult p
q <- initPlugin
r <- initPlugin
q <- initPlugin ctx
r <- initPlugin ctx
checkCallResult q
checkCallResult r
checkCallResult r)
pluginUpdate = do
withContext (\ctx -> do
p <- initPlugin 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 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)
@@ -65,8 +62,8 @@ main = do
[
t "Plugin.FunctionExists" pluginFunctionExists
, t "Plugin.Call" pluginCall
, t "Plugin.CallHostFunction" pluginCallHostFunction
, t "Plugin.Multiple" pluginMultiple
, t "Plugin.Update" pluginUpdate
, t "Plugin.Config" pluginConfig
, t "SetLogFile" testSetLogFile
])

BIN
haskell/test/code.wasm Executable file

Binary file not shown.

View File

@@ -1,194 +1,190 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.extism.sdk</groupId>
<artifactId>extism</artifactId>
<packaging>jar</packaging>
<version>0.5.0</version>
<name>extism</name>
<url>https://github.com/extism/extism</url>
<description>Java-SDK for Extism to use webassembly from Java</description>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.extism.sdk</groupId>
<artifactId>extism</artifactId>
<packaging>jar</packaging>
<version>0.1.0</version>
<name>extism</name>
<url>https://github.com/extism/extism</url>
<description>Java-SDK for Extism to use webassembly from Java</description>
<licenses>
<license>
<name>BSD 3-Clause</name>
<url>https://opensource.org/licenses/BSD-3-Clause</url>
</license>
</licenses>
<licenses>
<license>
<name>BSD 3-Clause</name>
<url>https://opensource.org/licenses/BSD-3-Clause</url>
</license>
</licenses>
<organization>
<name>Dylibso, Inc.</name>
<url>https://dylib.so</url>
</organization>
<organization>
<name>Dylibso, Inc.</name>
<url>https://dylib.so</url>
</organization>
<developers>
<developer>
<name>The Extism Authors</name>
<email>oss@extism.org</email>
<roles>
<role>Maintainer</role>
</roles>
<organization>Dylibso, Inc.</organization>
<organizationUrl>https://dylib.so</organizationUrl>
</developer>
</developers>
<developers>
<developer>
<name>The Extism Authors</name>
<email>oss@extism.org</email>
<roles>
<role>Maintainer</role>
</roles>
<organization>Dylibso, Inc.</organization>
<organizationUrl>https://dylib.so</organizationUrl>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/extism/extism.git</connection>
<developerConnection>scm:git:ssh://git@github.com/extism/extism.git</developerConnection>
<url>https://github.com/extism/extism/tree/main/java</url>
<tag>main</tag>
</scm>
<scm>
<connection>scm:git:git://github.com/extism/extism.git</connection>
<developerConnection>scm:git:ssh://git@github.com/extism/extism.git</developerConnection>
<url>https://github.com/extism/extism/java</url>
<tag>main</tag>
</scm>
<issueManagement>
<system>Github</system>
<url>https://github.com/extism/extism/issues</url>
</issueManagement>
<issueManagement>
<system>Github</system>
<url>https://github.com/extism/extism/issues</url>
</issueManagement>
<properties>
<java.version>11</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- dependencies -->
<jna.version>5.12.1</jna.version>
<gson.version>2.10</gson.version>
<!-- dependencies -->
<jna.version>5.12.1</jna.version>
<gson.version>2.10</gson.version>
<!-- testing -->
<junit-jupiter-engine.version>5.9.1</junit-jupiter-engine.version>
<assertj-core.version>3.23.1</assertj-core.version>
<!-- testing -->
<junit-jupiter-engine.version>5.9.1</junit-jupiter-engine.version>
<assertj-core.version>3.23.1</assertj-core.version>
<!-- maven plugins -->
<maven-compiler-plugin.version>3.10.1</maven-compiler-plugin.version>
<maven-surefire-plugin.version>2.22.2</maven-surefire-plugin.version>
<!-- maven plugins -->
<maven-compiler-plugin.version>3.10.1</maven-compiler-plugin.version>
<maven-surefire-plugin.version>2.22.2</maven-surefire-plugin.version>
<!-- jreleaser -->
<jreleaser.git.root.search>true</jreleaser.git.root.search>
</properties>
<!-- jreleaser -->
<jreleaser.git.root.search>true</jreleaser.git.root.search>
</properties>
<profiles>
<profile>
<id>release</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.4.1</version>
<configuration>
<additionalJOption>-Xdoclint:none</additionalJOption>
</configuration>
<executions>
<execution>
<id>attach-javadoc</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<id>attach-source</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jreleaser</groupId>
<artifactId>jreleaser-maven-plugin</artifactId>
<version>1.3.1</version>
<configuration>
<jreleaser>
<release>
<github>
<skipRelease>true</skipRelease>
</github>
</release>
<signing>
<active>ALWAYS</active>
<armored>true</armored>
</signing>
<deploy>
<maven>
<nexus2>
<maven-central>
<active>ALWAYS</active>
<url>https://s01.oss.sonatype.org/service/local</url>
<!--
<profiles>
<profile>
<id>release</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<id>attach-javadoc</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<id>attach-source</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jreleaser</groupId>
<artifactId>jreleaser-maven-plugin</artifactId>
<version>1.3.1</version>
<configuration>
<jreleaser>
<release>
<github>
<skipRelease>true</skipRelease>
</github>
</release>
<signing>
<active>ALWAYS</active>
<armored>true</armored>
</signing>
<deploy>
<maven>
<nexus2>
<maven-central>
<active>ALWAYS</active>
<url>https://s01.oss.sonatype.org/service/local</url>
<!--
<closeRepository>false</closeRepository>
<releaseRepository>false</releaseRepository>
-->
<stagingRepositories>target/staging-deploy</stagingRepositories>
</maven-central>
</nexus2>
</maven>
</deploy>
</jreleaser>
</configuration>
</plugin>
<stagingRepositories>target/staging-deploy</stagingRepositories>
</maven-central>
</nexus2>
</maven>
</deploy>
</jreleaser>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<release>${java.version}</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration>
<systemPropertyVariables>
<jna.library.path>../target/release</jna.library.path>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</build>
<dependencies>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>${jna.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit-jupiter-engine.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj-core.version}</version>
<scope>test</scope>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<release>${java.version}</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration>
<systemPropertyVariables>
<jna.library.path>../target/release</jna.library.path>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>${jna.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit-jupiter-engine.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj-core.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>uk.org.webcompere</groupId>
<artifactId>model-assert</artifactId>
<version>1.0.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<dependency>
<groupId>uk.org.webcompere</groupId>
<artifactId>model-assert</artifactId>
<version>1.0.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -1,21 +0,0 @@
package org.extism.sdk;
import com.sun.jna.Pointer;
/**
* CancelHandle is used to cancel a running Plugin
*/
public class CancelHandle {
private Pointer handle;
public CancelHandle(Pointer handle) {
this.handle = handle;
}
/**
* Cancel execution of the Plugin associated with the CancelHandle
*/
boolean cancel() {
return LibExtism.INSTANCE.extism_plugin_cancel(this.handle);
}
}

View File

@@ -0,0 +1,89 @@
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
* @return the plugin instance
*/
public Plugin newPlugin(Manifest manifest, boolean withWASI) {
return new Plugin(this, manifest, withWASI);
}
/**
* Frees the context *and* frees all its Plugins. Use {@link #reset()}, if you just want to
* free the plugins but keep the context. You should ensure this is called when you are done
* with the context.
*/
public void free() {
LibExtism.INSTANCE.extism_context_free(this.contextPointer);
}
/**
* Resets the context by freeing all its Plugins. Unlike {@link #free()}, it does not
* free the context itself.
*/
public void reset() {
LibExtism.INSTANCE.extism_context_reset(this.contextPointer);
}
/**
* Get the version string of the linked Extism Runtime.
*
* @return the version
*/
public String getVersion() {
return LibExtism.INSTANCE.extism_version();
}
/**
* Get the error associated with a context, if plugin is {@literal null} then the context error will be returned.
*
* @param plugin
* @return the error message
*/
protected String error(Plugin plugin) {
return LibExtism.INSTANCE.extism_error(this.contextPointer, plugin == null ? -1 : plugin.getIndex());
}
/**
* Return the raw pointer to this context.
*
* @return the pointer
*/
public Pointer getPointer() {
return this.contextPointer;
}
/**
* Calls {@link #free()} if used in the context of a TWR block.
*/
@Override
public void close() {
this.free();
}
}

View File

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

View File

@@ -1,78 +0,0 @@
package org.extism.sdk;
import com.sun.jna.Pointer;
import java.nio.charset.StandardCharsets;
public class ExtismCurrentPlugin {
public Pointer pointer;
public ExtismCurrentPlugin(Pointer pointer) {
this.pointer = pointer;
}
public Pointer memory() {
return LibExtism.INSTANCE.extism_current_plugin_memory(this.pointer);
}
public int alloc(int n) {
return LibExtism.INSTANCE.extism_current_plugin_memory_alloc(this.pointer, n);
}
public void free(long offset) {
LibExtism.INSTANCE.extism_current_plugin_memory_free(this.pointer, offset);
}
public long memoryLength(long offset) {
return LibExtism.INSTANCE.extism_current_plugin_memory_length(this.pointer, offset);
}
/**
* Return a string from a host function
* @param output - The output to set
* @param s - The string to return
*/
public void returnString(LibExtism.ExtismVal output, String s) {
returnBytes(output, s.getBytes(StandardCharsets.UTF_8));
}
/**
* Return bytes from a host function
* @param output - The output to set
* @param b - The buffer to return
*/
public void returnBytes(LibExtism.ExtismVal output, byte[] b) {
int offs = this.alloc(b.length);
Pointer ptr = this.memory();
ptr.write(offs, b, 0, b.length);
output.v.i64 = offs;
}
/**
* Get bytes from host function parameter
* @param input - The input to read
*/
public byte[] inputBytes(LibExtism.ExtismVal input) {
switch (input.t) {
case 0:
return this.memory()
.getByteArray(input.v.i32,
LibExtism.INSTANCE.extism_current_plugin_memory_length(this.pointer, input.v.i32));
case 1:
return this.memory()
.getByteArray(input.v.i64,
LibExtism.INSTANCE.extism_current_plugin_memory_length(this.pointer, input.v.i64));
default:
throw new ExtismException("inputBytes error: ExtismValType " + LibExtism.ExtismValType.values()[input.t] + " not implemtented");
}
}
/**
* Get string from host function parameter
* @param input - The input to read
*/
public String inputString(LibExtism.ExtismVal input) {
return new String(this.inputBytes(input));
}
}

View File

@@ -1,12 +0,0 @@
package org.extism.sdk;
import java.util.Optional;
public interface ExtismFunction<T extends HostUserData> {
void invoke(
ExtismCurrentPlugin plugin,
LibExtism.ExtismVal[] params,
LibExtism.ExtismVal[] returns,
Optional<T> data
);
}

View File

@@ -1,92 +0,0 @@
package org.extism.sdk;
import com.sun.jna.Pointer;
import com.sun.jna.PointerType;
import java.util.Arrays;
import java.util.Optional;
public class HostFunction<T extends HostUserData> {
private final LibExtism.InternalExtismFunction callback;
public final Pointer pointer;
public final String name;
public final LibExtism.ExtismValType[] params;
public final LibExtism.ExtismValType[] returns;
public final Optional<T> userData;
public HostFunction(String name, LibExtism.ExtismValType[] params, LibExtism.ExtismValType[] returns, ExtismFunction f, Optional<T> userData) {
this.name = name;
this.params = params;
this.returns = returns;
this.userData = userData;
this.callback = (Pointer currentPlugin,
LibExtism.ExtismVal inputs,
int nInputs,
LibExtism.ExtismVal outs,
int nOutputs,
Pointer data) -> {
LibExtism.ExtismVal[] outputs = (LibExtism.ExtismVal []) outs.toArray(nOutputs);
f.invoke(
new ExtismCurrentPlugin(currentPlugin),
(LibExtism.ExtismVal []) inputs.toArray(nInputs),
outputs,
userData
);
for (LibExtism.ExtismVal output : outputs) {
convertOutput(output, output);
}
};
this.pointer = LibExtism.INSTANCE.extism_function_new(
this.name,
Arrays.stream(this.params).mapToInt(r -> r.v).toArray(),
this.params.length,
Arrays.stream(this.returns).mapToInt(r -> r.v).toArray(),
this.returns.length,
this.callback,
userData.map(PointerType::getPointer).orElse(null),
null
);
}
void convertOutput(LibExtism.ExtismVal original, LibExtism.ExtismVal fromHostFunction) {
if (fromHostFunction.t != original.t)
throw new ExtismException(String.format("Output type mismatch, got %d but expected %d", fromHostFunction.t, original.t));
if (fromHostFunction.t == LibExtism.ExtismValType.I32.v) {
original.v.setType(Integer.TYPE);
original.v.i32 = fromHostFunction.v.i32;
} else if (fromHostFunction.t == LibExtism.ExtismValType.I64.v) {
original.v.setType(Long.TYPE);
original.v.i64 = fromHostFunction.v.i64;
} else if (fromHostFunction.t == LibExtism.ExtismValType.F32.v) {
original.v.setType(Float.TYPE);
original.v.f32 = fromHostFunction.v.f32;
} else if (fromHostFunction.t == LibExtism.ExtismValType.F64.v) {
original.v.setType(Double.TYPE);
original.v.f64 = fromHostFunction.v.f64;
} else
throw new ExtismException(String.format("Unsupported return type: %s", original.t));
}
public void setNamespace(String name) {
if (this.pointer != null) {
LibExtism.INSTANCE.extism_function_set_namespace(this.pointer, name);
}
}
HostFunction withNamespace(String name) {
this.setNamespace(name);
return this;
}
}

View File

@@ -1,7 +0,0 @@
package org.extism.sdk;
import com.sun.jna.PointerType;
public class HostUserData extends PointerType {
}

View File

@@ -1,6 +1,8 @@
package org.extism.sdk;
import com.sun.jna.*;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
/**
* Wrapper around the Extism library.
@@ -13,78 +15,22 @@ public interface LibExtism extends Library {
*/
LibExtism INSTANCE = Native.load("extism", LibExtism.class);
interface InternalExtismFunction extends Callback {
void invoke(
Pointer currentPlugin,
ExtismVal inputs,
int nInputs,
ExtismVal outputs,
int nOutputs,
Pointer data
);
}
@Structure.FieldOrder({"t", "v"})
class ExtismVal extends Structure {
public int t;
public ExtismValUnion v;
}
class ExtismValUnion extends Union {
public int i32;
public long i64;
public float f32;
public double f64;
}
enum ExtismValType {
I32(0),
I64(1),
F32(2),
F64(3),
V128(4),
FuncRef(5),
ExternRef(6);
public final int v;
ExtismValType(int value) {
this.v = value;
}
}
Pointer extism_function_new(String name,
int[] inputs,
int nInputs,
int[] outputs,
int nOutputs,
InternalExtismFunction func,
Pointer userData,
Pointer freeUserData);
/**
* Create a new context
*/
Pointer extism_context_new();
/**
* Get the length of an allocated block
* NOTE: this should only be called from host functions.
* Free a context
*/
int extism_current_plugin_memory_length(Pointer plugin, long n);
void extism_context_free(Pointer contextPointer);
/**
* Returns a pointer to the memory of the currently running plugin
* NOTE: this should only be called from host functions.
* Remove all plugins from the registry.
*
* @param contextPointer
*/
Pointer extism_current_plugin_memory(Pointer plugin);
/**
* Allocate a memory block in the currently running plugin
* NOTE: this should only be called from host functions.
*/
int extism_current_plugin_memory_alloc(Pointer plugin, long n);
/**
* Free an allocated memory block
* NOTE: this should only be called from host functions.
*/
void extism_current_plugin_memory_free(Pointer plugin, long ptr);
void extism_context_reset(Pointer contextPointer);
/**
* Sets the logger to the given path with the given level of verbosity
@@ -96,74 +42,102 @@ 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(long contextPointer, byte[] wasm, long wasmSize, boolean withWASI);
/**
* Returns the Extism version string
*/
String extism_version();
/**
* 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 length the length of the `wasm` parameter
* @param withWASI enables/disables WASI
* @return id of the plugin or {@literal -1} in case of error
* @see #extism_plugin_new(long, byte[], long, boolean)
*/
int extism_plugin_new(Pointer contextPointer, byte[] wasm, int length, boolean withWASI);
/**
* 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(long, byte[], long, 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 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, 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);
void extism_function_set_namespace(Pointer p, String name);
int strlen(Pointer s);
boolean extism_plugin_config(Pointer contextPointer, int pluginIndex, byte[] json, int jsonLength);
}

View File

@@ -13,53 +13,57 @@ 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) {
Objects.requireNonNull(context, "context");
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;
}
Pointer[] errormsg = new Pointer[1];
Pointer p = LibExtism.INSTANCE.extism_plugin_new(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));
Pointer contextPointer = context.getPointer();
int index = LibExtism.INSTANCE.extism_plugin_new(contextPointer, manifestBytes, manifestBytes.length, withWASI);
if (index == -1) {
String error = context.error(this);
throw new ExtismException(error);
}
this.pluginPointer = p;
this.index= index;
this.context = context;
}
public Plugin(Manifest manifest, boolean withWASI, HostFunction[] functions) {
this(serialize(manifest), withWASI, functions);
public Plugin(Context context, Manifest manifest, boolean withWASI) {
this(context, serialize(manifest), withWASI);
}
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 +76,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 +104,36 @@ 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) {
return update(serialize(manifest), withWASI);
}
/**
* 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) {
Objects.requireNonNull(manifestBytes, "manifestBytes");
return LibExtism.INSTANCE.extism_plugin_update(context.getPointer(), index, manifestBytes, manifestBytes.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 +155,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);
}
/**
@@ -146,12 +165,4 @@ public class Plugin implements AutoCloseable {
public void close() {
free();
}
/**
* 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);
return new CancelHandle(handle);
}
}

View File

@@ -20,14 +20,11 @@ public class Manifest {
@SerializedName("allowed_hosts")
private final List<String> allowedHosts;
@SerializedName("allowed_paths")
private final Map<String, String> allowedPaths;
@SerializedName("config")
private final Map<String, String> config;
public Manifest() {
this(new ArrayList<>(), null, null, null, null);
this(new ArrayList<>(), null, null, null);
}
public Manifest(WasmSource source) {
@@ -35,27 +32,22 @@ public class Manifest {
}
public Manifest(List<WasmSource> sources) {
this(sources, null, null, null, null);
this(sources, null, null, null);
}
public Manifest(List<WasmSource> sources, MemoryOptions memoryOptions) {
this(sources, memoryOptions, null, null, null);
this(sources, memoryOptions, null, null);
}
public Manifest(List<WasmSource> sources, MemoryOptions memoryOptions, Map<String, String> config) {
this(sources, memoryOptions, config, null, null);
this(sources, memoryOptions, config, null);
}
public Manifest(List<WasmSource> sources, MemoryOptions memoryOptions, Map<String, String> config, List<String> allowedHosts) {
this(sources, memoryOptions, config, allowedHosts, null);
}
public Manifest(List<WasmSource> sources, MemoryOptions memoryOptions, Map<String, String> config, List<String> allowedHosts, Map<String, String> allowedPaths) {
this.sources = sources;
this.memoryOptions = memoryOptions;
this.config = config;
this.allowedHosts = allowedHosts;
this.allowedPaths = allowedPaths;
}
public void addSource(WasmSource source) {
@@ -83,11 +75,4 @@ public class Manifest {
}
return Collections.unmodifiableList(allowedHosts);
}
public Map<String, String> getAllowedPaths() {
if (allowedPaths == null || allowedPaths.isEmpty()) {
return Collections.emptyMap();
}
return Collections.unmodifiableMap(allowedPaths);
}
}

View File

@@ -3,27 +3,6 @@ package org.extism.sdk.manifest;
import java.util.Map;
// FIXME remove this and related stuff if not supported in java-sdk
public class ManifestHttpRequest {
public record ManifestHttpRequest(String url, Map<String, String> header, String method) {
}
private final String url;
private final Map<String, String> header;
private final String method;
public ManifestHttpRequest(String url, Map<String, String> header, String method) {
this.url = url;
this.header = header;
this.method = method;
}
public String url() {
return url;
}
public Map<String, String> header() {
return header;
}
public String method() {
return method;
}
}

View File

@@ -8,11 +8,5 @@ import com.google.gson.annotations.SerializedName;
*
* @param max Max number of pages.
*/
public class MemoryOptions {
@SerializedName("max")
private final Integer max;
public MemoryOptions(Integer max) {
this.max = max;
}
public record MemoryOptions(@SerializedName("max") Integer max) {
}

View File

@@ -1,14 +1,18 @@
package org.extism.sdk.support;
import com.google.gson.*;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import org.extism.sdk.manifest.Manifest;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class JsonSerde {
@@ -19,7 +23,7 @@ public class JsonSerde {
GSON = new GsonBuilder() //
.disableHtmlEscaping() //
// needed to convert the byte[] to a base64 encoded String
.registerTypeHierarchyAdapter(byte[].class, new ByteArrayAdapter()) //
.registerTypeHierarchyAdapter(byte[].class, new ByteArrayToBase64TypeAdapter()) //
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) //
.setPrettyPrinting() //
.create();
@@ -29,28 +33,14 @@ public class JsonSerde {
return GSON.toJson(manifest);
}
private static class ByteArrayAdapter extends TypeAdapter<byte[]> {
private static class ByteArrayToBase64TypeAdapter implements JsonSerializer<byte[]>, JsonDeserializer<byte[]> {
@Override
public void write(JsonWriter out, byte[] byteValue) throws IOException {
out.value(new String(Base64.getEncoder().encode(byteValue)));
public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return Base64.getDecoder().decode(json.getAsString());
}
@Override
public byte[] read(JsonReader in) {
try {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return new byte[]{};
}
String byteValue = in.nextString();
if (byteValue != null) {
return Base64.getDecoder().decode(byteValue);
}
return new byte[]{};
} catch (Exception e) {
throw new JsonParseException(e);
}
public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(Base64.getEncoder().withoutPadding().encodeToString(src));
}
}
}

View File

@@ -2,37 +2,10 @@ package org.extism.sdk.wasm;
/**
* WASM Source represented by raw bytes.
*
* @param name
* @param data the byte array representing the WASM code
* @param hash
*/
public class ByteArrayWasmSource implements WasmSource {
private final String name;
private final byte[] data;
private final String hash;
/**
* Constructor
* @param name
* @param data the byte array representing the WASM code
* @param hash
*/
public ByteArrayWasmSource(String name, byte[] data, String hash) {
this.name = name;
this.data = data;
this.hash = hash;
}
@Override
public String name() {
return name;
}
@Override
public String hash() {
return hash;
}
public byte[] data() {
return data;
}
public record ByteArrayWasmSource(String name, byte[] data, String hash) implements WasmSource {
}

View File

@@ -2,39 +2,11 @@ package org.extism.sdk.wasm;
/**
* WASM Source represented by a file referenced by a path.
*
* @param name
* @param path
* @param hash
*/
public class PathWasmSource implements WasmSource {
private final String name;
private final String path;
private final String hash;
/**
* Constructor
* @param name
* @param path
* @param hash
*/
public PathWasmSource(String name, String path, String hash) {
this.name = name;
this.path = path;
this.hash = hash;
}
@Override
public String name() {
return name;
}
@Override
public String hash() {
return hash;
}
public String path() {
return path;
}
public record PathWasmSource(String name, String path, String hash) implements WasmSource {
}

View File

@@ -0,0 +1,23 @@
package org.extism.sdk;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class ContextTests {
@Test
public void shouldReturnVersionString() {
try (var ctx = new Context()) {
var version = ctx.getVersion();
assertThat(version).isNotNull();
}
}
@Test
public void shouldAllowResetOnEmptyContext() {
try (var ctx = new Context()) {
ctx.reset();
}
}
}

View File

@@ -6,7 +6,6 @@ import org.extism.sdk.support.JsonSerde;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.HashMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.extism.sdk.TestWasmSources.CODE;
@@ -17,16 +16,13 @@ public class ManifestTests {
@Test
public void shouldSerializeManifestWithWasmSourceToJson() {
var paths = new HashMap<String, String>();
paths.put("/tmp/foo", "/tmp/extism-plugins/foo");
var manifest = new Manifest(List.of(CODE.pathWasmSource()), null, null, null, paths);
var manifest = new Manifest(CODE.pathWasmSource());
var json = JsonSerde.toJson(manifest);
assertNotNull(json);
assertJson(json).at("/wasm").isArray();
assertJson(json).at("/wasm").hasSize(1);
assertJson(json).at("/allowed_paths").isObject();
assertJson(json).at("/allowed_paths").hasSize(1);
}
@Test

View File

@@ -1,12 +1,12 @@
package org.extism.sdk;
import com.sun.jna.Pointer;
import org.extism.sdk.manifest.Manifest;
import org.extism.sdk.manifest.MemoryOptions;
import org.extism.sdk.wasm.WasmSourceResolver;
import org.junit.jupiter.api.Test;
import java.util.*;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.extism.sdk.TestWasmSources.CODE;
@@ -20,10 +20,10 @@ public class PluginTests {
@Test
public void shouldInvokeFunctionWithMemoryOptions() {
//FIXME check whether memory options are effective
var manifest = new Manifest(List.of(CODE.pathWasmSource()), new MemoryOptions(0));
assertThrows(ExtismException.class, () -> {
Extism.invokeFunction(manifest, "count_vowels", "Hello World");
});
var output = Extism.invokeFunction(manifest, "count_vowels", "Hello World");
assertThat(output).isEqualTo("{\"count\": 3}");
}
@Test
@@ -42,12 +42,14 @@ public class PluginTests {
assertThat(output).isEqualTo("{\"count\": 3}");
}
@Test
public void shouldInvokeFunctionFromByteArrayWasmSource() {
var manifest = new Manifest(CODE.byteArrayWasmSource());
var output = Extism.invokeFunction(manifest, "count_vowels", "Hello World");
assertThat(output).isEqualTo("{\"count\": 3}");
}
// TODO This test breaks on CI with error:
// data did not match any variant of untagged enum Wasm at line 8 column 3
// @Test
// public void shouldInvokeFunctionFromByteArrayWasmSource() {
// var manifest = new Manifest(CODE.byteArrayWasmSource());
// var output = Extism.invokeFunction(manifest, "count_vowels", "Hello World");
// assertThat(output).isEqualTo("{\"count\": 3}");
// }
@Test
public void shouldFailToInvokeUnknownFunction() {
@@ -57,6 +59,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,133 +79,29 @@ 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)) {
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)) {
var output = plugin.call(functionName, input);
assertThat(output).isEqualTo("{\"count\": 3}");
output = plugin.call(functionName, input);
assertThat(output).isEqualTo("{\"count\": 3}");
}
}
@Test
public void shouldAllowInvokeHostFunctionFromPDK() {
var parametersTypes = new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64};
var resultsTypes = new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64};
class MyUserData extends HostUserData {
private String data1;
private int data2;
public MyUserData(String data1, int data2) {
super();
this.data1 = data1;
this.data2 = data2;
output = plugin.call(functionName, input);
assertThat(output).isEqualTo("{\"count\": 3}");
}
}
ExtismFunction helloWorldFunction = (ExtismFunction<MyUserData>) (plugin, params, returns, data) -> {
System.out.println("Hello from Java Host Function!");
System.out.println(String.format("Input string received from plugin, %s", plugin.inputString(params[0])));
int offs = plugin.alloc(4);
Pointer mem = plugin.memory();
mem.write(offs, "test".getBytes(), 0, 4);
returns[0].v.i64 = offs;
data.ifPresent(d -> System.out.println(String.format("Host user data, %s, %d", d.data1, d.data2)));
};
HostFunction helloWorld = new HostFunction<>(
"hello_world",
parametersTypes,
resultsTypes,
helloWorldFunction,
Optional.of(new MyUserData("test", 2))
);
HostFunction[] functions = {helloWorld};
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");
}
}
@Test
public void shouldAllowInvokeHostFunctionWithoutUserData() {
var parametersTypes = new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64};
var resultsTypes = new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64};
ExtismFunction helloWorldFunction = (plugin, params, returns, data) -> {
System.out.println("Hello from Java Host Function!");
System.out.println(String.format("Input string received from plugin, %s", plugin.inputString(params[0])));
int offs = plugin.alloc(4);
Pointer mem = plugin.memory();
mem.write(offs, "test".getBytes(), 0, 4);
returns[0].v.i64 = offs;
assertThat(data.isEmpty());
};
HostFunction f = new HostFunction<>(
"hello_world",
parametersTypes,
resultsTypes,
helloWorldFunction,
Optional.empty()
)
.withNamespace("env");
HostFunction g = new HostFunction<>(
"hello_world",
parametersTypes,
resultsTypes,
helloWorldFunction,
Optional.empty()
)
.withNamespace("test");
HostFunction[] functions = {f,g};
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");
}
}
@Test
public void shouldFailToInvokeUnknownHostFunction() {
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");
}
}
}

View File

@@ -16,28 +16,19 @@ public enum TestWasmSources {
public Path getWasmFilePath() {
return Paths.get(WASM_LOCATION, "code.wasm");
}
public Path getWasmFunctionsFilePath() {
return Paths.get(WASM_LOCATION, "code-functions.wasm");
}
};
public static final String WASM_LOCATION = "src/test/resources";
public abstract Path getWasmFilePath();
public abstract Path getWasmFunctionsFilePath();
public PathWasmSource pathWasmSource() {
return resolvePathWasmSource(getWasmFilePath());
}
public PathWasmSource pathWasmFunctionsSource() {
return resolvePathWasmSource(getWasmFunctionsFilePath());
}
public ByteArrayWasmSource byteArrayWasmSource() {
try {
byte[] wasmBytes = Files.readAllBytes(getWasmFilePath());
var wasmBytes = Files.readAllBytes(getWasmFilePath());
return new WasmSourceResolver().resolve("wasm@" + Arrays.hashCode(wasmBytes), wasmBytes);
} catch (IOException ioe) {
throw new RuntimeException(ioe);

View File

@@ -1,2 +0,0 @@
[build]
target = "wasm32-unknown-unknown"

View File

@@ -1,11 +0,0 @@
[package]
name = "extism-runtime-kernel"
version = "0.1.0"
edition = "2021"
[dependencies]
[workspace]
members = [
"."
]

View File

@@ -1,20 +0,0 @@
# Extism kernel
The Extism kernel implements core parts of the Extism runtime in Rust compiled to WebAssembly. This code is a conceptual
re-write of [memory.rs][] with the goal of making core parts of the Extism implementation more portable across WebAssembly
runtimes.
See [lib.rs][] for more details about the implementation itself.
## Building
Because this crate is built using the `wasm32-unknown-unknown` target, it is a separate build process from the `extism-runtime` crate.
To build `extism-runtime.wasm`, strip it and copy it to the proper location in the `extism-runtime` tree you can run:
```shell
$ sh build.sh
```
[memory.rs]: https://github.com/extism/extism/blob/f4aa139eced4a74eb4a103f78222ba503e146109/runtime/src/memory.rs
[lib.rs]: ./src/lib.rs

View File

@@ -1,7 +0,0 @@
#!/usr/bin/env bash
cargo build --release --target wasm32-unknown-unknown --package extism-runtime-kernel --bin extism-runtime
cp target/wasm32-unknown-unknown/release/extism-runtime.wasm .
wasm-strip extism-runtime.wasm
mv extism-runtime.wasm ../runtime/src/extism-runtime.wasm

View File

@@ -1,10 +0,0 @@
#![no_main]
#![no_std]
pub use extism_runtime_kernel::*;
#[cfg(target_arch = "wasm32")]
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
core::arch::wasm32::unreachable()
}

View File

@@ -1,446 +0,0 @@
//! # Extism kernel
//!
//! - Isolated memory from both host and plugin
//! - An allocator for managing that memory
//! - Input/output handling
//! - Error message handling
//! - Backward compatible `extism_*` functions
//!
//! ## Allocator
//!
//! The Extism allocator is a bump allocator that tracks the `length` of the total number of bytes
//! available to the allocator and `position` to track how much of the data has been used. Things like memory
//! have not really been optimized at all. When a new allocation that is larger than the remaning size is made,
//! the allocator attempts to call `memory.grow` if that fails a `0` offset is returned, which should be interpreted
//! as a failed allocation.
//!
//! ## Input/Output
//!
//! Input and output are just allocated blocks of memory that are marked as either input or output using
//! the `extism_input_set` or `extism_output_set` functions. The global variables `INPUT_OFFSET` contains
//! the offset in memory to the input data and `INPUT_LENGTH` contains the size of the input data. `OUTPUT_OFFSET`
//! and `OUTPUT_LENGTH` are used for the output data.
//!
//! ## Error handling
//!
//! The `ERROR` global is used to track the current error message. If it is set to `0` then there is no error.
//! The length of the error message can be retreived using `extism_length`.
//!
//! ## Memory offsets
//! An offset of `0` is similar to a `NULL` pointer in C - it implies an allocation failure or memory error
//! of some kind
//!
//! ## Extism functions
//!
//! These functions are backward compatible with the pre-kernel runtime, but a few new functions are added to
//! give runtimes more access to the internals necesarry to load data in and out of a plugin.
#![no_std]
#![allow(clippy::missing_safety_doc)]
use core::sync::atomic::*;
pub type Pointer = u64;
pub type Length = u64;
/// WebAssembly page size
const PAGE_SIZE: usize = 65536;
/// Determines the amount of bytes that can be wasted by re-using a block. If more than this number is wasted by re-using
/// a block then it will be split into two smaller blocks.
const BLOCK_SPLIT_SIZE: usize = 128;
/// Offset to the input data
static mut INPUT_OFFSET: Pointer = 0;
/// Length of the input data
static mut INPUT_LENGTH: Length = 0;
/// Offset to the output data
static mut OUTPUT_OFFSET: Pointer = 0;
/// Offset to the input data
static mut OUTPUT_LENGTH: Length = 0;
/// Current error message
static mut ERROR: AtomicU64 = AtomicU64::new(0);
/// Determines if the kernel has been initialized already
static mut INITIALIZED: AtomicBool = AtomicBool::new(false);
/// A pointer to the first page that will be managed by Extism, this is set during initialization
static mut START_PAGE: usize = 0;
/// Provides information about the usage status of a `MemoryBlock`
#[repr(u8)]
#[derive(PartialEq)]
pub enum MemoryStatus {
/// Unused memory that is available b
Unused = 0,
/// In-use memory
Active = 1,
/// Free memory that is available for re-use
Free = 2,
}
/// A single `MemoryRoot` exists at the start of the memory to track information about the total
/// size of the allocated memory and the position of the bump allocator.
///
/// The overall layout of the Extism-manged memory is organized like this:
/// |------|-------|---------|-------|--------------|
/// | Root | Block | Data | Block | Data | ...
/// |------|-------|---------|-------|--------------|
///
/// Where `Root` and `Block` are fixed to the size of the `MemoryRoot` and `MemoryBlock` structs. But
/// the size of `Data` is dependent on the allocation size.
#[repr(C)]
pub struct MemoryRoot {
/// Position of the bump allocator, relative to `START_PAGE`
pub position: AtomicU64,
/// The total size of all data allocated using this allocator
pub length: AtomicU64,
/// A pointer to where the blocks begin
pub blocks: [MemoryBlock; 0],
}
/// A `MemoryBlock` contains some metadata about a single allocation
#[repr(C)]
pub struct MemoryBlock {
/// The usage status of the block, `Unused` or `Free` blocks can be re-used.
pub status: AtomicU8,
/// The total size of the allocation
pub size: usize,
/// The number of bytes currently being used. If this block is a fresh allocation then `size` and `used` will
/// always be the same. If a block is re-used then these numbers may differ.
pub used: usize,
/// A pointer to the block data
pub data: [u8; 0],
}
/// 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
}
// Get the `MemoryRoot` at the correct offset in memory
#[inline]
unsafe fn memory_root() -> &'static mut MemoryRoot {
&mut *((START_PAGE * PAGE_SIZE) as *mut MemoryRoot)
}
impl MemoryRoot {
/// Initialize or load the `MemoryRoot` from the correct position in memory
pub unsafe fn new() -> &'static mut MemoryRoot {
// If this fails then `INITIALIZED` is already `true` and we can just return the
// already initialized `MemoryRoot`
if INITIALIZED
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
return memory_root();
}
// Ensure that at least one page is allocated to store the `MemoryRoot` data
START_PAGE = core::arch::wasm32::memory_grow(0, 1);
if START_PAGE == usize::MAX {
panic!("Out of memory");
}
// Initialize the `MemoryRoot` length, position and data
let root = memory_root();
root.length.store(
PAGE_SIZE as u64 - core::mem::size_of::<MemoryRoot>() as u64,
Ordering::Release,
);
root.position.store(0, Ordering::Release);
// Ensure the first block is marked as `Unused`
#[allow(clippy::size_of_in_element_count)]
core::ptr::write_bytes(
root.blocks.as_mut_ptr() as *mut _,
MemoryStatus::Unused as u8,
core::mem::size_of::<MemoryBlock>(),
);
root
}
/// Resets the position of the allocator and zeroes out all allocations
pub unsafe fn reset(&mut self) {
core::ptr::write_bytes(
self.blocks.as_mut_ptr() as *mut u8,
0,
self.length.load(Ordering::Acquire) as usize,
);
self.position.store(0, Ordering::Release);
}
// Find a block that is free to use, this can be a new block or an existing freed block. The `self_position` argument
// is used to avoid loading the allocators position more than once when performing an allocation.
unsafe fn find_free_block(
&mut self,
length: Length,
self_position: u64,
) -> Option<&'static mut MemoryBlock> {
// Get the first block
let mut block = self.blocks.as_mut_ptr();
// Only loop while the block pointer is less then the current position
while (block as u64) < self.blocks.as_ptr() as u64 + self_position {
let b = &mut *block;
let status = b.status.load(Ordering::Acquire);
// An unused block is safe to use
if status == MemoryStatus::Unused as u8 {
return Some(b);
}
// Re-use freed blocks when they're large enough
if status == MemoryStatus::Free as u8 && b.size >= length as usize {
// Split block if there is too much excess
if b.size - length as usize >= BLOCK_SPLIT_SIZE {
b.size -= length as usize;
b.used = 0;
let block1 = b.data.as_mut_ptr().add(b.size) as *mut MemoryBlock;
let b1 = &mut *block1;
b1.size = length as usize;
b1.used = 0;
b1.status.store(MemoryStatus::Free as u8, Ordering::Release);
return Some(b1);
}
// Otherwise return the whole block
return Some(b);
}
// Get the next block
block = b.next_ptr();
}
None
}
/// Create a new `MemoryBlock`, when `Some(block)` is returned, `block` will contain at least enough room for `length` bytes
/// but may be as large as `length` + `BLOCK_SPLIT_SIZE` bytes. When `None` is returned the allocation has failed.
pub unsafe fn alloc(&mut self, length: Length) -> Option<&'static mut MemoryBlock> {
let self_position = self.position.load(Ordering::Acquire);
let self_length = self.length.load(Ordering::Acquire);
let b = self.find_free_block(length, self_position);
// If there's a free block then re-use it
if let Some(b) = b {
b.used = length as usize;
b.status
.store(MemoryStatus::Active as u8, Ordering::Release);
return Some(b);
}
// Get the current index for a new block
let curr = self.blocks.as_ptr() as u64 + self_position;
// Get the number of bytes available
let mem_left = self_length - self_position;
// 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 x = core::arch::wasm32::memory_grow(0, npages);
if x == usize::MAX {
return None;
}
self.length
.fetch_add(npages as u64 * PAGE_SIZE as u64, Ordering::SeqCst);
}
// Bump the position by the size of the actual data + the size of the MemoryBlock structure
self.position.fetch_add(
length + core::mem::size_of::<MemoryBlock>() as u64,
Ordering::SeqCst,
);
// Initialize a new block at the current position
let ptr = curr as *mut MemoryBlock;
let block = &mut *ptr;
block
.status
.store(MemoryStatus::Active as u8, Ordering::Release);
block.size = length as usize;
block.used = length as usize;
Some(block)
}
/// Finds the block at an offset in memory
pub unsafe fn find_block(&mut self, offs: Pointer) -> Option<&mut MemoryBlock> {
if offs >= self.blocks.as_ptr() as Pointer + self.length.load(Ordering::Acquire) as Pointer
{
return None;
}
let ptr = offs - core::mem::size_of::<MemoryBlock>() as u64;
let ptr = ptr as *mut MemoryBlock;
Some(&mut *ptr)
}
}
impl MemoryBlock {
/// Get a pointer to the next block
///
/// NOTE: This does no checking to ensure the resulting pointer is valid, the offset
/// is calculated based on metadata provided by the current block
#[inline]
pub unsafe fn next_ptr(&mut self) -> *mut MemoryBlock {
self.data
.as_mut_ptr()
.add(self.size + core::mem::size_of::<MemoryBlock>()) as *mut MemoryBlock
}
/// Mark a block as free
pub fn free(&mut self) {
self.status
.store(MemoryStatus::Free as u8, Ordering::Release);
}
}
// Extism functions - these functions should be
/// Allocate a block of memory and return the offset
#[no_mangle]
pub unsafe fn extism_alloc(n: Length) -> Pointer {
let region = MemoryRoot::new();
let block = region.alloc(n);
match block {
Some(block) => block.data.as_mut_ptr() as Pointer,
None => 0,
}
}
/// Free allocated memory
#[no_mangle]
pub unsafe fn extism_free(p: Pointer) {
let block = MemoryRoot::new().find_block(p);
if let Some(block) = block {
block.free();
}
}
/// Get the length of an allocated memory block
#[no_mangle]
pub unsafe fn extism_length(p: Pointer) -> Length {
if p == 0 {
return 0;
}
if let Some(block) = MemoryRoot::new().find_block(p) {
block.used as Length
} else {
0
}
}
/// Load a byte from Extism-managed memory
#[no_mangle]
pub unsafe fn extism_load_u8(p: Pointer) -> u8 {
*(p as *mut u8)
}
/// Load a u64 from Extism-managed memory
#[no_mangle]
pub unsafe fn extism_load_u64(p: Pointer) -> u64 {
*(p as *mut u64)
}
/// Load a byte from the input data
#[no_mangle]
pub unsafe fn extism_input_load_u8(p: Pointer) -> u8 {
*((INPUT_OFFSET + p) as *mut u8)
}
/// Load a u64 from the input data
#[no_mangle]
pub unsafe fn extism_input_load_u64(p: Pointer) -> u64 {
*((INPUT_OFFSET + p) as *mut u64)
}
/// Write a byte in Extism-managed memory
#[no_mangle]
pub unsafe fn extism_store_u8(p: Pointer, x: u8) {
*(p as *mut u8) = x;
}
/// Write a u64 in Extism-managed memory
#[no_mangle]
pub unsafe fn extism_store_u64(p: Pointer, x: u64) {
unsafe {
*(p as *mut u64) = x;
}
}
/// Set the range of the input data in memory
#[no_mangle]
pub fn extism_input_set(p: Pointer, len: Length) {
unsafe {
INPUT_OFFSET = p;
INPUT_LENGTH = len;
}
}
/// Set the range of the output data in memory
#[no_mangle]
pub fn extism_output_set(p: Pointer, len: Length) {
unsafe {
OUTPUT_OFFSET = p;
OUTPUT_LENGTH = len;
}
}
/// Get the input length
#[no_mangle]
pub fn extism_input_length() -> Length {
unsafe { INPUT_LENGTH }
}
/// Get the input offset in Exitsm-managed memory
#[no_mangle]
pub fn extism_input_offset() -> Length {
unsafe { INPUT_OFFSET }
}
/// Get the output length
#[no_mangle]
pub fn extism_output_length() -> Length {
unsafe { OUTPUT_LENGTH }
}
/// Get the output offset in Extism-managed memory
#[no_mangle]
pub fn extism_output_offset() -> Length {
unsafe { OUTPUT_OFFSET }
}
/// Reset the allocator
#[no_mangle]
pub unsafe fn extism_reset() {
ERROR.store(0, Ordering::SeqCst);
MemoryRoot::new().reset()
}
/// Set the error message offset
#[no_mangle]
pub unsafe fn extism_error_set(ptr: Pointer) {
ERROR.store(ptr, Ordering::SeqCst);
}
/// Get the error message offset, if it's `0` then no error has been set
#[no_mangle]
pub unsafe fn extism_error_get() -> Pointer {
ERROR.load(Ordering::SeqCst)
}
/// Get the position of the allocator, this can be used as an indication of how many bytes are currently in-use
#[no_mangle]
pub unsafe fn extism_memory_bytes() -> Length {
MemoryRoot::new().position.load(Ordering::Acquire)
}

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