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
166 changed files with 5930 additions and 13162 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

@@ -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,15 @@ jobs:
override: true
target: ${{ matrix.target }}
- name: Release Rust Manifest Crate
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 5
- 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 5
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

@@ -4,5 +4,5 @@ members = [
"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");
@@ -56,18 +41,13 @@ int main(int argc, char *argv[]) {
ExtismContext *ctx = extism_context_new();
size_t len = 0;
uint8_t *data = read_file("../wasm/code-functions.wasm", &len);
ExtismValType inputs[] = {I64};
ExtismValType outputs[] = {I64};
ExtismFunction *f = extism_function_new("hello_world", inputs, 1, outputs, 1,
hello_world, "Hello, again!", NULL);
ExtismPlugin plugin =
extism_plugin_new(ctx, data, len, (const ExtismFunction **)&f, 1, true);
uint8_t *data = read_file("../wasm/code.wasm", &len);
ExtismPlugin plugin = extism_plugin_new(ctx, data, len, false);
free(data);
if (plugin < 0) {
puts(extism_error(ctx, -1));
exit(1);
}
assert(extism_plugin_call(ctx, plugin, "count_vowels", (uint8_t *)argv[1],
strlen(argv[1])) == 0);
ExtismSize out_len = extism_plugin_output_length(ctx, plugin);
@@ -76,7 +56,6 @@ int main(int argc, char *argv[]) {
write(STDOUT_FILENO, "\n", 1);
extism_plugin_free(ctx, plugin);
extism_function_free(f);
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,141 +166,14 @@ public:
}
};
typedef ExtismValType ValType;
typedef ExtismValUnion ValUnion;
typedef ExtismVal Val;
class CurrentPlugin {
ExtismCurrentPlugin *pointer;
public:
CurrentPlugin(ExtismCurrentPlugin *p) : pointer(p) {}
uint8_t *memory() { return extism_current_plugin_memory(this->pointer); }
ExtismSize memory_length(uint64_t offs) {
return extism_current_plugin_memory_length(this->pointer, offs);
}
uint64_t alloc(ExtismSize size) {
return extism_current_plugin_memory_alloc(this->pointer, size);
}
void free(uint64_t offs) {
extism_current_plugin_memory_free(this->pointer, offs);
}
void 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->memory_length(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 set_namespace(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::shared_ptr<ExtismContext> context;
ExtismPlugin plugin;
std::vector<Function> functions;
public:
// Create a new plugin
Plugin(const uint8_t *wasm, ExtismSize length, bool with_wasi = false,
std::vector<Function> functions = std::vector<Function>(),
std::shared_ptr<ExtismContext> ctx = std::shared_ptr<ExtismContext>(
extism_context_new(), extism_context_free))
: functions(functions) {
std::vector<const ExtismFunction *> ptrs;
for (auto i : this->functions) {
ptrs.push_back(i.get());
}
this->plugin = extism_plugin_new(ctx.get(), wasm, length, ptrs.data(),
ptrs.size(), with_wasi);
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);
@@ -360,39 +181,12 @@ public:
this->context = ctx;
}
Plugin(const std::string &str, bool with_wasi = false,
std::vector<Function> functions = {},
std::shared_ptr<ExtismContext> ctx = std::shared_ptr<ExtismContext>(
extism_context_new(), extism_context_free))
: Plugin((const uint8_t *)str.c_str(), str.size(), with_wasi, functions,
ctx) {}
Plugin(const std::vector<uint8_t> &data, bool with_wasi = false,
std::vector<Function> functions = {},
std::shared_ptr<ExtismContext> ctx = std::shared_ptr<ExtismContext>(
extism_context_new(), extism_context_free))
: Plugin(data.data(), data.size(), with_wasi, functions, ctx) {}
CancelHandle cancel_handle() {
return CancelHandle(
extism_plugin_cancel_handle(this->context.get(), this->id()));
}
#ifndef EXTISM_NO_JSON
// Create a new plugin from Manifest
Plugin(const Manifest &manifest, bool with_wasi = false,
std::vector<Function> functions = {},
std::shared_ptr<ExtismContext> ctx = std::shared_ptr<ExtismContext>(
extism_context_new(), extism_context_free)) {
std::vector<const ExtismFunction *> ptrs;
for (auto i : this->functions) {
ptrs.push_back(i.get());
}
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(), ptrs.data(), ptrs.size(), with_wasi);
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);
@@ -410,15 +204,9 @@ public:
ExtismContext *get_context() const { return this->context.get(); }
void update(const uint8_t *wasm, size_t length, bool with_wasi = false,
std::vector<Function> functions = {}) {
this->functions = functions;
std::vector<const ExtismFunction *> ptrs;
for (auto i : this->functions) {
ptrs.push_back(i.get());
}
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, ptrs.data(), ptrs.size(), with_wasi);
length, with_wasi);
if (!b) {
const char *err = extism_error(this->context.get(), -1);
throw Error(err == nullptr ? "Unable to update plugin" : err);
@@ -426,17 +214,11 @@ public:
}
#ifndef EXTISM_NO_JSON
void update(const Manifest &manifest, bool with_wasi = false,
std::vector<Function> functions = {}) {
this->functions = functions;
std::vector<const ExtismFunction *> ptrs;
for (auto i : this->functions) {
ptrs.push_back(i.get());
}
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(), ptrs.data(), ptrs.size(), with_wasi);
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);
@@ -469,7 +251,6 @@ 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->context.get(), this->plugin,
@@ -490,19 +271,15 @@ public:
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 function_exists(const std::string &func) const {
return extism_plugin_function_exists(this->context.get(), this->plugin,
func.c_str());
@@ -513,49 +290,38 @@ class Context {
public:
std::shared_ptr<ExtismContext> pointer;
// Create a new context;
Context() {
this->pointer = std::shared_ptr<ExtismContext>(extism_context_new(),
extism_context_free);
}
// Create plugin from uint8_t*
Plugin plugin(const uint8_t *wasm, size_t length, bool with_wasi = false,
std::vector<Function> functions = {}) const {
return Plugin(wasm, length, with_wasi, functions, this->pointer);
Plugin plugin(const uint8_t *wasm, size_t length,
bool with_wasi = false) const {
return Plugin(this->pointer, wasm, length, with_wasi);
}
// Create plugin from std::string
Plugin plugin(const std::string &str, bool with_wasi = false,
std::vector<Function> functions = {}) const {
return Plugin((const uint8_t *)str.c_str(), str.size(), with_wasi,
functions, this->pointer);
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);
}
// Create plugin from uint8_t vector
Plugin plugin(const std::vector<uint8_t> &data, bool with_wasi = false,
std::vector<Function> functions = {}) const {
return Plugin(data.data(), data.size(), with_wasi, functions,
this->pointer);
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
// Create plugin from Manifest
Plugin plugin(const Manifest &manifest, bool with_wasi = false,
std::vector<Function> functions = {}) const {
return Plugin(manifest, with_wasi, functions, this->pointer);
Plugin plugin(const Manifest &manifest, bool with_wasi = false) const {
return Plugin(this->pointer, manifest, with_wasi);
}
#endif
// Remove all plugins
void reset() { extism_context_reset(this->pointer.get()); }
};
// Set global log file for plugins
inline bool set_log_file(const char *filename, const char *level) {
return extism_log_file(filename, level);
}
// Get libextism version
inline std::string version() { return std::string(extism_version()); }
} // namespace extism

View File

@@ -10,8 +10,6 @@ std::vector<uint8_t> read(const char *filename) {
std::istreambuf_iterator<char>());
}
const std::string code = "../../wasm/code.wasm";
namespace {
using namespace extism;
@@ -21,24 +19,26 @@ TEST(Context, Basic) {
}
TEST(Plugin, Manifest) {
Manifest manifest = Manifest::path(code);
Context context;
Manifest manifest = Manifest::path("code.wasm");
manifest.set_config("a", "1");
ASSERT_NO_THROW(Plugin plugin(manifest));
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) {
Context context;
auto wasm = read(code.c_str());
auto wasm = read("code.wasm");
ASSERT_NO_THROW(Plugin plugin = context.plugin(wasm));
Plugin plugin = context.plugin(wasm);
@@ -48,7 +48,7 @@ TEST(Plugin, Bytes) {
TEST(Plugin, UpdateConfig) {
Context context;
auto wasm = read(code.c_str());
auto wasm = read("code.wasm");
Plugin plugin = context.plugin(wasm);
Config config;
@@ -58,33 +58,13 @@ TEST(Plugin, UpdateConfig) {
TEST(Plugin, FunctionExists) {
Context context;
auto wasm = read(code.c_str());
auto wasm = read("code.wasm");
Plugin plugin = context.plugin(wasm);
ASSERT_FALSE(plugin.function_exists("bad_function"));
ASSERT_TRUE(plugin.function_exists("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");
}
}; // namespace
int main(int argc, char **argv) {

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.4.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,40 +1,11 @@
using Extism.Sdk;
using Extism.Sdk.Native;
using System.Runtime.InteropServices;
using System.Text;
var context = new Context();
var userData = Marshal.StringToHGlobalAnsi("Hello again!");
using var helloWorld = new HostFunction(
"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 wasm = File.ReadAllBytes("./code-functions.wasm");
using var plugin = context.CreatePlugin(wasm, new[] { helloWorld }, withWasi: true);
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

@@ -1,4 +1,3 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
@@ -7,10 +6,8 @@ namespace Extism.Sdk.Native;
/// <summary>
/// Represents an Extism context through which you can load <see cref="Plugin"/>s.
/// </summary>
public unsafe class Context : IDisposable
public class Context : IDisposable
{
private readonly ConcurrentDictionary<int, Plugin> _plugins = new ConcurrentDictionary<int, Plugin>();
private const int DisposedMarker = 1;
private int _disposed;
@@ -20,63 +17,33 @@ public unsafe class Context : IDisposable
/// </summary>
public Context()
{
unsafe
{
NativeHandle = LibExtism.extism_context_new();
}
NativeHandle = LibExtism.extism_context_new();
}
/// <summary>
/// Native pointer to the Extism Context.
/// </summary>
internal LibExtism.ExtismContext* NativeHandle { get; }
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="functions">List of host functions expected by the plugin.</param>
/// <param name="withWasi">Enable/Disable WASI.</param>
public Plugin CreatePlugin(ReadOnlySpan<byte> wasm, HostFunction[] functions, bool withWasi)
public Plugin CreatePlugin(ReadOnlySpan<byte> wasm, bool withWasi)
{
CheckNotDisposed();
var functionHandles = functions.Select(f => f.NativeHandle).ToArray();
unsafe
{
fixed (byte* wasmPtr = wasm)
fixed (IntPtr* functionsPtr = functionHandles)
{
var index = LibExtism.extism_plugin_new(NativeHandle, wasmPtr, wasm.Length, functionsPtr, functions.Length, withWasi);
if (index == -1)
{
var errorMsg = GetError();
if (errorMsg != null)
{
throw new ExtismException(errorMsg);
}
else
{
throw new ExtismException("Failed to create plugin.");
}
}
return _plugins[index] = new Plugin(this, functions, index);
var plugin = LibExtism.extism_plugin_new(NativeHandle, wasmPtr, wasm.Length, withWasi);
return new Plugin(this, plugin);
}
}
}
/// <summary>
/// Get a plugin by index.
/// </summary>
/// <param name="index">Index of plugin.</param>
/// <returns></returns>
public Plugin GetPlugin(int index)
{
return _plugins[index];
}
/// <summary>
/// Remove all plugins from this <see cref="Context"/>'s registry.
/// </summary>
@@ -143,11 +110,6 @@ public unsafe class Context : IDisposable
// Free up any managed resources here
}
foreach (var plugin in _plugins.Values)
{
plugin.Dispose();
}
// Free up unmanaged resources
LibExtism.extism_context_free(NativeHandle);
}
@@ -189,3 +151,34 @@ public unsafe class Context : IDisposable
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

@@ -10,7 +10,7 @@
<PropertyGroup>
<PackageId>Extism.Sdk</PackageId>
<Version>0.6.0</Version>
<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>

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,197 +2,24 @@ 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>
/// A `Context` is used to store and manage plugins.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct ExtismContext { }
/// <summary>
/// Host function signature
/// </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);
/// <summary>
/// Create a new context.
/// </summary>
/// <returns>A pointer to the newly created context.</returns>
[DllImport("extism")]
unsafe internal static extern ExtismContext* extism_context_new();
public static extern IntPtr extism_context_new();
/// <summary>
/// Remove a context from the registry and free associated memory.
/// </summary>
/// <param name="context"></param>
[DllImport("extism")]
unsafe internal static extern void extism_context_free(ExtismContext* context);
public static extern void extism_context_free(IntPtr context);
/// <summary>
/// Load a WASM plugin.
@@ -200,12 +27,10 @@ internal static class LibExtism
/// <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>
/// <returns></returns>
[DllImport("extism")]
unsafe internal static extern int extism_plugin_new(ExtismContext* context, byte* wasm, int wasmSize, IntPtr* functions, int nFunctions, bool withWasi);
unsafe public static extern IntPtr extism_plugin_new(IntPtr context, byte* wasm, int wasmSize, bool withWasi);
/// <summary>
/// Update a plugin, keeping the existing ID.
@@ -215,13 +40,11 @@ internal static class LibExtism
/// <param name="context">Pointer to the context the plugin is associated with.</param>
/// <param name="plugin">Pointer to the plugin you want to update.</param>
/// <param name="wasm">A WASM module (wat or wasm) or a JSON encoded manifest.</param>
/// <param name="wasmSize">The length of the `wasm` parameter.</param>
/// <param name="functions">Array of host function pointers.</param>
/// <param name="nFunctions">Number of host functions.</param>
/// <param name="wasmLength">The length of the `wasm` parameter.</param>
/// <param name="withWasi">Enables/disables WASI.</param>
/// <returns></returns>
[DllImport("extism")]
unsafe internal static extern bool extism_plugin_update(ExtismContext* context, int plugin, byte* wasm, long wasmSize, Span<IntPtr> functions, long nFunctions, bool withWasi);
unsafe 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.
@@ -229,14 +52,14 @@ internal static class LibExtism
/// <param name="context">Pointer to the context the plugin is associated with.</param>
/// <param name="plugin">Pointer to the plugin you want to free.</param>
[DllImport("extism")]
unsafe internal static extern void extism_plugin_free(ExtismContext* context, int plugin);
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")]
unsafe internal static extern void extism_context_reset(ExtismContext* context);
public static extern void extism_context_reset(IntPtr context);
/// <summary>
/// Update plugin config values, this will merge with the existing values.
@@ -247,7 +70,7 @@ internal static class LibExtism
/// <param name="jsonLength">The length of the `json` parameter.</param>
/// <returns></returns>
[DllImport("extism")]
unsafe internal static extern bool extism_plugin_config(ExtismContext* context, int plugin, byte* json, int jsonLength);
unsafe public static extern bool extism_plugin_config(IntPtr context, IntPtr plugin, byte* json, int jsonLength);
/// <summary>
/// Returns true if funcName exists.
@@ -257,7 +80,7 @@ internal static class LibExtism
/// <param name="funcName"></param>
/// <returns></returns>
[DllImport("extism")]
unsafe internal static extern bool extism_plugin_function_exists(ExtismContext* context, int plugin, string funcName);
public static extern bool extism_plugin_function_exists(IntPtr context, IntPtr plugin, string funcName);
/// <summary>
/// Call a function.
@@ -269,7 +92,7 @@ internal static class LibExtism
/// <param name="dataLen">The length of the `data` parameter.</param>
/// <returns></returns>
[DllImport("extism")]
unsafe internal static extern int extism_plugin_call(ExtismContext* context, int plugin, string funcName, byte* data, int dataLen);
unsafe public static extern int extism_plugin_call(IntPtr context, IntPtr plugin, string funcName, byte* data, int dataLen);
/// <summary>
/// Get the error associated with a Context or Plugin, if plugin is -1 then the context error will be returned.
@@ -278,7 +101,7 @@ internal static class LibExtism
/// <param name="plugin">A plugin pointer, or -1 for the context error.</param>
/// <returns></returns>
[DllImport("extism")]
unsafe internal static extern IntPtr extism_error(ExtismContext* context, nint plugin);
public static extern IntPtr extism_error(IntPtr context, nint plugin);
/// <summary>
/// Get the length of a plugin's output data.
@@ -287,7 +110,7 @@ internal static class LibExtism
/// <param name="plugin"></param>
/// <returns></returns>
[DllImport("extism")]
unsafe internal static extern long extism_plugin_output_length(ExtismContext* context, int plugin);
public static extern long extism_plugin_output_length(IntPtr context, IntPtr plugin);
/// <summary>
/// Get the plugin's output data.
@@ -296,7 +119,7 @@ internal static class LibExtism
/// <param name="plugin"></param>
/// <returns></returns>
[DllImport("extism")]
unsafe internal static extern IntPtr extism_plugin_output_data(ExtismContext* context, int plugin);
public static extern IntPtr extism_plugin_output_data(IntPtr context, IntPtr plugin);
/// <summary>
/// Set log file and level.
@@ -305,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 the Extism version string.
/// </summary>
/// <returns></returns>
[DllImport("extism", EntryPoint = "extism_version")]
internal static extern IntPtr 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

@@ -11,32 +11,18 @@ public class Plugin : IDisposable
private const int DisposedMarker = 1;
private readonly Context _context;
private readonly HostFunction[] _functions;
private int _disposed;
/// <summary>
/// Create a and load a plug-in
/// Using this constructor will give the plug-in it's own internal Context
/// </summary>
/// <param name="wasm">A WASM module (wat or wasm) or a JSON encoded manifest.</param>
/// <param name="functions">List of host functions expected by the plugin.</param>
/// <param name="withWasi">Enable/Disable WASI.</param>
public static Plugin Create(ReadOnlySpan<byte> wasm, HostFunction[] functions, bool withWasi) {
var context = new Context();
return context.CreatePlugin(wasm, functions, withWasi);
}
internal Plugin(Context context, HostFunction[] functions, int index)
internal Plugin(Context context, IntPtr handle)
{
_context = context;
_functions = functions;
Index = index;
NativeHandle = handle;
}
/// <summary>
/// A pointer to the native Plugin struct.
/// </summary>
internal int Index { get; }
internal IntPtr NativeHandle { get; }
/// <summary>
/// Update a plugin, keeping the existing ID.
@@ -47,10 +33,9 @@ public class Plugin : IDisposable
{
CheckNotDisposed();
var functions = _functions.Select(f => f.NativeHandle).ToArray();
fixed (byte* wasmPtr = wasm)
{
return LibExtism.extism_plugin_update(_context.NativeHandle, Index, wasmPtr, wasm.Length, functions, 0, withWasi);
return LibExtism.extism_plugin_update(_context.NativeHandle, NativeHandle, wasmPtr, wasm.Length, withWasi);
}
}
@@ -64,18 +49,18 @@ public class Plugin : IDisposable
fixed (byte* jsonPtr = json)
{
return LibExtism.extism_plugin_config(_context.NativeHandle, Index, 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(_context.NativeHandle, Index, name);
return LibExtism.extism_plugin_function_exists(_context.NativeHandle, NativeHandle, name);
}
/// <summary>
@@ -93,20 +78,14 @@ public class Plugin : IDisposable
fixed (byte* dataPtr = data)
{
int response = LibExtism.extism_plugin_call(_context.NativeHandle, Index, 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");
}
}
@@ -117,11 +96,11 @@ public 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(_context.NativeHandle, Index);
return (int)LibExtism.extism_plugin_output_length(_context.NativeHandle, NativeHandle);
}
/// <summary>
@@ -135,7 +114,7 @@ public class Plugin : IDisposable
unsafe
{
var ptr = LibExtism.extism_plugin_output_data(_context.NativeHandle, Index).ToPointer();
var ptr = LibExtism.extism_plugin_output_data(_context.NativeHandle, NativeHandle).ToPointer();
return new Span<byte>(ptr, length);
}
}
@@ -144,11 +123,11 @@ public 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_error(_context.NativeHandle, Index);
var result = LibExtism.extism_error(_context.NativeHandle, NativeHandle);
return Marshal.PtrToStringUTF8(result);
}
@@ -189,7 +168,7 @@ public 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)
{
@@ -197,7 +176,7 @@ public class Plugin : IDisposable
}
// Free up unmanaged resources
LibExtism.extism_plugin_free(_context.NativeHandle, Index);
LibExtism.extism_plugin_free(_context.NativeHandle, NativeHandle);
}
/// <summary>
@@ -207,4 +186,4 @@ public class Plugin : IDisposable
{
Dispose(false);
}
}
}

View File

@@ -1,7 +1,6 @@
using Extism.Sdk.Native;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using Xunit;
@@ -10,17 +9,6 @@ namespace Extism.Sdk.Tests;
public class BasicTests
{
[Fact]
public void CountHelloWorldVowelsWithoutContext()
{
var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
var wasm = File.ReadAllBytes(Path.Combine(binDirectory, "code.wasm"));
using var plugin = Plugin.Create(wasm, Array.Empty<HostFunction>(), withWasi: true);
var response = plugin.CallFunction("count_vowels", Encoding.UTF8.GetBytes("Hello World"));
Assert.Equal("{\"count\": 3}", Encoding.UTF8.GetString(response));
}
[Fact]
public void CountHelloWorldVowels()
{
@@ -28,46 +16,9 @@ public class BasicTests
var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
var wasm = File.ReadAllBytes(Path.Combine(binDirectory, "code.wasm"));
using var plugin = context.CreatePlugin(wasm, Array.Empty<HostFunction>(), withWasi: true);
using var plugin = 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()
{
using var context = new Context();
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 = context.CreatePlugin(wasm, new[] { helloWorld }, withWasi: true);
var response = plugin.CallFunction("count_vowels", Encoding.UTF8.GetBytes("Hello World"));
Assert.Equal("{\"count\": 3}", Encoding.UTF8.GetString(response));
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.4.0" />
</ItemGroup>
</Project>

View File

@@ -21,13 +21,12 @@
(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))
)
(tags
@@ -36,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

@@ -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. PluginIndex and the context
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

@@ -16,8 +16,6 @@ defmodule Extism.Native do
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(_ctx, _plugin_id), do: error()
def plugin_cancel(_handle), do: error()
defp error, do: :erlang.nif_error(:nif_not_loaded)
end

View File

@@ -15,25 +15,12 @@ defmodule Extism.Plugin do
}
end
@doc """
Creates a new plugin
"""
def new(manifest, wasi \\ false, context \\ nil) do
ctx = context || Extism.Context.new()
{:ok, manifest_payload} = JSON.encode(manifest)
case Extism.Native.plugin_new_with_manifest(ctx.ptr, manifest_payload, wasi) do
{:error, err} -> {:error, err}
res -> {:ok, Extism.Plugin.wrap_resource(ctx, 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}"}

View File

@@ -4,7 +4,7 @@ defmodule Extism.MixProject do
def project do
[
app: :extism,
version: "0.4.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.5", "aa6da96a5c23389d7dc7c381eba862710e108cee9cfdc629b7ec021313900e9e", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "88a1e115dcb91cefeef7e22df4a6ebbe4634fbf98b38adcbc25c9607d6d9d8e6"},
"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 = "0.4.0"
rustler = "0.26.0"
extism = { version = "0.1.0", path = "../../../rust" }
log = "0.4"

View File

@@ -17,24 +17,22 @@ mod atoms {
struct ExtismContext {
ctx: RwLock<Context>,
}
unsafe impl Sync for ExtismContext {}
unsafe impl Send for ExtismContext {}
struct ExtismCancelHandle {
handle: RwLock<extism::CancelHandle>,
}
unsafe impl Sync for ExtismCancelHandle {}
unsafe impl Send for ExtismCancelHandle {}
fn load(env: Env, _: Term) -> bool {
rustler::resource!(ExtismContext, env);
rustler::resource!(ExtismCancelHandle, 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())),
}
}
#[rustler::nif]
@@ -52,7 +50,7 @@ fn context_reset(ctx: ResourceArc<ExtismContext>) {
#[rustler::nif]
fn context_free(ctx: ResourceArc<ExtismContext>) {
let context = ctx.ctx.read().unwrap();
let context = &ctx.ctx.read().unwrap();
std::mem::drop(context)
}
@@ -62,8 +60,8 @@ fn plugin_new_with_manifest(
manifest_payload: String,
wasi: bool,
) -> Result<i32, rustler::Error> {
let context = ctx.ctx.write().unwrap();
let result = match Plugin::new(&context, manifest_payload, [], wasi) {
let context = &ctx.ctx.write().unwrap();
let result = match Plugin::new(context, manifest_payload, wasi) {
Err(e) => Err(to_rustler_error(e)),
Ok(plugin) => {
let plugin_id = plugin.as_i32();
@@ -87,7 +85,7 @@ fn plugin_call(
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(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",
@@ -109,7 +107,7 @@ fn plugin_update_manifest(
) -> 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) {
let result = match plugin.update(manifest_payload, wasi) {
Ok(()) => Ok(()),
Err(e) => Err(to_rustler_error(e)),
};
@@ -119,24 +117,6 @@ fn plugin_update_manifest(
result
}
#[rustler::nif]
fn plugin_cancel_handle(
ctx: ResourceArc<ExtismContext>,
plugin_id: i32,
) -> Result<ResourceArc<ExtismCancelHandle>, rustler::Error> {
let context = &ctx.ctx.read().unwrap();
let plugin = unsafe { Plugin::from_id(plugin_id, context) };
let handle = plugin.cancel_handle();
Ok(ResourceArc::new(ExtismCancelHandle {
handle: RwLock::new(handle),
}))
}
#[rustler::nif]
fn plugin_cancel(handle: ResourceArc<ExtismCancelHandle>) -> bool {
handle.handle.read().unwrap().cancel()
}
#[rustler::nif]
fn plugin_free(ctx: ResourceArc<ExtismContext>, plugin_id: i32) -> Result<(), rustler::Error> {
let context = &ctx.ctx.read().unwrap();
@@ -190,8 +170,6 @@ rustler::init!(
plugin_call,
plugin_update_manifest,
plugin_has_function,
plugin_cancel_handle,
plugin_cancel,
plugin_free,
set_log_file,
],

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

332
extism.go
View File

@@ -5,50 +5,13 @@ 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"
@@ -57,106 +20,6 @@ type Context struct {
pointer *C.ExtismContext
}
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
}
// Free a function
func (f *Function) Free() {
C.extism_function_free(f.pointer)
f.pointer = nil
f.userData.Delete()
}
// NewFunction creates a new host function with the given name, input/outputs and optional user data, which can be an
// 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),
}
}
func (p *CurrentPlugin) Memory(offs uint) []byte {
length := C.extism_current_plugin_memory_length(p.pointer, C.uint64_t(offs))
data := unsafe.Pointer(C.extism_current_plugin_memory(p.pointer))
return unsafe.Slice((*byte)(unsafe.Add(data, offs)), C.int(length))
}
// Alloc a new memory block of the given length, returning its offset
func (p *CurrentPlugin) Alloc(n uint) uint {
return uint(C.extism_current_plugin_memory_alloc(p.pointer, C.uint64_t(n)))
}
// Free the memory block specified by the given offset
func (p *CurrentPlugin) Free(offs uint) {
C.extism_current_plugin_memory_free(p.pointer, C.uint64_t(offs))
}
// Length returns the number of bytes allocated at the specified offset
func (p *CurrentPlugin) Length(offs uint) uint {
return uint(C.extism_current_plugin_memory_length(p.pointer, C.uint64_t(offs)))
}
// NewContext creates a new context, it should be freed using the `Free` method
func NewContext() Context {
p := C.extism_context_new()
@@ -173,9 +36,8 @@ func (ctx *Context) Free() {
// Plugin is used to call WASM functions
type Plugin struct {
ctx *Context
id int32
functions []Function
ctx *Context
id int32
}
type WasmData struct {
@@ -234,32 +96,14 @@ func ExtismVersion() string {
return C.GoString(C.extism_version())
}
func register(ctx *Context, 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.int32_t(-1)
if len(functions) == 0 {
plugin = C.extism_plugin_new(
ctx.pointer,
(*C.uchar)(ptr),
C.uint64_t(len(data)),
nil,
0,
C._Bool(wasi))
} else {
plugin = C.extism_plugin_new(
ctx.pointer,
(*C.uchar)(ptr),
C.uint64_t(len(data)),
&functionPointers[0],
C.uint64_t(len(functions)),
C._Bool(wasi),
)
}
plugin := C.extism_plugin_new(
ctx.pointer,
(*C.uchar)(ptr),
C.uint64_t(len(data)),
C._Bool(wasi),
)
if plugin < 0 {
err := C.extism_error(ctx.pointer, C.int32_t(-1))
@@ -273,44 +117,21 @@ func register(ctx *Context, data []byte, functions []Function, wasi bool) (Plugi
)
}
return Plugin{id: int32(plugin), ctx: ctx, functions: functions}, nil
return Plugin{id: int32(plugin), ctx: ctx}, nil
}
func update(ctx *Context, plugin int32, data []byte, functions []Function, wasi bool) error {
func update(ctx *Context, plugin int32, data []byte, wasi bool) error {
ptr := makePointer(data)
functionPointers := []*C.ExtismFunction{}
for _, f := range functions {
functionPointers = append(functionPointers, f.pointer)
}
b := bool(C.extism_plugin_update(
ctx.pointer,
C.int32_t(plugin),
(*C.uchar)(ptr),
C.uint64_t(len(data)),
C._Bool(wasi),
))
if len(functions) == 0 {
b := bool(C.extism_plugin_update(
ctx.pointer,
C.int32_t(plugin),
(*C.uchar)(ptr),
C.uint64_t(len(data)),
nil,
0,
C._Bool(wasi),
))
if b {
return nil
}
} else {
b := bool(C.extism_plugin_update(
ctx.pointer,
C.int32_t(plugin),
(*C.uchar)(ptr),
C.uint64_t(len(data)),
&functionPointers[0],
C.uint64_t(len(functions)),
C._Bool(wasi),
))
if b {
return nil
}
if b {
return nil
}
err := C.extism_error(ctx.pointer, C.int32_t(-1))
@@ -324,57 +145,44 @@ func update(ctx *Context, plugin int32, data []byte, functions []Function, wasi
)
}
// NewPlugin creates a plugin in its own context
func NewPlugin(module io.Reader, functions []Function, wasi bool) (Plugin, error) {
ctx := NewContext()
return ctx.Plugin(module, functions, wasi)
}
// NewPlugin creates a plugin in its own context from a manifest
func NewPluginFromManifest(manifest Manifest, functions []Function, wasi bool) (Plugin, error) {
ctx := NewContext()
return ctx.PluginFromManifest(manifest, functions, wasi)
}
// PluginFromManifest creates a plugin from a `Manifest`
func (ctx *Context) PluginFromManifest(manifest Manifest, functions []Function, wasi bool) (Plugin, error) {
func (ctx *Context) PluginFromManifest(manifest Manifest, wasi bool) (Plugin, error) {
data, err := json.Marshal(manifest)
if err != nil {
return Plugin{id: -1}, err
}
return register(ctx, data, functions, wasi)
return register(ctx, data, wasi)
}
// Plugin creates a plugin from a WASM module
func (ctx *Context) Plugin(module io.Reader, functions []Function, wasi bool) (Plugin, error) {
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, functions, wasi)
return register(ctx, wasm, wasi)
}
// Update a plugin with a new WASM module
func (p *Plugin) Update(module io.Reader, functions []Function, wasi bool) error {
func (p *Plugin) Update(module io.Reader, wasi bool) error {
wasm, err := io.ReadAll(module)
if err != nil {
return err
}
p.functions = functions
return update(p.ctx, p.id, wasm, functions, wasi)
return update(p.ctx, p.id, wasm, wasi)
}
// Update a plugin with a new Manifest
func (p *Plugin) UpdateManifest(manifest Manifest, functions []Function, wasi bool) error {
func (p *Plugin) UpdateManifest(manifest Manifest, wasi bool) error {
data, err := json.Marshal(manifest)
if err != nil {
return err
}
p.functions = functions
return update(p.ctx, p.id, data, functions, wasi)
return update(p.ctx, p.id, data, wasi)
}
// Set configuration values
@@ -425,7 +233,8 @@ func (plugin Plugin) Call(functionName string, input []byte) ([]byte, error) {
if length > 0 {
x := C.extism_plugin_output_data(plugin.ctx.pointer, C.int32_t(plugin.id))
return unsafe.Slice((*byte)(x), C.int(length)), nil
y := (*[]byte)(unsafe.Pointer(&x))
return []byte((*y)[0:length]), nil
}
return []byte{}, nil
@@ -444,80 +253,3 @@ func (plugin *Plugin) Free() {
func (ctx Context) Reset() {
C.extism_context_reset(ctx.pointer)
}
// ValGetI64 returns an I64 from an ExtismVal, it accepts a pointer to a C.ExtismVal
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.ctx.pointer, C.int(p.id))
return CancelHandle{pointer}
}
func (c *CancelHandle) Cancel() bool {
return bool(C.extism_plugin_cancel(c.pointer))
}

View File

@@ -11,13 +11,12 @@ 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"}
"odoc" {with-doc}
]
@@ -36,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",
},
},
}
@@ -44,7 +38,7 @@ func TestCallPlugin(t *testing.T) {
ctx := NewContext()
defer ctx.Free()
plugin, err := ctx.PluginFromManifest(manifest(false), []Function{}, false)
plugin, err := ctx.PluginFromManifest(manifest(), false)
if err != nil {
t.Error(err)
}
@@ -64,7 +58,7 @@ func TestFreePlugin(t *testing.T) {
ctx := NewContext()
defer ctx.Free()
plugin, err := ctx.PluginFromManifest(manifest(false), []Function{}, false)
plugin, err := ctx.PluginFromManifest(manifest(), false)
if err != nil {
t.Error(err)
}
@@ -84,7 +78,7 @@ func TestContextReset(t *testing.T) {
ctx := NewContext()
defer ctx.Free()
plugin, err := ctx.PluginFromManifest(manifest(false), []Function{}, false)
plugin, err := ctx.PluginFromManifest(manifest(), false)
if err != nil {
t.Error(err)
}
@@ -104,7 +98,7 @@ func TestCanUpdateAManifest(t *testing.T) {
ctx := NewContext()
defer ctx.Free()
plugin, err := ctx.PluginFromManifest(manifest(false), []Function{}, false)
plugin, err := ctx.PluginFromManifest(manifest(), false)
if err != nil {
t.Error(err)
}
@@ -113,7 +107,7 @@ func TestCanUpdateAManifest(t *testing.T) {
t.Error(err)
}
plugin.UpdateManifest(manifest(false), []Function{}, false)
plugin.UpdateManifest(manifest(), false)
// can still call the plugin
if err := expectVowelCount(plugin, "this is a test", 4); err != nil {
@@ -125,7 +119,7 @@ func TestFunctionExists(t *testing.T) {
ctx := NewContext()
defer ctx.Free()
plugin, err := ctx.PluginFromManifest(manifest(false), []Function{}, false)
plugin, err := ctx.PluginFromManifest(manifest(), false)
if err != nil {
t.Error(err)
}
@@ -142,7 +136,7 @@ func TestErrorsOnUnknownFunction(t *testing.T) {
ctx := NewContext()
defer ctx.Free()
plugin, err := ctx.PluginFromManifest(manifest(false), []Function{}, false)
plugin, err := ctx.PluginFromManifest(manifest(), false)
if err != nil {
t.Error(err)
}
@@ -152,33 +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",
},
},
}
ctx := NewContext()
defer ctx.Free()
plugin, err := ctx.PluginFromManifest(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,24 +1,16 @@
module Main where
import Extism
import Extism.CurrentPlugin
import Extism.Manifest(manifest, wasmFile)
unwrap (Right x) = x
unwrap (Left (ExtismError msg)) = do
error msg
hello plugin params msg = do
putStrLn "Hello from Haskell!"
putStrLn msg
offs <- allocBytes plugin (toByteString "{\"count\": 999}")
return [toI64 offs]
main = do
setLogFile "stdout" Error
let m = manifest [wasmFile "../wasm/code-functions.wasm"]
f <- hostFunction "hello_world" [I64] [I64] hello "Hello, again"
plugin <- unwrap <$> createPluginFromManifest m [f] True
res <- unwrap <$> call plugin "count_vowels" (toByteString "this is a test")
putStrLn (fromByteString res)
free plugin
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.4.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.CurrentPlugin
exposed-modules: Extism
reexported-modules: Extism.Manifest
hs-source-dirs: src
other-modules: Extism.Bindings
@@ -19,10 +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
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

@@ -16,16 +16,14 @@ 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

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,21 +1,10 @@
module Extism (
module Extism,
module Extism.Manifest,
ValType(..),
Val(..)
) 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.Storable
import Foreign.StablePtr
import Foreign.Concurrent
import Foreign.Marshal.Utils (copyBytes, moveBytes)
import Data.ByteString as B
import Data.ByteString.Internal (c2w, w2c)
import Data.ByteString.Unsafe (unsafeUseAsCString)
@@ -27,17 +16,8 @@ import Extism.Bindings
-- | Context for managing plugins
newtype Context = Context (ForeignPtr ExtismContext)
-- | Host function
data Function = Function (ForeignPtr ExtismFunction) (StablePtr ())
-- | Plugins can be used to call WASM function
data Plugin = Plugin Context Int32 [Function]
-- | Cancellation handle for Plugins
newtype CancelHandle = CancelHandle (Ptr ExtismCancelHandle)
-- | Access the plugin that is currently executing from inside a host function
type CurrentPlugin = Ptr ExtismCurrentPlugin
data Plugin = Plugin Context Int32
-- | Log level
data LogLevel = Error | Warn | Info | Debug | Trace deriving (Show)
@@ -71,94 +51,68 @@ reset (Context ctx) =
newContext :: IO Context
newContext = do
ptr <- extism_context_new
fptr <- Foreign.ForeignPtr.newForeignPtr extism_context_free ptr
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
-- | Execute a function with the provided 'Plugin' as a parameter, then frees the 'Plugin'
-- | before returning the result.
withPlugin :: (Plugin -> IO a) -> Plugin -> IO a
withPlugin f plugin = do
res <- f plugin
free plugin
return res
-- | Create a 'Plugin' from a WASM module, `useWasi` determines if WASI should
-- | be linked
plugin :: Context -> B.ByteString -> [Function] -> Bool -> IO (Result Plugin)
plugin c wasm functions useWasi =
let nfunctions = fromIntegral (Prelude.length functions) in
plugin :: 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
funcs <- Prelude.mapM (\(Function ptr _) -> withForeignPtr ptr (\x -> do return x)) functions
withForeignPtr ctx (\ctx -> do
p <- unsafeUseAsCString wasm (\s ->
withArray funcs (\funcs ->
extism_plugin_new ctx (castPtr s) length funcs nfunctions wasi ))
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 functions))
-- | Create a 'Plugin' with its own 'Context'
createPlugin :: B.ByteString -> [Function] -> Bool -> IO (Result Plugin)
createPlugin c functions useWasi = do
ctx <- newContext
plugin ctx c functions useWasi
return $ Right (Plugin c p))
-- | Create a 'Plugin' from a 'Manifest'
pluginFromManifest :: Context -> Manifest -> [Function] -> Bool -> IO (Result Plugin)
pluginFromManifest ctx manifest functions useWasi =
pluginFromManifest :: Context -> Manifest -> Bool -> IO (Result Plugin)
pluginFromManifest ctx manifest useWasi =
let wasm = toByteString $ toString manifest in
plugin ctx wasm functions useWasi
-- | Create a 'Plugin' with its own 'Context' from a 'Manifest'
createPluginFromManifest :: Manifest -> [Function] -> Bool -> IO (Result Plugin)
createPluginFromManifest manifest functions useWasi = do
ctx <- newContext
pluginFromManifest ctx manifest functions useWasi
plugin ctx wasm useWasi
-- | Update a 'Plugin' with a new WASM module
update :: Plugin -> B.ByteString -> [Function] -> Bool -> IO (Result Plugin)
update (Plugin (Context ctx) id _) wasm functions useWasi =
let nfunctions = fromIntegral (Prelude.length functions) in
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 <- Prelude.mapM (\(Function ptr _ ) -> withForeignPtr ptr (\x -> do return x)) functions
withForeignPtr ctx (\ctx' -> do
withForeignPtr ctx (\ctx -> do
b <- unsafeUseAsCString wasm (\s ->
withArray funcs (\funcs ->
extism_plugin_update ctx' id (castPtr s) length funcs nfunctions wasi))
extism_plugin_update ctx id (castPtr s) length wasi)
if b <= 0 then do
err <- extism_error ctx' (-1)
err <- extism_error ctx (-1)
e <- peekCString err
return $ Left (ExtismError e)
else
return (Right (Plugin (Context ctx) id functions)))
return (Right ()))
-- | Update a 'Plugin' with a new 'Manifest'
updateManifest :: Plugin -> Manifest -> [Function] -> Bool -> IO (Result Plugin)
updateManifest plugin manifest functions useWasi =
updateManifest :: Plugin -> Manifest -> Bool -> IO (Result ())
updateManifest plugin manifest useWasi =
let wasm = toByteString $ toString manifest in
update plugin wasm functions useWasi
update plugin wasm useWasi
-- | Check if a 'Plugin' is valid
isValid :: Plugin -> Bool
isValid (Plugin _ p _) = p >= 0
isValid (Plugin _ p) = p >= 0
-- | Set configuration values for a plugin
setConfig :: Plugin -> [(String, Maybe String)] -> IO Bool
setConfig (Plugin (Context ctx) plugin _) x =
setConfig (Plugin (Context ctx) plugin) x =
if plugin < 0
then return False
else
@@ -187,14 +141,14 @@ setLogFile filename level =
-- | Check if a function exists in the given plugin
functionExists :: Plugin -> String -> IO Bool
functionExists (Plugin (Context ctx) plugin _) name = do
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 (Context ctx) plugin _) name input =
call (Plugin (Context ctx) plugin) name input =
let length = fromIntegral (B.length input) in
do
withForeignPtr ctx (\ctx -> do
@@ -216,73 +170,5 @@ call (Plugin (Context ctx) plugin _) name input =
-- | 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 _) =
free (Plugin (Context ctx) plugin) =
withForeignPtr ctx (`extism_plugin_free` plugin)
-- | Create a new 'CancelHandle' that can be used to cancel a running plugin
-- | from another thread.
cancelHandle :: Plugin -> IO CancelHandle
cancelHandle (Plugin (Context ctx) plugin _) = do
handle <- withForeignPtr ctx (`extism_plugin_cancel_handle` plugin)
return (CancelHandle handle)
-- | Cancel a running plugin using a 'CancelHandle'
cancel :: CancelHandle -> IO Bool
cancel (CancelHandle handle) =
extism_plugin_cancel handle
-- | Create a new 'Function' that can be called from a 'Plugin'
hostFunction :: String -> [ValType] -> [ValType] -> (CurrentPlugin -> [Val] -> a -> IO [Val]) -> a -> IO Function
hostFunction name params results f v =
let nparams = fromIntegral $ Prelude.length params in
let nresults = fromIntegral $ Prelude.length results in
do
cb <- callbackWrap (callback f :: CCallback)
free <- freePtrWrap freePtr
userData <- newStablePtr (v, free, cb)
let userDataPtr = castStablePtrToPtr userData
x <- withCString name (\name -> do
withArray params (\params ->
withArray results (\results -> do
extism_function_new name params nparams results nresults cb userDataPtr free)))
let freeFn = extism_function_free x
fptr <- Foreign.Concurrent.newForeignPtr x freeFn
return $ Function fptr (castPtrToStablePtr userDataPtr)
-- | Create a new I32 'Val'
toI32 :: Integral a => a -> Val
toI32 x = ValI32 (fromIntegral x)
-- | Create a new I64 'Val'
toI64 :: Integral a => a -> Val
toI64 x = ValI64 (fromIntegral x)
-- | Create a new F32 'Val'
toF32 :: Float -> Val
toF32 = ValF32
-- | Create a new F64 'Val'
toF64 :: Double -> Val
toF64 = ValF64
-- | Get I32 'Val'
fromI32 :: Integral a => Val -> Maybe a
fromI32 (ValI32 x) = Just (fromIntegral x)
fromI32 _ = Nothing
-- | Get I64 'Val'
fromI64 :: Integral a => Val -> Maybe a
fromI64 (ValI64 x) = Just (fromIntegral x)
fromI64 _ = Nothing
-- | Get F32 'Val'
fromF32 :: Val -> Maybe Float
fromF32 (ValF32 x) = Just x
fromF32 _ = Nothing
-- | Get F64 'Val'
fromF64 :: Val -> Maybe Double
fromF64 (ValF64 x) = Just x
fromF64 _ = Nothing

View File

@@ -7,116 +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 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_context_new" extism_context_new :: IO (Ptr ExtismContext)
foreign import ccall safe "extism.h &extism_context_free" extism_context_free :: FunPtr (Ptr ExtismContext -> IO ())
foreign import ccall safe "extism.h extism_plugin_new" extism_plugin_new :: Ptr ExtismContext -> Ptr Word8 -> Word64 -> Ptr (Ptr ExtismFunction) -> Word64 -> CBool -> IO Int32
foreign import ccall safe "extism.h extism_plugin_update" extism_plugin_update :: Ptr ExtismContext -> Int32 -> Ptr Word8 -> Word64 -> Ptr (Ptr ExtismFunction) -> Word64 -> CBool -> IO CBool
foreign import ccall safe "extism.h extism_plugin_call" extism_plugin_call :: Ptr ExtismContext -> Int32 -> CString -> Ptr Word8 -> Word64 -> IO Int32
foreign import ccall safe "extism.h extism_plugin_function_exists" extism_plugin_function_exists :: Ptr ExtismContext -> Int32 -> CString -> IO CBool
foreign import ccall safe "extism.h extism_error" extism_error :: Ptr ExtismContext -> Int32 -> IO CString
foreign import ccall safe "extism.h extism_plugin_output_length" extism_plugin_output_length :: Ptr ExtismContext -> Int32 -> IO Word64
foreign import ccall safe "extism.h extism_plugin_output_data" extism_plugin_output_data :: Ptr ExtismContext -> Int32 -> IO (Ptr Word8)
foreign import ccall safe "extism.h extism_log_file" extism_log_file :: CString -> CString -> IO CBool
foreign import ccall safe "extism.h extism_plugin_config" extism_plugin_config :: Ptr ExtismContext -> Int32 -> Ptr Word8 -> Int64 -> IO CBool
foreign import ccall safe "extism.h extism_plugin_free" extism_plugin_free :: Ptr ExtismContext -> Int32 -> IO ()
foreign import ccall safe "extism.h extism_context_reset" extism_context_reset :: Ptr ExtismContext -> IO ()
foreign import ccall safe "extism.h extism_version" extism_version :: IO CString
foreign import ccall safe "extism.h extism_plugin_cancel_handle" extism_plugin_cancel_handle :: Ptr ExtismContext -> Int32 -> IO (Ptr ExtismCancelHandle)
foreign import ccall safe "extism.h extism_plugin_cancel" extism_plugin_cancel :: Ptr ExtismCancelHandle -> IO Bool
foreign import ccall safe "extism.h extism_function_new" extism_function_new :: CString -> Ptr ValType -> Word64 -> Ptr ValType -> Word64 -> FunPtr CCallback -> Ptr () -> FunPtr FreeCallback -> IO (Ptr ExtismFunction)
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,48 +0,0 @@
module Extism.CurrentPlugin where
import Extism
import Extism.Bindings
import Data.Word
import Data.ByteString as B
import Foreign.Ptr
import Foreign.Marshal.Array
-- | Allocate a new handle of the given size
memoryAlloc :: CurrentPlugin -> Word64 -> IO Word64
memoryAlloc = extism_current_plugin_memory_alloc
-- | Get the length of a handle, returns 0 if the handle is invalid
memoryLength :: CurrentPlugin -> Word64 -> IO Word64
memoryLength = extism_current_plugin_memory_length
-- | Free allocated memory
memoryFree :: CurrentPlugin -> Word64 -> IO ()
memoryFree = extism_current_plugin_memory_free
-- | Access a pointer to the entire memory region
memory :: CurrentPlugin -> IO (Ptr Word8)
memory = extism_current_plugin_memory
-- | Access a pointer the a specific offset in memory
memoryOffset :: CurrentPlugin -> Word64 -> IO (Ptr Word8)
memoryOffset plugin offs = do
x <- extism_current_plugin_memory plugin
return $ plusPtr x (fromIntegral offs)
-- | Access the data associated with a handle as a 'ByteString'
memoryBytes :: CurrentPlugin -> Word64 -> IO B.ByteString
memoryBytes plugin offs = do
ptr <- memoryOffset plugin offs
len <- memoryLength plugin offs
arr <- peekArray (fromIntegral len) ptr
return $ B.pack arr
-- | Allocate memory and copy an existing 'ByteString' into it
allocBytes :: CurrentPlugin -> B.ByteString -> IO Word64
allocBytes plugin s = do
let length = B.length s
offs <- memoryAlloc plugin (fromIntegral length)
ptr <- memoryOffset plugin offs
pokeArray ptr (B.unpack s)
return offs

View File

@@ -1,66 +1,53 @@
import Test.HUnit
import Extism
import Extism.Manifest
import Extism.CurrentPlugin
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 :: Maybe Context -> IO Plugin
initPlugin Nothing =
Extism.createPluginFromManifest defaultManifest [] False >>= unwrap
initPlugin (Just ctx) =
Extism.pluginFromManifest ctx defaultManifest [] False >>= unwrap
initPlugin :: Context -> IO Plugin
initPlugin context =
Extism.pluginFromManifest context defaultManifest False >>= unwrap
pluginFunctionExists = do
p <- initPlugin Nothing
exists <- functionExists p "count_vowels"
assertBool "function exists" exists
exists' <- functionExists p "function_doesnt_exist"
assertBool "function doesn't exist" (not exists')
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") >>= unwrap
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 Nothing
checkCallResult p
hello plugin params () = do
putStrLn "Hello from Haskell!"
offs <- allocBytes plugin (toByteString "{\"count\": 999}")
return [toI64 offs]
pluginCallHostFunction = do
p <- Extism.createPluginFromManifest hostFunctionManifest [] False >>= unwrap
res <- call p "count_vowels" (toByteString "this is a test") >>= unwrap
assertEqual "count vowels output" "{\"count\": 999}" (fromByteString res)
withContext (\ctx -> do
p <- initPlugin ctx
checkCallResult p)
pluginMultiple = do
withContext(\ctx -> do
p <- initPlugin (Just ctx)
withContext (\ctx -> do
p <- initPlugin ctx
checkCallResult p
q <- initPlugin (Just ctx)
r <- initPlugin (Just ctx)
q <- initPlugin ctx
r <- initPlugin ctx
checkCallResult q
checkCallResult r)
pluginUpdate = do
withContext (\ctx -> do
p <- initPlugin (Just ctx)
updateManifest p defaultManifest [] True >>= unwrap
p <- initPlugin ctx
updateManifest p defaultManifest True >>= unwrap
checkCallResult p)
pluginConfig = do
withContext (\ctx -> do
p <- initPlugin (Just ctx)
p <- initPlugin ctx
b <- setConfig p [("a", Just "1"), ("b", Just "2"), ("c", Just "3"), ("d", Nothing)]
assertBool "set config" b)
@@ -75,7 +62,6 @@ 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

BIN
haskell/test/code.wasm Executable file

Binary file not shown.

View File

@@ -4,7 +4,7 @@
<groupId>org.extism.sdk</groupId>
<artifactId>extism</artifactId>
<packaging>jar</packaging>
<version>0.4.0</version>
<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>
@@ -36,7 +36,7 @@
<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>
<url>https://github.com/extism/extism/java</url>
<tag>main</tag>
</scm>
@@ -46,7 +46,7 @@
</issueManagement>
<properties>
<java.version>11</java.version>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- dependencies -->
@@ -74,9 +74,6 @@
<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>

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

@@ -28,11 +28,10 @@ public class Context implements AutoCloseable {
*
* @param manifest The manifest for the plugin
* @param withWASI Set to true to enable WASI
* @param functions List of Host functions
* @return the plugin instance
*/
public Plugin newPlugin(Manifest manifest, boolean withWASI, HostFunction[] functions) {
return new Plugin(this, manifest, withWASI, functions);
public Plugin newPlugin(Manifest manifest, boolean withWASI) {
return new Plugin(this, manifest, withWASI);
}
/**

View File

@@ -42,7 +42,7 @@ public class Extism {
*/
public static String invokeFunction(Manifest manifest, String function, String input) throws ExtismException {
try (var ctx = new Context()) {
try (var plugin = ctx.newPlugin(manifest, false, null)) {
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,80 +15,7 @@ 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);
/**
* Get the length of an allocated block
* NOTE: this should only be called from host functions.
*/
int extism_current_plugin_memory_length(Pointer plugin, long n);
/**
* Returns a pointer to the memory of the currently running plugin
* NOTE: this should only be called from host functions.
*/
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);
/**
* Create a new context
*/
Pointer extism_context_new();
@@ -127,18 +56,27 @@ public interface LibExtism extends Library {
* @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
* @return id of the plugin or {@literal -1} in case of error
*/
int extism_plugin_new(Pointer contextPointer, byte[] wasm, long wasmSize, Pointer[] functions, int nFunctions, boolean withWASI);
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}.
@@ -172,19 +110,17 @@ public interface LibExtism extends Library {
/**
* Update a plugin, keeping the existing ID.
* Similar to {@link #extism_plugin_new(Pointer, byte[], long, Pointer[], int, boolean)} but takes an {@code pluginIndex} argument to specify which plugin to update.
* 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 functions host functions
* @param nFunctions the number of host functions
* @param withWASI
* @return {@literal true} if update was successful
*/
boolean extism_plugin_update(Pointer contextPointer, int pluginIndex, byte[] wasm, int length, Pointer[] functions, int nFunctions, boolean withWASI);
boolean extism_plugin_update(Pointer contextPointer, int pluginIndex, byte[] wasm, int length, boolean withWASI);
/**
* Remove a plugin from the registry and free associated memory.
@@ -204,7 +140,4 @@ public interface LibExtism extends Library {
* @return {@literal true} if update was successful
*/
boolean extism_plugin_config(Pointer contextPointer, int pluginIndex, byte[] json, int jsonLength);
Pointer extism_plugin_cancel_handle(Pointer contextPointer, int n);
boolean extism_plugin_cancel(Pointer contextPointer);
void extism_function_set_namespace(Pointer p, String name);
}

View File

@@ -28,27 +28,15 @@ public class Plugin implements AutoCloseable {
*
* @param context The context to manage the plugin
* @param manifestBytes The manifest for the plugin
* @param functions The Host functions for th eplugin
* @param withWASI Set to true to enable WASI
*/
public Plugin(Context context, byte[] manifestBytes, boolean withWASI, HostFunction[] functions) {
public Plugin(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 contextPointer = context.getPointer();
int index = LibExtism.INSTANCE.extism_plugin_new(contextPointer, manifestBytes, manifestBytes.length,
ptrArr,
functions == null ? 0 : functions.length,
withWASI);
int index = LibExtism.INSTANCE.extism_plugin_new(contextPointer, manifestBytes, manifestBytes.length, withWASI);
if (index == -1) {
String error = context.error(this);
throw new ExtismException(error);
@@ -58,18 +46,8 @@ public class Plugin implements AutoCloseable {
this.context = context;
}
public Plugin(Context context, Manifest manifest, boolean withWASI, HostFunction[] functions) {
this(context, serialize(manifest), withWASI, functions);
}
public Plugin(byte[] manifestBytes, boolean withWASI, HostFunction[] functions) {
this(new Context(), manifestBytes, withWASI, functions);
}
public Plugin(Manifest manifest, boolean withWASI, HostFunction[] functions) {
this(new Context(), serialize(manifest), withWASI, functions);
public Plugin(Context context, Manifest manifest, boolean withWASI) {
this(context, serialize(manifest), withWASI);
}
private static byte[] serialize(Manifest manifest) {
@@ -134,8 +112,8 @@ public class Plugin implements AutoCloseable {
* @param withWASI Set to true to enable WASI
* @return {@literal true} if update was successful
*/
public boolean update(Manifest manifest, boolean withWASI, HostFunction[] functions) {
return update(serialize(manifest), withWASI, functions);
public boolean update(Manifest manifest, boolean withWASI) {
return update(serialize(manifest), withWASI);
}
/**
@@ -145,19 +123,9 @@ public class Plugin implements AutoCloseable {
* @param withWASI Set to true to enable WASI
* @return {@literal true} if update was successful
*/
public boolean update(byte[] manifestBytes, boolean withWASI, HostFunction[] functions) {
public boolean update(byte[] manifestBytes, boolean withWASI) {
Objects.requireNonNull(manifestBytes, "manifestBytes");
Pointer[] ptrArr = new Pointer[functions == null ? 0 : functions.length];
if (functions != null)
for (int i = 0; i < functions.length; i++) {
ptrArr[i] = functions[i].pointer;
}
return LibExtism.INSTANCE.extism_plugin_update(context.getPointer(), index, manifestBytes, manifestBytes.length,
ptrArr,
functions == null ? 0 : functions.length,
withWASI);
return LibExtism.INSTANCE.extism_plugin_update(context.getPointer(), index, manifestBytes, manifestBytes.length, withWASI);
}
/**
@@ -197,15 +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() {
if (this.context.getPointer() == null) {
throw new ExtismException("No Context set");
}
Pointer handle = LibExtism.INSTANCE.extism_plugin_cancel_handle(this.context.getPointer(), this.index);
return new CancelHandle(handle);
}
}

View File

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

@@ -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() {
@@ -78,7 +80,7 @@ public class PluginTests {
var input = "Hello World";
try (var ctx = new Context()) {
try (var plugin = ctx.newPlugin(manifest, false, null)) {
try (var plugin = ctx.newPlugin(manifest, false)) {
var output = plugin.call(functionName, input);
assertThat(output).isEqualTo("{\"count\": 3}");
}
@@ -92,7 +94,7 @@ public class PluginTests {
var input = "Hello World";
try (var ctx = new Context()) {
try (var plugin = ctx.newPlugin(manifest, false, null)) {
try (var plugin = ctx.newPlugin(manifest, false)) {
var output = plugin.call(functionName, input);
assertThat(output).isEqualTo("{\"count\": 3}");
@@ -102,118 +104,4 @@ public class PluginTests {
}
}
@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;
}
}
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};
try (var ctx = new Context()) {
Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource()));
String functionName = "count_vowels";
try (var plugin = ctx.newPlugin(manifest, true, functions)) {
var output = plugin.call(functionName, "this is a test");
assertThat(output).isEqualTo("test");
}
}
}
@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};
try (var ctx = new Context()) {
Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource()));
String functionName = "count_vowels";
try (var plugin = ctx.newPlugin(manifest, true, functions)) {
var output = plugin.call(functionName, "this is a test");
assertThat(output).isEqualTo("test");
}
}
}
@Test
public void shouldFailToInvokeUnknownHostFunction() {
try (var ctx = new Context()) {
Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource()));
String functionName = "count_vowels";
try {
var plugin = ctx.newPlugin(manifest, true, null);
plugin.call(functionName, "this is a test");
} catch (ExtismException e) {
assertThat(e.getMessage()).contains("unknown import: `env::hello_world` has not been defined");
}
}
}
}

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

10
libextism.pc Normal file
View File

@@ -0,0 +1,10 @@
prefix=/usr/local
exec_prefix=${prefix}
includedir=${prefix}/include
libdir=${exec_prefix}/lib
Name: extism
Description: The Extism universal plug-in system.
Version: 0.1.0
Cflags: -I${includedir}
Libs: -L${libdir} -lextism

View File

@@ -1,6 +1,6 @@
[package]
name = "libextism"
version = "0.5.0"
version = "0.1.0"
edition = "2021"
authors = ["The Extism Authors", "oss@extism.org"]
license = "BSD-3-Clause"
@@ -20,4 +20,4 @@ default = ["http", "register-http", "register-filesystem"]
nn = ["extism-runtime/nn"]
register-http = ["extism-runtime/register-http"] # enables wasm to be downloaded using http
register-filesystem = ["extism-runtime/register-filesystem"] # enables wasm to be loaded from disk
http = ["extism-runtime/http"] # enables extism_http_request
http = ["extism-runtime/http"] # enables extism_http_request

View File

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

View File

@@ -4,17 +4,15 @@ use std::path::{Path, PathBuf};
#[deprecated]
pub type ManifestMemory = MemoryOptions;
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[derive(Default, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct MemoryOptions {
#[serde(alias = "max")]
pub max_pages: Option<u32>,
}
#[derive(Clone, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct HttpRequest {
pub url: String,
#[serde(default)]
@@ -43,9 +41,8 @@ impl HttpRequest {
}
}
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[derive(Default, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct WasmMetadata {
pub name: Option<String>,
pub hash: Option<String>,
@@ -81,10 +78,9 @@ impl From<Vec<u8>> for Wasm {
#[deprecated]
pub type ManifestWasm = Wasm;
#[derive(Clone, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
#[serde(untagged)]
#[serde(deny_unknown_fields)]
pub enum Wasm {
File {
path: PathBuf,
@@ -153,9 +149,8 @@ fn base64_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::
schema.into()
}
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[derive(Default, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Manifest {
#[serde(default)]
pub wasm: Vec<Wasm>,
@@ -255,19 +250,16 @@ impl Manifest {
}
mod base64 {
use base64::{engine::general_purpose, Engine as _};
use serde::{Deserialize, Serialize};
use serde::{Deserializer, Serializer};
pub fn serialize<S: Serializer>(v: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {
let base64 = general_purpose::STANDARD.encode(v);
let base64 = base64::encode(v);
String::serialize(&base64, s)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
let base64 = String::deserialize(d)?;
general_purpose::STANDARD
.decode(base64.as_bytes())
.map_err(serde::de::Error::custom)
base64::decode(base64.as_bytes()).map_err(serde::de::Error::custom)
}
}

View File

@@ -1,45 +1,22 @@
const {
Plugin,
HostFunction,
ValType,
} = require("./dist/index.js");
const { readFileSync } = require("fs");
const { withContext, Context } = require('./dist/index.js');
const { readFileSync } = require('fs');
function f(currentPlugin, inputs, outputs, userData) {
console.log(currentPlugin.inputString(inputs[0]));
console.log("Hello from Javascript!");
console.log(userData);
outputs[0] = inputs[0];
}
const hello_world = new HostFunction(
"hello_world",
[ValType.I64],
[ValType.I64],
f,
"Hello again!",
);
async function main() {
const functions = [hello_world];
const wasm = readFileSync("../wasm/code-functions.wasm");
const p = new Plugin(wasm, true, functions);
withContext(async function (context) {
let wasm = readFileSync("../wasm/code.wasm");
let p = context.plugin(wasm);
if (!p.functionExists("count_vowels")) {
console.log("no function 'count_vowels' in wasm");
process.exit(1);
}
const buf = await p.call("count_vowels", process.argv[2] || "this is a test");
let buf = await p.call("count_vowels", process.argv[2] || "this is a test");
console.log(JSON.parse(buf.toString())["count"]);
p.free();
}
main();
});
// or, use a context like this:
// let ctx = new Context();
// let wasm = readFileSync("../wasm/code.wasm");
// let p = ctx.plugin(wasm);
let ctx = new Context();
let wasm = readFileSync("../wasm/code.wasm");
let p = ctx.plugin(wasm);
// ... where the context can be passed around to various functions etc.

4003
node/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@extism/extism",
"version": "0.4.0",
"version": "0.1.0",
"description": "Extism Host SDK for Node",
"keywords": [
"extism",
@@ -21,17 +21,12 @@
},
"scripts": {
"prepare": "npm run build",
"example": "npm run build && node example.js",
"example": "node example.js",
"build": "tsc",
"test": "jest --coverage"
},
"dependencies": {
"@types/ref-array-di": "^1.2.5",
"ffi-napi": "^4.0.3",
"ref-array-di": "^1.2.2",
"ref-napi": "^3.0.3",
"ref-struct-di": "^1.1.1",
"ref-union-di": "^1.0.1"
"ffi-napi": "^4.0.3"
},
"publishConfig": {
"access": "public"
@@ -39,12 +34,12 @@
"devDependencies": {
"@types/ffi-napi": "^4.0.6",
"@types/jest": "^29.2.0",
"@types/node": "^20.1.0",
"@types/node": "^18.11.4",
"jest": "^29.2.2",
"prettier": "3.0.2",
"prettier": "2.8.2",
"ts-jest": "^29.0.3",
"ts-node": "^10.9.1",
"typedoc": "^0.24.1",
"typescript": "^5.0.4"
"typedoc": "^0.23.18",
"typescript": "^4.8.4"
}
}

View File

@@ -1,100 +1,30 @@
import ffi from "ffi-napi";
import ref from "ref-napi";
import path from "path";
var ArrayType = require("ref-array-di")(ref);
var StructType = require("ref-struct-di")(ref);
var UnionType = require("ref-union-di")(ref);
const opaque = ref.types.void;
const context = ref.refType(opaque);
const function_t = ref.refType(opaque);
const pluginIndex = ref.types.int32;
let ValTypeArray = ArrayType(ref.types.int);
let PtrArray = new ArrayType("void*");
let ValUnion = new UnionType({
i32: ref.types.uint32,
i64: ref.types.uint64,
f32: ref.types.float,
f64: ref.types.double,
});
/**
* Val struct, low-level WebAssembly values
*/
let Val = new StructType({
t: ref.types.int,
v: ValUnion,
});
/**
* Array of `Val`
*/
let ValArray = ArrayType(Val);
const context = "void*";
const _functions = {
extism_context_new: [context, []],
extism_context_free: ["void", [context]],
extism_plugin_new: [
pluginIndex,
[context, "string", "uint64", PtrArray, "uint64", "bool"],
],
extism_plugin_new: ["int32", [context, "string", "uint64", "bool"]],
extism_plugin_update: [
"bool",
[context, pluginIndex, "string", "uint64", PtrArray, "uint64", "bool"],
[context, "int32", "string", "uint64", "bool"],
],
extism_error: ["string", [context, pluginIndex]],
extism_error: ["char*", [context, "int32"]],
extism_plugin_call: [
"int32",
[context, pluginIndex, "string", "string", "uint64"],
[context, "int32", "string", "string", "uint64"],
],
extism_plugin_output_length: ["uint64", [context, pluginIndex]],
extism_plugin_output_data: ["uint8*", [context, pluginIndex]],
extism_plugin_output_length: ["uint64", [context, "int32"]],
extism_plugin_output_data: ["uint8*", [context, "int32"]],
extism_log_file: ["bool", ["string", "char*"]],
extism_plugin_function_exists: ["bool", [context, pluginIndex, "string"]],
extism_plugin_config: ["void", [context, pluginIndex, "char*", "uint64"]],
extism_plugin_free: ["void", [context, pluginIndex]],
extism_plugin_function_exists: ["bool", [context, "int32", "string"]],
extism_plugin_config: ["void", [context, "int32", "char*", "uint64"]],
extism_plugin_free: ["void", [context, "int32"]],
extism_context_reset: ["void", [context]],
extism_version: ["string", []],
extism_function_new: [
function_t,
[
"string",
ValTypeArray,
"uint64",
ValTypeArray,
"uint64",
"void*",
"void*",
"void*",
],
],
extism_function_free: ["void", [function_t]],
extism_function_set_namespace: ["void", [function_t, "string"]],
extism_current_plugin_memory: ["uint8*", ["void*"]],
extism_current_plugin_memory_alloc: ["uint64", ["void*", "uint64"]],
extism_current_plugin_memory_length: ["uint64", ["void*", "uint64"]],
extism_current_plugin_memory_free: ["void", ["void*", "uint64"]],
extism_plugin_cancel_handle: ["void*", [context, pluginIndex]],
extism_plugin_cancel: ["bool", ["void*"]],
extism_version: ["char*", []],
};
/**
* An enumeration of all possible `Val` types
*/
export enum ValType {
I32 = 0,
I64,
F32,
F64,
V128,
FuncRef,
ExternRef,
}
interface LibExtism {
extism_context_new: () => Buffer;
extism_context_free: (ctx: Buffer) => void;
@@ -102,62 +32,40 @@ interface LibExtism {
ctx: Buffer,
data: string | Buffer,
data_len: number,
functions: Buffer,
nfunctions: number,
wasi: boolean,
wasi: boolean
) => number;
extism_plugin_update: (
ctx: Buffer,
plugin_id: number,
data: string | Buffer,
data_len: number,
functions: Buffer,
nfunctions: number,
wasi: boolean,
wasi: boolean
) => boolean;
extism_error: (ctx: Buffer, plugin_id: number) => string;
extism_error: (ctx: Buffer, plugin_id: number) => Buffer;
extism_plugin_call: (
ctx: Buffer,
plugin_id: number,
func: string,
input: string,
input_len: number,
input_len: number
) => number;
extism_plugin_output_length: (ctx: Buffer, plugin_id: number) => number;
extism_plugin_output_data: (ctx: Buffer, plugin_id: number) => Uint8Array;
extism_plugin_output_data: (ctx: Buffer, plugin_id: Number) => Uint8Array;
extism_log_file: (file: string, level: string) => boolean;
extism_plugin_function_exists: (
ctx: Buffer,
plugin_id: number,
func: string,
func: string
) => boolean;
extism_plugin_config: (
ctx: Buffer,
plugin_id: number,
data: string | Buffer,
data_len: number,
data_len: number
) => void;
extism_plugin_free: (ctx: Buffer, plugin_id: number) => void;
extism_context_reset: (ctx: Buffer) => void;
extism_version: () => string;
extism_function_new: (
name: string,
inputs: Buffer,
nInputs: number,
outputs: Buffer,
nOutputs: number,
f: Buffer,
user_data: Buffer | null,
free: Buffer | null,
) => Buffer;
extism_function_set_namespace: (f: Buffer, s: string) => void;
extism_function_free: (f: Buffer) => void;
extism_current_plugin_memory: (p: Buffer) => Buffer;
extism_current_plugin_memory_alloc: (p: Buffer, n: number) => number;
extism_current_plugin_memory_length: (p: Buffer, n: number) => number;
extism_current_plugin_memory_free: (p: Buffer, n: number) => void;
extism_plugin_cancel_handle: (p: Buffer, n: number) => Buffer;
extism_plugin_cancel: (p: Buffer) => boolean;
extism_version: () => Buffer;
}
function locate(paths: string[]): LibExtism {
@@ -202,19 +110,19 @@ export function setLogFile(filename: string, level?: string) {
* @returns The version string of the Extism runtime
*/
export function extismVersion(): string {
return lib.extism_version();
return lib.extism_version().toString();
}
// @ts-ignore
const pluginRegistry = new FinalizationRegistry(({ id, pointer }) => {
if (id && pointer) lib.extism_plugin_free(pointer, id);
});
// @ts-ignore
const contextRegistry = new FinalizationRegistry((pointer) => {
if (pointer) lib.extism_context_free(pointer);
});
// @ts-ignore
const functionRegistry = new FinalizationRegistry((pointer) => {
if (pointer) lib.extism_function_free(pointer);
});
/**
* Represents a path or url to a WASM module
*/
@@ -253,7 +161,7 @@ export type ManifestWasm = ManifestWasmFile | ManifestWasmData;
/**
* The manifest which describes the {@link Plugin} code and
* runtime constraints.
*
*
* @see [Extism > Concepts > Manifest](https://extism.org/docs/concepts/manifest)
*/
export type Manifest = {
@@ -306,7 +214,7 @@ export class Context {
*/
constructor() {
this.pointer = lib.extism_context_new();
contextRegistry.register(this, this.pointer, this.pointer);
contextRegistry.register(this, this.pointer, this);
}
/**
@@ -317,24 +225,19 @@ export class Context {
* @param config - Config details for the plugin
* @returns A new Plugin scoped to this Context
*/
plugin(
manifest: ManifestData,
wasi: boolean = false,
functions: HostFunction[] = [],
config?: PluginConfig,
) {
return new Plugin(manifest, wasi, functions, config, this);
plugin(manifest: ManifestData, wasi: boolean = false, config?: PluginConfig) {
return new Plugin(this, manifest, wasi, config);
}
/**
* Frees the context. Should be called after the context is not needed to reclaim the memory.
*/
free() {
contextRegistry.unregister(this.pointer);
if (this.pointer) {
contextRegistry.unregister(this);
lib.extism_context_free(this.pointer);
this.pointer = null;
}
this.pointer = null;
}
/**
@@ -365,246 +268,27 @@ export async function withContext(f: (ctx: Context) => Promise<any>) {
}
}
/**
* Provides access to the plugin that is currently running from inside a {@link HostFunction}
*/
export class CurrentPlugin {
pointer: Buffer;
constructor(pointer: Buffer) {
this.pointer = pointer;
}
/**
* Access plugin's memory
* @param offset - The offset in memory
* @returns a pointer to the provided offset
*/
memory(offset: number): Buffer {
let length = lib.extism_current_plugin_memory_length(this.pointer, offset);
return Buffer.from(
lib.extism_current_plugin_memory(this.pointer).buffer,
offset,
length,
);
}
/**
* Allocate a new memory block
* @param n - The number of bytes to allocate
* @returns the offset to the newly allocated block
*/
memoryAlloc(n: number): number {
return lib.extism_current_plugin_memory_alloc(this.pointer, n);
}
/**
* Free a memory block
* @param offset - The offset of the block to free
*/
memoryFree(offset: number) {
return lib.extism_current_plugin_memory_free(this.pointer, offset);
}
/**
* Get the length of a memory block
* @param offset - The offset of the block
* @returns the length of the block specified by `offset`
*/
memoryLength(offset: number): number {
return lib.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
*/
returnString(output: typeof Val, s: string) {
var offs = this.memoryAlloc(Buffer.byteLength(s));
this.memory(offs).write(s);
output.v.i64 = offs;
}
/**
* Return bytes from a host function
* @param output - The output to set
* @param b - The buffer to return
*/
returnBytes(output: typeof Val, b: Buffer) {
var offs = this.memoryAlloc(b.length);
this.memory(offs).fill(b);
output.v.i64 = offs;
}
/**
* Get bytes from host function parameter
* @param input - The input to read
*/
inputBytes(input: typeof Val): Buffer {
return this.memory(input.v.i64);
}
/**
* Get string from host function parameter
* @param input - The input to read
*/
inputString(input: typeof Val): string {
return this.memory(input.v.i64).toString();
}
}
/**
* Allows for the host to define functions that can be called from WebAseembly
*/
export class HostFunction {
callback: any;
pointer: Buffer | null;
name: string;
userData: any[];
inputs: typeof ValTypeArray;
outputs: typeof ValTypeArray;
constructor(
name: string,
inputs: ValType[],
outputs: ValType[],
f: any,
...userData: any
) {
this.userData = userData;
this.callback = ffi.Callback(
"void",
[
"void*",
ref.refType(Val),
"uint64",
ref.refType(Val),
"uint64",
"void*",
],
(
currentPlugin: Buffer,
inputs: Buffer,
nInputs: number,
outputs: Buffer,
nOutputs: number,
user_data,
) => {
let inputArr = [];
let outputArr = [];
for (var i = 0; i < nInputs; i++) {
inputArr.push(Val.get(inputs, i));
}
for (var i = 0; i < nOutputs; i++) {
outputArr.push(Val.get(outputs, i));
}
f(
new CurrentPlugin(currentPlugin),
inputArr,
outputArr,
...this.userData,
);
for (var i = 0; i < nOutputs; i++) {
Val.set(outputs, i, outputArr[i]);
}
},
);
this.name = name;
this.inputs = new ValTypeArray(inputs);
this.outputs = new ValTypeArray(outputs);
this.pointer = lib.extism_function_new(
this.name,
this.inputs,
this.inputs.length,
this.outputs,
this.outputs.length,
this.callback,
null,
null,
);
this.userData = userData;
functionRegistry.register(this, this.pointer, this.pointer);
}
/**
* Set function namespace
*/
setNamespace(name: string) {
if (this.pointer !== null) {
lib.extism_function_set_namespace(this.pointer, name);
}
}
withNamespace(name: string): HostFunction {
this.setNamespace(name);
return this;
}
/**
* Free a host function - this should be called to cleanup the associated resources
*/
free() {
functionRegistry.unregister(this.pointer);
if (this.pointer === null) {
return;
}
lib.extism_function_free(this.pointer);
this.pointer = null;
}
}
/**
* CancelHandle is used to cancel a running Plugin
*/
export class CancelHandle {
handle: Buffer;
constructor(handle: Buffer) {
this.handle = handle;
}
/**
* Cancel execution of the Plugin associated with the CancelHandle
*/
cancel(): boolean {
return lib.extism_plugin_cancel(this.handle);
}
}
/**
* A Plugin represents an instance of your WASM program from the given manifest.
*/
export class Plugin {
id: number;
ctx: Context;
functions: typeof PtrArray;
token: { id: number; pointer: Buffer };
/**
* Constructor for a plugin. @see {@link Context#plugin}.
*
* @param ctx - The context to manage this plugin
* @param manifest - The {@link Manifest}
* @param wasi - Set to true to enable WASI support
* @param functions - An array of {@link HostFunction}
* @param config - The plugin config
* @param ctx - The context to manage this plugin, or null to use a new context
*/
constructor(
ctx: Context,
manifest: ManifestData,
wasi: boolean = false,
functions: HostFunction[] = [],
config?: PluginConfig,
ctx: Context | null = null,
config?: PluginConfig
) {
if (ctx == null) {
ctx = new Context();
}
let dataRaw: string | Buffer;
if (Buffer.isBuffer(manifest) || typeof manifest === "string") {
dataRaw = manifest;
@@ -614,17 +298,11 @@ export class Plugin {
throw Error(`Unknown manifest type ${typeof manifest}`);
}
if (!ctx.pointer) throw Error("No Context set");
this.functions = new PtrArray(functions.length);
for (var i = 0; i < functions.length; i++) {
this.functions[i] = functions[i].pointer;
}
let plugin = lib.extism_plugin_new(
ctx.pointer,
dataRaw,
Buffer.byteLength(dataRaw, "utf-8"),
this.functions,
functions.length,
wasi,
Buffer.byteLength(dataRaw, 'utf-8'),
wasi
);
if (plugin < 0) {
var err = lib.extism_error(ctx.pointer, -1);
@@ -634,43 +312,27 @@ export class Plugin {
throw `Unable to load plugin: ${err.toString()}`;
}
this.id = plugin;
this.token = { id: this.id, pointer: ctx.pointer };
this.ctx = ctx;
pluginRegistry.register(
this,
{ id: this.id, pointer: this.ctx.pointer },
this
);
if (config != null) {
let s = JSON.stringify(config);
lib.extism_plugin_config(
ctx.pointer,
this.id,
s,
Buffer.byteLength(s, "utf-8"),
);
lib.extism_plugin_config(ctx.pointer, this.id, s, Buffer.byteLength(s, 'utf-8'),);
}
}
/**
* Return a new `CancelHandle`, which can be used to cancel a running Plugin
*/
cancelHandle(): CancelHandle {
if (!this.ctx.pointer) throw Error("No Context set");
let handle = lib.extism_plugin_cancel_handle(this.ctx.pointer, this.id);
return new CancelHandle(handle);
}
/**
* Update an existing plugin with new WASM or manifest
*
* @param manifest - The new {@link Manifest} data
* @param wasi - Set to true to enable WASI support
* @param functions - An array of {@link HostFunction}
* @param config - The new plugin config
*/
update(
manifest: ManifestData,
wasi: boolean = false,
functions: HostFunction[] = [],
config?: PluginConfig,
) {
update(manifest: ManifestData, wasi: boolean = false, config?: PluginConfig) {
let dataRaw: string | Buffer;
if (Buffer.isBuffer(manifest) || typeof manifest === "string") {
dataRaw = manifest;
@@ -680,18 +342,12 @@ export class Plugin {
throw Error("Unknown manifest type type");
}
if (!this.ctx.pointer) throw Error("No Context set");
this.functions = new PtrArray(functions.length);
for (var i = 0; i < functions.length; i++) {
this.functions[i] = functions[i].pointer;
}
const ok = lib.extism_plugin_update(
this.ctx.pointer,
this.id,
dataRaw,
Buffer.byteLength(dataRaw, "utf-8"),
this.functions,
functions.length,
wasi,
Buffer.byteLength(dataRaw, 'utf-8'),
wasi
);
if (!ok) {
var err = lib.extism_error(this.ctx.pointer, -1);
@@ -703,12 +359,7 @@ export class Plugin {
if (config != null) {
let s = JSON.stringify(config);
lib.extism_plugin_config(
this.ctx.pointer,
this.id,
s,
Buffer.byteLength(s, "utf-8"),
);
lib.extism_plugin_config(this.ctx.pointer, this.id, s, Buffer.byteLength(s, 'utf-8'),);
}
}
@@ -724,7 +375,7 @@ export class Plugin {
return lib.extism_plugin_function_exists(
this.ctx.pointer,
this.id,
functionName,
functionName
);
}
@@ -752,7 +403,7 @@ export class Plugin {
this.id,
functionName,
input.toString(),
Buffer.byteLength(input, "utf-8"),
Buffer.byteLength(input, 'utf-8'),
);
if (rc !== 0) {
var err = lib.extism_error(this.ctx.pointer, this.id);
@@ -766,7 +417,7 @@ export class Plugin {
var buf = Buffer.from(
lib.extism_plugin_output_data(this.ctx.pointer, this.id).buffer,
0,
out_len,
out_len
);
resolve(buf);
});
@@ -776,7 +427,8 @@ export class Plugin {
* Free a plugin, this should be called when the plugin is no longer needed
*/
free() {
if (this.ctx.pointer && this.id >= 0) {
if (this.ctx.pointer && this.id !== -1) {
pluginRegistry.unregister(this);
lib.extism_plugin_free(this.ctx.pointer, this.id);
this.id = -1;
}

BIN
node/tests/code.wasm Executable file

Binary file not shown.

View File

@@ -2,23 +2,14 @@ import * as extism from "../src/index";
import { readFileSync } from "fs";
import { join } from "path";
function manifest(functions: boolean = false): extism.Manifest {
function manifest(): extism.Manifest {
return {
wasm: [
{
path: join(
__dirname,
functions
? "/../../wasm/code-functions.wasm"
: "/../../wasm/code.wasm"
),
},
],
wasm: [{ path: join(__dirname, "/code.wasm") }],
};
}
function wasmBuffer(): Buffer {
return readFileSync(join(__dirname, "/../../wasm/code.wasm"));
return readFileSync(join(__dirname, "/code.wasm"));
}
describe("test extism", () => {
@@ -111,27 +102,4 @@ describe("test extism", () => {
).rejects.toMatch(/Plugin error/);
});
});
test("host functions work", async () => {
await extism.withContext(async (ctx: extism.Context) => {
const plugin = ctx.plugin(manifest(true), true, [
new extism.HostFunction(
"hello_world",
[extism.ValType.I64],
[extism.ValType.I64],
(plugin: any, params: any, results: any, user_data: string) => {
const offs = plugin.memoryAlloc(user_data.length);
const mem = plugin.memory(offs);
mem.write(user_data);
results[0].v.i64 = offs;
},
"test"
),
]);
const res = await plugin.call("count_vowels", "aaa");
expect(res.toString()).toBe("test");
});
});
});

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