mirror of
https://github.com/extism/extism.git
synced 2026-01-11 23:08:06 -05:00
Compare commits
1 Commits
fix-dotnet
...
test-java-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce67aecf61 |
2
.github/workflows/browser-ci.yml
vendored
2
.github/workflows/browser-ci.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
rust:
|
||||
- stable
|
||||
steps:
|
||||
|
||||
4
.github/workflows/ci-go.yml
vendored
4
.github/workflows/ci-go.yml
vendored
@@ -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
|
||||
|
||||
9
.github/workflows/ci-java.yml
vendored
9
.github/workflows/ci-java.yml
vendored
@@ -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
|
||||
|
||||
22
.github/workflows/ci-rust.yml
vendored
22
.github/workflows/ci-rust.yml
vendored
@@ -12,8 +12,9 @@ on:
|
||||
name: Rust CI
|
||||
|
||||
env:
|
||||
RUNTIME_CRATE: extism
|
||||
RUNTIME_CRATE: extism-runtime
|
||||
LIBEXTISM_CRATE: libextism
|
||||
RUST_SDK_CRATE: extism
|
||||
|
||||
jobs:
|
||||
lib:
|
||||
@@ -85,9 +86,20 @@ jobs:
|
||||
- name: Lint
|
||||
run: cargo clippy --release --all-features --no-deps -p ${{ env.RUNTIME_CRATE }}
|
||||
- name: Test
|
||||
run: cargo test --release -p ${{ env.RUNTIME_CRATE }}
|
||||
- name: Test all features
|
||||
run: cargo test --all-features --release -p ${{ env.RUNTIME_CRATE }}
|
||||
- name: Test no features
|
||||
run: cargo test --no-default-features --release -p ${{ env.RUNTIME_CRATE }}
|
||||
|
||||
rust:
|
||||
name: Rust
|
||||
needs: lib
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
rust:
|
||||
- stable
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/extism
|
||||
- name: Test Rust Host SDK
|
||||
run: LD_LIBRARY_PATH=/usr/local/lib cargo test --release -p ${{ env.RUST_SDK_CRATE }}
|
||||
|
||||
3
.github/workflows/ci-zig.yml
vendored
3
.github/workflows/ci-zig.yml
vendored
@@ -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: |
|
||||
|
||||
3
.github/workflows/release-haskell.yaml
vendored
3
.github/workflows/release-haskell.yaml
vendored
@@ -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 }}"
|
||||
|
||||
10
.github/workflows/release-rust.yaml
vendored
10
.github/workflows/release-rust.yaml
vendored
@@ -19,19 +19,17 @@ jobs:
|
||||
override: true
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
- name: Release Rust Manifest Crate
|
||||
- name: Release Rust Host SDK
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_API_TOKEN }}
|
||||
run: |
|
||||
# order of crate publication matter: manifest, runtime, rust
|
||||
|
||||
cargo publish --manifest-path manifest/Cargo.toml
|
||||
# allow for crates.io to update so dependant crates can locate extism-manifest
|
||||
sleep 10
|
||||
sleep 5
|
||||
|
||||
- name: Release Runtime
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_API_TOKEN }}
|
||||
run: |
|
||||
cargo publish --manifest-path runtime/Cargo.toml --no-verify
|
||||
cargo publish --manifest-path rust/Cargo.toml
|
||||
|
||||
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
version = 0.26.0
|
||||
version = 0.24.1
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
members = [
|
||||
"manifest",
|
||||
"runtime",
|
||||
"rust",
|
||||
"libextism",
|
||||
"elixir/native/extism_nif"
|
||||
]
|
||||
exclude = ["kernel"]
|
||||
|
||||
14
Makefile
14
Makefile
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
[](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._
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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
5145
browser/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import ExtismContext from './context';
|
||||
import { ExtismFunction, ExtismPlugin } from './plugin';
|
||||
|
||||
export { ExtismContext, ExtismFunction, ExtismPlugin };
|
||||
export { ExtismContext };
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
build:
|
||||
clang -g -o main main.c -lextism -L .
|
||||
clang -o main main.c -lextism -L .
|
||||
42
c/main.c
42
c/main.c
@@ -9,21 +9,6 @@
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
void hello_world(ExtismCurrentPlugin *plugin, const ExtismVal *inputs,
|
||||
uint64_t n_inputs, ExtismVal *outputs, uint64_t n_outputs,
|
||||
void *data) {
|
||||
puts("Hello from C!");
|
||||
puts(data);
|
||||
|
||||
ExtismSize ptr_offs = inputs[0].v.i64;
|
||||
|
||||
uint8_t *buf = extism_current_plugin_memory(plugin) + ptr_offs;
|
||||
uint64_t length = extism_current_plugin_memory_length(plugin, ptr_offs);
|
||||
fwrite(buf, length, 1, stdout);
|
||||
fputc('\n', stdout);
|
||||
outputs[0].v.i64 = inputs[0].v.i64;
|
||||
}
|
||||
|
||||
uint8_t *read_file(const char *filename, size_t *len) {
|
||||
|
||||
FILE *fp = fopen(filename, "rb");
|
||||
@@ -53,29 +38,24 @@ int main(int argc, char *argv[]) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
size_t len = 0;
|
||||
uint8_t *data = read_file("../wasm/code-functions.wasm", &len);
|
||||
ExtismValType inputs[] = {I64};
|
||||
ExtismValType outputs[] = {I64};
|
||||
ExtismFunction *f = extism_function_new("hello_world", inputs, 1, outputs, 1,
|
||||
hello_world, "Hello, again!", NULL);
|
||||
ExtismContext *ctx = extism_context_new();
|
||||
|
||||
char *errmsg = NULL;
|
||||
ExtismPlugin *plugin = extism_plugin_new(
|
||||
data, len, (const ExtismFunction **)&f, 1, true, &errmsg);
|
||||
size_t len = 0;
|
||||
uint8_t *data = read_file("../wasm/code.wasm", &len);
|
||||
ExtismPlugin plugin = extism_plugin_new(ctx, data, len, false);
|
||||
free(data);
|
||||
if (plugin == NULL) {
|
||||
puts(errmsg);
|
||||
extism_plugin_new_error_free(errmsg);
|
||||
if (plugin < 0) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
assert(extism_plugin_call(plugin, "count_vowels", (uint8_t *)argv[1],
|
||||
assert(extism_plugin_call(ctx, plugin, "count_vowels", (uint8_t *)argv[1],
|
||||
strlen(argv[1])) == 0);
|
||||
ExtismSize out_len = extism_plugin_output_length(plugin);
|
||||
const uint8_t *output = extism_plugin_output_data(plugin);
|
||||
ExtismSize out_len = extism_plugin_output_length(ctx, plugin);
|
||||
const uint8_t *output = extism_plugin_output_data(ctx, plugin);
|
||||
write(STDOUT_FILENO, output, out_len);
|
||||
write(STDOUT_FILENO, "\n", 1);
|
||||
extism_plugin_free(plugin);
|
||||
|
||||
extism_plugin_free(ctx, plugin);
|
||||
extism_context_free(ctx);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
389
cpp/extism.hpp
389
cpp/extism.hpp
@@ -1,10 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -23,62 +20,27 @@ extern "C" {
|
||||
namespace extism {
|
||||
|
||||
typedef std::map<std::string, std::string> Config;
|
||||
|
||||
template <typename T> class ManifestKey {
|
||||
bool is_set = false;
|
||||
|
||||
public:
|
||||
T value;
|
||||
ManifestKey(T x, bool is_set = false) : is_set(is_set) { value = x; }
|
||||
|
||||
void set(T x) {
|
||||
value = x;
|
||||
is_set = true;
|
||||
}
|
||||
|
||||
bool empty() const { return is_set == false; }
|
||||
};
|
||||
|
||||
class Wasm {
|
||||
std::string _path;
|
||||
std::string _url;
|
||||
// TODO: add base64 encoded raw data
|
||||
ManifestKey<std::string> _hash =
|
||||
ManifestKey<std::string>(std::string(), false);
|
||||
|
||||
public:
|
||||
// Create Wasm pointing to a path
|
||||
static Wasm path(std::string s, std::string hash = std::string()) {
|
||||
Wasm w;
|
||||
w._path = s;
|
||||
if (!hash.empty()) {
|
||||
w._hash.set(hash);
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
// Create Wasm pointing to a URL
|
||||
static Wasm url(std::string s, std::string hash = std::string()) {
|
||||
Wasm w;
|
||||
w._url = s;
|
||||
if (!hash.empty()) {
|
||||
w._hash.set(hash);
|
||||
}
|
||||
return w;
|
||||
}
|
||||
std::string path;
|
||||
std::string url;
|
||||
// TODO: add base64 encoded raw data
|
||||
std::string hash;
|
||||
|
||||
#ifndef EXTISM_NO_JSON
|
||||
Json::Value json() const {
|
||||
Json::Value doc;
|
||||
|
||||
if (!this->_path.empty()) {
|
||||
doc["path"] = this->_path;
|
||||
} else if (!this->_url.empty()) {
|
||||
doc["url"] = this->_url;
|
||||
if (!this->path.empty()) {
|
||||
doc["path"] = this->path;
|
||||
}
|
||||
|
||||
if (!this->_hash.empty()) {
|
||||
doc["hash"] = this->_hash.value;
|
||||
if (!this->url.empty()) {
|
||||
doc["url"] = this->url;
|
||||
}
|
||||
|
||||
if (!this->hash.empty()) {
|
||||
doc["hash"] = this->hash;
|
||||
}
|
||||
|
||||
return doc;
|
||||
@@ -90,23 +52,18 @@ class Manifest {
|
||||
public:
|
||||
Config config;
|
||||
std::vector<Wasm> wasm;
|
||||
ManifestKey<std::vector<std::string>> allowed_hosts;
|
||||
ManifestKey<std::map<std::string, std::string>> allowed_paths;
|
||||
ManifestKey<uint64_t> timeout_ms;
|
||||
std::vector<std::string> allowed_hosts;
|
||||
std::map<std::string, std::string> allowed_paths;
|
||||
uint64_t timeout_ms;
|
||||
|
||||
// Empty manifest
|
||||
Manifest()
|
||||
: timeout_ms(0, false), allowed_hosts(std::vector<std::string>(), false),
|
||||
allowed_paths(std::map<std::string, std::string>(), false) {}
|
||||
Manifest() : timeout_ms(30000) {}
|
||||
|
||||
// Create manifest with a single Wasm from a path
|
||||
static Manifest path(std::string s, std::string hash = std::string()) {
|
||||
Manifest m;
|
||||
m.add_wasm_path(s, hash);
|
||||
return m;
|
||||
}
|
||||
|
||||
// Create manifest with a single Wasm from a URL
|
||||
static Manifest url(std::string s, std::string hash = std::string()) {
|
||||
Manifest m;
|
||||
m.add_wasm_url(s, hash);
|
||||
@@ -135,7 +92,7 @@ public:
|
||||
if (!this->allowed_hosts.empty()) {
|
||||
Json::Value h;
|
||||
|
||||
for (auto s : this->allowed_hosts.value) {
|
||||
for (auto s : this->allowed_hosts) {
|
||||
h.append(s);
|
||||
}
|
||||
doc["allowed_hosts"] = h;
|
||||
@@ -143,63 +100,54 @@ public:
|
||||
|
||||
if (!this->allowed_paths.empty()) {
|
||||
Json::Value h;
|
||||
for (auto k : this->allowed_paths.value) {
|
||||
for (auto k : this->allowed_paths) {
|
||||
h[k.first] = k.second;
|
||||
}
|
||||
doc["allowed_paths"] = h;
|
||||
}
|
||||
|
||||
if (!this->timeout_ms.empty()) {
|
||||
doc["timeout_ms"] = Json::Value(this->timeout_ms.value);
|
||||
}
|
||||
doc["timeout_ms"] = Json::Value(this->timeout_ms);
|
||||
|
||||
Json::FastWriter writer;
|
||||
return writer.write(doc);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Add Wasm from path
|
||||
void add_wasm_path(std::string s, std::string hash = std::string()) {
|
||||
Wasm w = Wasm::path(s, hash);
|
||||
Wasm w;
|
||||
w.path = s;
|
||||
w.hash = hash;
|
||||
this->wasm.push_back(w);
|
||||
}
|
||||
|
||||
// Add Wasm from URL
|
||||
void add_wasm_url(std::string u, std::string hash = std::string()) {
|
||||
Wasm w = Wasm::url(u, hash);
|
||||
Wasm w;
|
||||
w.url = u;
|
||||
w.hash = hash;
|
||||
this->wasm.push_back(w);
|
||||
}
|
||||
|
||||
// Add host to allowed hosts
|
||||
void allow_host(std::string host) {
|
||||
if (this->allowed_hosts.empty()) {
|
||||
this->allowed_hosts.set(std::vector<std::string>{});
|
||||
}
|
||||
this->allowed_hosts.value.push_back(host);
|
||||
}
|
||||
void allow_host(std::string host) { this->allowed_hosts.push_back(host); }
|
||||
|
||||
// Add path to allowed paths
|
||||
void allow_path(std::string src, std::string dest = std::string()) {
|
||||
if (this->allowed_paths.empty()) {
|
||||
this->allowed_paths.set(std::map<std::string, std::string>{});
|
||||
}
|
||||
|
||||
if (dest.empty()) {
|
||||
dest = src;
|
||||
}
|
||||
this->allowed_paths.value[src] = dest;
|
||||
this->allowed_paths[src] = dest;
|
||||
}
|
||||
|
||||
// Set timeout
|
||||
void set_timeout_ms(uint64_t ms) { this->timeout_ms = ms; }
|
||||
|
||||
// Set config key/value
|
||||
void set_config(std::string k, std::string v) { this->config[k] = v; }
|
||||
};
|
||||
|
||||
class Error : public std::runtime_error {
|
||||
class Error : public std::exception {
|
||||
private:
|
||||
std::string message;
|
||||
|
||||
public:
|
||||
Error(std::string msg) : std::runtime_error(msg) {}
|
||||
Error(std::string msg) : message(msg) {}
|
||||
const char *what() { return message.c_str(); }
|
||||
};
|
||||
|
||||
class Buffer {
|
||||
@@ -218,172 +166,63 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
typedef ExtismValType ValType;
|
||||
typedef ExtismValUnion ValUnion;
|
||||
typedef ExtismVal Val;
|
||||
typedef uint64_t MemoryHandle;
|
||||
|
||||
class CurrentPlugin {
|
||||
ExtismCurrentPlugin *pointer;
|
||||
|
||||
public:
|
||||
CurrentPlugin(ExtismCurrentPlugin *p) : pointer(p) {}
|
||||
|
||||
uint8_t *memory() { return extism_current_plugin_memory(this->pointer); }
|
||||
uint8_t *memory(MemoryHandle offs) { return this->memory() + offs; }
|
||||
|
||||
ExtismSize memoryLength(MemoryHandle offs) {
|
||||
return extism_current_plugin_memory_length(this->pointer, offs);
|
||||
}
|
||||
|
||||
MemoryHandle alloc(ExtismSize size) {
|
||||
return extism_current_plugin_memory_alloc(this->pointer, size);
|
||||
}
|
||||
|
||||
void free(MemoryHandle handle) {
|
||||
extism_current_plugin_memory_free(this->pointer, handle);
|
||||
}
|
||||
|
||||
void returnString(Val &output, const std::string &s) {
|
||||
this->returnBytes(output, (const uint8_t *)s.c_str(), s.size());
|
||||
}
|
||||
|
||||
void returnBytes(Val &output, const uint8_t *bytes, size_t len) {
|
||||
auto offs = this->alloc(len);
|
||||
memcpy(this->memory() + offs, bytes, len);
|
||||
output.v.i64 = offs;
|
||||
}
|
||||
|
||||
uint8_t *inputBytes(Val &inp, size_t *length = nullptr) {
|
||||
if (inp.t != ValType::I64) {
|
||||
return nullptr;
|
||||
}
|
||||
if (length != nullptr) {
|
||||
*length = this->memoryLength(inp.v.i64);
|
||||
}
|
||||
return this->memory() + inp.v.i64;
|
||||
}
|
||||
|
||||
std::string inputString(Val &inp) {
|
||||
size_t length = 0;
|
||||
char *buf = (char *)this->inputBytes(inp, &length);
|
||||
return std::string(buf, length);
|
||||
}
|
||||
};
|
||||
|
||||
typedef std::function<void(CurrentPlugin, const std::vector<Val> &,
|
||||
std::vector<Val> &, void *user_data)>
|
||||
FunctionType;
|
||||
|
||||
struct UserData {
|
||||
FunctionType func;
|
||||
void *user_data = NULL;
|
||||
std::function<void(void *)> free_user_data;
|
||||
};
|
||||
|
||||
static void function_callback(ExtismCurrentPlugin *plugin,
|
||||
const ExtismVal *inputs, ExtismSize n_inputs,
|
||||
ExtismVal *outputs, ExtismSize n_outputs,
|
||||
void *user_data) {
|
||||
UserData *data = (UserData *)user_data;
|
||||
const std::vector<Val> inp(inputs, inputs + n_inputs);
|
||||
std::vector<Val> outp(outputs, outputs + n_outputs);
|
||||
data->func(CurrentPlugin(plugin), inp, outp, data->user_data);
|
||||
|
||||
for (ExtismSize i = 0; i < n_outputs; i++) {
|
||||
outputs[i] = outp[i];
|
||||
}
|
||||
}
|
||||
|
||||
static void free_user_data(void *user_data) {
|
||||
UserData *data = (UserData *)user_data;
|
||||
if (data->user_data != NULL && data->free_user_data != NULL) {
|
||||
data->free_user_data(data->user_data);
|
||||
}
|
||||
}
|
||||
|
||||
class Function {
|
||||
std::shared_ptr<ExtismFunction> func;
|
||||
std::string name;
|
||||
UserData user_data;
|
||||
|
||||
public:
|
||||
Function(std::string name, const std::vector<ValType> inputs,
|
||||
const std::vector<ValType> outputs, FunctionType f,
|
||||
void *user_data = NULL, std::function<void(void *)> free = nullptr)
|
||||
: name(name) {
|
||||
this->user_data.func = f;
|
||||
this->user_data.user_data = user_data;
|
||||
this->user_data.free_user_data = free;
|
||||
auto ptr = extism_function_new(
|
||||
this->name.c_str(), inputs.data(), inputs.size(), outputs.data(),
|
||||
outputs.size(), function_callback, &this->user_data, free_user_data);
|
||||
this->func = std::shared_ptr<ExtismFunction>(ptr, extism_function_free);
|
||||
}
|
||||
|
||||
void setNamespace(std::string s) {
|
||||
extism_function_set_namespace(this->func.get(), s.c_str());
|
||||
}
|
||||
|
||||
Function(const Function &f) { this->func = f.func; }
|
||||
|
||||
ExtismFunction *get() { return this->func.get(); }
|
||||
};
|
||||
|
||||
class CancelHandle {
|
||||
const ExtismCancelHandle *handle;
|
||||
|
||||
public:
|
||||
CancelHandle(const ExtismCancelHandle *x) : handle(x){};
|
||||
bool cancel() { return extism_plugin_cancel(this->handle); }
|
||||
};
|
||||
|
||||
class Plugin {
|
||||
std::vector<Function> functions;
|
||||
std::shared_ptr<ExtismContext> context;
|
||||
ExtismPlugin plugin;
|
||||
|
||||
public:
|
||||
ExtismPlugin *plugin;
|
||||
// Create a new plugin
|
||||
Plugin(const uint8_t *wasm, ExtismSize length, bool with_wasi = false,
|
||||
std::vector<Function> functions = std::vector<Function>())
|
||||
: functions(functions) {
|
||||
std::vector<const ExtismFunction *> ptrs;
|
||||
for (auto i : this->functions) {
|
||||
ptrs.push_back(i.get());
|
||||
Plugin(std::shared_ptr<ExtismContext> ctx, const uint8_t *wasm,
|
||||
ExtismSize length, bool with_wasi = false) {
|
||||
this->plugin = extism_plugin_new(ctx.get(), wasm, length, with_wasi);
|
||||
if (this->plugin < 0) {
|
||||
const char *err = extism_error(ctx.get(), -1);
|
||||
throw Error(err == nullptr ? "Unable to load plugin" : err);
|
||||
}
|
||||
|
||||
char *errmsg = nullptr;
|
||||
this->plugin = extism_plugin_new(wasm, length, ptrs.data(), ptrs.size(),
|
||||
with_wasi, &errmsg);
|
||||
if (this->plugin == nullptr) {
|
||||
std::string s(errmsg);
|
||||
extism_plugin_new_error_free(errmsg);
|
||||
throw Error(s);
|
||||
}
|
||||
}
|
||||
|
||||
Plugin(const std::string &str, bool with_wasi = false,
|
||||
std::vector<Function> functions = {})
|
||||
: Plugin((const uint8_t *)str.c_str(), str.size(), with_wasi, functions) {
|
||||
}
|
||||
|
||||
Plugin(const std::vector<uint8_t> &data, bool with_wasi = false,
|
||||
std::vector<Function> functions = {})
|
||||
: Plugin(data.data(), data.size(), with_wasi, functions) {}
|
||||
|
||||
CancelHandle cancelHandle() {
|
||||
return CancelHandle(extism_plugin_cancel_handle(this->plugin));
|
||||
this->context = ctx;
|
||||
}
|
||||
|
||||
#ifndef EXTISM_NO_JSON
|
||||
// Create a new plugin from Manifest
|
||||
Plugin(const Manifest &manifest, bool with_wasi = false,
|
||||
std::vector<Function> functions = {})
|
||||
: Plugin(manifest.json().c_str(), with_wasi, functions) {}
|
||||
Plugin(std::shared_ptr<ExtismContext> ctx, const Manifest &manifest,
|
||||
bool with_wasi = false) {
|
||||
auto buffer = manifest.json();
|
||||
this->plugin = extism_plugin_new(ctx.get(), (const uint8_t *)buffer.c_str(),
|
||||
buffer.size(), with_wasi);
|
||||
if (this->plugin < 0) {
|
||||
const char *err = extism_error(ctx.get(), -1);
|
||||
throw Error(err == nullptr ? "Unable to load plugin from manifest" : err);
|
||||
}
|
||||
this->context = ctx;
|
||||
}
|
||||
#endif
|
||||
|
||||
~Plugin() {
|
||||
extism_plugin_free(this->plugin);
|
||||
this->plugin = nullptr;
|
||||
extism_plugin_free(this->context.get(), this->plugin);
|
||||
this->plugin = -1;
|
||||
}
|
||||
|
||||
ExtismPlugin id() const { return this->plugin; }
|
||||
|
||||
ExtismContext *get_context() const { return this->context.get(); }
|
||||
|
||||
void update(const uint8_t *wasm, size_t length, bool with_wasi = false) {
|
||||
bool b = extism_plugin_update(this->context.get(), this->plugin, wasm,
|
||||
length, with_wasi);
|
||||
if (!b) {
|
||||
const char *err = extism_error(this->context.get(), -1);
|
||||
throw Error(err == nullptr ? "Unable to update plugin" : err);
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef EXTISM_NO_JSON
|
||||
void update(const Manifest &manifest, bool with_wasi = false) {
|
||||
auto buffer = manifest.json();
|
||||
bool b = extism_plugin_update(this->context.get(), this->plugin,
|
||||
(const uint8_t *)buffer.c_str(),
|
||||
buffer.size(), with_wasi);
|
||||
if (!b) {
|
||||
const char *err = extism_error(this->context.get(), -1);
|
||||
throw Error(err == nullptr ? "Unable to update plugin" : err);
|
||||
}
|
||||
}
|
||||
|
||||
void config(const Config &data) {
|
||||
@@ -400,9 +239,10 @@ public:
|
||||
#endif
|
||||
|
||||
void config(const char *json, size_t length) {
|
||||
bool b = extism_plugin_config(this->plugin, (const uint8_t *)json, length);
|
||||
bool b = extism_plugin_config(this->context.get(), this->plugin,
|
||||
(const uint8_t *)json, length);
|
||||
if (!b) {
|
||||
const char *err = extism_plugin_error(this->plugin);
|
||||
const char *err = extism_error(this->context.get(), this->plugin);
|
||||
throw Error(err == nullptr ? "Unable to update plugin config" : err);
|
||||
}
|
||||
}
|
||||
@@ -411,13 +251,12 @@ public:
|
||||
this->config(json.c_str(), json.size());
|
||||
}
|
||||
|
||||
// Call a plugin
|
||||
Buffer call(const std::string &func, const uint8_t *input,
|
||||
ExtismSize input_length) const {
|
||||
int32_t rc =
|
||||
extism_plugin_call(this->plugin, func.c_str(), input, input_length);
|
||||
int32_t rc = extism_plugin_call(this->context.get(), this->plugin,
|
||||
func.c_str(), input, input_length);
|
||||
if (rc != 0) {
|
||||
const char *error = extism_plugin_error(this->plugin);
|
||||
const char *error = extism_error(this->context.get(), this->plugin);
|
||||
if (error == nullptr) {
|
||||
throw Error("extism_call failed");
|
||||
}
|
||||
@@ -425,34 +264,64 @@ public:
|
||||
throw Error(error);
|
||||
}
|
||||
|
||||
ExtismSize length = extism_plugin_output_length(this->plugin);
|
||||
const uint8_t *ptr = extism_plugin_output_data(this->plugin);
|
||||
ExtismSize length =
|
||||
extism_plugin_output_length(this->context.get(), this->plugin);
|
||||
const uint8_t *ptr =
|
||||
extism_plugin_output_data(this->context.get(), this->plugin);
|
||||
return Buffer(ptr, length);
|
||||
}
|
||||
|
||||
// Call a plugin function with std::vector<uint8_t> input
|
||||
Buffer call(const std::string &func,
|
||||
const std::vector<uint8_t> &input) const {
|
||||
return this->call(func, input.data(), input.size());
|
||||
}
|
||||
|
||||
// Call a plugin function with string input
|
||||
Buffer call(const std::string &func,
|
||||
const std::string &input = std::string()) const {
|
||||
Buffer call(const std::string &func, const std::string &input) const {
|
||||
return this->call(func, (const uint8_t *)input.c_str(), input.size());
|
||||
}
|
||||
|
||||
// Returns true if the specified function exists
|
||||
bool functionExists(const std::string &func) const {
|
||||
return extism_plugin_function_exists(this->plugin, func.c_str());
|
||||
bool function_exists(const std::string &func) const {
|
||||
return extism_plugin_function_exists(this->context.get(), this->plugin,
|
||||
func.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
// Set global log file for plugins
|
||||
inline bool setLogFile(const char *filename, const char *level) {
|
||||
class Context {
|
||||
public:
|
||||
std::shared_ptr<ExtismContext> pointer;
|
||||
|
||||
Context() {
|
||||
this->pointer = std::shared_ptr<ExtismContext>(extism_context_new(),
|
||||
extism_context_free);
|
||||
}
|
||||
|
||||
Plugin plugin(const uint8_t *wasm, size_t length,
|
||||
bool with_wasi = false) const {
|
||||
return Plugin(this->pointer, wasm, length, with_wasi);
|
||||
}
|
||||
|
||||
Plugin plugin(const std::string &str, bool with_wasi = false) const {
|
||||
return Plugin(this->pointer, (const uint8_t *)str.c_str(), str.size(),
|
||||
with_wasi);
|
||||
}
|
||||
|
||||
Plugin plugin(const std::vector<uint8_t> &data,
|
||||
bool with_wasi = false) const {
|
||||
return Plugin(this->pointer, data.data(), data.size(), with_wasi);
|
||||
}
|
||||
|
||||
#ifndef EXTISM_NO_JSON
|
||||
Plugin plugin(const Manifest &manifest, bool with_wasi = false) const {
|
||||
return Plugin(this->pointer, manifest, with_wasi);
|
||||
}
|
||||
#endif
|
||||
|
||||
void reset() { extism_context_reset(this->pointer.get()); }
|
||||
};
|
||||
|
||||
inline bool set_log_file(const char *filename, const char *level) {
|
||||
return extism_log_file(filename, level);
|
||||
}
|
||||
|
||||
// Get libextism version
|
||||
inline std::string version() { return std::string(extism_version()); }
|
||||
} // namespace extism
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "../extism.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <thread>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
@@ -11,38 +10,46 @@ std::vector<uint8_t> read(const char *filename) {
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
const std::string code = "../../wasm/code.wasm";
|
||||
|
||||
namespace {
|
||||
using namespace extism;
|
||||
|
||||
TEST(Context, Basic) {
|
||||
Context context;
|
||||
ASSERT_NE(context.pointer, nullptr);
|
||||
}
|
||||
|
||||
TEST(Plugin, Manifest) {
|
||||
Manifest manifest = Manifest::path(code);
|
||||
Context context;
|
||||
Manifest manifest = Manifest::path("code.wasm");
|
||||
manifest.set_config("a", "1");
|
||||
|
||||
Plugin plugin(manifest);
|
||||
ASSERT_NO_THROW(Plugin plugin = context.plugin(manifest));
|
||||
Plugin plugin = context.plugin(manifest);
|
||||
|
||||
Buffer buf = plugin.call("count_vowels", "this is a test");
|
||||
ASSERT_EQ((std::string)buf, "{\"count\": 4}");
|
||||
}
|
||||
|
||||
TEST(Plugin, BadManifest) {
|
||||
Context context;
|
||||
Manifest manifest;
|
||||
ASSERT_THROW(Plugin plugin(manifest), Error);
|
||||
ASSERT_THROW(Plugin plugin = context.plugin(manifest), Error);
|
||||
}
|
||||
|
||||
TEST(Plugin, Bytes) {
|
||||
auto wasm = read(code.c_str());
|
||||
ASSERT_NO_THROW(Plugin plugin(wasm));
|
||||
Plugin plugin(wasm);
|
||||
Context context;
|
||||
auto wasm = read("code.wasm");
|
||||
ASSERT_NO_THROW(Plugin plugin = context.plugin(wasm));
|
||||
Plugin plugin = context.plugin(wasm);
|
||||
|
||||
Buffer buf = plugin.call("count_vowels", "this is another test");
|
||||
ASSERT_EQ(buf.string(), "{\"count\": 6}");
|
||||
}
|
||||
|
||||
TEST(Plugin, UpdateConfig) {
|
||||
auto wasm = read(code.c_str());
|
||||
Plugin plugin(wasm);
|
||||
Context context;
|
||||
auto wasm = read("code.wasm");
|
||||
Plugin plugin = context.plugin(wasm);
|
||||
|
||||
Config config;
|
||||
config["abc"] = "123";
|
||||
@@ -50,63 +57,12 @@ TEST(Plugin, UpdateConfig) {
|
||||
}
|
||||
|
||||
TEST(Plugin, FunctionExists) {
|
||||
auto wasm = read(code.c_str());
|
||||
Plugin plugin(wasm);
|
||||
Context context;
|
||||
auto wasm = read("code.wasm");
|
||||
Plugin plugin = context.plugin(wasm);
|
||||
|
||||
ASSERT_FALSE(plugin.functionExists("bad_function"));
|
||||
ASSERT_TRUE(plugin.functionExists("count_vowels"));
|
||||
}
|
||||
|
||||
TEST(Plugin, HostFunction) {
|
||||
auto wasm = read("../../wasm/code-functions.wasm");
|
||||
auto t = std::vector<ValType>{ValType::I64};
|
||||
Function hello_world =
|
||||
Function("hello_world", t, t,
|
||||
[](CurrentPlugin plugin, const std::vector<Val> ¶ms,
|
||||
std::vector<Val> &results, void *user_data) {
|
||||
auto offs = plugin.alloc(4);
|
||||
memcpy(plugin.memory() + offs, "test", 4);
|
||||
results[0].v.i64 = (int64_t)offs;
|
||||
});
|
||||
auto functions = std::vector<Function>{
|
||||
hello_world,
|
||||
};
|
||||
Plugin plugin(wasm, true, functions);
|
||||
auto buf = plugin.call("count_vowels", "aaa");
|
||||
ASSERT_EQ(buf.length, 4);
|
||||
ASSERT_EQ((std::string)buf, "test");
|
||||
}
|
||||
|
||||
void callThread(Plugin *plugin) {
|
||||
auto buf = plugin->call("count_vowels", "aaa").string();
|
||||
ASSERT_EQ(buf.size(), 10);
|
||||
ASSERT_EQ(buf, "testing123");
|
||||
}
|
||||
|
||||
TEST(Plugin, MultipleThreads) {
|
||||
auto wasm = read("../../wasm/code-functions.wasm");
|
||||
auto t = std::vector<ValType>{ValType::I64};
|
||||
Function hello_world =
|
||||
Function("hello_world", t, t,
|
||||
[](CurrentPlugin plugin, const std::vector<Val> ¶ms,
|
||||
std::vector<Val> &results, void *user_data) {
|
||||
auto offs = plugin.alloc(10);
|
||||
memcpy(plugin.memory() + offs, "testing123", 10);
|
||||
results[0].v.i64 = (int64_t)offs;
|
||||
});
|
||||
auto functions = std::vector<Function>{
|
||||
hello_world,
|
||||
};
|
||||
Plugin plugin(wasm, true, functions);
|
||||
|
||||
std::vector<std::thread> threads;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
threads.push_back(std::thread(callThread, &plugin));
|
||||
}
|
||||
|
||||
for (auto &th : threads) {
|
||||
th.join();
|
||||
}
|
||||
ASSERT_FALSE(plugin.function_exists("bad_function"));
|
||||
ASSERT_TRUE(plugin.function_exists("count_vowels"));
|
||||
}
|
||||
|
||||
}; // namespace
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\..\wasm\code.wasm" Link="code.wasm">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Include="..\..\..\wasm\code-functions.wasm" Link="code-functions.wasm">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\..\..\wasm\code.wasm" Link="code.wasm">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- <PackageReference Include="Extism.runtime.win-x64" Version="0.7.0" /> -->
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Extism.Sdk\Extism.Sdk.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Extism.Sdk\Extism.Sdk.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -1,49 +1,11 @@
|
||||
using Extism.Sdk;
|
||||
using Extism.Sdk.Native;
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
Console.WriteLine($"Version: {Plugin.ExtismVersion()}");
|
||||
|
||||
var userData = Marshal.StringToHGlobalAnsi("Hello again!");
|
||||
|
||||
using var helloWorld = new HostFunction(
|
||||
"hello_world",
|
||||
"env",
|
||||
new[] { ExtismValType.I64 },
|
||||
new[] { ExtismValType.I64 },
|
||||
userData,
|
||||
HelloWorld);
|
||||
|
||||
void HelloWorld(CurrentPlugin plugin, Span<ExtismVal> inputs, Span<ExtismVal> outputs, nint data)
|
||||
{
|
||||
Console.WriteLine("Hello from .NET!");
|
||||
|
||||
var text = Marshal.PtrToStringAnsi(data);
|
||||
Console.WriteLine(text);
|
||||
|
||||
var input = plugin.ReadString(new nint(inputs[0].v.i64));
|
||||
Console.WriteLine($"Input: {input}");
|
||||
|
||||
outputs[0].v.i64 = plugin.WriteString(input);
|
||||
}
|
||||
|
||||
var manifest = new Manifest(new PathWasmSource("./code-functions.wasm"))
|
||||
{
|
||||
Config = new Dictionary<string, string>
|
||||
{
|
||||
{ "my-key", "some cool value" }
|
||||
},
|
||||
};
|
||||
|
||||
using var plugin = new Plugin(manifest, new[] { helloWorld }, withWasi: true);
|
||||
|
||||
Console.WriteLine("Plugin creatd!!!");
|
||||
var context = new Context();
|
||||
var wasm = await File.ReadAllBytesAsync("./code.wasm");
|
||||
using var plugin = context.CreatePlugin(wasm, withWasi: true);
|
||||
|
||||
var output = Encoding.UTF8.GetString(
|
||||
plugin.CallFunction("count_vowels", Encoding.UTF8.GetBytes("Hello World!"))
|
||||
);
|
||||
|
||||
Console.WriteLine($"Output: {output}");
|
||||
|
||||
Console.WriteLine(output); // prints {"count": 3}
|
||||
|
||||
Binary file not shown.
184
dotnet/src/Extism.Sdk/Context.cs
Normal file
184
dotnet/src/Extism.Sdk/Context.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Extism.Sdk.Native;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an Extism context through which you can load <see cref="Plugin"/>s.
|
||||
/// </summary>
|
||||
public class Context : IDisposable
|
||||
{
|
||||
private const int DisposedMarker = 1;
|
||||
|
||||
private int _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new Extism Context.
|
||||
/// </summary>
|
||||
public Context()
|
||||
{
|
||||
NativeHandle = LibExtism.extism_context_new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Native pointer to the Extism Context.
|
||||
/// </summary>
|
||||
internal IntPtr NativeHandle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Loads an Extism <see cref="Plugin"/>.
|
||||
/// </summary>
|
||||
/// <param name="wasm">A WASM module (wat or wasm) or a JSON encoded manifest.</param>
|
||||
/// <param name="withWasi">Enable/Disable WASI.</param>
|
||||
public Plugin CreatePlugin(ReadOnlySpan<byte> wasm, bool withWasi)
|
||||
{
|
||||
CheckNotDisposed();
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* wasmPtr = wasm)
|
||||
{
|
||||
var plugin = LibExtism.extism_plugin_new(NativeHandle, wasmPtr, wasm.Length, withWasi);
|
||||
return new Plugin(this, plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove all plugins from this <see cref="Context"/>'s registry.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
CheckNotDisposed();
|
||||
|
||||
LibExtism.extism_context_reset(NativeHandle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get this this <see cref="Context"/>'s last error.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
internal string? GetError()
|
||||
{
|
||||
CheckNotDisposed();
|
||||
|
||||
var result = LibExtism.extism_error(NativeHandle, -1);
|
||||
return Marshal.PtrToStringUTF8(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Frees all resources held by this Context.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, DisposedMarker) == DisposedMarker)
|
||||
{
|
||||
// Already disposed.
|
||||
return;
|
||||
}
|
||||
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throw an appropriate exception if the plugin has been disposed.
|
||||
/// </summary>
|
||||
/// <exception cref="ObjectDisposedException"></exception>
|
||||
protected void CheckNotDisposed()
|
||||
{
|
||||
Interlocked.MemoryBarrier();
|
||||
if (_disposed == DisposedMarker)
|
||||
{
|
||||
ThrowDisposedException();
|
||||
}
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
private static void ThrowDisposedException()
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(Context));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Frees all resources held by this Context.
|
||||
/// </summary>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// Free up any managed resources here
|
||||
}
|
||||
|
||||
// Free up unmanaged resources
|
||||
LibExtism.extism_context_free(NativeHandle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Destructs the current Context and frees all resources used by it.
|
||||
/// </summary>
|
||||
~Context()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the Extism version string.
|
||||
/// </summary>
|
||||
public static string GetExtismVersion()
|
||||
{
|
||||
var pointer = LibExtism.extism_version();
|
||||
return Marshal.PtrToStringUTF8(pointer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set Extism's log file and level. This is applied for all <see cref="Context"/>s.
|
||||
/// </summary>
|
||||
/// <param name="logPath">Log file; can be 'stdout' or 'stderr' to write logs to the console.</param>
|
||||
/// <param name="level">The log level to write at.</param>
|
||||
public static bool SetExtismLogFile(string logPath, LogLevel level)
|
||||
{
|
||||
var logLevel = level switch
|
||||
{
|
||||
LogLevel.Error => LibExtism.LogLevels.Error,
|
||||
LogLevel.Warning => LibExtism.LogLevels.Warn,
|
||||
LogLevel.Info => LibExtism.LogLevels.Info,
|
||||
LogLevel.Debug => LibExtism.LogLevels.Debug,
|
||||
LogLevel.Trace => LibExtism.LogLevels.Trace,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
|
||||
return LibExtism.extism_log_file(logPath, logLevel);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extism Log Levels
|
||||
/// </summary>
|
||||
public enum LogLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// Designates very serious errors.
|
||||
/// </summary>
|
||||
Error,
|
||||
|
||||
/// <summary>
|
||||
/// Designates hazardous situations.
|
||||
/// </summary>
|
||||
Warning,
|
||||
|
||||
/// <summary>
|
||||
/// Designates useful information.
|
||||
/// </summary>
|
||||
Info,
|
||||
|
||||
/// <summary>
|
||||
/// Designates lower priority information.
|
||||
/// </summary>
|
||||
Debug,
|
||||
|
||||
/// <summary>
|
||||
/// Designates very low priority, often extremely verbose, information.
|
||||
/// </summary>
|
||||
Trace
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -1,29 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
<LangVersion>11</LangVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
<LangVersion>10</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<PackageId>Extism.Sdk</PackageId>
|
||||
<Version>0.7.0</Version>
|
||||
<Authors>Extism Contributors</Authors>
|
||||
<Description>Extism SDK that allows hosting Extism plugins in .NET apps.</Description>
|
||||
<Tags>extism, wasm, plugin</Tags>
|
||||
<PackageLicenseExpression>BSD-3-Clause</PackageLicenseExpression>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<PackageId>Extism.Sdk</PackageId>
|
||||
<Version>0.2.0</Version>
|
||||
<Authors>Extism Contributors</Authors>
|
||||
<Description>Extism SDK that allows hosting Extism plugins in .NET apps.</Description>
|
||||
<Tags>extism, wasm, plugin</Tags>
|
||||
<PackageLicenseExpression>BSD-3-Clause</PackageLicenseExpression>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="README.md" Pack="true" PackagePath="\"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="7.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,264 +2,124 @@ using System.Runtime.InteropServices;
|
||||
|
||||
namespace Extism.Sdk.Native;
|
||||
|
||||
/// <summary>
|
||||
/// A union type for host function argument/return values.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
public struct ExtismValUnion
|
||||
{
|
||||
/// <summary>
|
||||
/// Set this for 32 bit integers
|
||||
/// </summary>
|
||||
[FieldOffset(0)]
|
||||
public int i32;
|
||||
|
||||
/// <summary>
|
||||
/// Set this for 64 bit integers
|
||||
/// </summary>
|
||||
[FieldOffset(0)]
|
||||
public long i64;
|
||||
|
||||
/// <summary>
|
||||
/// Set this for 32 bit floats
|
||||
/// </summary>
|
||||
[FieldOffset(0)]
|
||||
public float f32;
|
||||
|
||||
/// <summary>
|
||||
/// Set this for 64 bit floats
|
||||
/// </summary>
|
||||
[FieldOffset(0)]
|
||||
public double f64;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents Wasm data types that Extism can understand
|
||||
/// </summary>
|
||||
public enum ExtismValType : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Signed 32 bit integer. Equivalent of <see cref="int"/> or <see cref="uint"/>
|
||||
/// </summary>
|
||||
I32,
|
||||
|
||||
/// <summary>
|
||||
/// Signed 64 bit integer. Equivalent of <see cref="long"/> or <see cref="ulong"/>
|
||||
/// </summary>
|
||||
I64,
|
||||
|
||||
/// <summary>
|
||||
/// Floating point 32 bit integer. Equivalent of <see cref="float"/>
|
||||
/// </summary>
|
||||
F32,
|
||||
|
||||
/// <summary>
|
||||
/// Floating point 64 bit integer. Equivalent of <see cref="double"/>
|
||||
/// </summary>
|
||||
F64,
|
||||
|
||||
/// <summary>
|
||||
/// A 128 bit number.
|
||||
/// </summary>
|
||||
V128,
|
||||
|
||||
/// <summary>
|
||||
/// A reference to opaque data in the Wasm instance.
|
||||
/// </summary>
|
||||
FuncRef,
|
||||
|
||||
/// <summary>
|
||||
/// A reference to opaque data in the Wasm instance.
|
||||
/// </summary>
|
||||
ExternRef
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// `ExtismVal` holds the type and value of a function argument/return
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct ExtismVal
|
||||
{
|
||||
/// <summary>
|
||||
/// The type for the argument
|
||||
/// </summary>
|
||||
public ExtismValType t;
|
||||
|
||||
/// <summary>
|
||||
/// The value for the argument
|
||||
/// </summary>
|
||||
public ExtismValUnion v;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Functions exposed by the native Extism library.
|
||||
/// </summary>
|
||||
internal static class LibExtism
|
||||
{
|
||||
/// <summary>
|
||||
/// An Extism Plugin
|
||||
/// Create a new context.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct ExtismPlugin { }
|
||||
/// <returns>A pointer to the newly created context.</returns>
|
||||
[DllImport("extism")]
|
||||
public static extern IntPtr extism_context_new();
|
||||
|
||||
/// <summary>
|
||||
/// Host function signature
|
||||
/// Remove a context from the registry and free associated memory.
|
||||
/// </summary>
|
||||
/// <param name="plugin"></param>
|
||||
/// <param name="inputs"></param>
|
||||
/// <param name="n_inputs"></param>
|
||||
/// <param name="outputs"></param>
|
||||
/// <param name="n_outputs"></param>
|
||||
/// <param name="data"></param>
|
||||
unsafe internal delegate void InternalExtismFunction(nint plugin, ExtismVal* inputs, uint n_inputs, ExtismVal* outputs, uint n_outputs, IntPtr data);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a pointer to the memory of the currently running plugin.
|
||||
/// NOTE: this should only be called from host functions.
|
||||
/// </summary>
|
||||
/// <param name="plugin"></param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism", EntryPoint = "extism_current_plugin_memory")]
|
||||
internal static extern IntPtr extism_current_plugin_memory(nint plugin);
|
||||
|
||||
/// <summary>
|
||||
/// Allocate a memory block in the currently running plugin
|
||||
/// </summary>
|
||||
/// <param name="plugin"></param>
|
||||
/// <param name="n"></param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism", EntryPoint = "extism_current_plugin_memory_alloc")]
|
||||
internal static extern IntPtr extism_current_plugin_memory_alloc(nint plugin, long n);
|
||||
|
||||
/// <summary>
|
||||
/// Get the length of an allocated block.
|
||||
/// NOTE: this should only be called from host functions.
|
||||
/// </summary>
|
||||
/// <param name="plugin"></param>
|
||||
/// <param name="n"></param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism", EntryPoint = "extism_current_plugin_memory_length")]
|
||||
internal static extern long extism_current_plugin_memory_length(nint plugin, long n);
|
||||
|
||||
/// <summary>
|
||||
/// Get the length of an allocated block.
|
||||
/// NOTE: this should only be called from host functions.
|
||||
/// </summary>
|
||||
/// <param name="plugin"></param>
|
||||
/// <param name="ptr"></param>
|
||||
[DllImport("extism", EntryPoint = "extism_current_plugin_memory_free")]
|
||||
internal static extern void extism_current_plugin_memory_free(nint plugin, IntPtr ptr);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new host function.
|
||||
/// </summary>
|
||||
/// <param name="name">function name, this should be valid UTF-8</param>
|
||||
/// <param name="inputs">argument types</param>
|
||||
/// <param name="nInputs">number of argument types</param>
|
||||
/// <param name="outputs">return types</param>
|
||||
/// <param name="nOutputs">number of return types</param>
|
||||
/// <param name="func">the function to call</param>
|
||||
/// <param name="userData">a pointer that will be passed to the function when it's called this value should live as long as the function exists</param>
|
||||
/// <param name="freeUserData">a callback to release the `user_data` value when the resulting `ExtismFunction` is freed.</param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism", EntryPoint = "extism_function_new")]
|
||||
unsafe internal static extern IntPtr extism_function_new(string name, ExtismValType* inputs, long nInputs, ExtismValType* outputs, long nOutputs, InternalExtismFunction func, IntPtr userData, IntPtr freeUserData);
|
||||
|
||||
/// <summary>
|
||||
/// Set the namespace of an <see cref="ExtismFunction"/>
|
||||
/// </summary>
|
||||
/// <param name="ptr"></param>
|
||||
/// <param name="namespace"></param>
|
||||
[DllImport("extism", EntryPoint = "extism_function_set_namespace")]
|
||||
internal static extern void extism_function_set_namespace(IntPtr ptr, string @namespace);
|
||||
|
||||
/// <summary>
|
||||
/// Free an <see cref="ExtismFunction"/>
|
||||
/// </summary>
|
||||
/// <param name="ptr"></param>
|
||||
[DllImport("extism", EntryPoint = "extism_function_free")]
|
||||
internal static extern void extism_function_free(IntPtr ptr);
|
||||
/// <param name="context"></param>
|
||||
[DllImport("extism")]
|
||||
public static extern void extism_context_free(IntPtr context);
|
||||
|
||||
/// <summary>
|
||||
/// Load a WASM plugin.
|
||||
/// </summary>
|
||||
/// <param name="context">Pointer to the context the plugin will be associated with.</param>
|
||||
/// <param name="wasm">A WASM module (wat or wasm) or a JSON encoded manifest.</param>
|
||||
/// <param name="wasmSize">The length of the `wasm` parameter.</param>
|
||||
/// <param name="functions">Array of host function pointers.</param>
|
||||
/// <param name="nFunctions">Number of host functions.</param>
|
||||
/// <param name="withWasi">Enables/disables WASI.</param>
|
||||
/// <param name="errmsg"></param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
unsafe internal static extern ExtismPlugin* extism_plugin_new(byte* wasm, ulong wasmSize, IntPtr* functions, ulong nFunctions, [MarshalAs(UnmanagedType.I1)] bool withWasi, out char** errmsg);
|
||||
unsafe public static extern IntPtr extism_plugin_new(IntPtr context, byte* wasm, int wasmSize, bool withWasi);
|
||||
|
||||
/// <summary>
|
||||
/// Frees a plugin error message.
|
||||
/// Update a plugin, keeping the existing ID.
|
||||
/// Similar to <see cref="extism_plugin_new"/> but takes an `plugin` argument to specify which plugin to update.
|
||||
/// Memory for this plugin will be reset upon update.
|
||||
/// </summary>
|
||||
/// <param name="errorMessage"></param>
|
||||
/// <param name="context">Pointer to the context the plugin is associated with.</param>
|
||||
/// <param name="plugin">Pointer to the plugin you want to update.</param>
|
||||
/// <param name="wasm">A WASM module (wat or wasm) or a JSON encoded manifest.</param>
|
||||
/// <param name="wasmLength">The length of the `wasm` parameter.</param>
|
||||
/// <param name="withWasi">Enables/disables WASI.</param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
unsafe internal static extern void extism_plugin_new_error_free(IntPtr errorMessage);
|
||||
unsafe public static extern bool extism_plugin_update(IntPtr context, IntPtr plugin, byte* wasm, int wasmLength, bool withWasi);
|
||||
|
||||
/// <summary>
|
||||
/// Remove a plugin from the registry and free associated memory.
|
||||
/// </summary>
|
||||
/// <param name="context">Pointer to the context the plugin is associated with.</param>
|
||||
/// <param name="plugin">Pointer to the plugin you want to free.</param>
|
||||
[DllImport("extism")]
|
||||
unsafe internal static extern void extism_plugin_free(ExtismPlugin* plugin);
|
||||
public static extern void extism_plugin_free(IntPtr context, IntPtr plugin);
|
||||
|
||||
/// <summary>
|
||||
/// Remove all plugins from the registry.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
[DllImport("extism")]
|
||||
public static extern void extism_context_reset(IntPtr context);
|
||||
|
||||
/// <summary>
|
||||
/// Update plugin config values, this will merge with the existing values.
|
||||
/// </summary>
|
||||
/// <param name="context">Pointer to the context the plugin is associated with.</param>
|
||||
/// <param name="plugin">Pointer to the plugin you want to update the configurations for.</param>
|
||||
/// <param name="json">The configuration JSON encoded in UTF8.</param>
|
||||
/// <param name="jsonLength">The length of the `json` parameter.</param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
unsafe internal static extern bool extism_plugin_config(ExtismPlugin* plugin, byte* json, int jsonLength);
|
||||
unsafe public static extern bool extism_plugin_config(IntPtr context, IntPtr plugin, byte* json, int jsonLength);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if funcName exists.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="plugin"></param>
|
||||
/// <param name="funcName"></param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
unsafe internal static extern bool extism_plugin_function_exists(ExtismPlugin* plugin, string funcName);
|
||||
public static extern bool extism_plugin_function_exists(IntPtr context, IntPtr plugin, string funcName);
|
||||
|
||||
/// <summary>
|
||||
/// Call a function.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="plugin"></param>
|
||||
/// <param name="funcName">The function to call.</param>
|
||||
/// <param name="data">Input data.</param>
|
||||
/// <param name="dataLen">The length of the `data` parameter.</param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
unsafe internal static extern int extism_plugin_call(ExtismPlugin* plugin, string funcName, byte* data, int dataLen);
|
||||
unsafe public static extern int extism_plugin_call(IntPtr context, IntPtr plugin, string funcName, byte* data, int dataLen);
|
||||
|
||||
/// <summary>
|
||||
/// Get the error associated with a Plugin
|
||||
/// Get the error associated with a Context or Plugin, if plugin is -1 then the context error will be returned.
|
||||
/// </summary>
|
||||
/// <param name="plugin">A plugin pointer</param>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="plugin">A plugin pointer, or -1 for the context error.</param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
unsafe internal static extern IntPtr extism_plugin_error(ExtismPlugin* plugin);
|
||||
public static extern IntPtr extism_error(IntPtr context, nint plugin);
|
||||
|
||||
/// <summary>
|
||||
/// Get the length of a plugin's output data.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="plugin"></param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
unsafe internal static extern long extism_plugin_output_length(ExtismPlugin* plugin);
|
||||
public static extern long extism_plugin_output_length(IntPtr context, IntPtr plugin);
|
||||
|
||||
/// <summary>
|
||||
/// Get the plugin's output data.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="plugin"></param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
unsafe internal static extern IntPtr extism_plugin_output_data(ExtismPlugin* plugin);
|
||||
public static extern IntPtr extism_plugin_output_data(IntPtr context, IntPtr plugin);
|
||||
|
||||
/// <summary>
|
||||
/// Set log file and level.
|
||||
@@ -268,43 +128,43 @@ internal static class LibExtism
|
||||
/// <param name="logLevel"></param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
internal static extern bool extism_log_file(string filename, string logLevel);
|
||||
public static extern bool extism_log_file(string filename, string logLevel);
|
||||
|
||||
/// <summary>
|
||||
/// Get Extism Runtime version.
|
||||
/// Get the Extism version string.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
internal static extern IntPtr extism_version();
|
||||
[DllImport("extism", EntryPoint = "extism_version")]
|
||||
public static extern IntPtr extism_version();
|
||||
|
||||
/// <summary>
|
||||
/// Extism Log Levels
|
||||
/// </summary>
|
||||
internal static class LogLevels
|
||||
public static class LogLevels
|
||||
{
|
||||
/// <summary>
|
||||
/// Designates very serious errors.
|
||||
/// </summary>
|
||||
internal const string Error = "Error";
|
||||
public const string Error = "Error";
|
||||
|
||||
/// <summary>
|
||||
/// Designates hazardous situations.
|
||||
/// </summary>
|
||||
internal const string Warn = "Warn";
|
||||
public const string Warn = "Warn";
|
||||
|
||||
/// <summary>
|
||||
/// Designates useful information.
|
||||
/// </summary>
|
||||
internal const string Info = "Info";
|
||||
public const string Info = "Info";
|
||||
|
||||
/// <summary>
|
||||
/// Designates lower priority information.
|
||||
/// </summary>
|
||||
internal const string Debug = "Debug";
|
||||
public const string Debug = "Debug";
|
||||
|
||||
/// <summary>
|
||||
/// Designates very low priority, often extremely verbose, information.
|
||||
/// </summary>
|
||||
internal const string Trace = "Trace";
|
||||
public const string Trace = "Trace";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Extism.Sdk
|
||||
{
|
||||
/// <summary>
|
||||
/// The manifest is a description of your plugin and some of the runtime constraints to apply to it.
|
||||
/// You can think of it as a blueprint to build your plugin.
|
||||
/// </summary>
|
||||
public class Manifest
|
||||
{
|
||||
/// <summary>
|
||||
/// Create an empty manifest.
|
||||
/// </summary>
|
||||
public Manifest()
|
||||
{
|
||||
AllowedPaths = new Dictionary<string, string>
|
||||
{
|
||||
{ "/usr/plugins/1/data", "/data" }, // src, dest
|
||||
{ "d:/plugins/1/data", "/data" } // src, dest
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a manifest from one or more Wasm sources.
|
||||
/// </summary>
|
||||
/// <param name="sources"></param>
|
||||
public Manifest(params WasmSource[] sources)
|
||||
{
|
||||
Sources.AddRange(sources);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// List of Wasm sources. See <see cref="PathWasmSource"/> and <see cref="ByteArrayWasmSource"/>.
|
||||
/// </summary>
|
||||
[JsonPropertyName("wasm")]
|
||||
public List<WasmSource> Sources { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Configures memory for the Wasm runtime.
|
||||
/// Memory is described in units of pages (64KB) and represent contiguous chunks of addressable memory.
|
||||
/// </summary>
|
||||
[JsonPropertyName("memory")]
|
||||
public MemoryOptions? MemoryOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of host names the plugins can access. Example:
|
||||
/// <code>
|
||||
/// AllowedHosts = new List<string> {
|
||||
/// "www.example.com",
|
||||
/// "api.*.com",
|
||||
/// "example.*",
|
||||
/// }
|
||||
/// </code>
|
||||
/// </summary>
|
||||
[JsonPropertyName("allowed_hosts")]
|
||||
public List<string> AllowedHosts { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// List of directories that can be accessed by the plugins. Examples:
|
||||
/// <code>
|
||||
/// AllowedPaths = new Dictionary<string, string>
|
||||
/// {
|
||||
/// { "/usr/plugins/1/data", "/data" }, // src, dest
|
||||
/// { "d:/plugins/1/data", "/data" } // src, dest
|
||||
/// };
|
||||
/// </code>
|
||||
/// </summary>
|
||||
[JsonPropertyName("allowed_paths")]
|
||||
public Dictionary<string, string> AllowedPaths { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Configurations available to the plugins. Examples:
|
||||
/// <code>
|
||||
/// Config = new Dictionary<string, string>
|
||||
/// {
|
||||
/// { "userId", "55" }, // key, value
|
||||
/// { "mySecret", "super-secret-key" } // key, value
|
||||
/// };
|
||||
/// </code>
|
||||
/// </summary>
|
||||
[JsonPropertyName("config")]
|
||||
public Dictionary<string, string> Config { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures memory for the Wasm runtime.
|
||||
/// Memory is described in units of pages (64KB) and represent contiguous chunks of addressable memory.
|
||||
/// </summary>
|
||||
public class MemoryOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Max number of pages. Each page is 64KB.
|
||||
/// </summary>
|
||||
[JsonPropertyName("max")]
|
||||
public int MaxPages { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A named Wasm source.
|
||||
/// </summary>
|
||||
public abstract class WasmSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Logical name of the Wasm source
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash of the WASM source
|
||||
/// </summary>
|
||||
[JsonPropertyName("hash")]
|
||||
public string? Hash { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wasm Source represented by a file referenced by a path.
|
||||
/// </summary>
|
||||
public class PathWasmSource : WasmSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="path">path to wasm plugin.</param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="hash"></param>
|
||||
public PathWasmSource(string path, string? name = null, string? hash = null)
|
||||
{
|
||||
Path = System.IO.Path.GetFullPath(path);
|
||||
Name = name ?? System.IO.Path.GetFileNameWithoutExtension(path);
|
||||
Hash = hash;
|
||||
|
||||
if (Hash is null)
|
||||
{
|
||||
using var file = File.OpenRead(Path);
|
||||
Hash = Helpers.ComputeSha256Hash(file);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Path to wasm plugin.
|
||||
/// </summary>
|
||||
[JsonPropertyName("path")]
|
||||
public string Path { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wasm Source represented by raw bytes.
|
||||
/// </summary>
|
||||
public class ByteArrayWasmSource : WasmSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="data">the byte array representing the Wasm code</param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="hash"></param>
|
||||
public ByteArrayWasmSource(byte[] data, string? name, string? hash = null)
|
||||
{
|
||||
Data = data;
|
||||
Name = name;
|
||||
Hash = hash;
|
||||
|
||||
if (Hash is null)
|
||||
{
|
||||
using var memory = new MemoryStream(data);
|
||||
Hash = Helpers.ComputeSha256Hash(memory);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The byte array representing the Wasm code
|
||||
/// </summary>
|
||||
[JsonPropertyName("data")]
|
||||
[JsonConverter(typeof(Base64EncodedStringConverter))]
|
||||
public byte[] Data { get; }
|
||||
}
|
||||
|
||||
static class Helpers
|
||||
{
|
||||
public static string ComputeSha256Hash(Stream stream)
|
||||
{
|
||||
using (SHA256 sha256 = SHA256.Create())
|
||||
{
|
||||
byte[] hashBytes = sha256.ComputeHash(stream);
|
||||
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Base64EncodedStringConverter : JsonConverter<string>
|
||||
{
|
||||
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
|
||||
Encoding.UTF8.GetString(reader.GetBytesFromBase64());
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) =>
|
||||
writer.WriteBase64StringValue(Encoding.UTF8.GetBytes(value));
|
||||
}
|
||||
|
||||
class WasmSourceConverter : JsonConverter<WasmSource>
|
||||
{
|
||||
public override WasmSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, WasmSource value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value is PathWasmSource path)
|
||||
JsonSerializer.Serialize(writer, path, typeof(PathWasmSource), options);
|
||||
else if (value is ByteArrayWasmSource bytes)
|
||||
JsonSerializer.Serialize(writer, bytes, typeof(ByteArrayWasmSource), options);
|
||||
else
|
||||
throw new ArgumentOutOfRangeException(nameof(value), "Unknown Wasm Source");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,92 +1,44 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Extism.Sdk.Native;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a WASM Extism plugin.
|
||||
/// </summary>
|
||||
public unsafe class Plugin : IDisposable
|
||||
public class Plugin : IDisposable
|
||||
{
|
||||
private const int DisposedMarker = 1;
|
||||
|
||||
private readonly HostFunction[] _functions;
|
||||
private readonly Context _context;
|
||||
private int _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Native pointer to the Extism Plugin.
|
||||
/// </summary>
|
||||
internal LibExtism.ExtismPlugin* NativeHandle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a plugin from a Manifest.
|
||||
/// </summary>
|
||||
/// <param name="manifest"></param>
|
||||
/// <param name="functions"></param>
|
||||
/// <param name="withWasi"></param>
|
||||
public Plugin(Manifest manifest, HostFunction[] functions, bool withWasi)
|
||||
internal Plugin(Context context, IntPtr handle)
|
||||
{
|
||||
_functions = functions;
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
};
|
||||
|
||||
options.Converters.Add(new WasmSourceConverter());
|
||||
var json = JsonSerializer.Serialize(manifest, options);
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
|
||||
var functionHandles = functions.Select(f => f.NativeHandle).ToArray();
|
||||
fixed (byte* wasmPtr = bytes)
|
||||
fixed (IntPtr* functionsPtr = functionHandles)
|
||||
{
|
||||
NativeHandle = Initialize(wasmPtr, bytes.Length, functions, withWasi, functionsPtr);
|
||||
}
|
||||
_context = context;
|
||||
NativeHandle = handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create and load a plugin from a byte array.
|
||||
/// A pointer to the native Plugin struct.
|
||||
/// </summary>
|
||||
/// <param name="wasm">A WASM module (wat or wasm) or a JSON encoded manifest.</param>
|
||||
/// <param name="functions">List of host functions expected by the plugin.</param>
|
||||
internal IntPtr NativeHandle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Update a plugin, keeping the existing ID.
|
||||
/// </summary>
|
||||
/// <param name="wasm">The plugin WASM bytes.</param>
|
||||
/// <param name="withWasi">Enable/Disable WASI.</param>
|
||||
public Plugin(ReadOnlySpan<byte> wasm, HostFunction[] functions, bool withWasi)
|
||||
unsafe public bool Update(ReadOnlySpan<byte> wasm, bool withWasi)
|
||||
{
|
||||
_functions = functions;
|
||||
CheckNotDisposed();
|
||||
|
||||
var functionHandles = functions.Select(f => f.NativeHandle).ToArray();
|
||||
fixed (byte* wasmPtr = wasm)
|
||||
fixed (IntPtr* functionsPtr = functionHandles)
|
||||
{
|
||||
NativeHandle = Initialize(wasmPtr, wasm.Length, functions, withWasi, functionsPtr);
|
||||
return LibExtism.extism_plugin_update(_context.NativeHandle, NativeHandle, wasmPtr, wasm.Length, withWasi);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe LibExtism.ExtismPlugin* Initialize(byte* wasmPtr, int wasmLength, HostFunction[] functions, bool withWasi, IntPtr* functionsPtr)
|
||||
{
|
||||
char** errorMsgPtr;
|
||||
|
||||
var handle = LibExtism.extism_plugin_new(wasmPtr, (ulong)wasmLength, functionsPtr, (ulong)functions.Length, withWasi, out errorMsgPtr);
|
||||
if (handle == null)
|
||||
{
|
||||
var msg = "Unable to create plugin";
|
||||
|
||||
if (errorMsgPtr is not null)
|
||||
{
|
||||
msg = Marshal.PtrToStringAnsi(new IntPtr(errorMsgPtr));
|
||||
}
|
||||
|
||||
throw new ExtismException(msg);
|
||||
}
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update plugin config values, this will merge with the existing values.
|
||||
/// </summary>
|
||||
@@ -97,18 +49,18 @@ public unsafe class Plugin : IDisposable
|
||||
|
||||
fixed (byte* jsonPtr = json)
|
||||
{
|
||||
return LibExtism.extism_plugin_config(NativeHandle, jsonPtr, json.Length);
|
||||
return LibExtism.extism_plugin_config(_context.NativeHandle, NativeHandle, jsonPtr, json.Length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a specific function exists in the current plugin.
|
||||
/// </summary>
|
||||
unsafe public bool FunctionExists(string name)
|
||||
public bool FunctionExists(string name)
|
||||
{
|
||||
CheckNotDisposed();
|
||||
|
||||
return LibExtism.extism_plugin_function_exists(NativeHandle, name);
|
||||
return LibExtism.extism_plugin_function_exists(_context.NativeHandle, NativeHandle, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -126,20 +78,14 @@ public unsafe class Plugin : IDisposable
|
||||
|
||||
fixed (byte* dataPtr = data)
|
||||
{
|
||||
int response = LibExtism.extism_plugin_call(NativeHandle, functionName, dataPtr, data.Length);
|
||||
if (response == 0)
|
||||
{
|
||||
int response = LibExtism.extism_plugin_call(_context.NativeHandle, NativeHandle, functionName, dataPtr, data.Length);
|
||||
if (response == 0) {
|
||||
return OutputData();
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
var errorMsg = GetError();
|
||||
if (errorMsg != null)
|
||||
{
|
||||
if (errorMsg != null) {
|
||||
throw new ExtismException(errorMsg);
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
throw new ExtismException("Call to Extism failed");
|
||||
}
|
||||
}
|
||||
@@ -150,11 +96,11 @@ public unsafe class Plugin : IDisposable
|
||||
/// Get the length of a plugin's output data.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
unsafe internal int OutputLength()
|
||||
internal int OutputLength()
|
||||
{
|
||||
CheckNotDisposed();
|
||||
|
||||
return (int)LibExtism.extism_plugin_output_length(NativeHandle);
|
||||
return (int)LibExtism.extism_plugin_output_length(_context.NativeHandle, NativeHandle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -168,7 +114,7 @@ public unsafe class Plugin : IDisposable
|
||||
|
||||
unsafe
|
||||
{
|
||||
var ptr = LibExtism.extism_plugin_output_data(NativeHandle).ToPointer();
|
||||
var ptr = LibExtism.extism_plugin_output_data(_context.NativeHandle, NativeHandle).ToPointer();
|
||||
return new Span<byte>(ptr, length);
|
||||
}
|
||||
}
|
||||
@@ -177,11 +123,11 @@ public unsafe class Plugin : IDisposable
|
||||
/// Get the error associated with the current plugin.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
unsafe internal string? GetError()
|
||||
internal string? GetError()
|
||||
{
|
||||
CheckNotDisposed();
|
||||
|
||||
var result = LibExtism.extism_plugin_error(NativeHandle);
|
||||
var result = LibExtism.extism_error(_context.NativeHandle, NativeHandle);
|
||||
return Marshal.PtrToStringUTF8(result);
|
||||
}
|
||||
|
||||
@@ -222,7 +168,7 @@ public unsafe class Plugin : IDisposable
|
||||
/// <summary>
|
||||
/// Frees all resources held by this Plugin.
|
||||
/// </summary>
|
||||
unsafe protected virtual void Dispose(bool disposing)
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
@@ -230,7 +176,7 @@ public unsafe class Plugin : IDisposable
|
||||
}
|
||||
|
||||
// Free up unmanaged resources
|
||||
LibExtism.extism_plugin_free(NativeHandle);
|
||||
LibExtism.extism_plugin_free(_context.NativeHandle, NativeHandle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -240,15 +186,4 @@ public unsafe class Plugin : IDisposable
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get Extism Runtime version.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static string ExtismVersion()
|
||||
{
|
||||
var version = LibExtism.extism_version();
|
||||
return Marshal.PtrToStringAnsi(version);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using Extism.Sdk.Native;
|
||||
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
using Xunit;
|
||||
@@ -13,46 +12,13 @@ public class BasicTests
|
||||
[Fact]
|
||||
public void CountHelloWorldVowels()
|
||||
{
|
||||
using var context = new Context();
|
||||
|
||||
var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
|
||||
var wasm = File.ReadAllBytes(Path.Combine(binDirectory, "code.wasm"));
|
||||
using var plugin = new Plugin(wasm, Array.Empty<HostFunction>(), withWasi: true);
|
||||
using var plugin = context.CreatePlugin(wasm, withWasi: true);
|
||||
|
||||
var response = plugin.CallFunction("count_vowels", Encoding.UTF8.GetBytes("Hello World"));
|
||||
Assert.Equal("{\"count\": 3}", Encoding.UTF8.GetString(response));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CountVowelsHostFunctions()
|
||||
{
|
||||
var userData = Marshal.StringToHGlobalAnsi("Hello again!");
|
||||
|
||||
using var helloWorld = new HostFunction(
|
||||
"hello_world",
|
||||
"env",
|
||||
new[] { ExtismValType.I64 },
|
||||
new[] { ExtismValType.I64 },
|
||||
userData,
|
||||
HelloWorld);
|
||||
|
||||
var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
|
||||
var wasm = File.ReadAllBytes(Path.Combine(binDirectory, "code-functions.wasm"));
|
||||
using var plugin = new Plugin(wasm, new[] { helloWorld }, withWasi: true);
|
||||
|
||||
var response = plugin.CallFunction("count_vowels", Encoding.UTF8.GetBytes("Hello World"));
|
||||
Assert.Equal("{\"count\": 3}", Encoding.UTF8.GetString(response));
|
||||
|
||||
void HelloWorld(CurrentPlugin plugin, Span<ExtismVal> inputs, Span<ExtismVal> outputs, nint data)
|
||||
{
|
||||
Console.WriteLine("Hello from .NET!");
|
||||
|
||||
var text = Marshal.PtrToStringAnsi(data);
|
||||
Console.WriteLine(text);
|
||||
|
||||
var input = plugin.ReadString(new nint(inputs[0].v.i64));
|
||||
Console.WriteLine($"Input: {input}");
|
||||
|
||||
var output = new string(input); // clone the string
|
||||
outputs[0].v.i64 = plugin.WriteString(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,9 +25,6 @@
|
||||
<None Include="..\..\..\wasm\code.wasm" Link="code.wasm">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Include="..\..\..\wasm\code-functions.wasm" Link="code-functions.wasm">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -35,8 +32,4 @@
|
||||
<ProjectReference Include="..\..\src\Extism.Sdk\Extism.Sdk.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Extism.runtime.win-x64" Version="0.7.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
17
dune-project
17
dune-project
@@ -21,15 +21,13 @@
|
||||
(description "Bindings to Extism, the universal plugin system")
|
||||
(depends
|
||||
(ocaml (>= 4.14.1))
|
||||
dune
|
||||
(ctypes (>= 0.18.0))
|
||||
(dune (>= 3.2))
|
||||
(ctypes-foreign (>= 0.18.0))
|
||||
(bigstringaf (>= 0.9.0))
|
||||
(ppx_yojson_conv (>= v0.15.0))
|
||||
(extism-manifest (= :version))
|
||||
(ppx_inline_test (>= v0.15.0))
|
||||
(ppx_yojson_conv (>= 0.15.0))
|
||||
extism-manifest
|
||||
(ppx_inline_test (>= 0.15.0))
|
||||
(cmdliner (>= 1.1.1))
|
||||
(uuidm (>= 0.9.0))
|
||||
)
|
||||
(tags
|
||||
(topics wasm plugin)))
|
||||
@@ -37,12 +35,11 @@
|
||||
(package
|
||||
(name extism-manifest)
|
||||
(synopsis "Extism manifest bindings")
|
||||
(description "Bindings to the Extism manifest format")
|
||||
(description "Bindings to Extism, the universal plugin system")
|
||||
(depends
|
||||
(ocaml (>= 4.14.1))
|
||||
dune
|
||||
(ppx_yojson_conv (>= v0.15.0))
|
||||
(ppx_inline_test (>= v0.15.0))
|
||||
(dune (>= 3.2))
|
||||
(ppx_yojson_conv (>= 0.15.0))
|
||||
(base64 (>= 3.5.0))
|
||||
)
|
||||
(tags
|
||||
|
||||
@@ -5,7 +5,7 @@ prepare:
|
||||
mix compile
|
||||
|
||||
test: prepare
|
||||
mix test -v
|
||||
mix test
|
||||
|
||||
clean:
|
||||
mix clean
|
||||
|
||||
@@ -23,9 +23,12 @@ end
|
||||
### Example
|
||||
|
||||
```elixir
|
||||
# Create a context for which plugins can be allocated and cleaned
|
||||
ctx = Extism.Context.new()
|
||||
|
||||
# point to some wasm code, this is the count_vowels example that ships with extism
|
||||
manifest = %{ wasm: [ %{ path: "/Users/ben/code/extism/wasm/code.wasm" } ]}
|
||||
{:ok, plugin} = Extism.Plugin.new(manifest, false)
|
||||
{:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
|
||||
# {:ok,
|
||||
# %Extism.Plugin{
|
||||
# resource: 0,
|
||||
@@ -35,20 +38,36 @@ manifest = %{ wasm: [ %{ path: "/Users/ben/code/extism/wasm/code.wasm" } ]}
|
||||
# {:ok, "{\"count\": 4}"}
|
||||
{:ok, result} = JSON.decode(output)
|
||||
# {:ok, %{"count" => 4}}
|
||||
|
||||
# free up the context and any plugins we allocated
|
||||
Extism.Context.free(ctx)
|
||||
```
|
||||
|
||||
### Modules
|
||||
|
||||
The primary modules you should learn is:
|
||||
The two primary modules you should learn are:
|
||||
|
||||
* [Extism.Context](Extism.Context.html)
|
||||
* [Extism.Plugin](Extism.Plugin.html)
|
||||
|
||||
#### Context
|
||||
|
||||
The [Context](Extism.Context.html) can be thought of as a session. You need a context to interact with the Extism runtime. The context holds your plugins and when you free the context, it frees your plugins. It's important to free up your context and plugins when you are done with them.
|
||||
|
||||
```elixir
|
||||
ctx = Extism.Context.new()
|
||||
# frees all the plugins
|
||||
Extism.Context.reset(ctx)
|
||||
# frees the context and all its plugins
|
||||
Extism.Context.free(ctx)
|
||||
```
|
||||
|
||||
#### Plugin
|
||||
|
||||
The [Plugin](Extism.Plugin.html) represents an instance of your WASM program from the given manifest.
|
||||
The key method to know here is [Extism.Plugin#call](Extism.Plugin.html#call/3) which takes a function name to invoke and some input data, and returns the results from the plugin.
|
||||
|
||||
```elixir
|
||||
{:ok, plugin} = Extism.Plugin.new(manifest, false)
|
||||
{:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
|
||||
{:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
|
||||
```
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
defmodule Extism.CancelHandle do
|
||||
@moduledoc """
|
||||
A CancelHandle is a handle generated by a plugin that allows it to be cancelled from another
|
||||
thread while running.
|
||||
"""
|
||||
defstruct [
|
||||
# The actual NIF Resource
|
||||
handle: nil
|
||||
]
|
||||
|
||||
def wrap_resource(handle) do
|
||||
%__MODULE__{
|
||||
handle: handle
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Cancel plugin execution
|
||||
"""
|
||||
def cancel(handle) do
|
||||
Extism.Native.plugin_cancel(handle.handle)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Extim.CancelHandle do
|
||||
import Inspect.Algebra
|
||||
|
||||
def inspect(dict, opts) do
|
||||
concat(["#Extism.CancelHandle<", to_doc(dict.handle, opts), ">"])
|
||||
end
|
||||
end
|
||||
64
elixir/lib/extism/context.ex
Normal file
64
elixir/lib/extism/context.ex
Normal file
@@ -0,0 +1,64 @@
|
||||
defmodule Extism.Context do
|
||||
@moduledoc """
|
||||
A Context is needed to create plugins. The Context is where your plugins
|
||||
live. Freeing the context frees all of the plugins in its scope.
|
||||
"""
|
||||
|
||||
defstruct [
|
||||
# The actual NIF Resource. A pointer in this case
|
||||
ptr: nil
|
||||
]
|
||||
|
||||
def wrap_resource(ptr) do
|
||||
%__MODULE__{
|
||||
ptr: ptr
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a new context.
|
||||
"""
|
||||
def new() do
|
||||
ptr = Extism.Native.context_new()
|
||||
Extism.Context.wrap_resource(ptr)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Resets the context. This has the effect of freeing all the plugins created so far.
|
||||
"""
|
||||
def reset(ctx) do
|
||||
Extism.Native.context_reset(ctx.ptr)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Frees the context from memory and all of its plugins.
|
||||
"""
|
||||
def free(ctx) do
|
||||
Extism.Native.context_free(ctx.ptr)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create a new plugin from a WASM module or manifest
|
||||
|
||||
## Examples:
|
||||
|
||||
iex> ctx = Extism.Context.new()
|
||||
iex> manifest = %{ wasm: [ %{ path: "/Users/ben/code/extism/wasm/code.wasm" } ]}
|
||||
iex> {:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
|
||||
|
||||
## Parameters
|
||||
|
||||
- ctx: The Context to manage this plugin
|
||||
- manifest: The String or Map of the WASM module or [manifest](https://extism.org/docs/concepts/manifest)
|
||||
- wasi: A bool you set to true if you want WASI support
|
||||
|
||||
"""
|
||||
def new_plugin(ctx, manifest, wasi \\ false) do
|
||||
{:ok, manifest_payload} = JSON.encode(manifest)
|
||||
|
||||
case Extism.Native.plugin_new_with_manifest(ctx.ptr, manifest_payload, wasi) do
|
||||
{:error, err} -> {:error, err}
|
||||
res -> {:ok, Extism.Plugin.wrap_resource(ctx, res)}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -7,13 +7,15 @@ defmodule Extism.Native do
|
||||
otp_app: :extism,
|
||||
crate: :extism_nif
|
||||
|
||||
def plugin_new_with_manifest(_manifest, _wasi), do: error()
|
||||
def plugin_call(_plugin, _name, _input), do: error()
|
||||
def plugin_has_function(_plugin, _function_name), do: error()
|
||||
def plugin_free(_plugin), do: error()
|
||||
def context_new(), do: error()
|
||||
def context_reset(_ctx), do: error()
|
||||
def context_free(_ctx), do: error()
|
||||
def plugin_new_with_manifest(_ctx, _manifest, _wasi), do: error()
|
||||
def plugin_call(_ctx, _plugin_id, _name, _input), do: error()
|
||||
def plugin_update_manifest(_ctx, _plugin_id, _manifest, _wasi), do: error()
|
||||
def plugin_has_function(_ctx, _plugin_id, _function_name), do: error()
|
||||
def plugin_free(_ctx, _plugin_id), do: error()
|
||||
def set_log_file(_filename, _level), do: error()
|
||||
def plugin_cancel_handle(_plugin), do: error()
|
||||
def plugin_cancel(_handle), do: error()
|
||||
|
||||
defp error, do: :erlang.nif_error(:nif_not_loaded)
|
||||
end
|
||||
|
||||
@@ -3,34 +3,24 @@ defmodule Extism.Plugin do
|
||||
A Plugin represents an instance of your WASM program from the given manifest.
|
||||
"""
|
||||
defstruct [
|
||||
# The actual NIF Resource
|
||||
plugin: nil,
|
||||
# The actual NIF Resource. PluginIndex and the context
|
||||
plugin_id: nil,
|
||||
ctx: nil
|
||||
]
|
||||
|
||||
def wrap_resource(plugin) do
|
||||
def wrap_resource(ctx, plugin_id) do
|
||||
%__MODULE__{
|
||||
plugin: plugin
|
||||
ctx: ctx,
|
||||
plugin_id: plugin_id
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a new plugin
|
||||
"""
|
||||
def new(manifest, wasi \\ false) do
|
||||
{:ok, manifest_payload} = JSON.encode(manifest)
|
||||
|
||||
case Extism.Native.plugin_new_with_manifest(manifest_payload, wasi) do
|
||||
{:error, err} -> {:error, err}
|
||||
res -> {:ok, Extism.Plugin.wrap_resource(res)}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Call a plugin's function by name
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, plugin} = Extism.Plugin.new(manifest, false)
|
||||
iex> {:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
|
||||
iex> {:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
|
||||
# {:ok, "{\"count\": 4}"}
|
||||
|
||||
@@ -46,24 +36,49 @@ defmodule Extism.Plugin do
|
||||
|
||||
"""
|
||||
def call(plugin, name, input) do
|
||||
case Extism.Native.plugin_call(plugin.plugin, name, input) do
|
||||
case Extism.Native.plugin_call(plugin.ctx.ptr, plugin.plugin_id, name, input) do
|
||||
{:error, err} -> {:error, err}
|
||||
res -> {:ok, res}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates the manifest of the given plugin
|
||||
|
||||
## Parameters
|
||||
|
||||
- ctx: The Context to manage this plugin
|
||||
- manifest: The String or Map of the WASM module or [manifest](https://extism.org/docs/concepts/manifest)
|
||||
- wasi: A bool you set to true if you want WASI support
|
||||
|
||||
|
||||
"""
|
||||
def update(plugin, manifest, wasi) when is_map(manifest) do
|
||||
{:ok, manifest_payload} = JSON.encode(manifest)
|
||||
|
||||
case Extism.Native.plugin_update_manifest(
|
||||
plugin.ctx.ptr,
|
||||
plugin.plugin_id,
|
||||
manifest_payload,
|
||||
wasi
|
||||
) do
|
||||
{:error, err} -> {:error, err}
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Frees the plugin
|
||||
"""
|
||||
def free(plugin) do
|
||||
Extism.Native.plugin_free(plugin.plugin)
|
||||
Extism.Native.plugin_free(plugin.ctx.ptr, plugin.plugin_id)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the given plugin responds to the given function name
|
||||
"""
|
||||
def has_function(plugin, function_name) do
|
||||
Extism.Native.plugin_has_function(plugin.plugin, function_name)
|
||||
Extism.Native.plugin_has_function(plugin.ctx.ptr, plugin.plugin_id, function_name)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -71,6 +86,6 @@ defimpl Inspect, for: Extim.Plugin do
|
||||
import Inspect.Algebra
|
||||
|
||||
def inspect(dict, opts) do
|
||||
concat(["#Extism.Plugin<", to_doc(dict.plugin, opts), ">"])
|
||||
concat(["#Extism.Plugin<", to_doc(dict.plugin_id, opts), ">"])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,7 +4,7 @@ defmodule Extism.MixProject do
|
||||
def project do
|
||||
[
|
||||
app: :extism,
|
||||
version: "0.5.0",
|
||||
version: "0.1.0",
|
||||
elixir: "~> 1.12",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps(),
|
||||
@@ -23,7 +23,7 @@ defmodule Extism.MixProject do
|
||||
|
||||
defp deps do
|
||||
[
|
||||
{:rustler, "~> 0.29.1"},
|
||||
{:rustler, "~> 0.26.0"},
|
||||
{:json, "~> 1.4"},
|
||||
{:ex_doc, "~> 0.21", only: :dev, runtime: false}
|
||||
]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
%{
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.30.6", "5f8b54854b240a2b55c9734c4b1d0dd7bdd41f71a095d42a70445c03cf05a281", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bd48f2ddacf4e482c727f9293d9498e0881597eae6ddc3d9562bd7923375109f"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.29.1", "b1c652fa5f92ee9cf15c75271168027f92039b3877094290a75abcaac82a9f77", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "b7745fa6374a36daf484e2a2012274950e084815b936b1319aeebcf7809574f6"},
|
||||
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
|
||||
"json": {:hex, :json, "1.4.1", "8648f04a9439765ad449bc56a3ff7d8b11dd44ff08ffcdefc4329f7c93843dfa", [:mix], [], "hexpm", "9abf218dbe4ea4fcb875e087d5f904ef263d012ee5ed21d46e9dbca63f053d16"},
|
||||
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
|
||||
"makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
|
||||
"makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
|
||||
"rustler": {:hex, :rustler, "0.29.1", "880f20ae3027bd7945def6cea767f5257bc926f33ff50c0d5d5a5315883c084d", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "109497d701861bfcd26eb8f5801fe327a8eef304f56a5b63ef61151ff44ac9b6"},
|
||||
"toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
|
||||
"makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"},
|
||||
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
|
||||
"rustler": {:hex, :rustler, "0.26.0", "06a2773d453ee3e9109efda643cf2ae633dedea709e2455ac42b83637c9249bf", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "42961e9d2083d004d5a53e111ad1f0c347efd9a05cb2eb2ffa1d037cdc74db91"},
|
||||
"toml": {:hex, :toml, "0.6.2", "38f445df384a17e5d382befe30e3489112a48d3ba4c459e543f748c2f25dd4d1", [:mix], [], "hexpm", "d013e45126d74c0c26a38d31f5e8e9b83ea19fc752470feb9a86071ca5a672fa"},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "extism_nif"
|
||||
version = "0.3.0"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Benjamin Eckel <bhelx@simst.im>"]
|
||||
|
||||
@@ -9,10 +9,7 @@ name = "extism_nif"
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
# need this to be here and be empty
|
||||
[workspace]
|
||||
|
||||
[dependencies]
|
||||
rustler = "0.28.0"
|
||||
extism = {path = "../../../runtime"} # "0.5.0"
|
||||
rustler = "0.26.0"
|
||||
extism = { version = "0.1.0", path = "../../../rust" }
|
||||
log = "0.4"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use extism::Plugin;
|
||||
use extism::{Context, Plugin};
|
||||
use rustler::{Atom, Env, ResourceArc, Term};
|
||||
use std::mem;
|
||||
use std::path::Path;
|
||||
use std::str;
|
||||
use std::str::FromStr;
|
||||
@@ -13,96 +14,114 @@ mod atoms {
|
||||
}
|
||||
}
|
||||
|
||||
struct ExtismPlugin {
|
||||
plugin: RwLock<Option<Plugin>>,
|
||||
}
|
||||
unsafe impl Sync for ExtismPlugin {}
|
||||
unsafe impl Send for ExtismPlugin {}
|
||||
|
||||
struct ExtismCancelHandle {
|
||||
handle: RwLock<extism::CancelHandle>,
|
||||
struct ExtismContext {
|
||||
ctx: RwLock<Context>,
|
||||
}
|
||||
|
||||
unsafe impl Sync for ExtismCancelHandle {}
|
||||
unsafe impl Send for ExtismCancelHandle {}
|
||||
unsafe impl Sync for ExtismContext {}
|
||||
unsafe impl Send for ExtismContext {}
|
||||
|
||||
fn load(env: Env, _: Term) -> bool {
|
||||
rustler::resource!(ExtismPlugin, env);
|
||||
rustler::resource!(ExtismCancelHandle, env);
|
||||
rustler::resource!(ExtismContext, env);
|
||||
true
|
||||
}
|
||||
|
||||
fn to_rustler_error(extism_error: extism::Error) -> rustler::Error {
|
||||
rustler::Error::Term(Box::new(extism_error.to_string()))
|
||||
match extism_error {
|
||||
extism::Error::UnableToLoadPlugin(msg) => rustler::Error::Term(Box::new(msg)),
|
||||
extism::Error::Message(msg) => rustler::Error::Term(Box::new(msg)),
|
||||
extism::Error::Json(json_err) => rustler::Error::Term(Box::new(json_err.to_string())),
|
||||
extism::Error::Runtime(e) => rustler::Error::Term(Box::new(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
fn freed_error() -> rustler::Error {
|
||||
rustler::Error::Term(Box::new("Plugin has already been freed".to_string()))
|
||||
#[rustler::nif]
|
||||
fn context_new() -> ResourceArc<ExtismContext> {
|
||||
ResourceArc::new(ExtismContext {
|
||||
ctx: RwLock::new(Context::new()),
|
||||
})
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
fn context_reset(ctx: ResourceArc<ExtismContext>) {
|
||||
let context = &mut ctx.ctx.write().unwrap();
|
||||
context.reset()
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
fn context_free(ctx: ResourceArc<ExtismContext>) {
|
||||
let context = &ctx.ctx.read().unwrap();
|
||||
std::mem::drop(context)
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
fn plugin_new_with_manifest(
|
||||
ctx: ResourceArc<ExtismContext>,
|
||||
manifest_payload: String,
|
||||
wasi: bool,
|
||||
) -> Result<ResourceArc<ExtismPlugin>, rustler::Error> {
|
||||
let result = match Plugin::new(manifest_payload, [], wasi) {
|
||||
) -> Result<i32, rustler::Error> {
|
||||
let context = &ctx.ctx.write().unwrap();
|
||||
let result = match Plugin::new(context, manifest_payload, wasi) {
|
||||
Err(e) => Err(to_rustler_error(e)),
|
||||
Ok(plugin) => Ok(ResourceArc::new(ExtismPlugin {
|
||||
plugin: RwLock::new(Some(plugin)),
|
||||
})),
|
||||
Ok(plugin) => {
|
||||
let plugin_id = plugin.as_i32();
|
||||
// this forget should be safe because the context will clean up
|
||||
// all it's plugins when it is dropped
|
||||
mem::forget(plugin);
|
||||
Ok(plugin_id)
|
||||
}
|
||||
};
|
||||
result
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
fn plugin_call(
|
||||
plugin: ResourceArc<ExtismPlugin>,
|
||||
ctx: ResourceArc<ExtismContext>,
|
||||
plugin_id: i32,
|
||||
name: String,
|
||||
input: String,
|
||||
) -> Result<String, rustler::Error> {
|
||||
let mut plugin = plugin.plugin.write().unwrap();
|
||||
if let Some(plugin) = &mut *plugin {
|
||||
let result = match plugin.call(name, input) {
|
||||
Err(e) => Err(to_rustler_error(e)),
|
||||
Ok(result) => match str::from_utf8(result) {
|
||||
Ok(output) => Ok(output.to_string()),
|
||||
Err(_e) => Err(rustler::Error::Term(Box::new(
|
||||
"Could not read output from plugin",
|
||||
))),
|
||||
},
|
||||
};
|
||||
result
|
||||
} else {
|
||||
Err(freed_error())
|
||||
}
|
||||
let context = &ctx.ctx.read().unwrap();
|
||||
let mut plugin = unsafe { Plugin::from_id(plugin_id, context) };
|
||||
let result = match plugin.call(name, input) {
|
||||
Err(e) => Err(to_rustler_error(e)),
|
||||
Ok(result) => match str::from_utf8(&result) {
|
||||
Ok(output) => Ok(output.to_string()),
|
||||
Err(_e) => Err(rustler::Error::Term(Box::new(
|
||||
"Could not read output from plugin",
|
||||
))),
|
||||
},
|
||||
};
|
||||
// this forget should be safe because the context will clean up
|
||||
// all it's plugins when it is dropped
|
||||
mem::forget(plugin);
|
||||
result
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
fn plugin_cancel_handle(
|
||||
plugin: ResourceArc<ExtismPlugin>,
|
||||
) -> Result<ResourceArc<ExtismCancelHandle>, rustler::Error> {
|
||||
let mut plugin = plugin.plugin.write().unwrap();
|
||||
if let Some(plugin) = &mut *plugin {
|
||||
let handle = plugin.cancel_handle();
|
||||
Ok(ResourceArc::new(ExtismCancelHandle {
|
||||
handle: RwLock::new(handle),
|
||||
}))
|
||||
} else {
|
||||
Err(freed_error())
|
||||
}
|
||||
fn plugin_update_manifest(
|
||||
ctx: ResourceArc<ExtismContext>,
|
||||
plugin_id: i32,
|
||||
manifest_payload: String,
|
||||
wasi: bool,
|
||||
) -> Result<(), rustler::Error> {
|
||||
let context = &ctx.ctx.read().unwrap();
|
||||
let mut plugin = unsafe { Plugin::from_id(plugin_id, context) };
|
||||
let result = match plugin.update(manifest_payload, wasi) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) => Err(to_rustler_error(e)),
|
||||
};
|
||||
// this forget should be safe because the context will clean up
|
||||
// all it's plugins when it is dropped
|
||||
mem::forget(plugin);
|
||||
result
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
fn plugin_cancel(handle: ResourceArc<ExtismCancelHandle>) -> bool {
|
||||
handle.handle.read().unwrap().cancel().is_ok()
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
fn plugin_free(plugin: ResourceArc<ExtismPlugin>) -> Result<(), rustler::Error> {
|
||||
let mut plugin = plugin.plugin.write().unwrap();
|
||||
if let Some(plugin) = plugin.take() {
|
||||
drop(plugin);
|
||||
}
|
||||
fn plugin_free(ctx: ResourceArc<ExtismContext>, plugin_id: i32) -> Result<(), rustler::Error> {
|
||||
let context = &ctx.ctx.read().unwrap();
|
||||
let plugin = unsafe { Plugin::from_id(plugin_id, context) };
|
||||
std::mem::drop(plugin);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -114,37 +133,43 @@ fn set_log_file(filename: String, log_level: String) -> Result<Atom, rustler::Er
|
||||
"{} not a valid log level",
|
||||
log_level
|
||||
)))),
|
||||
Ok(level) => match extism::set_log_file(path, level) {
|
||||
Ok(()) => Ok(atoms::ok()),
|
||||
Err(e) => Err(rustler::Error::Term(Box::new(format!(
|
||||
"Did not set log file: {e:?}"
|
||||
)))),
|
||||
},
|
||||
Ok(level) => {
|
||||
if extism::set_log_file(path, Some(level)) {
|
||||
Ok(atoms::ok())
|
||||
} else {
|
||||
Err(rustler::Error::Term(Box::new(
|
||||
"Did not set log file, received false from the API.",
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
fn plugin_has_function(
|
||||
plugin: ResourceArc<ExtismPlugin>,
|
||||
ctx: ResourceArc<ExtismContext>,
|
||||
plugin_id: i32,
|
||||
function_name: String,
|
||||
) -> Result<bool, rustler::Error> {
|
||||
let mut plugin = plugin.plugin.write().unwrap();
|
||||
if let Some(plugin) = &mut *plugin {
|
||||
let has_function = plugin.function_exists(function_name);
|
||||
Ok(has_function)
|
||||
} else {
|
||||
Err(freed_error())
|
||||
}
|
||||
let context = &ctx.ctx.read().unwrap();
|
||||
let plugin = unsafe { Plugin::from_id(plugin_id, context) };
|
||||
let has_function = plugin.has_function(function_name);
|
||||
// this forget should be safe because the context will clean up
|
||||
// all it's plugins when it is dropped
|
||||
mem::forget(plugin);
|
||||
Ok(has_function)
|
||||
}
|
||||
|
||||
rustler::init!(
|
||||
"Elixir.Extism.Native",
|
||||
[
|
||||
context_new,
|
||||
context_reset,
|
||||
context_free,
|
||||
plugin_new_with_manifest,
|
||||
plugin_call,
|
||||
plugin_update_manifest,
|
||||
plugin_has_function,
|
||||
plugin_cancel_handle,
|
||||
plugin_cancel,
|
||||
plugin_free,
|
||||
set_log_file,
|
||||
],
|
||||
|
||||
@@ -2,21 +2,33 @@ defmodule ExtismTest do
|
||||
use ExUnit.Case
|
||||
doctest Extism
|
||||
|
||||
defp new_plugin() do
|
||||
test "context create & reset" do
|
||||
ctx = Extism.Context.new()
|
||||
path = Path.join([__DIR__, "../../wasm/code.wasm"])
|
||||
manifest = %{wasm: [%{path: path}]}
|
||||
{:ok, plugin} = Extism.Plugin.new(manifest, false)
|
||||
plugin
|
||||
{:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
|
||||
Extism.Context.reset(ctx)
|
||||
# we should expect an error after resetting context
|
||||
{:error, _err} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
|
||||
end
|
||||
|
||||
defp new_plugin() do
|
||||
ctx = Extism.Context.new()
|
||||
path = Path.join([__DIR__, "../../wasm/code.wasm"])
|
||||
manifest = %{wasm: [%{path: path}]}
|
||||
{:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
|
||||
{ctx, plugin}
|
||||
end
|
||||
|
||||
test "counts vowels" do
|
||||
plugin = new_plugin()
|
||||
{ctx, plugin} = new_plugin()
|
||||
{:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
|
||||
assert JSON.decode(output) == {:ok, %{"count" => 4}}
|
||||
Extism.Context.free(ctx)
|
||||
end
|
||||
|
||||
test "can make multiple calls on a plugin" do
|
||||
plugin = new_plugin()
|
||||
{ctx, plugin} = new_plugin()
|
||||
{:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
|
||||
assert JSON.decode(output) == {:ok, %{"count" => 4}}
|
||||
{:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "this is a test again")
|
||||
@@ -25,24 +37,37 @@ defmodule ExtismTest do
|
||||
assert JSON.decode(output) == {:ok, %{"count" => 6}}
|
||||
{:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "🌎hello🌎world🌎")
|
||||
assert JSON.decode(output) == {:ok, %{"count" => 3}}
|
||||
Extism.Context.free(ctx)
|
||||
end
|
||||
|
||||
test "can free a plugin" do
|
||||
plugin = new_plugin()
|
||||
{ctx, plugin} = new_plugin()
|
||||
{:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
|
||||
assert JSON.decode(output) == {:ok, %{"count" => 4}}
|
||||
Extism.Plugin.free(plugin)
|
||||
# Expect an error when calling a plugin that was freed
|
||||
{:error, _err} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
|
||||
Extism.Context.free(ctx)
|
||||
end
|
||||
|
||||
test "can update manifest" do
|
||||
{ctx, plugin} = new_plugin()
|
||||
path = Path.join([__DIR__, "../../wasm/code.wasm"])
|
||||
manifest = %{wasm: [%{path: path}]}
|
||||
assert Extism.Plugin.update(plugin, manifest, true) == :ok
|
||||
Extism.Context.free(ctx)
|
||||
end
|
||||
|
||||
test "errors on bad manifest" do
|
||||
{:error, _msg} = Extism.Plugin.new(%{"wasm" => 123}, false)
|
||||
ctx = Extism.Context.new()
|
||||
{:error, _msg} = Extism.Context.new_plugin(ctx, %{"wasm" => 123}, false)
|
||||
Extism.Context.free(ctx)
|
||||
end
|
||||
|
||||
test "errors on unknown function" do
|
||||
plugin = new_plugin()
|
||||
{ctx, plugin} = new_plugin()
|
||||
{:error, _msg} = Extism.Plugin.call(plugin, "unknown", "this is a test")
|
||||
Extism.Context.free(ctx)
|
||||
end
|
||||
|
||||
test "set_log_file" do
|
||||
@@ -50,8 +75,9 @@ defmodule ExtismTest do
|
||||
end
|
||||
|
||||
test "has_function" do
|
||||
plugin = new_plugin()
|
||||
{ctx, plugin} = new_plugin()
|
||||
assert Extism.Plugin.has_function(plugin, "count_vowels")
|
||||
assert !Extism.Plugin.has_function(plugin, "unknown")
|
||||
Extism.Context.free(ctx)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This file is generated by dune, edit dune-project instead
|
||||
opam-version: "2.0"
|
||||
synopsis: "Extism manifest bindings"
|
||||
description: "Bindings to the Extism manifest format"
|
||||
description: "Bindings to Extism, the universal plugin system"
|
||||
maintainer: ["Extism Authors <oss@extism.org>"]
|
||||
authors: ["Extism Authors <oss@extism.org>"]
|
||||
license: "BSD-3-Clause"
|
||||
@@ -11,9 +11,8 @@ doc: "https://github.com/extism/extism"
|
||||
bug-reports: "https://github.com/extism/extism/issues"
|
||||
depends: [
|
||||
"ocaml" {>= "4.14.1"}
|
||||
"dune" {>= "3.2"}
|
||||
"ppx_yojson_conv" {>= "v0.15.0"}
|
||||
"ppx_inline_test" {>= "v0.15.0"}
|
||||
"dune" {>= "3.2" & >= "3.2"}
|
||||
"ppx_yojson_conv" {>= "0.15.0"}
|
||||
"base64" {>= "3.5.0"}
|
||||
"odoc" {with-doc}
|
||||
]
|
||||
|
||||
371
extism.go
371
extism.go
@@ -5,161 +5,39 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime/cgo"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -I/usr/local/include
|
||||
#cgo LDFLAGS: -L/usr/local/lib -lextism
|
||||
#cgo pkg-config: libextism.pc
|
||||
#include <extism.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int64_t extism_val_i64(ExtismValUnion* x){
|
||||
return x->i64;
|
||||
}
|
||||
|
||||
int32_t extism_val_i32(ExtismValUnion* x){
|
||||
return x->i32;
|
||||
}
|
||||
|
||||
float extism_val_f32(ExtismValUnion* x){
|
||||
return x->f32;
|
||||
}
|
||||
|
||||
double extism_val_f64(ExtismValUnion* x){
|
||||
return x->f64;
|
||||
}
|
||||
|
||||
|
||||
void extism_val_set_i64(ExtismValUnion* x, int64_t i){
|
||||
x->i64 = i;
|
||||
}
|
||||
|
||||
|
||||
void extism_val_set_i32(ExtismValUnion* x, int32_t i){
|
||||
x->i32 = i;
|
||||
}
|
||||
|
||||
void extism_val_set_f32(ExtismValUnion* x, float f){
|
||||
x->f32 = f;
|
||||
}
|
||||
|
||||
void extism_val_set_f64(ExtismValUnion* x, double f){
|
||||
x->f64 = f;
|
||||
}
|
||||
|
||||
*/
|
||||
import "C"
|
||||
|
||||
type ValType = C.ExtismValType
|
||||
|
||||
type Val = C.ExtismVal
|
||||
|
||||
type Size = C.ExtismSize
|
||||
|
||||
var (
|
||||
I32 ValType = C.I32
|
||||
I64 ValType = C.I64
|
||||
F32 ValType = C.F32
|
||||
F64 ValType = C.F64
|
||||
V128 ValType = C.V128
|
||||
FuncRef ValType = C.FuncRef
|
||||
ExternRef ValType = C.ExternRef
|
||||
)
|
||||
|
||||
// Function is used to define host functions
|
||||
type Function struct {
|
||||
pointer *C.ExtismFunction
|
||||
userData cgo.Handle
|
||||
// Context is used to manage Plugins
|
||||
type Context struct {
|
||||
pointer *C.ExtismContext
|
||||
}
|
||||
|
||||
// Free a function
|
||||
func (f *Function) Free() {
|
||||
if f.pointer != nil {
|
||||
C.extism_function_free(f.pointer)
|
||||
f.pointer = nil
|
||||
f.userData.Delete()
|
||||
// NewContext creates a new context, it should be freed using the `Free` method
|
||||
func NewContext() Context {
|
||||
p := C.extism_context_new()
|
||||
return Context{
|
||||
pointer: p,
|
||||
}
|
||||
}
|
||||
|
||||
// NewFunction creates a new host function with the given name, input/outputs and optional user data, which can be an
|
||||
// arbitrary `interface{}`
|
||||
func NewFunction(name string, inputs []ValType, outputs []ValType, f unsafe.Pointer, userData interface{}) Function {
|
||||
var function Function
|
||||
function.userData = cgo.NewHandle(userData)
|
||||
cname := C.CString(name)
|
||||
ptr := unsafe.Pointer(function.userData)
|
||||
var inputsPtr *C.ExtismValType = nil
|
||||
if len(inputs) > 0 {
|
||||
inputsPtr = (*C.ExtismValType)(&inputs[0])
|
||||
}
|
||||
var outputsPtr *C.ExtismValType = nil
|
||||
if len(outputs) > 0 {
|
||||
outputsPtr = (*C.ExtismValType)(&outputs[0])
|
||||
}
|
||||
function.pointer = C.extism_function_new(
|
||||
cname,
|
||||
inputsPtr,
|
||||
C.uint64_t(len(inputs)),
|
||||
outputsPtr,
|
||||
C.uint64_t(len(outputs)),
|
||||
(*[0]byte)(f),
|
||||
ptr,
|
||||
nil,
|
||||
)
|
||||
C.free(unsafe.Pointer(cname))
|
||||
return function
|
||||
}
|
||||
|
||||
func (f *Function) SetNamespace(s string) {
|
||||
cstr := C.CString(s)
|
||||
defer C.free(unsafe.Pointer(cstr))
|
||||
C.extism_function_set_namespace(f.pointer, cstr)
|
||||
}
|
||||
|
||||
func (f Function) WithNamespace(s string) Function {
|
||||
f.SetNamespace(s)
|
||||
return f
|
||||
}
|
||||
|
||||
type CurrentPlugin struct {
|
||||
pointer *C.ExtismCurrentPlugin
|
||||
}
|
||||
|
||||
func GetCurrentPlugin(ptr unsafe.Pointer) CurrentPlugin {
|
||||
return CurrentPlugin{
|
||||
pointer: (*C.ExtismCurrentPlugin)(ptr),
|
||||
}
|
||||
}
|
||||
|
||||
type MemoryHandle = uint
|
||||
|
||||
func (p *CurrentPlugin) Memory(offs MemoryHandle) []byte {
|
||||
length := C.extism_current_plugin_memory_length(p.pointer, C.uint64_t(offs))
|
||||
data := unsafe.Pointer(C.extism_current_plugin_memory(p.pointer))
|
||||
return unsafe.Slice((*byte)(unsafe.Add(data, offs)), C.int(length))
|
||||
}
|
||||
|
||||
// Alloc a new memory block of the given length, returning its offset
|
||||
func (p *CurrentPlugin) Alloc(n uint) MemoryHandle {
|
||||
return uint(C.extism_current_plugin_memory_alloc(p.pointer, C.uint64_t(n)))
|
||||
}
|
||||
|
||||
// Free the memory block specified by the given offset
|
||||
func (p *CurrentPlugin) Free(offs MemoryHandle) {
|
||||
C.extism_current_plugin_memory_free(p.pointer, C.uint64_t(offs))
|
||||
}
|
||||
|
||||
// Length returns the number of bytes allocated at the specified offset
|
||||
func (p *CurrentPlugin) Length(offs MemoryHandle) int {
|
||||
return int(C.extism_current_plugin_memory_length(p.pointer, C.uint64_t(offs)))
|
||||
// Free a context
|
||||
func (ctx *Context) Free() {
|
||||
C.extism_context_free(ctx.pointer)
|
||||
ctx.pointer = nil
|
||||
}
|
||||
|
||||
// Plugin is used to call WASM functions
|
||||
type Plugin struct {
|
||||
ptr *C.ExtismPlugin
|
||||
functions []Function
|
||||
ctx *Context
|
||||
id int32
|
||||
}
|
||||
|
||||
type WasmData struct {
|
||||
@@ -218,99 +96,121 @@ func ExtismVersion() string {
|
||||
return C.GoString(C.extism_version())
|
||||
}
|
||||
|
||||
func register(data []byte, functions []Function, wasi bool) (Plugin, error) {
|
||||
func register(ctx *Context, data []byte, wasi bool) (Plugin, error) {
|
||||
ptr := makePointer(data)
|
||||
functionPointers := []*C.ExtismFunction{}
|
||||
for _, f := range functions {
|
||||
functionPointers = append(functionPointers, f.pointer)
|
||||
}
|
||||
plugin := C.extism_plugin_new(
|
||||
ctx.pointer,
|
||||
(*C.uchar)(ptr),
|
||||
C.uint64_t(len(data)),
|
||||
C._Bool(wasi),
|
||||
)
|
||||
|
||||
var plugin *C.ExtismPlugin
|
||||
errmsg := (*C.char)(nil)
|
||||
if len(functions) == 0 {
|
||||
plugin = C.extism_plugin_new(
|
||||
(*C.uchar)(ptr),
|
||||
C.uint64_t(len(data)),
|
||||
nil,
|
||||
0,
|
||||
C._Bool(wasi),
|
||||
&errmsg)
|
||||
} else {
|
||||
plugin = C.extism_plugin_new(
|
||||
(*C.uchar)(ptr),
|
||||
C.uint64_t(len(data)),
|
||||
&functionPointers[0],
|
||||
C.uint64_t(len(functions)),
|
||||
C._Bool(wasi),
|
||||
&errmsg,
|
||||
)
|
||||
}
|
||||
if plugin < 0 {
|
||||
err := C.extism_error(ctx.pointer, C.int32_t(-1))
|
||||
msg := "Unknown"
|
||||
if err != nil {
|
||||
msg = C.GoString(err)
|
||||
}
|
||||
|
||||
if plugin == nil {
|
||||
msg := C.GoString(errmsg)
|
||||
C.extism_plugin_new_error_free(errmsg)
|
||||
return Plugin{}, errors.New(
|
||||
return Plugin{id: -1}, errors.New(
|
||||
fmt.Sprintf("Unable to load plugin: %s", msg),
|
||||
)
|
||||
}
|
||||
|
||||
return Plugin{ptr: plugin, functions: functions}, nil
|
||||
return Plugin{id: int32(plugin), ctx: ctx}, nil
|
||||
}
|
||||
|
||||
// NewPlugin creates a plugin
|
||||
func NewPlugin(module io.Reader, functions []Function, wasi bool) (Plugin, error) {
|
||||
wasm, err := io.ReadAll(module)
|
||||
if err != nil {
|
||||
return Plugin{}, err
|
||||
func update(ctx *Context, plugin int32, data []byte, wasi bool) error {
|
||||
ptr := makePointer(data)
|
||||
b := bool(C.extism_plugin_update(
|
||||
ctx.pointer,
|
||||
C.int32_t(plugin),
|
||||
(*C.uchar)(ptr),
|
||||
C.uint64_t(len(data)),
|
||||
C._Bool(wasi),
|
||||
))
|
||||
|
||||
if b {
|
||||
return nil
|
||||
}
|
||||
|
||||
return register(wasm, functions, wasi)
|
||||
err := C.extism_error(ctx.pointer, C.int32_t(-1))
|
||||
msg := "Unknown"
|
||||
if err != nil {
|
||||
msg = C.GoString(err)
|
||||
}
|
||||
|
||||
return errors.New(
|
||||
fmt.Sprintf("Unable to load plugin: %s", msg),
|
||||
)
|
||||
}
|
||||
|
||||
// NewPlugin creates a plugin from a manifest
|
||||
func NewPluginFromManifest(manifest Manifest, functions []Function, wasi bool) (Plugin, error) {
|
||||
// PluginFromManifest creates a plugin from a `Manifest`
|
||||
func (ctx *Context) PluginFromManifest(manifest Manifest, wasi bool) (Plugin, error) {
|
||||
data, err := json.Marshal(manifest)
|
||||
if err != nil {
|
||||
return Plugin{}, err
|
||||
return Plugin{id: -1}, err
|
||||
}
|
||||
|
||||
return register(data, functions, wasi)
|
||||
return register(ctx, data, wasi)
|
||||
}
|
||||
|
||||
// Plugin creates a plugin from a WASM module
|
||||
func (ctx *Context) Plugin(module io.Reader, wasi bool) (Plugin, error) {
|
||||
wasm, err := io.ReadAll(module)
|
||||
if err != nil {
|
||||
return Plugin{id: -1}, err
|
||||
}
|
||||
|
||||
return register(ctx, wasm, wasi)
|
||||
}
|
||||
|
||||
// Update a plugin with a new WASM module
|
||||
func (p *Plugin) Update(module io.Reader, wasi bool) error {
|
||||
wasm, err := io.ReadAll(module)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return update(p.ctx, p.id, wasm, wasi)
|
||||
}
|
||||
|
||||
// Update a plugin with a new Manifest
|
||||
func (p *Plugin) UpdateManifest(manifest Manifest, wasi bool) error {
|
||||
data, err := json.Marshal(manifest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return update(p.ctx, p.id, data, wasi)
|
||||
}
|
||||
|
||||
// Set configuration values
|
||||
func (plugin Plugin) SetConfig(data map[string][]byte) error {
|
||||
if plugin.ptr == nil {
|
||||
return errors.New("Cannot set config, Plugin already freed")
|
||||
}
|
||||
s, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ptr := makePointer(s)
|
||||
C.extism_plugin_config(plugin.ptr, (*C.uchar)(ptr), C.uint64_t(len(s)))
|
||||
C.extism_plugin_config(plugin.ctx.pointer, C.int(plugin.id), (*C.uchar)(ptr), C.uint64_t(len(s)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// FunctionExists returns true when the named function is present in the plugin
|
||||
func (plugin Plugin) FunctionExists(functionName string) bool {
|
||||
if plugin.ptr == nil {
|
||||
return false
|
||||
}
|
||||
name := C.CString(functionName)
|
||||
b := C.extism_plugin_function_exists(plugin.ptr, name)
|
||||
b := C.extism_plugin_function_exists(plugin.ctx.pointer, C.int(plugin.id), name)
|
||||
C.free(unsafe.Pointer(name))
|
||||
return bool(b)
|
||||
}
|
||||
|
||||
// Call a function by name with the given input, returning the output
|
||||
func (plugin Plugin) Call(functionName string, input []byte) ([]byte, error) {
|
||||
if plugin.ptr == nil {
|
||||
return []byte{}, errors.New("Plugin has already been freed")
|
||||
}
|
||||
ptr := makePointer(input)
|
||||
name := C.CString(functionName)
|
||||
rc := C.extism_plugin_call(
|
||||
plugin.ptr,
|
||||
plugin.ctx.pointer,
|
||||
C.int32_t(plugin.id),
|
||||
name,
|
||||
(*C.uchar)(ptr),
|
||||
C.uint64_t(len(input)),
|
||||
@@ -318,7 +218,7 @@ func (plugin Plugin) Call(functionName string, input []byte) ([]byte, error) {
|
||||
C.free(unsafe.Pointer(name))
|
||||
|
||||
if rc != 0 {
|
||||
err := C.extism_plugin_error(plugin.ptr)
|
||||
err := C.extism_error(plugin.ctx.pointer, C.int32_t(plugin.id))
|
||||
msg := "<unset by plugin>"
|
||||
if err != nil {
|
||||
msg = C.GoString(err)
|
||||
@@ -329,11 +229,12 @@ func (plugin Plugin) Call(functionName string, input []byte) ([]byte, error) {
|
||||
)
|
||||
}
|
||||
|
||||
length := C.extism_plugin_output_length(plugin.ptr)
|
||||
length := C.extism_plugin_output_length(plugin.ctx.pointer, C.int32_t(plugin.id))
|
||||
|
||||
if length > 0 {
|
||||
x := C.extism_plugin_output_data(plugin.ptr)
|
||||
return unsafe.Slice((*byte)(x), C.int(length)), nil
|
||||
x := C.extism_plugin_output_data(plugin.ctx.pointer, C.int32_t(plugin.id))
|
||||
y := (*[]byte)(unsafe.Pointer(&x))
|
||||
return []byte((*y)[0:length]), nil
|
||||
}
|
||||
|
||||
return []byte{}, nil
|
||||
@@ -341,86 +242,14 @@ func (plugin Plugin) Call(functionName string, input []byte) ([]byte, error) {
|
||||
|
||||
// Free a plugin
|
||||
func (plugin *Plugin) Free() {
|
||||
if plugin.ptr == nil {
|
||||
if plugin.ctx.pointer == nil {
|
||||
return
|
||||
}
|
||||
C.extism_plugin_free(plugin.ptr)
|
||||
plugin.ptr = nil
|
||||
C.extism_plugin_free(plugin.ctx.pointer, C.int32_t(plugin.id))
|
||||
plugin.id = -1
|
||||
}
|
||||
|
||||
// ValGetI64 returns an I64 from an ExtismVal, it accepts a pointer to a C.ExtismVal
|
||||
func ValGetI64(v unsafe.Pointer) int64 {
|
||||
return int64(C.extism_val_i64(&(*Val)(v).v))
|
||||
}
|
||||
|
||||
// ValGetUInt returns a uint from an ExtismVal, it accepts a pointer to a C.ExtismVal
|
||||
func ValGetUInt(v unsafe.Pointer) uint {
|
||||
return uint(C.extism_val_i64(&(*Val)(v).v))
|
||||
}
|
||||
|
||||
// ValGetI32 returns an int32 from an ExtismVal, it accepts a pointer to a C.ExtismVal
|
||||
func ValGetI32(v unsafe.Pointer) int32 {
|
||||
return int32(C.extism_val_i32(&(*Val)(v).v))
|
||||
}
|
||||
|
||||
// ValGetF32 returns a float32 from an ExtismVal, it accepts a pointer to a C.ExtismVal
|
||||
func ValGetF32(v unsafe.Pointer) float32 {
|
||||
return float32(C.extism_val_f32(&(*Val)(v).v))
|
||||
}
|
||||
|
||||
// ValGetF32 returns a float64 from an ExtismVal, it accepts a pointer to a C.ExtismVal
|
||||
func ValGetF64(v unsafe.Pointer) float64 {
|
||||
return float64(C.extism_val_i64(&(*Val)(v).v))
|
||||
}
|
||||
|
||||
// ValSetI64 stores an int64 in an ExtismVal, it accepts a pointer to a C.ExtismVal and the new value
|
||||
func ValSetI64(v unsafe.Pointer, i int64) {
|
||||
C.extism_val_set_i64(&(*Val)(v).v, C.int64_t(i))
|
||||
}
|
||||
|
||||
// ValSetI32 stores an int32 in an ExtismVal, it accepts a pointer to a C.ExtismVal and the new value
|
||||
func ValSetI32(v unsafe.Pointer, i int32) {
|
||||
C.extism_val_set_i32(&(*Val)(v).v, C.int32_t(i))
|
||||
}
|
||||
|
||||
// ValSetF32 stores a float32 in an ExtismVal, it accepts a pointer to a C.ExtismVal and the new value
|
||||
func ValSetF32(v unsafe.Pointer, i float32) {
|
||||
C.extism_val_set_f32(&(*Val)(v).v, C.float(i))
|
||||
}
|
||||
|
||||
// ValSetF64 stores a float64 in an ExtismVal, it accepts a pointer to a C.ExtismVal and the new value
|
||||
func ValSetF64(v unsafe.Pointer, f float64) {
|
||||
C.extism_val_set_f64(&(*Val)(v).v, C.double(f))
|
||||
}
|
||||
|
||||
func (p *CurrentPlugin) ReturnBytes(v unsafe.Pointer, b []byte) {
|
||||
mem := p.Alloc(uint(len(b)))
|
||||
ptr := p.Memory(mem)
|
||||
copy(ptr, b)
|
||||
ValSetI64(v, int64(mem))
|
||||
}
|
||||
|
||||
func (p *CurrentPlugin) ReturnString(v unsafe.Pointer, s string) {
|
||||
p.ReturnBytes(v, []byte(s))
|
||||
}
|
||||
|
||||
func (p *CurrentPlugin) InputBytes(v unsafe.Pointer) []byte {
|
||||
return p.Memory(ValGetUInt(v))
|
||||
}
|
||||
|
||||
func (p *CurrentPlugin) InputString(v unsafe.Pointer) string {
|
||||
return string(p.InputBytes(v))
|
||||
}
|
||||
|
||||
type CancelHandle struct {
|
||||
pointer *C.ExtismCancelHandle
|
||||
}
|
||||
|
||||
func (p *Plugin) CancelHandle() CancelHandle {
|
||||
pointer := C.extism_plugin_cancel_handle(p.ptr)
|
||||
return CancelHandle{pointer}
|
||||
}
|
||||
|
||||
func (c *CancelHandle) Cancel() bool {
|
||||
return bool(C.extism_plugin_cancel(c.pointer))
|
||||
// Reset removes all registered plugins in a Context
|
||||
func (ctx Context) Reset() {
|
||||
C.extism_context_reset(ctx.pointer)
|
||||
}
|
||||
|
||||
12
extism.opam
12
extism.opam
@@ -11,15 +11,13 @@ doc: "https://github.com/extism/extism"
|
||||
bug-reports: "https://github.com/extism/extism/issues"
|
||||
depends: [
|
||||
"ocaml" {>= "4.14.1"}
|
||||
"dune" {>= "3.2"}
|
||||
"ctypes" {>= "0.18.0"}
|
||||
"dune" {>= "3.2" & >= "3.2"}
|
||||
"ctypes-foreign" {>= "0.18.0"}
|
||||
"bigstringaf" {>= "0.9.0"}
|
||||
"ppx_yojson_conv" {>= "v0.15.0"}
|
||||
"extism-manifest" {= version}
|
||||
"ppx_inline_test" {>= "v0.15.0"}
|
||||
"ppx_yojson_conv" {>= "0.15.0"}
|
||||
"extism-manifest"
|
||||
"ppx_inline_test" {>= "0.15.0"}
|
||||
"cmdliner" {>= "1.1.1"}
|
||||
"uuidm" {>= "0.9.0"}
|
||||
"odoc" {with-doc}
|
||||
]
|
||||
build: [
|
||||
@@ -37,5 +35,3 @@ build: [
|
||||
]
|
||||
]
|
||||
dev-repo: "git+https://github.com/extism/extism.git"
|
||||
build-env: [EXTISM_TEST_NO_LIB = ""]
|
||||
post-messages: ["See https://extism.org/docs/install/ for information about installing libextism"]
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
build-env: [EXTISM_TEST_NO_LIB = ""]
|
||||
post-messages: ["See https://extism.org/docs/install/ for information about installing libextism"]
|
||||
103
extism_test.go
103
extism_test.go
@@ -4,19 +4,13 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func manifest(functions bool) Manifest {
|
||||
path := "./wasm/code.wasm"
|
||||
if functions {
|
||||
path = "./wasm/code-functions.wasm"
|
||||
}
|
||||
|
||||
func manifest() Manifest {
|
||||
return Manifest{
|
||||
Wasm: []Wasm{
|
||||
WasmFile{
|
||||
Path: path,
|
||||
Path: "./wasm/code.wasm",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -35,8 +29,16 @@ func expectVowelCount(plugin Plugin, input string, count int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestCreateAndFreeContext(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
ctx.Free()
|
||||
}
|
||||
|
||||
func TestCallPlugin(t *testing.T) {
|
||||
plugin, err := NewPluginFromManifest(manifest(false), []Function{}, false)
|
||||
ctx := NewContext()
|
||||
defer ctx.Free()
|
||||
|
||||
plugin, err := ctx.PluginFromManifest(manifest(), false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -53,7 +55,10 @@ func TestCallPlugin(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFreePlugin(t *testing.T) {
|
||||
plugin, err := NewPluginFromManifest(manifest(false), []Function{}, false)
|
||||
ctx := NewContext()
|
||||
defer ctx.Free()
|
||||
|
||||
plugin, err := ctx.PluginFromManifest(manifest(), false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -69,8 +74,52 @@ func TestFreePlugin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextReset(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
defer ctx.Free()
|
||||
|
||||
plugin, err := ctx.PluginFromManifest(manifest(), false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := expectVowelCount(plugin, "this is a test", 4); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// reset the context dropping all plugins
|
||||
ctx.Reset()
|
||||
|
||||
if err := expectVowelCount(plugin, "this is a test", 4); err == nil {
|
||||
t.Fatal("Expected an error after plugin was freed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanUpdateAManifest(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
defer ctx.Free()
|
||||
|
||||
plugin, err := ctx.PluginFromManifest(manifest(), false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := expectVowelCount(plugin, "this is a test", 4); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
plugin.UpdateManifest(manifest(), false)
|
||||
|
||||
// can still call the plugin
|
||||
if err := expectVowelCount(plugin, "this is a test", 4); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFunctionExists(t *testing.T) {
|
||||
plugin, err := NewPluginFromManifest(manifest(false), []Function{}, false)
|
||||
ctx := NewContext()
|
||||
defer ctx.Free()
|
||||
|
||||
plugin, err := ctx.PluginFromManifest(manifest(), false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -84,7 +133,10 @@ func TestFunctionExists(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestErrorsOnUnknownFunction(t *testing.T) {
|
||||
plugin, err := NewPluginFromManifest(manifest(false), []Function{}, false)
|
||||
ctx := NewContext()
|
||||
defer ctx.Free()
|
||||
|
||||
plugin, err := ctx.PluginFromManifest(manifest(), false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -94,30 +146,3 @@ func TestErrorsOnUnknownFunction(t *testing.T) {
|
||||
t.Fatal("Was expecting call to unknown function to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancel(t *testing.T) {
|
||||
manifest := Manifest{
|
||||
Wasm: []Wasm{
|
||||
WasmFile{
|
||||
Path: "./wasm/loop.wasm",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
plugin, err := NewPluginFromManifest(manifest, []Function{}, false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
cancelHandle := plugin.CancelHandle()
|
||||
|
||||
go func(handle CancelHandle) {
|
||||
time.Sleep(time.Second * 1)
|
||||
handle.Cancel()
|
||||
}(cancelHandle)
|
||||
|
||||
_, err = plugin.Call("infinite_loop", []byte(""))
|
||||
if err == nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
34
go/main.go
34
go/main.go
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
module Main where
|
||||
|
||||
import Extism
|
||||
import Extism.HostFunction
|
||||
import Extism.Manifest(manifest, wasmFile)
|
||||
|
||||
hello plugin params msg = do
|
||||
putStrLn "Hello from Haskell!"
|
||||
putStrLn msg
|
||||
offs <- allocBytes plugin (toByteString "{\"count\": 999}")
|
||||
return [toI64 offs]
|
||||
unwrap (Right x) = x
|
||||
unwrap (Left (ExtismError msg)) = do
|
||||
error msg
|
||||
|
||||
main = do
|
||||
setLogFile "stdout" LogError
|
||||
let m = manifest [wasmFile "../wasm/code-functions.wasm"]
|
||||
f <- hostFunction "hello_world" [I64] [I64] hello "Hello, again"
|
||||
plugin <- unwrap <$> pluginFromManifest m [f] True
|
||||
id <- pluginID plugin
|
||||
print id
|
||||
res <- unwrap <$> call plugin "count_vowels" (toByteString "this is a test")
|
||||
putStrLn (fromByteString res)
|
||||
let m = manifest [wasmFile "../wasm/code.wasm"]
|
||||
context <- Extism.newContext
|
||||
plugin <- unwrap <$> Extism.pluginFromManifest context m False
|
||||
res <- unwrap <$> Extism.call plugin "count_vowels" (Extism.toByteString "this is a test")
|
||||
putStrLn (Extism.fromByteString res)
|
||||
Extism.free plugin
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
cabal-version: 3.0
|
||||
name: extism
|
||||
version: 0.5.0
|
||||
version: 0.0.1
|
||||
license: BSD-3-Clause
|
||||
maintainer: oss@extism.org
|
||||
author: Extism authors
|
||||
@@ -8,10 +8,10 @@ bug-reports: https://github.com/extism/extism
|
||||
synopsis: Extism bindings
|
||||
description: Bindings to Extism, the universal plugin system
|
||||
category: Plugins, WebAssembly
|
||||
extra-doc-files: CHANGELOG.md
|
||||
extra-source-files: CHANGELOG.md
|
||||
|
||||
library
|
||||
exposed-modules: Extism Extism.HostFunction
|
||||
exposed-modules: Extism
|
||||
reexported-modules: Extism.Manifest
|
||||
hs-source-dirs: src
|
||||
other-modules: Extism.Bindings
|
||||
@@ -19,11 +19,10 @@ library
|
||||
extra-libraries: extism
|
||||
extra-lib-dirs: /usr/local/lib
|
||||
build-depends:
|
||||
base >= 4.16.1 && < 5,
|
||||
bytestring >= 0.11.3 && <= 0.12,
|
||||
json >= 0.10 && <= 0.11,
|
||||
extism-manifest >= 0.0.0 && < 0.4.0,
|
||||
uuid >= 1.3 && < 2
|
||||
base >= 4.16.1 && < 4.18.0,
|
||||
bytestring >= 0.11.3 && < 0.12,
|
||||
json >= 0.10 && < 0.11,
|
||||
extism-manifest >= 0.0.0 && < 0.1.0
|
||||
|
||||
test-suite extism-example
|
||||
type: exitcode-stdio-1.0
|
||||
|
||||
@@ -9,23 +9,21 @@ import Data.ByteString.Internal (c2w, w2c)
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
import qualified Data.ByteString.Char8 as BS (unpack)
|
||||
|
||||
data Nullable a = Null | NotNull a deriving Eq
|
||||
data Nullable a = Null | NotNull a
|
||||
|
||||
makeArray x = JSArray [showJSON a | a <- x]
|
||||
isNull JSNull = True
|
||||
isNull _ = False
|
||||
filterNulls obj = [(a, b) | (a, b) <- obj, not (isNull b)]
|
||||
object x = makeObj $ filterNulls x
|
||||
objectWithNulls = makeObj
|
||||
nonNull = NotNull
|
||||
objectWithNulls x = makeObj x
|
||||
nonNull x = NotNull x
|
||||
null' = Null
|
||||
(.=) a b = (a, showJSON b)
|
||||
toNullable (Just x) = NotNull x
|
||||
toNullable Nothing = Null
|
||||
fromNullable (NotNull x) = Just x
|
||||
fromNullable Null = Nothing
|
||||
fromNotNull (NotNull x) = x
|
||||
fromNotNull Null = error "Value is Null"
|
||||
mapNullable f Null = Null
|
||||
mapNullable f (NotNull x) = NotNull (f x)
|
||||
|
||||
@@ -40,7 +38,7 @@ find :: JSON a => String -> JSValue -> Nullable a
|
||||
find k obj = obj .? k
|
||||
|
||||
update :: JSON a => String -> a -> JSValue -> JSValue
|
||||
update k v (JSObject obj) = object $ fromJSObject obj ++ [k .= v]
|
||||
update k v (JSObject obj) = object $ (fromJSObject obj) ++ [k .= v]
|
||||
|
||||
instance JSON a => JSON (Nullable a) where
|
||||
showJSON (NotNull x) = showJSON x
|
||||
@@ -49,7 +47,7 @@ instance JSON a => JSON (Nullable a) where
|
||||
readJSON x = readJSON x
|
||||
|
||||
|
||||
newtype Base64 = Base64 B.ByteString deriving (Eq, Show)
|
||||
newtype Base64 = Base64 B.ByteString
|
||||
|
||||
instance JSON Base64 where
|
||||
showJSON (Base64 bs) = showJSON (BS.unpack $ B64.encode bs)
|
||||
|
||||
@@ -9,7 +9,6 @@ newtype Memory = Memory
|
||||
{
|
||||
memoryMaxPages :: Nullable Int
|
||||
}
|
||||
deriving Eq
|
||||
|
||||
instance JSON Memory where
|
||||
showJSON (Memory max) =
|
||||
@@ -27,7 +26,6 @@ data HTTPRequest = HTTPRequest
|
||||
, headers :: Nullable [(String, String)]
|
||||
, method :: Nullable String
|
||||
}
|
||||
deriving Eq
|
||||
|
||||
makeKV x =
|
||||
object [(k, showJSON v) | (k, v) <- x]
|
||||
@@ -57,7 +55,6 @@ data WasmFile = WasmFile
|
||||
, fileName :: Nullable String
|
||||
, fileHash :: Nullable String
|
||||
}
|
||||
deriving Eq
|
||||
|
||||
instance JSON WasmFile where
|
||||
showJSON (WasmFile path name hash) =
|
||||
@@ -83,7 +80,9 @@ data WasmData = WasmData
|
||||
, dataName :: Nullable String
|
||||
, dataHash :: Nullable String
|
||||
}
|
||||
deriving Eq
|
||||
|
||||
|
||||
|
||||
|
||||
instance JSON WasmData where
|
||||
showJSON (WasmData bytes name hash) =
|
||||
@@ -111,7 +110,6 @@ data WasmURL = WasmURL
|
||||
, urlName :: Nullable String
|
||||
, urlHash :: Nullable String
|
||||
}
|
||||
deriving Eq
|
||||
|
||||
|
||||
instance JSON WasmURL where
|
||||
@@ -129,7 +127,7 @@ instance JSON WasmURL where
|
||||
Just req -> Ok (WasmURL req name hash)
|
||||
|
||||
-- | Specifies where to get WASM module data
|
||||
data Wasm = File WasmFile | Data WasmData | URL WasmURL deriving Eq
|
||||
data Wasm = File WasmFile | Data WasmData | URL WasmURL
|
||||
|
||||
instance JSON Wasm where
|
||||
showJSON x =
|
||||
|
||||
@@ -1,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,
|
||||
|
||||
@@ -1,69 +1,40 @@
|
||||
module Extism (
|
||||
module Extism.Manifest,
|
||||
Function(..),
|
||||
Plugin(..),
|
||||
CancelHandle(..),
|
||||
LogLevel(..),
|
||||
Error(..),
|
||||
Result(..),
|
||||
toByteString,
|
||||
fromByteString,
|
||||
extismVersion,
|
||||
plugin,
|
||||
pluginFromManifest,
|
||||
isValid,
|
||||
setConfig,
|
||||
setLogFile,
|
||||
functionExists,
|
||||
call,
|
||||
cancelHandle,
|
||||
cancel,
|
||||
pluginID,
|
||||
unwrap
|
||||
) where
|
||||
|
||||
module Extism (module Extism, module Extism.Manifest) where
|
||||
import Data.Int
|
||||
import Data.Word
|
||||
import Control.Monad (void)
|
||||
import Foreign.ForeignPtr
|
||||
import Foreign.C.String
|
||||
import Foreign.Ptr
|
||||
import Foreign.Marshal.Array
|
||||
import Foreign.Marshal.Alloc
|
||||
import Foreign.Storable
|
||||
import Foreign.StablePtr
|
||||
import Foreign.Concurrent
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
import Data.ByteString as B
|
||||
import Data.ByteString.Internal (c2w, w2c)
|
||||
import Data.ByteString.Unsafe (unsafeUseAsCString)
|
||||
import qualified Text.JSON (encode, toJSObject, showJSON)
|
||||
import Data.Bifunctor (second)
|
||||
import Text.JSON (encode, toJSObject, showJSON)
|
||||
import Extism.Manifest (Manifest, toString)
|
||||
import Extism.Bindings
|
||||
import qualified Data.UUID (UUID, fromByteString)
|
||||
|
||||
-- | Host function, see 'Extism.HostFunction.hostFunction'
|
||||
data Function = Function (ForeignPtr ExtismFunction) (StablePtr ()) deriving Eq
|
||||
-- | Context for managing plugins
|
||||
newtype Context = Context (ForeignPtr ExtismContext)
|
||||
|
||||
-- | Plugins can be used to call WASM function
|
||||
newtype Plugin = Plugin (ForeignPtr ExtismPlugin) deriving Eq
|
||||
|
||||
-- | Cancellation handle for Plugins
|
||||
newtype CancelHandle = CancelHandle (Ptr ExtismCancelHandle)
|
||||
data Plugin = Plugin Context Int32
|
||||
|
||||
-- | Log level
|
||||
data LogLevel = LogError | LogWarn | LogInfo | LogDebug | LogTrace deriving (Show, Eq)
|
||||
data LogLevel = Error | Warn | Info | Debug | Trace deriving (Show)
|
||||
|
||||
-- | Extism error
|
||||
newtype Error = ExtismError String deriving (Show, Eq)
|
||||
newtype Error = ExtismError String deriving Show
|
||||
|
||||
-- | Result type
|
||||
type Result a = Either Error a
|
||||
|
||||
-- | Helper function to convert a 'String' to a 'ByteString'
|
||||
toByteString :: String -> B.ByteString
|
||||
toByteString x = B.pack (map c2w x)
|
||||
toByteString :: String -> ByteString
|
||||
toByteString x = B.pack (Prelude.map c2w x)
|
||||
|
||||
-- | Helper function to convert a 'ByteString' to a 'String'
|
||||
fromByteString :: B.ByteString -> String
|
||||
fromByteString bs = map w2c $ B.unpack bs
|
||||
fromByteString :: ByteString -> String
|
||||
fromByteString bs = Prelude.map w2c $ B.unpack bs
|
||||
|
||||
-- | Get the Extism version string
|
||||
extismVersion :: () -> IO String
|
||||
@@ -71,55 +42,93 @@ extismVersion () = do
|
||||
v <- extism_version
|
||||
peekCString v
|
||||
|
||||
-- | Remove all registered plugins in a 'Context'
|
||||
reset :: Context -> IO ()
|
||||
reset (Context ctx) =
|
||||
withForeignPtr ctx extism_context_reset
|
||||
|
||||
-- | Create a new 'Context'
|
||||
newContext :: IO Context
|
||||
newContext = do
|
||||
ptr <- extism_context_new
|
||||
fptr <- newForeignPtr extism_context_free ptr
|
||||
return (Context fptr)
|
||||
|
||||
-- | Execute a function with a new 'Context' that is destroyed when it returns
|
||||
withContext :: (Context -> IO a) -> IO a
|
||||
withContext f = do
|
||||
ctx <- newContext
|
||||
f ctx
|
||||
|
||||
-- | Create a 'Plugin' from a WASM module, `useWasi` determines if WASI should
|
||||
-- | be linked
|
||||
plugin :: B.ByteString -> [Function] -> Bool -> IO (Result Plugin)
|
||||
plugin wasm functions useWasi =
|
||||
let nfunctions = fromIntegral (length functions) in
|
||||
plugin :: Context -> B.ByteString -> Bool -> IO (Result Plugin)
|
||||
plugin c wasm useWasi =
|
||||
let length = fromIntegral (B.length wasm) in
|
||||
let wasi = fromInteger (if useWasi then 1 else 0) in
|
||||
let Context ctx = c in
|
||||
do
|
||||
withForeignPtr ctx (\ctx -> do
|
||||
p <- unsafeUseAsCString wasm (\s ->
|
||||
extism_plugin_new ctx (castPtr s) length wasi)
|
||||
if p < 0 then do
|
||||
err <- extism_error ctx (-1)
|
||||
e <- peekCString err
|
||||
return $ Left (ExtismError e)
|
||||
else
|
||||
return $ Right (Plugin c p))
|
||||
|
||||
-- | Create a 'Plugin' from a 'Manifest'
|
||||
pluginFromManifest :: Context -> Manifest -> Bool -> IO (Result Plugin)
|
||||
pluginFromManifest ctx manifest useWasi =
|
||||
let wasm = toByteString $ toString manifest in
|
||||
plugin ctx wasm useWasi
|
||||
|
||||
-- | Update a 'Plugin' with a new WASM module
|
||||
update :: Plugin -> B.ByteString -> Bool -> IO (Result ())
|
||||
update (Plugin (Context ctx) id) wasm useWasi =
|
||||
let length = fromIntegral (B.length wasm) in
|
||||
let wasi = fromInteger (if useWasi then 1 else 0) in
|
||||
do
|
||||
funcs <- mapM (\(Function ptr _) -> withForeignPtr ptr (\x -> do return x)) functions
|
||||
alloca (\e-> do
|
||||
let errmsg = (e :: Ptr CString)
|
||||
p <- unsafeUseAsCString wasm (\s ->
|
||||
withArray funcs (\funcs ->
|
||||
extism_plugin_new (castPtr s) length funcs nfunctions wasi errmsg ))
|
||||
if p == nullPtr then do
|
||||
err <- peek errmsg
|
||||
e <- peekCString err
|
||||
extism_plugin_new_error_free err
|
||||
withForeignPtr ctx (\ctx -> do
|
||||
b <- unsafeUseAsCString wasm (\s ->
|
||||
extism_plugin_update ctx id (castPtr s) length wasi)
|
||||
if b <= 0 then do
|
||||
err <- extism_error ctx (-1)
|
||||
e <- peekCString err
|
||||
return $ Left (ExtismError e)
|
||||
else do
|
||||
ptr <- Foreign.Concurrent.newForeignPtr p (extism_plugin_free p)
|
||||
return $ Right (Plugin ptr))
|
||||
else
|
||||
return (Right ()))
|
||||
|
||||
-- | Create a 'Plugin' from a 'Manifest'
|
||||
pluginFromManifest :: Manifest -> [Function] -> Bool -> IO (Result Plugin)
|
||||
pluginFromManifest manifest functions useWasi =
|
||||
-- | Update a 'Plugin' with a new 'Manifest'
|
||||
updateManifest :: Plugin -> Manifest -> Bool -> IO (Result ())
|
||||
updateManifest plugin manifest useWasi =
|
||||
let wasm = toByteString $ toString manifest in
|
||||
plugin wasm functions useWasi
|
||||
update plugin wasm useWasi
|
||||
|
||||
-- | Check if a 'Plugin' is valid
|
||||
isValid :: Plugin -> IO Bool
|
||||
isValid (Plugin p) = withForeignPtr p (\x -> return (x /= nullPtr))
|
||||
isValid :: Plugin -> Bool
|
||||
isValid (Plugin _ p) = p >= 0
|
||||
|
||||
-- | Set configuration values for a plugin
|
||||
setConfig :: Plugin -> [(String, Maybe String)] -> IO Bool
|
||||
setConfig (Plugin plugin) x =
|
||||
let obj = Text.JSON.toJSObject [(k, Text.JSON.showJSON v) | (k, v) <- x] in
|
||||
let bs = toByteString (Text.JSON.encode obj) in
|
||||
let length = fromIntegral (B.length bs) in
|
||||
unsafeUseAsCString bs (\s -> do
|
||||
withForeignPtr plugin (\plugin-> do
|
||||
b <- extism_plugin_config plugin (castPtr s) length
|
||||
return $ b /= 0))
|
||||
setConfig (Plugin (Context ctx) plugin) x =
|
||||
if plugin < 0
|
||||
then return False
|
||||
else
|
||||
let obj = toJSObject [(k, showJSON v) | (k, v) <- x] in
|
||||
let bs = toByteString (encode obj) in
|
||||
let length = fromIntegral (B.length bs) in
|
||||
unsafeUseAsCString bs (\s -> do
|
||||
withForeignPtr ctx (\ctx -> do
|
||||
b <- extism_plugin_config ctx plugin (castPtr s) length
|
||||
return $ b /= 0))
|
||||
|
||||
levelStr LogError = "error"
|
||||
levelStr LogDebug = "debug"
|
||||
levelStr LogWarn = "warn"
|
||||
levelStr LogTrace = "trace"
|
||||
levelStr LogInfo = "info"
|
||||
levelStr Error = "error"
|
||||
levelStr Debug = "debug"
|
||||
levelStr Warn = "warn"
|
||||
levelStr Trace = "trace"
|
||||
levelStr Info = "info"
|
||||
|
||||
-- | Set the log file and level, this is a global configuration
|
||||
setLogFile :: String -> LogLevel -> IO Bool
|
||||
@@ -132,63 +141,34 @@ setLogFile filename level =
|
||||
|
||||
-- | Check if a function exists in the given plugin
|
||||
functionExists :: Plugin -> String -> IO Bool
|
||||
functionExists (Plugin plugin) name = do
|
||||
withForeignPtr plugin (\plugin -> do
|
||||
b <- withCString name (extism_plugin_function_exists plugin)
|
||||
functionExists (Plugin (Context ctx) plugin) name = do
|
||||
withForeignPtr ctx (\ctx -> do
|
||||
b <- withCString name (extism_plugin_function_exists ctx plugin)
|
||||
if b == 1 then return True else return False)
|
||||
|
||||
--- | Call a function provided by the given plugin
|
||||
call :: Plugin -> String -> B.ByteString -> IO (Result B.ByteString)
|
||||
call (Plugin plugin) name input =
|
||||
call (Plugin (Context ctx) plugin) name input =
|
||||
let length = fromIntegral (B.length input) in
|
||||
do
|
||||
withForeignPtr plugin (\plugin -> do
|
||||
withForeignPtr ctx (\ctx -> do
|
||||
rc <- withCString name (\name ->
|
||||
unsafeUseAsCString input (\input ->
|
||||
extism_plugin_call plugin name (castPtr input) length))
|
||||
err <- extism_error plugin
|
||||
extism_plugin_call ctx plugin name (castPtr input) length))
|
||||
err <- extism_error ctx plugin
|
||||
if err /= nullPtr
|
||||
then do e <- peekCString err
|
||||
return $ Left (ExtismError e)
|
||||
else if rc == 0
|
||||
then do
|
||||
length <- extism_plugin_output_length plugin
|
||||
ptr <- extism_plugin_output_data plugin
|
||||
buf <- B.packCStringLen (castPtr ptr, fromIntegral length)
|
||||
length <- extism_plugin_output_length ctx plugin
|
||||
ptr <- extism_plugin_output_data ctx plugin
|
||||
buf <- packCStringLen (castPtr ptr, fromIntegral length)
|
||||
return $ Right buf
|
||||
else return $ Left (ExtismError "Call failed"))
|
||||
|
||||
-- | Call a function with a string argument and return a string
|
||||
callString :: Plugin -> String -> String -> IO (Result String)
|
||||
callString p name input = do
|
||||
res <- call p name (toByteString input)
|
||||
case res of
|
||||
Left x -> return $ Left x
|
||||
Right x -> return $ Right (fromByteString x)
|
||||
|
||||
|
||||
-- | Create a new 'CancelHandle' that can be used to cancel a running plugin
|
||||
-- | from another thread.
|
||||
cancelHandle :: Plugin -> IO CancelHandle
|
||||
cancelHandle (Plugin plugin) = do
|
||||
handle <- withForeignPtr plugin extism_plugin_cancel_handle
|
||||
return (CancelHandle handle)
|
||||
|
||||
-- | Cancel a running plugin using a 'CancelHandle'
|
||||
cancel :: CancelHandle -> IO Bool
|
||||
cancel (CancelHandle handle) =
|
||||
extism_plugin_cancel handle
|
||||
|
||||
pluginID :: Plugin -> IO Data.UUID.UUID
|
||||
pluginID (Plugin plugin) =
|
||||
withForeignPtr plugin (\plugin -> do
|
||||
ptr <- extism_plugin_id plugin
|
||||
buf <- B.packCStringLen (castPtr ptr, 16)
|
||||
case Data.UUID.fromByteString (BL.fromStrict buf) of
|
||||
Nothing -> error "Invalid Plugin ID"
|
||||
Just x -> return x)
|
||||
|
||||
|
||||
unwrap (Right x) = x
|
||||
unwrap (Left (ExtismError msg)) = do
|
||||
error msg
|
||||
-- | Free a 'Plugin', this will automatically be called for every plugin
|
||||
-- | associated with a 'Context' when that 'Context' is freed
|
||||
free :: Plugin -> IO ()
|
||||
free (Plugin (Context ctx) plugin) =
|
||||
withForeignPtr ctx (`extism_plugin_free` plugin)
|
||||
|
||||
@@ -7,114 +7,20 @@ import Foreign.Ptr
|
||||
import Foreign.C.String
|
||||
import Data.Int
|
||||
import Data.Word
|
||||
import Foreign.Storable
|
||||
import Foreign.Marshal.Array
|
||||
import Foreign.StablePtr
|
||||
|
||||
type FreeCallback = Ptr () -> IO ()
|
||||
newtype ExtismContext = ExtismContext () deriving Show
|
||||
|
||||
newtype ExtismPlugin = ExtismPlugin () deriving Show
|
||||
newtype ExtismFunction = ExtismFunction () deriving Show
|
||||
newtype ExtismCancelHandle = ExtismCancelHandle () deriving Show
|
||||
newtype ExtismCurrentPlugin = ExtismCurrentPlugin () deriving Show
|
||||
data ValType = I32 | I64 | F32 | F64 | V128 | FuncRef | ExternRef deriving (Show, Eq)
|
||||
data Val = ValI32 Int32 | ValI64 Int64 | ValF32 Float | ValF64 Double deriving (Show, Eq)
|
||||
|
||||
typeOfVal (ValI32 _) = I32
|
||||
typeOfVal (ValI64 _) = I64
|
||||
typeOfVal (ValF32 _) = F32
|
||||
typeOfVal (ValF64 _) = F64
|
||||
|
||||
type CCallback = Ptr ExtismCurrentPlugin -> Ptr Val -> Word64 -> Ptr Val -> Word64 -> Ptr () -> IO ()
|
||||
|
||||
_32Bit = sizeOf (undefined :: Int) == 4
|
||||
|
||||
instance Storable Val where
|
||||
sizeOf _ =
|
||||
if _32Bit then 12 else 16
|
||||
alignment _ = 1
|
||||
peek ptr = do
|
||||
let offs = if _32Bit then 4 else 8
|
||||
t <- valTypeOfInt <$> peekByteOff ptr 0
|
||||
case t of
|
||||
I32 -> ValI32 <$> peekByteOff ptr offs
|
||||
I64 -> ValI64 <$> peekByteOff ptr offs
|
||||
F32 -> ValF32 <$> peekByteOff ptr offs
|
||||
F64 -> ValF64 <$> peekByteOff ptr offs
|
||||
poke ptr x = do
|
||||
let offs = if _32Bit then 4 else 8
|
||||
pokeByteOff ptr 0 (typeOfVal x)
|
||||
case x of
|
||||
ValI32 x -> pokeByteOff ptr offs x
|
||||
ValI64 x -> pokeByteOff ptr offs x
|
||||
ValF32 x -> pokeByteOff ptr offs x
|
||||
ValF64 x -> pokeByteOff ptr offs x
|
||||
|
||||
|
||||
intOfValType :: ValType -> CInt
|
||||
intOfValType I32 = 0
|
||||
intOfValType I64 = 1
|
||||
intOfValType F32 = 2
|
||||
intOfValType F64 = 3
|
||||
intOfValType V128 = 4
|
||||
intOfValType FuncRef = 5
|
||||
intOfValType ExternRef = 6
|
||||
|
||||
valTypeOfInt :: CInt -> ValType
|
||||
valTypeOfInt 0 = I32
|
||||
valTypeOfInt 1 = I64
|
||||
valTypeOfInt 2 = F32
|
||||
valTypeOfInt 3 = F64
|
||||
valTypeOfInt 4 = V128
|
||||
valTypeOfInt 5 = FuncRef
|
||||
valTypeOfInt 6 = ExternRef
|
||||
valTypeOfInt _ = error "Invalid ValType"
|
||||
|
||||
instance Storable ValType where
|
||||
sizeOf _ = 4
|
||||
alignment _ = 1
|
||||
peek ptr = do
|
||||
x <- peekByteOff ptr 0
|
||||
return $ valTypeOfInt (x :: CInt)
|
||||
poke ptr x = do
|
||||
pokeByteOff ptr 0 (intOfValType x)
|
||||
|
||||
foreign import ccall safe "extism.h extism_plugin_new" extism_plugin_new :: Ptr Word8 -> Word64 -> Ptr (Ptr ExtismFunction) -> Word64 -> CBool -> Ptr CString -> IO (Ptr ExtismPlugin)
|
||||
foreign import ccall safe "extism.h extism_plugin_call" extism_plugin_call :: Ptr ExtismPlugin -> CString -> Ptr Word8 -> Word64 -> IO Int32
|
||||
foreign import ccall safe "extism.h extism_plugin_function_exists" extism_plugin_function_exists :: Ptr ExtismPlugin -> CString -> IO CBool
|
||||
foreign import ccall safe "extism.h extism_plugin_error" extism_error :: Ptr ExtismPlugin -> IO CString
|
||||
foreign import ccall safe "extism.h extism_plugin_output_length" extism_plugin_output_length :: Ptr ExtismPlugin -> IO Word64
|
||||
foreign import ccall safe "extism.h extism_plugin_output_data" extism_plugin_output_data :: Ptr ExtismPlugin -> IO (Ptr Word8)
|
||||
foreign import ccall safe "extism.h extism_log_file" extism_log_file :: CString -> CString -> IO CBool
|
||||
foreign import ccall safe "extism.h extism_plugin_config" extism_plugin_config :: Ptr ExtismPlugin -> Ptr Word8 -> Int64 -> IO CBool
|
||||
foreign import ccall safe "extism.h extism_plugin_free" extism_plugin_free :: Ptr ExtismPlugin -> IO ()
|
||||
foreign import ccall safe "extism.h extism_plugin_new_error_free" extism_plugin_new_error_free :: CString -> IO ()
|
||||
foreign import ccall safe "extism.h extism_version" extism_version :: IO CString
|
||||
foreign import ccall safe "extism.h extism_plugin_id" extism_plugin_id :: Ptr ExtismPlugin -> IO (Ptr Word8)
|
||||
foreign import ccall safe "extism.h extism_plugin_cancel_handle" extism_plugin_cancel_handle :: Ptr ExtismPlugin -> IO (Ptr ExtismCancelHandle)
|
||||
foreign import ccall safe "extism.h extism_plugin_cancel" extism_plugin_cancel :: Ptr ExtismCancelHandle -> IO Bool
|
||||
|
||||
foreign import ccall safe "extism.h extism_function_new" extism_function_new :: CString -> Ptr ValType -> Word64 -> Ptr ValType -> Word64 -> FunPtr CCallback -> Ptr () -> FunPtr FreeCallback -> IO (Ptr ExtismFunction)
|
||||
foreign import ccall safe "extism.h extism_function_free" extism_function_free :: Ptr ExtismFunction -> IO ()
|
||||
foreign import ccall safe "extism.h extism_current_plugin_memory" extism_current_plugin_memory :: Ptr ExtismCurrentPlugin -> IO (Ptr Word8)
|
||||
foreign import ccall safe "extism.h extism_current_plugin_memory_alloc" extism_current_plugin_memory_alloc :: Ptr ExtismCurrentPlugin -> Word64 -> IO Word64
|
||||
foreign import ccall safe "extism.h extism_current_plugin_memory_length" extism_current_plugin_memory_length :: Ptr ExtismCurrentPlugin -> Word64 -> IO Word64
|
||||
foreign import ccall safe "extism.h extism_current_plugin_memory_free" extism_current_plugin_memory_free :: Ptr ExtismCurrentPlugin -> Word64 -> IO ()
|
||||
|
||||
freePtr ptr = do
|
||||
let s = castPtrToStablePtr ptr
|
||||
(a, b, c) <- deRefStablePtr s
|
||||
freeHaskellFunPtr b
|
||||
freeHaskellFunPtr c
|
||||
freeStablePtr s
|
||||
|
||||
foreign import ccall "wrapper" freePtrWrap :: FreeCallback -> IO (FunPtr FreeCallback)
|
||||
|
||||
foreign import ccall "wrapper" callbackWrap :: CCallback -> IO (FunPtr CCallback)
|
||||
|
||||
callback :: (Ptr ExtismCurrentPlugin -> [Val] -> a -> IO [Val]) -> (Ptr ExtismCurrentPlugin -> Ptr Val -> Word64 -> Ptr Val -> Word64 -> Ptr () -> IO ())
|
||||
callback f plugin params nparams results nresults ptr = do
|
||||
p <- peekArray (fromIntegral nparams) params
|
||||
(userData, _, _) <- deRefStablePtr (castPtrToStablePtr ptr)
|
||||
res <- f plugin p userData
|
||||
pokeArray results res
|
||||
foreign import ccall unsafe "extism.h extism_context_new" extism_context_new :: IO (Ptr ExtismContext)
|
||||
foreign import ccall unsafe "extism.h &extism_context_free" extism_context_free :: FunPtr (Ptr ExtismContext -> IO ())
|
||||
foreign import ccall unsafe "extism.h extism_plugin_new" extism_plugin_new :: Ptr ExtismContext -> Ptr Word8 -> Word64 -> CBool -> IO Int32
|
||||
foreign import ccall unsafe "extism.h extism_plugin_update" extism_plugin_update :: Ptr ExtismContext -> Int32 -> Ptr Word8 -> Word64 -> CBool -> IO CBool
|
||||
foreign import ccall unsafe "extism.h extism_plugin_call" extism_plugin_call :: Ptr ExtismContext -> Int32 -> CString -> Ptr Word8 -> Word64 -> IO Int32
|
||||
foreign import ccall unsafe "extism.h extism_plugin_function_exists" extism_plugin_function_exists :: Ptr ExtismContext -> Int32 -> CString -> IO CBool
|
||||
foreign import ccall unsafe "extism.h extism_error" extism_error :: Ptr ExtismContext -> Int32 -> IO CString
|
||||
foreign import ccall unsafe "extism.h extism_plugin_output_length" extism_plugin_output_length :: Ptr ExtismContext -> Int32 -> IO Word64
|
||||
foreign import ccall unsafe "extism.h extism_plugin_output_data" extism_plugin_output_data :: Ptr ExtismContext -> Int32 -> IO (Ptr Word8)
|
||||
foreign import ccall unsafe "extism.h extism_log_file" extism_log_file :: CString -> CString -> IO CBool
|
||||
foreign import ccall unsafe "extism.h extism_plugin_config" extism_plugin_config :: Ptr ExtismContext -> Int32 -> Ptr Word8 -> Int64 -> IO CBool
|
||||
foreign import ccall unsafe "extism.h extism_plugin_free" extism_plugin_free :: Ptr ExtismContext -> Int32 -> IO ()
|
||||
foreign import ccall unsafe "extism.h extism_context_reset" extism_context_reset :: Ptr ExtismContext -> IO ()
|
||||
foreign import ccall unsafe "extism.h extism_version" extism_version :: IO CString
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
|
||||
module Extism.HostFunction(
|
||||
CurrentPlugin(..),
|
||||
ValType(..),
|
||||
Val(..),
|
||||
MemoryHandle,
|
||||
memoryAlloc,
|
||||
memoryLength,
|
||||
memoryFree,
|
||||
memory,
|
||||
memoryOffset,
|
||||
memoryBytes,
|
||||
memoryString,
|
||||
allocBytes,
|
||||
allocString,
|
||||
toI32,
|
||||
toI64,
|
||||
toF32,
|
||||
toF64,
|
||||
fromI32,
|
||||
fromI64,
|
||||
fromF32,
|
||||
fromF64,
|
||||
hostFunction
|
||||
) where
|
||||
|
||||
import Extism
|
||||
import Extism.Bindings
|
||||
import Data.Word
|
||||
import qualified Data.ByteString as B
|
||||
import Foreign.Ptr
|
||||
import Foreign.ForeignPtr
|
||||
import Foreign.C.String
|
||||
import Foreign.StablePtr
|
||||
import Foreign.Concurrent
|
||||
import Foreign.Marshal.Array
|
||||
import qualified Data.ByteString.Internal as BS (c2w)
|
||||
|
||||
-- | Access the plugin that is currently executing from inside a host function
|
||||
type CurrentPlugin = Ptr ExtismCurrentPlugin
|
||||
|
||||
-- | A memory handle represents an allocated block of Extism memory
|
||||
newtype MemoryHandle = MemoryHandle Word64 deriving (Num, Enum, Eq, Ord, Real, Integral, Show)
|
||||
|
||||
-- | Allocate a new handle of the given size
|
||||
memoryAlloc :: CurrentPlugin -> Word64 -> IO MemoryHandle
|
||||
memoryAlloc p n = MemoryHandle <$> extism_current_plugin_memory_alloc p n
|
||||
|
||||
-- | Get the length of a handle, returns 0 if the handle is invalid
|
||||
memoryLength :: CurrentPlugin -> MemoryHandle -> IO Word64
|
||||
memoryLength p (MemoryHandle offs) = extism_current_plugin_memory_length p offs
|
||||
|
||||
-- | Free allocated memory
|
||||
memoryFree :: CurrentPlugin -> MemoryHandle -> IO ()
|
||||
memoryFree p (MemoryHandle offs) = extism_current_plugin_memory_free p offs
|
||||
|
||||
-- | Access a pointer to the entire memory region
|
||||
memory :: CurrentPlugin -> IO (Ptr Word8)
|
||||
memory = extism_current_plugin_memory
|
||||
|
||||
-- | Access the pointer for the given 'MemoryHandle'
|
||||
memoryOffset :: CurrentPlugin -> MemoryHandle -> IO (Ptr Word8)
|
||||
memoryOffset plugin (MemoryHandle offs) = do
|
||||
x <- extism_current_plugin_memory plugin
|
||||
return $ plusPtr x (fromIntegral offs)
|
||||
|
||||
-- | Access the data associated with a handle as a 'ByteString'
|
||||
memoryBytes :: CurrentPlugin -> MemoryHandle -> IO B.ByteString
|
||||
memoryBytes plugin offs = do
|
||||
ptr <- memoryOffset plugin offs
|
||||
len <- memoryLength plugin offs
|
||||
arr <- peekArray (fromIntegral len) ptr
|
||||
return $ B.pack arr
|
||||
|
||||
|
||||
-- | Access the data associated with a handle as a 'String'
|
||||
memoryString :: CurrentPlugin -> MemoryHandle -> IO String
|
||||
memoryString plugin offs = do
|
||||
ptr <- memoryOffset plugin offs
|
||||
len <- memoryLength plugin offs
|
||||
arr <- peekArray (fromIntegral len) ptr
|
||||
return $ fromByteString $ B.pack arr
|
||||
|
||||
-- | Allocate memory and copy an existing 'ByteString' into it
|
||||
allocBytes :: CurrentPlugin -> B.ByteString -> IO MemoryHandle
|
||||
allocBytes plugin s = do
|
||||
let length = B.length s
|
||||
offs <- memoryAlloc plugin (fromIntegral length)
|
||||
ptr <- memoryOffset plugin offs
|
||||
pokeArray ptr (B.unpack s)
|
||||
return offs
|
||||
|
||||
|
||||
-- | Allocate memory and copy an existing 'String' into it
|
||||
allocString :: CurrentPlugin -> String -> IO MemoryHandle
|
||||
allocString plugin s = do
|
||||
let length = Prelude.length s
|
||||
offs <- memoryAlloc plugin (fromIntegral length)
|
||||
ptr <- memoryOffset plugin offs
|
||||
pokeArray ptr (Prelude.map BS.c2w s)
|
||||
return offs
|
||||
|
||||
-- | Create a new I32 'Val'
|
||||
toI32 :: Integral a => a -> Val
|
||||
toI32 x = ValI32 (fromIntegral x)
|
||||
|
||||
-- | Create a new I64 'Val'
|
||||
toI64 :: Integral a => a -> Val
|
||||
toI64 x = ValI64 (fromIntegral x)
|
||||
|
||||
-- | Create a new F32 'Val'
|
||||
toF32 :: Float -> Val
|
||||
toF32 = ValF32
|
||||
|
||||
-- | Create a new F64 'Val'
|
||||
toF64 :: Double -> Val
|
||||
toF64 = ValF64
|
||||
|
||||
-- | Get I32 'Val'
|
||||
fromI32 :: Integral a => Val -> Maybe a
|
||||
fromI32 (ValI32 x) = Just (fromIntegral x)
|
||||
fromI32 _ = Nothing
|
||||
|
||||
-- | Get I64 'Val'
|
||||
fromI64 :: Integral a => Val -> Maybe a
|
||||
fromI64 (ValI64 x) = Just (fromIntegral x)
|
||||
fromI64 _ = Nothing
|
||||
|
||||
-- | Get F32 'Val'
|
||||
fromF32 :: Val -> Maybe Float
|
||||
fromF32 (ValF32 x) = Just x
|
||||
fromF32 _ = Nothing
|
||||
|
||||
-- | Get F64 'Val'
|
||||
fromF64 :: Val -> Maybe Double
|
||||
fromF64 (ValF64 x) = Just x
|
||||
fromF64 _ = Nothing
|
||||
|
||||
-- | Create a new 'Function' that can be called from a 'Plugin'
|
||||
hostFunction :: String -> [ValType] -> [ValType] -> (CurrentPlugin -> [Val] -> a -> IO [Val]) -> a -> IO Function
|
||||
hostFunction name params results f v =
|
||||
let nparams = fromIntegral $ length params in
|
||||
let nresults = fromIntegral $ length results in
|
||||
do
|
||||
cb <- callbackWrap (callback f :: CCallback)
|
||||
free <- freePtrWrap freePtr
|
||||
userData <- newStablePtr (v, free, cb)
|
||||
let userDataPtr = castStablePtrToPtr userData
|
||||
x <- withCString name (\name -> do
|
||||
withArray params (\params ->
|
||||
withArray results (\results -> do
|
||||
extism_function_new name params nparams results nresults cb userDataPtr free)))
|
||||
let freeFn = extism_function_free x
|
||||
fptr <- Foreign.Concurrent.newForeignPtr x freeFn
|
||||
return $ Function fptr (castPtrToStablePtr userDataPtr)
|
||||
|
||||
@@ -1,61 +1,58 @@
|
||||
import Test.HUnit
|
||||
import Extism
|
||||
import Extism.Manifest
|
||||
import Extism.HostFunction
|
||||
|
||||
|
||||
assertUnwrap (Right x) = return x
|
||||
assertUnwrap (Left (ExtismError msg)) =
|
||||
unwrap (Right x) = return x
|
||||
unwrap (Left (ExtismError msg)) =
|
||||
assertFailure msg
|
||||
|
||||
defaultManifest = manifest [wasmFile "../../wasm/code.wasm"]
|
||||
hostFunctionManifest = manifest [wasmFile "../../wasm/code-functions.wasm"]
|
||||
defaultManifest = manifest [wasmFile "test/code.wasm"]
|
||||
|
||||
initPlugin :: IO Plugin
|
||||
initPlugin =
|
||||
Extism.pluginFromManifest defaultManifest [] False >>= assertUnwrap
|
||||
initPlugin :: Context -> IO Plugin
|
||||
initPlugin context =
|
||||
Extism.pluginFromManifest context defaultManifest False >>= unwrap
|
||||
|
||||
pluginFunctionExists = do
|
||||
p <- initPlugin
|
||||
exists <- functionExists p "count_vowels"
|
||||
assertBool "function exists" exists
|
||||
exists' <- functionExists p "function_doesnt_exist"
|
||||
assertBool "function doesn't exist" (not exists')
|
||||
withContext (\ctx -> do
|
||||
p <- initPlugin ctx
|
||||
exists <- functionExists p "count_vowels"
|
||||
assertBool "function exists" exists
|
||||
exists' <- functionExists p "function_doesnt_exist"
|
||||
assertBool "function doesn't exist" (not exists'))
|
||||
|
||||
checkCallResult p = do
|
||||
res <- call p "count_vowels" (toByteString "this is a test") >>= assertUnwrap
|
||||
assertEqual "count vowels output" "{\"count\": 4}" (fromByteString res)
|
||||
res <- call p "count_vowels" (toByteString "this is a test") >>= unwrap
|
||||
assertEqual "count vowels output" "{\"count\": 4}" (fromByteString res)
|
||||
|
||||
pluginCall = do
|
||||
p <- initPlugin
|
||||
checkCallResult p
|
||||
|
||||
|
||||
hello plugin params () = do
|
||||
putStrLn "Hello from Haskell!"
|
||||
offs <- allocBytes plugin (toByteString "{\"count\": 999}")
|
||||
return [toI64 offs]
|
||||
|
||||
pluginCallHostFunction = do
|
||||
p <- Extism.pluginFromManifest hostFunctionManifest [] False >>= assertUnwrap
|
||||
res <- call p "count_vowels" (toByteString "this is a test") >>= assertUnwrap
|
||||
assertEqual "count vowels output" "{\"count\": 999}" (fromByteString res)
|
||||
withContext (\ctx -> do
|
||||
p <- initPlugin ctx
|
||||
checkCallResult p)
|
||||
|
||||
pluginMultiple = do
|
||||
p <- initPlugin
|
||||
withContext (\ctx -> do
|
||||
p <- initPlugin ctx
|
||||
checkCallResult p
|
||||
q <- initPlugin
|
||||
r <- initPlugin
|
||||
q <- initPlugin ctx
|
||||
r <- initPlugin ctx
|
||||
checkCallResult q
|
||||
checkCallResult r
|
||||
checkCallResult r)
|
||||
|
||||
pluginUpdate = do
|
||||
withContext (\ctx -> do
|
||||
p <- initPlugin ctx
|
||||
updateManifest p defaultManifest True >>= unwrap
|
||||
checkCallResult p)
|
||||
|
||||
pluginConfig = do
|
||||
p <- initPlugin
|
||||
b <- setConfig p [("a", Just "1"), ("b", Just "2"), ("c", Just "3"), ("d", Nothing)]
|
||||
assertBool "set config" b
|
||||
withContext (\ctx -> do
|
||||
p <- initPlugin ctx
|
||||
b <- setConfig p [("a", Just "1"), ("b", Just "2"), ("c", Just "3"), ("d", Nothing)]
|
||||
assertBool "set config" b)
|
||||
|
||||
testSetLogFile = do
|
||||
b <- setLogFile "stderr" Extism.LogError
|
||||
b <- setLogFile "stderr" Extism.Error
|
||||
assertBool "set log file" b
|
||||
|
||||
t name f = TestLabel name (TestCase f)
|
||||
@@ -65,8 +62,8 @@ main = do
|
||||
[
|
||||
t "Plugin.FunctionExists" pluginFunctionExists
|
||||
, t "Plugin.Call" pluginCall
|
||||
, t "Plugin.CallHostFunction" pluginCallHostFunction
|
||||
, t "Plugin.Multiple" pluginMultiple
|
||||
, t "Plugin.Update" pluginUpdate
|
||||
, t "Plugin.Config" pluginConfig
|
||||
, t "SetLogFile" testSetLogFile
|
||||
])
|
||||
|
||||
BIN
haskell/test/code.wasm
Executable file
BIN
haskell/test/code.wasm
Executable file
Binary file not shown.
350
java/pom.xml
350
java/pom.xml
@@ -1,194 +1,190 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.extism.sdk</groupId>
|
||||
<artifactId>extism</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>0.5.0</version>
|
||||
<name>extism</name>
|
||||
<url>https://github.com/extism/extism</url>
|
||||
<description>Java-SDK for Extism to use webassembly from Java</description>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.extism.sdk</groupId>
|
||||
<artifactId>extism</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>0.1.0</version>
|
||||
<name>extism</name>
|
||||
<url>https://github.com/extism/extism</url>
|
||||
<description>Java-SDK for Extism to use webassembly from Java</description>
|
||||
|
||||
<licenses>
|
||||
<license>
|
||||
<name>BSD 3-Clause</name>
|
||||
<url>https://opensource.org/licenses/BSD-3-Clause</url>
|
||||
</license>
|
||||
</licenses>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>BSD 3-Clause</name>
|
||||
<url>https://opensource.org/licenses/BSD-3-Clause</url>
|
||||
</license>
|
||||
</licenses>
|
||||
|
||||
<organization>
|
||||
<name>Dylibso, Inc.</name>
|
||||
<url>https://dylib.so</url>
|
||||
</organization>
|
||||
<organization>
|
||||
<name>Dylibso, Inc.</name>
|
||||
<url>https://dylib.so</url>
|
||||
</organization>
|
||||
|
||||
<developers>
|
||||
<developer>
|
||||
<name>The Extism Authors</name>
|
||||
<email>oss@extism.org</email>
|
||||
<roles>
|
||||
<role>Maintainer</role>
|
||||
</roles>
|
||||
<organization>Dylibso, Inc.</organization>
|
||||
<organizationUrl>https://dylib.so</organizationUrl>
|
||||
</developer>
|
||||
</developers>
|
||||
<developers>
|
||||
<developer>
|
||||
<name>The Extism Authors</name>
|
||||
<email>oss@extism.org</email>
|
||||
<roles>
|
||||
<role>Maintainer</role>
|
||||
</roles>
|
||||
<organization>Dylibso, Inc.</organization>
|
||||
<organizationUrl>https://dylib.so</organizationUrl>
|
||||
</developer>
|
||||
</developers>
|
||||
|
||||
<scm>
|
||||
<connection>scm:git:git://github.com/extism/extism.git</connection>
|
||||
<developerConnection>scm:git:ssh://git@github.com/extism/extism.git</developerConnection>
|
||||
<url>https://github.com/extism/extism/tree/main/java</url>
|
||||
<tag>main</tag>
|
||||
</scm>
|
||||
<scm>
|
||||
<connection>scm:git:git://github.com/extism/extism.git</connection>
|
||||
<developerConnection>scm:git:ssh://git@github.com/extism/extism.git</developerConnection>
|
||||
<url>https://github.com/extism/extism/java</url>
|
||||
<tag>main</tag>
|
||||
</scm>
|
||||
|
||||
<issueManagement>
|
||||
<system>Github</system>
|
||||
<url>https://github.com/extism/extism/issues</url>
|
||||
</issueManagement>
|
||||
<issueManagement>
|
||||
<system>Github</system>
|
||||
<url>https://github.com/extism/extism/issues</url>
|
||||
</issueManagement>
|
||||
|
||||
<properties>
|
||||
<java.version>11</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
<!-- dependencies -->
|
||||
<jna.version>5.12.1</jna.version>
|
||||
<gson.version>2.10</gson.version>
|
||||
<!-- dependencies -->
|
||||
<jna.version>5.12.1</jna.version>
|
||||
<gson.version>2.10</gson.version>
|
||||
|
||||
<!-- testing -->
|
||||
<junit-jupiter-engine.version>5.9.1</junit-jupiter-engine.version>
|
||||
<assertj-core.version>3.23.1</assertj-core.version>
|
||||
<!-- testing -->
|
||||
<junit-jupiter-engine.version>5.9.1</junit-jupiter-engine.version>
|
||||
<assertj-core.version>3.23.1</assertj-core.version>
|
||||
|
||||
<!-- maven plugins -->
|
||||
<maven-compiler-plugin.version>3.10.1</maven-compiler-plugin.version>
|
||||
<maven-surefire-plugin.version>2.22.2</maven-surefire-plugin.version>
|
||||
<!-- maven plugins -->
|
||||
<maven-compiler-plugin.version>3.10.1</maven-compiler-plugin.version>
|
||||
<maven-surefire-plugin.version>2.22.2</maven-surefire-plugin.version>
|
||||
|
||||
<!-- jreleaser -->
|
||||
<jreleaser.git.root.search>true</jreleaser.git.root.search>
|
||||
</properties>
|
||||
<!-- jreleaser -->
|
||||
<jreleaser.git.root.search>true</jreleaser.git.root.search>
|
||||
</properties>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>release</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.4.1</version>
|
||||
<configuration>
|
||||
<additionalJOption>-Xdoclint:none</additionalJOption>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadoc</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>3.2.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-source</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jreleaser</groupId>
|
||||
<artifactId>jreleaser-maven-plugin</artifactId>
|
||||
<version>1.3.1</version>
|
||||
<configuration>
|
||||
<jreleaser>
|
||||
<release>
|
||||
<github>
|
||||
<skipRelease>true</skipRelease>
|
||||
</github>
|
||||
</release>
|
||||
<signing>
|
||||
<active>ALWAYS</active>
|
||||
<armored>true</armored>
|
||||
</signing>
|
||||
<deploy>
|
||||
<maven>
|
||||
<nexus2>
|
||||
<maven-central>
|
||||
<active>ALWAYS</active>
|
||||
<url>https://s01.oss.sonatype.org/service/local</url>
|
||||
<!--
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>release</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.4.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadoc</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>3.2.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-source</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jreleaser</groupId>
|
||||
<artifactId>jreleaser-maven-plugin</artifactId>
|
||||
<version>1.3.1</version>
|
||||
<configuration>
|
||||
<jreleaser>
|
||||
<release>
|
||||
<github>
|
||||
<skipRelease>true</skipRelease>
|
||||
</github>
|
||||
</release>
|
||||
<signing>
|
||||
<active>ALWAYS</active>
|
||||
<armored>true</armored>
|
||||
</signing>
|
||||
<deploy>
|
||||
<maven>
|
||||
<nexus2>
|
||||
<maven-central>
|
||||
<active>ALWAYS</active>
|
||||
<url>https://s01.oss.sonatype.org/service/local</url>
|
||||
<!--
|
||||
<closeRepository>false</closeRepository>
|
||||
<releaseRepository>false</releaseRepository>
|
||||
-->
|
||||
<stagingRepositories>target/staging-deploy</stagingRepositories>
|
||||
</maven-central>
|
||||
</nexus2>
|
||||
</maven>
|
||||
</deploy>
|
||||
</jreleaser>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<stagingRepositories>target/staging-deploy</stagingRepositories>
|
||||
</maven-central>
|
||||
</nexus2>
|
||||
</maven>
|
||||
</deploy>
|
||||
</jreleaser>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>${maven-compiler-plugin.version}</version>
|
||||
<configuration>
|
||||
<release>${java.version}</release>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${maven-surefire-plugin.version}</version>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<jna.library.path>../target/release</jna.library.path>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</build>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>net.java.dev.jna</groupId>
|
||||
<artifactId>jna</artifactId>
|
||||
<version>${jna.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>${gson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>${junit-jupiter-engine.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<version>${assertj-core.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>${maven-compiler-plugin.version}</version>
|
||||
<configuration>
|
||||
<release>${java.version}</release>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${maven-surefire-plugin.version}</version>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<jna.library.path>../target/release</jna.library.path>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>net.java.dev.jna</groupId>
|
||||
<artifactId>jna</artifactId>
|
||||
<version>${jna.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>${gson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>${junit-jupiter-engine.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<version>${assertj-core.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>uk.org.webcompere</groupId>
|
||||
<artifactId>model-assert</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<dependency>
|
||||
<groupId>uk.org.webcompere</groupId>
|
||||
<artifactId>model-assert</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
89
java/src/main/java/org/extism/sdk/Context.java
Normal file
89
java/src/main/java/org/extism/sdk/Context.java
Normal file
@@ -0,0 +1,89 @@
|
||||
package org.extism.sdk;
|
||||
|
||||
import com.sun.jna.Pointer;
|
||||
import org.extism.sdk.manifest.Manifest;
|
||||
|
||||
/**
|
||||
* Extism Context is used to store and manage plugins.
|
||||
*/
|
||||
public class Context implements AutoCloseable {
|
||||
|
||||
/**
|
||||
* Holds a pointer to the native ExtismContext struct.
|
||||
*/
|
||||
private final Pointer contextPointer;
|
||||
|
||||
/**
|
||||
* Creates a new context.
|
||||
* <p>
|
||||
* A Context is used to manage Plugins
|
||||
* and make sure they are cleaned up when you are done with them.
|
||||
*/
|
||||
public Context() {
|
||||
this.contextPointer = LibExtism.INSTANCE.extism_context_new();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new plugin managed by this context.
|
||||
*
|
||||
* @param manifest The manifest for the plugin
|
||||
* @param withWASI Set to true to enable WASI
|
||||
* @return the plugin instance
|
||||
*/
|
||||
public Plugin newPlugin(Manifest manifest, boolean withWASI) {
|
||||
return new Plugin(this, manifest, withWASI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees the context *and* frees all its Plugins. Use {@link #reset()}, if you just want to
|
||||
* free the plugins but keep the context. You should ensure this is called when you are done
|
||||
* with the context.
|
||||
*/
|
||||
public void free() {
|
||||
LibExtism.INSTANCE.extism_context_free(this.contextPointer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the context by freeing all its Plugins. Unlike {@link #free()}, it does not
|
||||
* free the context itself.
|
||||
*/
|
||||
public void reset() {
|
||||
LibExtism.INSTANCE.extism_context_reset(this.contextPointer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version string of the linked Extism Runtime.
|
||||
*
|
||||
* @return the version
|
||||
*/
|
||||
public String getVersion() {
|
||||
return LibExtism.INSTANCE.extism_version();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error associated with a context, if plugin is {@literal null} then the context error will be returned.
|
||||
*
|
||||
* @param plugin
|
||||
* @return the error message
|
||||
*/
|
||||
protected String error(Plugin plugin) {
|
||||
return LibExtism.INSTANCE.extism_error(this.contextPointer, plugin == null ? -1 : plugin.getIndex());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the raw pointer to this context.
|
||||
*
|
||||
* @return the pointer
|
||||
*/
|
||||
public Pointer getPointer() {
|
||||
return this.contextPointer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls {@link #free()} if used in the context of a TWR block.
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
this.free();
|
||||
}
|
||||
}
|
||||
@@ -41,8 +41,10 @@ public class Extism {
|
||||
* @throws ExtismException if the call fails
|
||||
*/
|
||||
public static String invokeFunction(Manifest manifest, String function, String input) throws ExtismException {
|
||||
try (var plugin = new Plugin(manifest, false, null)) {
|
||||
return plugin.call(function, input);
|
||||
try (var ctx = new Context()) {
|
||||
try (var plugin = ctx.newPlugin(manifest, false)) {
|
||||
return plugin.call(function, input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package org.extism.sdk;
|
||||
|
||||
import com.sun.jna.PointerType;
|
||||
|
||||
public class HostUserData extends PointerType {
|
||||
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.extism.sdk;
|
||||
|
||||
import com.sun.jna.*;
|
||||
import com.sun.jna.Library;
|
||||
import com.sun.jna.Native;
|
||||
import com.sun.jna.Pointer;
|
||||
|
||||
/**
|
||||
* Wrapper around the Extism library.
|
||||
@@ -13,78 +15,22 @@ public interface LibExtism extends Library {
|
||||
*/
|
||||
LibExtism INSTANCE = Native.load("extism", LibExtism.class);
|
||||
|
||||
interface InternalExtismFunction extends Callback {
|
||||
void invoke(
|
||||
Pointer currentPlugin,
|
||||
ExtismVal inputs,
|
||||
int nInputs,
|
||||
ExtismVal outputs,
|
||||
int nOutputs,
|
||||
Pointer data
|
||||
);
|
||||
}
|
||||
|
||||
@Structure.FieldOrder({"t", "v"})
|
||||
class ExtismVal extends Structure {
|
||||
public int t;
|
||||
public ExtismValUnion v;
|
||||
}
|
||||
|
||||
class ExtismValUnion extends Union {
|
||||
public int i32;
|
||||
public long i64;
|
||||
public float f32;
|
||||
public double f64;
|
||||
}
|
||||
|
||||
enum ExtismValType {
|
||||
I32(0),
|
||||
I64(1),
|
||||
F32(2),
|
||||
F64(3),
|
||||
V128(4),
|
||||
FuncRef(5),
|
||||
ExternRef(6);
|
||||
|
||||
public final int v;
|
||||
|
||||
ExtismValType(int value) {
|
||||
this.v = value;
|
||||
}
|
||||
}
|
||||
|
||||
Pointer extism_function_new(String name,
|
||||
int[] inputs,
|
||||
int nInputs,
|
||||
int[] outputs,
|
||||
int nOutputs,
|
||||
InternalExtismFunction func,
|
||||
Pointer userData,
|
||||
Pointer freeUserData);
|
||||
/**
|
||||
* Create a new context
|
||||
*/
|
||||
Pointer extism_context_new();
|
||||
|
||||
/**
|
||||
* Get the length of an allocated block
|
||||
* NOTE: this should only be called from host functions.
|
||||
* Free a context
|
||||
*/
|
||||
int extism_current_plugin_memory_length(Pointer plugin, long n);
|
||||
void extism_context_free(Pointer contextPointer);
|
||||
|
||||
/**
|
||||
* Returns a pointer to the memory of the currently running plugin
|
||||
* NOTE: this should only be called from host functions.
|
||||
* Remove all plugins from the registry.
|
||||
*
|
||||
* @param contextPointer
|
||||
*/
|
||||
Pointer extism_current_plugin_memory(Pointer plugin);
|
||||
|
||||
/**
|
||||
* Allocate a memory block in the currently running plugin
|
||||
* NOTE: this should only be called from host functions.
|
||||
*/
|
||||
int extism_current_plugin_memory_alloc(Pointer plugin, long n);
|
||||
|
||||
/**
|
||||
* Free an allocated memory block
|
||||
* NOTE: this should only be called from host functions.
|
||||
*/
|
||||
void extism_current_plugin_memory_free(Pointer plugin, long ptr);
|
||||
void extism_context_reset(Pointer contextPointer);
|
||||
|
||||
/**
|
||||
* Sets the logger to the given path with the given level of verbosity
|
||||
@@ -96,74 +42,102 @@ public interface LibExtism extends Library {
|
||||
boolean extism_log_file(String path, String logLevel);
|
||||
|
||||
/**
|
||||
* Returns the error associated with a @{@link Plugin}
|
||||
* Returns the error associated with a @{@link Context} or @{@link Plugin}, if {@code pluginId} is {@literal -1} then the context error will be returned
|
||||
*
|
||||
* @param pluginPointer
|
||||
* @param contextPointer
|
||||
* @param pluginId
|
||||
* @return
|
||||
*/
|
||||
String extism_plugin_error(Pointer pluginPointer);
|
||||
String extism_error(Pointer contextPointer, int pluginId);
|
||||
|
||||
/**
|
||||
* Create a new plugin.
|
||||
*
|
||||
* @param contextPointer pointer to the {@link Context}.
|
||||
* @param wasm is a WASM module (wat or wasm) or a JSON encoded manifest
|
||||
* @param wasmSize the length of the `wasm` parameter
|
||||
* @param functions host functions
|
||||
* @param nFunctions the number of host functions
|
||||
* @param withWASI enables/disables WASI
|
||||
* @param errmsg get the error message if the return value is null
|
||||
* @return id of the plugin or {@literal -1} in case of error
|
||||
*/
|
||||
Pointer extism_plugin_new(byte[] wasm, long wasmSize, Pointer[] functions, int nFunctions, boolean withWASI, Pointer[] errmsg);
|
||||
|
||||
/**
|
||||
* Free error message from `extism_plugin_new`
|
||||
*/
|
||||
void extism_plugin_new_error_free(Pointer errmsg);
|
||||
int extism_plugin_new(long contextPointer, byte[] wasm, long wasmSize, boolean withWASI);
|
||||
|
||||
/**
|
||||
* Returns the Extism version string
|
||||
*/
|
||||
String extism_version();
|
||||
|
||||
/**
|
||||
* Create a new plugin.
|
||||
*
|
||||
* @param contextPointer pointer to the {@link Context}.
|
||||
* @param wasm is a WASM module (wat or wasm) or a JSON encoded manifest
|
||||
* @param length the length of the `wasm` parameter
|
||||
* @param withWASI enables/disables WASI
|
||||
* @return id of the plugin or {@literal -1} in case of error
|
||||
* @see #extism_plugin_new(long, byte[], long, boolean)
|
||||
*/
|
||||
int extism_plugin_new(Pointer contextPointer, byte[] wasm, int length, boolean withWASI);
|
||||
|
||||
/**
|
||||
* Calls a function from the @{@link Plugin} at the given {@code pluginIndex}.
|
||||
*
|
||||
* @param pluginPointer
|
||||
* @param contextPointer
|
||||
* @param pluginIndex
|
||||
* @param function_name is the function to call
|
||||
* @param data is the data input data
|
||||
* @param dataLength is the data input data length
|
||||
* @return the result code of the plugin call. {@literal -1} in case of error, {@literal 0} otherwise.
|
||||
*/
|
||||
int extism_plugin_call(Pointer pluginPointer, String function_name, byte[] data, int dataLength);
|
||||
int extism_plugin_call(Pointer contextPointer, int pluginIndex, String function_name, byte[] data, int dataLength);
|
||||
|
||||
/**
|
||||
* Returns
|
||||
* Returns the length of a plugin's output data.
|
||||
*
|
||||
* @param contextPointer
|
||||
* @param pluginIndex
|
||||
* @return the length of the output data in bytes.
|
||||
*/
|
||||
int extism_plugin_output_length(Pointer pluginPointer);
|
||||
int extism_plugin_output_length(Pointer contextPointer, int pluginIndex);
|
||||
|
||||
/**
|
||||
|
||||
* Returns the plugin's output data.
|
||||
*
|
||||
* @param contextPointer
|
||||
* @param pluginIndex
|
||||
* @return
|
||||
*/
|
||||
Pointer extism_plugin_output_data(Pointer pluginPointer);
|
||||
Pointer extism_plugin_output_data(Pointer contextPointer, int pluginIndex);
|
||||
|
||||
/**
|
||||
* Remove a plugin from the
|
||||
* Update a plugin, keeping the existing ID.
|
||||
* Similar to {@link #extism_plugin_new(long, byte[], long, boolean)} but takes an {@code pluginIndex} argument to specify which plugin to update.
|
||||
* Note: Memory for this plugin will be reset upon update.
|
||||
*
|
||||
* @param contextPointer
|
||||
* @param pluginIndex
|
||||
* @param wasm
|
||||
* @param length
|
||||
* @param withWASI
|
||||
* @return {@literal true} if update was successful
|
||||
*/
|
||||
void extism_plugin_free(Pointer pluginPointer);
|
||||
boolean extism_plugin_update(Pointer contextPointer, int pluginIndex, byte[] wasm, int length, boolean withWASI);
|
||||
|
||||
/**
|
||||
* Update plugin config values, this
|
||||
* Remove a plugin from the registry and free associated memory.
|
||||
*
|
||||
* @param contextPointer
|
||||
* @param pluginIndex
|
||||
*/
|
||||
void extism_plugin_free(Pointer contextPointer, int pluginIndex);
|
||||
|
||||
/**
|
||||
* Update plugin config values, this will merge with the existing values.
|
||||
*
|
||||
* @param contextPointer
|
||||
* @param pluginIndex
|
||||
* @param json
|
||||
* @param jsonLength
|
||||
* @return {@literal true} if update was successful
|
||||
*/
|
||||
boolean extism_plugin_config(Pointer pluginPointer, byte[] json, int jsonLength);
|
||||
Pointer extism_plugin_cancel_handle(Pointer pluginPointer);
|
||||
boolean extism_plugin_cancel(Pointer cancelHandle);
|
||||
void extism_function_set_namespace(Pointer p, String name);
|
||||
int strlen(Pointer s);
|
||||
boolean extism_plugin_config(Pointer contextPointer, int pluginIndex, byte[] json, int jsonLength);
|
||||
}
|
||||
|
||||
@@ -13,53 +13,57 @@ import java.util.Objects;
|
||||
public class Plugin implements AutoCloseable {
|
||||
|
||||
/**
|
||||
* Holds the Extism plugin pointer
|
||||
* Holds the Extism {@link Context} that the plugin belongs to.
|
||||
*/
|
||||
private final Pointer pluginPointer;
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* Holds the index of the plugin
|
||||
*/
|
||||
private final int index;
|
||||
|
||||
/**
|
||||
* Constructor for a Plugin. Only expose internally. Plugins should be created and
|
||||
* managed from {@link org.extism.sdk.Context}.
|
||||
*
|
||||
* @param context The context to manage the plugin
|
||||
* @param manifestBytes The manifest for the plugin
|
||||
* @param functions The Host functions for th eplugin
|
||||
* @param withWASI Set to true to enable WASI
|
||||
*/
|
||||
public Plugin(byte[] manifestBytes, boolean withWASI, HostFunction[] functions) {
|
||||
public Plugin(Context context, byte[] manifestBytes, boolean withWASI) {
|
||||
|
||||
Objects.requireNonNull(context, "context");
|
||||
Objects.requireNonNull(manifestBytes, "manifestBytes");
|
||||
|
||||
Pointer[] ptrArr = new Pointer[functions == null ? 0 : functions.length];
|
||||
|
||||
if (functions != null)
|
||||
for (int i = 0; i < functions.length; i++) {
|
||||
ptrArr[i] = functions[i].pointer;
|
||||
}
|
||||
|
||||
Pointer[] errormsg = new Pointer[1];
|
||||
Pointer p = LibExtism.INSTANCE.extism_plugin_new(manifestBytes, manifestBytes.length,
|
||||
ptrArr,
|
||||
functions == null ? 0 : functions.length,
|
||||
withWASI,
|
||||
errormsg);
|
||||
if (p == null) {
|
||||
int errlen = LibExtism.INSTANCE.strlen(errormsg[0]);
|
||||
byte[] msg = new byte[errlen];
|
||||
errormsg[0].read(0, msg, 0, errlen);
|
||||
LibExtism.INSTANCE.extism_plugin_new_error_free(errormsg[0]);
|
||||
throw new ExtismException(new String(msg));
|
||||
Pointer contextPointer = context.getPointer();
|
||||
int index = LibExtism.INSTANCE.extism_plugin_new(contextPointer, manifestBytes, manifestBytes.length, withWASI);
|
||||
if (index == -1) {
|
||||
String error = context.error(this);
|
||||
throw new ExtismException(error);
|
||||
}
|
||||
|
||||
this.pluginPointer = p;
|
||||
this.index= index;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public Plugin(Manifest manifest, boolean withWASI, HostFunction[] functions) {
|
||||
this(serialize(manifest), withWASI, functions);
|
||||
public Plugin(Context context, Manifest manifest, boolean withWASI) {
|
||||
this(context, serialize(manifest), withWASI);
|
||||
}
|
||||
|
||||
|
||||
private static byte[] serialize(Manifest manifest) {
|
||||
Objects.requireNonNull(manifest, "manifest");
|
||||
return JsonSerde.toJson(manifest).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the internal index pointer to this plugin.
|
||||
*
|
||||
* @return the plugin index
|
||||
*/
|
||||
public int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke a function with the given name and input.
|
||||
*
|
||||
@@ -72,19 +76,19 @@ public class Plugin implements AutoCloseable {
|
||||
|
||||
Objects.requireNonNull(functionName, "functionName");
|
||||
|
||||
Pointer contextPointer = context.getPointer();
|
||||
int inputDataLength = inputData == null ? 0 : inputData.length;
|
||||
int exitCode = LibExtism.INSTANCE.extism_plugin_call(this.pluginPointer, functionName, inputData, inputDataLength);
|
||||
int exitCode = LibExtism.INSTANCE.extism_plugin_call(contextPointer, index, functionName, inputData, inputDataLength);
|
||||
if (exitCode == -1) {
|
||||
String error = this.error();
|
||||
String error = context.error(this);
|
||||
throw new ExtismException(error);
|
||||
}
|
||||
|
||||
int length = LibExtism.INSTANCE.extism_plugin_output_length(this.pluginPointer);
|
||||
Pointer output = LibExtism.INSTANCE.extism_plugin_output_data(this.pluginPointer);
|
||||
int length = LibExtism.INSTANCE.extism_plugin_output_length(contextPointer, index);
|
||||
Pointer output = LibExtism.INSTANCE.extism_plugin_output_data(contextPointer, index);
|
||||
return output.getByteArray(0, length);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Invoke a function with the given name and input.
|
||||
*
|
||||
@@ -100,21 +104,36 @@ public class Plugin implements AutoCloseable {
|
||||
var outputBytes = call(functionName, inputBytes);
|
||||
return new String(outputBytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the error associated with a plugin
|
||||
* Update the plugin code given manifest changes
|
||||
*
|
||||
* @return the error message
|
||||
* @param manifest The manifest for the plugin
|
||||
* @param withWASI Set to true to enable WASI
|
||||
* @return {@literal true} if update was successful
|
||||
*/
|
||||
protected String error() {
|
||||
return LibExtism.INSTANCE.extism_plugin_error(this.pluginPointer);
|
||||
public boolean update(Manifest manifest, boolean withWASI) {
|
||||
return update(serialize(manifest), withWASI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees a plugin from memory
|
||||
* Update the plugin code given manifest changes
|
||||
*
|
||||
* @param manifestBytes The manifest for the plugin
|
||||
* @param withWASI Set to true to enable WASI
|
||||
* @return {@literal true} if update was successful
|
||||
*/
|
||||
public boolean update(byte[] manifestBytes, boolean withWASI) {
|
||||
Objects.requireNonNull(manifestBytes, "manifestBytes");
|
||||
return LibExtism.INSTANCE.extism_plugin_update(context.getPointer(), index, manifestBytes, manifestBytes.length, withWASI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees a plugin from memory. Plugins will be automatically cleaned up
|
||||
* if you free their parent Context using {@link org.extism.sdk.Context#free() free()} or {@link org.extism.sdk.Context#reset() reset()}
|
||||
*/
|
||||
public void free() {
|
||||
LibExtism.INSTANCE.extism_plugin_free(this.pluginPointer);
|
||||
LibExtism.INSTANCE.extism_plugin_free(context.getPointer(), index);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,7 +155,7 @@ public class Plugin implements AutoCloseable {
|
||||
*/
|
||||
public boolean updateConfig(byte[] jsonBytes) {
|
||||
Objects.requireNonNull(jsonBytes, "jsonBytes");
|
||||
return LibExtism.INSTANCE.extism_plugin_config(this.pluginPointer, jsonBytes, jsonBytes.length);
|
||||
return LibExtism.INSTANCE.extism_plugin_config(context.getPointer(), index, jsonBytes, jsonBytes.length);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,12 +165,4 @@ public class Plugin implements AutoCloseable {
|
||||
public void close() {
|
||||
free();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new `CancelHandle`, which can be used to cancel a running Plugin
|
||||
*/
|
||||
public CancelHandle cancelHandle() {
|
||||
Pointer handle = LibExtism.INSTANCE.extism_plugin_cancel_handle(this.pluginPointer);
|
||||
return new CancelHandle(handle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
|
||||
23
java/src/test/java/org/extism/sdk/ContextTests.java
Normal file
23
java/src/test/java/org/extism/sdk/ContextTests.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package org.extism.sdk;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class ContextTests {
|
||||
|
||||
@Test
|
||||
public void shouldReturnVersionString() {
|
||||
try (var ctx = new Context()) {
|
||||
var version = ctx.getVersion();
|
||||
assertThat(version).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAllowResetOnEmptyContext() {
|
||||
try (var ctx = new Context()) {
|
||||
ctx.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package org.extism.sdk;
|
||||
|
||||
import com.sun.jna.Pointer;
|
||||
import org.extism.sdk.manifest.Manifest;
|
||||
import org.extism.sdk.manifest.MemoryOptions;
|
||||
import org.extism.sdk.wasm.WasmSourceResolver;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.extism.sdk.TestWasmSources.CODE;
|
||||
@@ -20,10 +20,10 @@ public class PluginTests {
|
||||
|
||||
@Test
|
||||
public void shouldInvokeFunctionWithMemoryOptions() {
|
||||
//FIXME check whether memory options are effective
|
||||
var manifest = new Manifest(List.of(CODE.pathWasmSource()), new MemoryOptions(0));
|
||||
assertThrows(ExtismException.class, () -> {
|
||||
Extism.invokeFunction(manifest, "count_vowels", "Hello World");
|
||||
});
|
||||
var output = Extism.invokeFunction(manifest, "count_vowels", "Hello World");
|
||||
assertThat(output).isEqualTo("{\"count\": 3}");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -42,12 +42,14 @@ public class PluginTests {
|
||||
assertThat(output).isEqualTo("{\"count\": 3}");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldInvokeFunctionFromByteArrayWasmSource() {
|
||||
var manifest = new Manifest(CODE.byteArrayWasmSource());
|
||||
var output = Extism.invokeFunction(manifest, "count_vowels", "Hello World");
|
||||
assertThat(output).isEqualTo("{\"count\": 3}");
|
||||
}
|
||||
// TODO This test breaks on CI with error:
|
||||
// data did not match any variant of untagged enum Wasm at line 8 column 3
|
||||
// @Test
|
||||
// public void shouldInvokeFunctionFromByteArrayWasmSource() {
|
||||
// var manifest = new Manifest(CODE.byteArrayWasmSource());
|
||||
// var output = Extism.invokeFunction(manifest, "count_vowels", "Hello World");
|
||||
// assertThat(output).isEqualTo("{\"count\": 3}");
|
||||
// }
|
||||
|
||||
@Test
|
||||
public void shouldFailToInvokeUnknownFunction() {
|
||||
@@ -57,6 +59,17 @@ public class PluginTests {
|
||||
}, "Function not found: unknown");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAllowInvokeFunctionFromFileWasmSourceMultipleTimes() {
|
||||
var wasmSource = CODE.pathWasmSource();
|
||||
var manifest = new Manifest(wasmSource);
|
||||
var output = Extism.invokeFunction(manifest, "count_vowels", "Hello World");
|
||||
assertThat(output).isEqualTo("{\"count\": 3}");
|
||||
|
||||
output = Extism.invokeFunction(manifest, "count_vowels", "Hello World");
|
||||
assertThat(output).isEqualTo("{\"count\": 3}");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAllowInvokeFunctionFromFileWasmSourceApiUsageExample() {
|
||||
|
||||
@@ -66,133 +79,29 @@ public class PluginTests {
|
||||
var functionName = "count_vowels";
|
||||
var input = "Hello World";
|
||||
|
||||
try (var plugin = new Plugin(manifest, false, null)) {
|
||||
var output = plugin.call(functionName, input);
|
||||
assertThat(output).isEqualTo("{\"count\": 3}");
|
||||
try (var ctx = new Context()) {
|
||||
try (var plugin = ctx.newPlugin(manifest, false)) {
|
||||
var output = plugin.call(functionName, input);
|
||||
assertThat(output).isEqualTo("{\"count\": 3}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAllowInvokeFunctionFromFileWasmSourceMultipleTimes() {
|
||||
public void shouldAllowInvokeFunctionFromFileWasmSourceMultipleTimesByReusingContext() {
|
||||
var manifest = new Manifest(CODE.pathWasmSource());
|
||||
var functionName = "count_vowels";
|
||||
var input = "Hello World";
|
||||
|
||||
try (var plugin = new Plugin(manifest, false, null)) {
|
||||
var output = plugin.call(functionName, input);
|
||||
assertThat(output).isEqualTo("{\"count\": 3}");
|
||||
try (var ctx = new Context()) {
|
||||
try (var plugin = ctx.newPlugin(manifest, false)) {
|
||||
var output = plugin.call(functionName, input);
|
||||
assertThat(output).isEqualTo("{\"count\": 3}");
|
||||
|
||||
output = plugin.call(functionName, input);
|
||||
assertThat(output).isEqualTo("{\"count\": 3}");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAllowInvokeHostFunctionFromPDK() {
|
||||
var parametersTypes = new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64};
|
||||
var resultsTypes = new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64};
|
||||
|
||||
class MyUserData extends HostUserData {
|
||||
private String data1;
|
||||
private int data2;
|
||||
|
||||
public MyUserData(String data1, int data2) {
|
||||
super();
|
||||
this.data1 = data1;
|
||||
this.data2 = data2;
|
||||
output = plugin.call(functionName, input);
|
||||
assertThat(output).isEqualTo("{\"count\": 3}");
|
||||
}
|
||||
}
|
||||
|
||||
ExtismFunction helloWorldFunction = (ExtismFunction<MyUserData>) (plugin, params, returns, data) -> {
|
||||
System.out.println("Hello from Java Host Function!");
|
||||
System.out.println(String.format("Input string received from plugin, %s", plugin.inputString(params[0])));
|
||||
|
||||
int offs = plugin.alloc(4);
|
||||
Pointer mem = plugin.memory();
|
||||
mem.write(offs, "test".getBytes(), 0, 4);
|
||||
returns[0].v.i64 = offs;
|
||||
|
||||
data.ifPresent(d -> System.out.println(String.format("Host user data, %s, %d", d.data1, d.data2)));
|
||||
};
|
||||
|
||||
HostFunction helloWorld = new HostFunction<>(
|
||||
"hello_world",
|
||||
parametersTypes,
|
||||
resultsTypes,
|
||||
helloWorldFunction,
|
||||
Optional.of(new MyUserData("test", 2))
|
||||
);
|
||||
|
||||
HostFunction[] functions = {helloWorld};
|
||||
|
||||
Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource()));
|
||||
String functionName = "count_vowels";
|
||||
|
||||
try (var plugin = new Plugin(manifest, true, functions)) {
|
||||
var output = plugin.call(functionName, "this is a test");
|
||||
assertThat(output).isEqualTo("test");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAllowInvokeHostFunctionWithoutUserData() {
|
||||
|
||||
var parametersTypes = new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64};
|
||||
var resultsTypes = new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64};
|
||||
|
||||
ExtismFunction helloWorldFunction = (plugin, params, returns, data) -> {
|
||||
System.out.println("Hello from Java Host Function!");
|
||||
System.out.println(String.format("Input string received from plugin, %s", plugin.inputString(params[0])));
|
||||
|
||||
int offs = plugin.alloc(4);
|
||||
Pointer mem = plugin.memory();
|
||||
mem.write(offs, "test".getBytes(), 0, 4);
|
||||
returns[0].v.i64 = offs;
|
||||
|
||||
assertThat(data.isEmpty());
|
||||
};
|
||||
|
||||
HostFunction f = new HostFunction<>(
|
||||
"hello_world",
|
||||
parametersTypes,
|
||||
resultsTypes,
|
||||
helloWorldFunction,
|
||||
Optional.empty()
|
||||
)
|
||||
.withNamespace("env");
|
||||
|
||||
HostFunction g = new HostFunction<>(
|
||||
"hello_world",
|
||||
parametersTypes,
|
||||
resultsTypes,
|
||||
helloWorldFunction,
|
||||
Optional.empty()
|
||||
)
|
||||
.withNamespace("test");
|
||||
|
||||
HostFunction[] functions = {f,g};
|
||||
|
||||
Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource()));
|
||||
String functionName = "count_vowels";
|
||||
|
||||
try (var plugin = new Plugin(manifest, true, functions)) {
|
||||
var output = plugin.call(functionName, "this is a test");
|
||||
assertThat(output).isEqualTo("test");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void shouldFailToInvokeUnknownHostFunction() {
|
||||
Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource()));
|
||||
String functionName = "count_vowels";
|
||||
|
||||
try {
|
||||
var plugin = new Plugin(manifest, true, null);
|
||||
plugin.call(functionName, "this is a test");
|
||||
} catch (ExtismException e) {
|
||||
assertThat(e.getMessage()).contains("unknown import: `env::hello_world` has not been defined");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Binary file not shown.
@@ -1,2 +0,0 @@
|
||||
[build]
|
||||
target = "wasm32-unknown-unknown"
|
||||
@@ -1,11 +0,0 @@
|
||||
[package]
|
||||
name = "extism-runtime-kernel"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"."
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -1,446 +0,0 @@
|
||||
//! # Extism kernel
|
||||
//!
|
||||
//! - Isolated memory from both host and plugin
|
||||
//! - An allocator for managing that memory
|
||||
//! - Input/output handling
|
||||
//! - Error message handling
|
||||
//! - Backward compatible `extism_*` functions
|
||||
//!
|
||||
//! ## Allocator
|
||||
//!
|
||||
//! The Extism allocator is a bump allocator that tracks the `length` of the total number of bytes
|
||||
//! available to the allocator and `position` to track how much of the data has been used. Things like memory
|
||||
//! have not really been optimized at all. When a new allocation that is larger than the remaning size is made,
|
||||
//! the allocator attempts to call `memory.grow` if that fails a `0` offset is returned, which should be interpreted
|
||||
//! as a failed allocation.
|
||||
//!
|
||||
//! ## Input/Output
|
||||
//!
|
||||
//! Input and output are just allocated blocks of memory that are marked as either input or output using
|
||||
//! the `extism_input_set` or `extism_output_set` functions. The global variables `INPUT_OFFSET` contains
|
||||
//! the offset in memory to the input data and `INPUT_LENGTH` contains the size of the input data. `OUTPUT_OFFSET`
|
||||
//! and `OUTPUT_LENGTH` are used for the output data.
|
||||
//!
|
||||
//! ## Error handling
|
||||
//!
|
||||
//! The `ERROR` global is used to track the current error message. If it is set to `0` then there is no error.
|
||||
//! The length of the error message can be retreived using `extism_length`.
|
||||
//!
|
||||
//! ## Memory offsets
|
||||
//! An offset of `0` is similar to a `NULL` pointer in C - it implies an allocation failure or memory error
|
||||
//! of some kind
|
||||
//!
|
||||
//! ## Extism functions
|
||||
//!
|
||||
//! These functions are backward compatible with the pre-kernel runtime, but a few new functions are added to
|
||||
//! give runtimes more access to the internals necesarry to load data in and out of a plugin.
|
||||
#![no_std]
|
||||
#![allow(clippy::missing_safety_doc)]
|
||||
|
||||
use core::sync::atomic::*;
|
||||
|
||||
pub type Pointer = u64;
|
||||
pub type Length = u64;
|
||||
|
||||
/// WebAssembly page size
|
||||
const PAGE_SIZE: usize = 65536;
|
||||
|
||||
/// Determines the amount of bytes that can be wasted by re-using a block. If more than this number is wasted by re-using
|
||||
/// a block then it will be split into two smaller blocks.
|
||||
const BLOCK_SPLIT_SIZE: usize = 128;
|
||||
|
||||
/// Offset to the input data
|
||||
static mut INPUT_OFFSET: Pointer = 0;
|
||||
|
||||
/// Length of the input data
|
||||
static mut INPUT_LENGTH: Length = 0;
|
||||
|
||||
/// Offset to the output data
|
||||
static mut OUTPUT_OFFSET: Pointer = 0;
|
||||
|
||||
/// Offset to the input data
|
||||
static mut OUTPUT_LENGTH: Length = 0;
|
||||
|
||||
/// Current error message
|
||||
static mut ERROR: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
/// Determines if the kernel has been initialized already
|
||||
static mut INITIALIZED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// A pointer to the first page that will be managed by Extism, this is set during initialization
|
||||
static mut START_PAGE: usize = 0;
|
||||
|
||||
/// Provides information about the usage status of a `MemoryBlock`
|
||||
#[repr(u8)]
|
||||
#[derive(PartialEq)]
|
||||
pub enum MemoryStatus {
|
||||
/// Unused memory that is available b
|
||||
Unused = 0,
|
||||
/// In-use memory
|
||||
Active = 1,
|
||||
/// Free memory that is available for re-use
|
||||
Free = 2,
|
||||
}
|
||||
|
||||
/// A single `MemoryRoot` exists at the start of the memory to track information about the total
|
||||
/// size of the allocated memory and the position of the bump allocator.
|
||||
///
|
||||
/// The overall layout of the Extism-manged memory is organized like this:
|
||||
|
||||
/// |------|-------|---------|-------|--------------|
|
||||
/// | Root | Block | Data | Block | Data | ...
|
||||
/// |------|-------|---------|-------|--------------|
|
||||
///
|
||||
/// Where `Root` and `Block` are fixed to the size of the `MemoryRoot` and `MemoryBlock` structs. But
|
||||
/// the size of `Data` is dependent on the allocation size.
|
||||
#[repr(C)]
|
||||
pub struct MemoryRoot {
|
||||
/// Position of the bump allocator, relative to `START_PAGE`
|
||||
pub position: AtomicU64,
|
||||
/// The total size of all data allocated using this allocator
|
||||
pub length: AtomicU64,
|
||||
/// A pointer to where the blocks begin
|
||||
pub blocks: [MemoryBlock; 0],
|
||||
}
|
||||
|
||||
/// A `MemoryBlock` contains some metadata about a single allocation
|
||||
#[repr(C)]
|
||||
pub struct MemoryBlock {
|
||||
/// The usage status of the block, `Unused` or `Free` blocks can be re-used.
|
||||
pub status: AtomicU8,
|
||||
/// The total size of the allocation
|
||||
pub size: usize,
|
||||
/// The number of bytes currently being used. If this block is a fresh allocation then `size` and `used` will
|
||||
/// always be the same. If a block is re-used then these numbers may differ.
|
||||
pub used: usize,
|
||||
/// A pointer to the block data
|
||||
pub data: [u8; 0],
|
||||
}
|
||||
|
||||
/// Returns the number of pages needed for the given number of bytes
|
||||
pub fn num_pages(nbytes: u64) -> usize {
|
||||
let nbytes = nbytes as f64;
|
||||
let page = PAGE_SIZE as f64;
|
||||
((nbytes / page) + 0.5) as usize
|
||||
}
|
||||
|
||||
// Get the `MemoryRoot` at the correct offset in memory
|
||||
#[inline]
|
||||
unsafe fn memory_root() -> &'static mut MemoryRoot {
|
||||
&mut *((START_PAGE * PAGE_SIZE) as *mut MemoryRoot)
|
||||
}
|
||||
|
||||
impl MemoryRoot {
|
||||
/// Initialize or load the `MemoryRoot` from the correct position in memory
|
||||
pub unsafe fn new() -> &'static mut MemoryRoot {
|
||||
// If this fails then `INITIALIZED` is already `true` and we can just return the
|
||||
// already initialized `MemoryRoot`
|
||||
if INITIALIZED
|
||||
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
|
||||
.is_err()
|
||||
{
|
||||
return memory_root();
|
||||
}
|
||||
|
||||
// Ensure that at least one page is allocated to store the `MemoryRoot` data
|
||||
START_PAGE = core::arch::wasm32::memory_grow(0, 1);
|
||||
if START_PAGE == usize::MAX {
|
||||
panic!("Out of memory");
|
||||
}
|
||||
|
||||
// Initialize the `MemoryRoot` length, position and data
|
||||
let root = memory_root();
|
||||
root.length.store(
|
||||
PAGE_SIZE as u64 - core::mem::size_of::<MemoryRoot>() as u64,
|
||||
Ordering::Release,
|
||||
);
|
||||
root.position.store(0, Ordering::Release);
|
||||
|
||||
// Ensure the first block is marked as `Unused`
|
||||
#[allow(clippy::size_of_in_element_count)]
|
||||
core::ptr::write_bytes(
|
||||
root.blocks.as_mut_ptr() as *mut _,
|
||||
MemoryStatus::Unused as u8,
|
||||
core::mem::size_of::<MemoryBlock>(),
|
||||
);
|
||||
root
|
||||
}
|
||||
|
||||
/// Resets the position of the allocator and zeroes out all allocations
|
||||
pub unsafe fn reset(&mut self) {
|
||||
core::ptr::write_bytes(
|
||||
self.blocks.as_mut_ptr() as *mut u8,
|
||||
0,
|
||||
self.length.load(Ordering::Acquire) as usize,
|
||||
);
|
||||
self.position.store(0, Ordering::Release);
|
||||
}
|
||||
|
||||
// Find a block that is free to use, this can be a new block or an existing freed block. The `self_position` argument
|
||||
// is used to avoid loading the allocators position more than once when performing an allocation.
|
||||
unsafe fn find_free_block(
|
||||
&mut self,
|
||||
length: Length,
|
||||
self_position: u64,
|
||||
) -> Option<&'static mut MemoryBlock> {
|
||||
// Get the first block
|
||||
let mut block = self.blocks.as_mut_ptr();
|
||||
|
||||
// Only loop while the block pointer is less then the current position
|
||||
while (block as u64) < self.blocks.as_ptr() as u64 + self_position {
|
||||
let b = &mut *block;
|
||||
|
||||
let status = b.status.load(Ordering::Acquire);
|
||||
|
||||
// An unused block is safe to use
|
||||
if status == MemoryStatus::Unused as u8 {
|
||||
return Some(b);
|
||||
}
|
||||
|
||||
// Re-use freed blocks when they're large enough
|
||||
if status == MemoryStatus::Free as u8 && b.size >= length as usize {
|
||||
// Split block if there is too much excess
|
||||
if b.size - length as usize >= BLOCK_SPLIT_SIZE {
|
||||
b.size -= length as usize;
|
||||
b.used = 0;
|
||||
|
||||
let block1 = b.data.as_mut_ptr().add(b.size) as *mut MemoryBlock;
|
||||
let b1 = &mut *block1;
|
||||
b1.size = length as usize;
|
||||
b1.used = 0;
|
||||
b1.status.store(MemoryStatus::Free as u8, Ordering::Release);
|
||||
return Some(b1);
|
||||
}
|
||||
|
||||
// Otherwise return the whole block
|
||||
return Some(b);
|
||||
}
|
||||
|
||||
// Get the next block
|
||||
block = b.next_ptr();
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Create a new `MemoryBlock`, when `Some(block)` is returned, `block` will contain at least enough room for `length` bytes
|
||||
/// but may be as large as `length` + `BLOCK_SPLIT_SIZE` bytes. When `None` is returned the allocation has failed.
|
||||
pub unsafe fn alloc(&mut self, length: Length) -> Option<&'static mut MemoryBlock> {
|
||||
let self_position = self.position.load(Ordering::Acquire);
|
||||
let self_length = self.length.load(Ordering::Acquire);
|
||||
let b = self.find_free_block(length, self_position);
|
||||
|
||||
// If there's a free block then re-use it
|
||||
if let Some(b) = b {
|
||||
b.used = length as usize;
|
||||
b.status
|
||||
.store(MemoryStatus::Active as u8, Ordering::Release);
|
||||
return Some(b);
|
||||
}
|
||||
|
||||
// Get the current index for a new block
|
||||
let curr = self.blocks.as_ptr() as u64 + self_position;
|
||||
|
||||
// Get the number of bytes available
|
||||
let mem_left = self_length - self_position;
|
||||
|
||||
// When the allocation is larger than the number of bytes available
|
||||
// we will need to try to grow the memory
|
||||
if length >= mem_left {
|
||||
// Calculate the number of pages needed to cover the remaining bytes
|
||||
let npages = num_pages(length);
|
||||
let x = core::arch::wasm32::memory_grow(0, npages);
|
||||
if x == usize::MAX {
|
||||
return None;
|
||||
}
|
||||
self.length
|
||||
.fetch_add(npages as u64 * PAGE_SIZE as u64, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
// Bump the position by the size of the actual data + the size of the MemoryBlock structure
|
||||
self.position.fetch_add(
|
||||
length + core::mem::size_of::<MemoryBlock>() as u64,
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
|
||||
// Initialize a new block at the current position
|
||||
let ptr = curr as *mut MemoryBlock;
|
||||
let block = &mut *ptr;
|
||||
block
|
||||
.status
|
||||
.store(MemoryStatus::Active as u8, Ordering::Release);
|
||||
block.size = length as usize;
|
||||
block.used = length as usize;
|
||||
Some(block)
|
||||
}
|
||||
|
||||
/// Finds the block at an offset in memory
|
||||
pub unsafe fn find_block(&mut self, offs: Pointer) -> Option<&mut MemoryBlock> {
|
||||
if offs >= self.blocks.as_ptr() as Pointer + self.length.load(Ordering::Acquire) as Pointer
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let ptr = offs - core::mem::size_of::<MemoryBlock>() as u64;
|
||||
let ptr = ptr as *mut MemoryBlock;
|
||||
Some(&mut *ptr)
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoryBlock {
|
||||
/// Get a pointer to the next block
|
||||
///
|
||||
/// NOTE: This does no checking to ensure the resulting pointer is valid, the offset
|
||||
/// is calculated based on metadata provided by the current block
|
||||
#[inline]
|
||||
pub unsafe fn next_ptr(&mut self) -> *mut MemoryBlock {
|
||||
self.data
|
||||
.as_mut_ptr()
|
||||
.add(self.size + core::mem::size_of::<MemoryBlock>()) as *mut MemoryBlock
|
||||
}
|
||||
|
||||
/// Mark a block as free
|
||||
pub fn free(&mut self) {
|
||||
self.status
|
||||
.store(MemoryStatus::Free as u8, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
// Extism functions - these functions should be
|
||||
|
||||
/// Allocate a block of memory and return the offset
|
||||
#[no_mangle]
|
||||
pub unsafe fn extism_alloc(n: Length) -> Pointer {
|
||||
let region = MemoryRoot::new();
|
||||
let block = region.alloc(n);
|
||||
match block {
|
||||
Some(block) => block.data.as_mut_ptr() as Pointer,
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Free allocated memory
|
||||
#[no_mangle]
|
||||
pub unsafe fn extism_free(p: Pointer) {
|
||||
let block = MemoryRoot::new().find_block(p);
|
||||
if let Some(block) = block {
|
||||
block.free();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the length of an allocated memory block
|
||||
#[no_mangle]
|
||||
pub unsafe fn extism_length(p: Pointer) -> Length {
|
||||
if p == 0 {
|
||||
return 0;
|
||||
}
|
||||
if let Some(block) = MemoryRoot::new().find_block(p) {
|
||||
block.used as Length
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a byte from Extism-managed memory
|
||||
#[no_mangle]
|
||||
pub unsafe fn extism_load_u8(p: Pointer) -> u8 {
|
||||
*(p as *mut u8)
|
||||
}
|
||||
|
||||
/// Load a u64 from Extism-managed memory
|
||||
#[no_mangle]
|
||||
pub unsafe fn extism_load_u64(p: Pointer) -> u64 {
|
||||
*(p as *mut u64)
|
||||
}
|
||||
|
||||
/// Load a byte from the input data
|
||||
#[no_mangle]
|
||||
pub unsafe fn extism_input_load_u8(p: Pointer) -> u8 {
|
||||
*((INPUT_OFFSET + p) as *mut u8)
|
||||
}
|
||||
|
||||
/// Load a u64 from the input data
|
||||
#[no_mangle]
|
||||
pub unsafe fn extism_input_load_u64(p: Pointer) -> u64 {
|
||||
*((INPUT_OFFSET + p) as *mut u64)
|
||||
}
|
||||
|
||||
/// Write a byte in Extism-managed memory
|
||||
#[no_mangle]
|
||||
pub unsafe fn extism_store_u8(p: Pointer, x: u8) {
|
||||
*(p as *mut u8) = x;
|
||||
}
|
||||
|
||||
/// Write a u64 in Extism-managed memory
|
||||
#[no_mangle]
|
||||
pub unsafe fn extism_store_u64(p: Pointer, x: u64) {
|
||||
unsafe {
|
||||
*(p as *mut u64) = x;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the range of the input data in memory
|
||||
#[no_mangle]
|
||||
pub fn extism_input_set(p: Pointer, len: Length) {
|
||||
unsafe {
|
||||
INPUT_OFFSET = p;
|
||||
INPUT_LENGTH = len;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the range of the output data in memory
|
||||
#[no_mangle]
|
||||
pub fn extism_output_set(p: Pointer, len: Length) {
|
||||
unsafe {
|
||||
OUTPUT_OFFSET = p;
|
||||
OUTPUT_LENGTH = len;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the input length
|
||||
#[no_mangle]
|
||||
pub fn extism_input_length() -> Length {
|
||||
unsafe { INPUT_LENGTH }
|
||||
}
|
||||
|
||||
/// Get the input offset in Exitsm-managed memory
|
||||
#[no_mangle]
|
||||
pub fn extism_input_offset() -> Length {
|
||||
unsafe { INPUT_OFFSET }
|
||||
}
|
||||
|
||||
/// Get the output length
|
||||
#[no_mangle]
|
||||
pub fn extism_output_length() -> Length {
|
||||
unsafe { OUTPUT_LENGTH }
|
||||
}
|
||||
|
||||
/// Get the output offset in Extism-managed memory
|
||||
#[no_mangle]
|
||||
pub fn extism_output_offset() -> Length {
|
||||
unsafe { OUTPUT_OFFSET }
|
||||
}
|
||||
|
||||
/// Reset the allocator
|
||||
#[no_mangle]
|
||||
pub unsafe fn extism_reset() {
|
||||
ERROR.store(0, Ordering::SeqCst);
|
||||
MemoryRoot::new().reset()
|
||||
}
|
||||
|
||||
/// Set the error message offset
|
||||
#[no_mangle]
|
||||
pub unsafe fn extism_error_set(ptr: Pointer) {
|
||||
ERROR.store(ptr, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Get the error message offset, if it's `0` then no error has been set
|
||||
#[no_mangle]
|
||||
pub unsafe fn extism_error_get() -> Pointer {
|
||||
ERROR.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Get the position of the allocator, this can be used as an indication of how many bytes are currently in-use
|
||||
#[no_mangle]
|
||||
pub unsafe fn extism_memory_bytes() -> Length {
|
||||
MemoryRoot::new().position.load(Ordering::Acquire)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user