mirror of
https://github.com/extism/extism.git
synced 2026-01-11 23:08:06 -05:00
Compare commits
107 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14d7eae99c | ||
|
|
e89ddd5a2a | ||
|
|
93392e0884 | ||
|
|
4ebd0eb372 | ||
|
|
8feee0c693 | ||
|
|
773ab32a45 | ||
|
|
6a041d0c39 | ||
|
|
745a03ece4 | ||
|
|
67eb8c1571 | ||
|
|
6a15884963 | ||
|
|
72f62c4035 | ||
|
|
e86398a612 | ||
|
|
461ac97529 | ||
|
|
f312b0bcce | ||
|
|
6102a99d98 | ||
|
|
b6f2e845d9 | ||
|
|
d1137fa739 | ||
|
|
40a42882ba | ||
|
|
889ec39e4d | ||
|
|
fa909fd53d | ||
|
|
8dd4992633 | ||
|
|
cfc9fb06a4 | ||
|
|
0e1a8677c5 | ||
|
|
d80584600b | ||
|
|
07623ef2da | ||
|
|
c7c7f9d024 | ||
|
|
3420960d7f | ||
|
|
baa060d062 | ||
|
|
3219d79a8f | ||
|
|
a3053a9ecc | ||
|
|
41450a03a9 | ||
|
|
26b34ed906 | ||
|
|
ab4995dac2 | ||
|
|
3da526286e | ||
|
|
618c132194 | ||
|
|
f4aa139ece | ||
|
|
4548480c0b | ||
|
|
7db38fd10e | ||
|
|
58f7d5fa95 | ||
|
|
f10539a431 | ||
|
|
e26e6d2da1 | ||
|
|
70a9a3da66 | ||
|
|
b86f6267f6 | ||
|
|
6a49a6ee7c | ||
|
|
3b5fea71d7 | ||
|
|
c0ccf7558c | ||
|
|
35a6887e9f | ||
|
|
81e51fb059 | ||
|
|
f606ab619f | ||
|
|
c78104a846 | ||
|
|
59101d80a6 | ||
|
|
b3b5e67abb | ||
|
|
128b3173a8 | ||
|
|
ef0b6b46ac | ||
|
|
2e5f5ef716 | ||
|
|
360df45e1a | ||
|
|
3bdf4ef0d0 | ||
|
|
0517aca413 | ||
|
|
a6807a44c9 | ||
|
|
dc6f99d924 | ||
|
|
62267e874a | ||
|
|
ab0a7c1650 | ||
|
|
12820aecff | ||
|
|
1db4528490 | ||
|
|
d0f77dd886 | ||
|
|
4016b86135 | ||
|
|
6a73b23076 | ||
|
|
0c70be285d | ||
|
|
c1c84379d7 | ||
|
|
0f8954c203 | ||
|
|
deb717b0e8 | ||
|
|
bb3902e318 | ||
|
|
86326117cc | ||
|
|
a0ec6a3083 | ||
|
|
3e5785e50c | ||
|
|
67aa3627fc | ||
|
|
15a74e07fb | ||
|
|
16950cbdda | ||
|
|
ba7098b52a | ||
|
|
3718f21f4a | ||
|
|
4e5108bc69 | ||
|
|
9d758e7fd3 | ||
|
|
48699a0126 | ||
|
|
32e5ab161c | ||
|
|
9e57369bbb | ||
|
|
26424a1581 | ||
|
|
039196b8ac | ||
|
|
6bec3f8d45 | ||
|
|
b2e0884442 | ||
|
|
c22e97a82b | ||
|
|
0c51e26820 | ||
|
|
83365e72b9 | ||
|
|
4c06ef14c0 | ||
|
|
670f364184 | ||
|
|
a4093e229a | ||
|
|
74ba0cdf0d | ||
|
|
82fae7cf29 | ||
|
|
1f9c469e31 | ||
|
|
415f423147 | ||
|
|
6bd1b665eb | ||
|
|
d4e364f883 | ||
|
|
0b7589a3eb | ||
|
|
eda80134f0 | ||
|
|
300d801d1a | ||
|
|
524f069a08 | ||
|
|
8dd5c8a138 | ||
|
|
c7f533f9c6 |
10
.github/workflows/release-rust.yaml
vendored
10
.github/workflows/release-rust.yaml
vendored
@@ -19,17 +19,21 @@ jobs:
|
||||
override: true
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
- name: Release Rust Host SDK
|
||||
- name: Release Rust Manifest Crate
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_API_TOKEN }}
|
||||
run: |
|
||||
# order of crate publication matter: manifest, runtime, rust
|
||||
|
||||
cargo publish --manifest-path manifest/Cargo.toml
|
||||
# allow for crates.io to update so dependant crates can locate extism-manifest
|
||||
sleep 5
|
||||
|
||||
cargo publish --manifest-path runtime/Cargo.toml --no-verify
|
||||
- name: Release Rust Host SDK
|
||||
if: always()
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_API_TOKEN }}
|
||||
run: |
|
||||
#cargo publish --manifest-path runtime/Cargo.toml --no-verify
|
||||
cargo publish --manifest-path rust/Cargo.toml
|
||||
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -30,6 +30,7 @@ rust/test.log
|
||||
duniverse
|
||||
_build
|
||||
php/Extism.php
|
||||
python/docs
|
||||
dist-newstyle
|
||||
.stack-work
|
||||
vendor
|
||||
|
||||
@@ -4,5 +4,5 @@ members = [
|
||||
"runtime",
|
||||
"rust",
|
||||
"libextism",
|
||||
"elixir/native/extism_nif"
|
||||
]
|
||||
exclude = ["kernel"]
|
||||
|
||||
4
Makefile
4
Makefile
@@ -21,6 +21,10 @@ endif
|
||||
build:
|
||||
cargo build --release $(FEATURE_FLAGS) --manifest-path libextism/Cargo.toml
|
||||
|
||||
.PHONY: kernel
|
||||
kernel:
|
||||
cd kernel && bash build.sh
|
||||
|
||||
lint:
|
||||
cargo clippy --release --no-deps --manifest-path runtime/Cargo.toml
|
||||
|
||||
|
||||
@@ -103,7 +103,11 @@
|
||||
}
|
||||
|
||||
async loadFunctions(url) {
|
||||
let plugin = await this.extismContext.newPlugin({ "wasm": [ { "path": 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 functions = Object.keys(await plugin.getExports())
|
||||
console.log("funcs ", functions)
|
||||
this.setState({functions})
|
||||
@@ -135,7 +139,13 @@
|
||||
|
||||
async handleOnRun(e) {
|
||||
e && e.preventDefault && e.preventDefault();
|
||||
let plugin = await this.extismContext.newPlugin({ "wasm": [ { "path": this.state.url } ] })
|
||||
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 result = await plugin.call(this.state.func_name, this.state.input)
|
||||
let output = result
|
||||
this.setState({output})
|
||||
|
||||
42
browser/package-lock.json
generated
42
browser/package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "@extism/runtime-browser",
|
||||
"version": "0.2.2",
|
||||
"version": "0.3.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@extism/runtime-browser",
|
||||
"version": "0.2.2",
|
||||
"version": "0.3.0",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@bjorn3/browser_wasi_shim": "^0.2.1"
|
||||
"@bjorn3/browser_wasi_shim": "^0.2.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.2.2",
|
||||
@@ -568,9 +568,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@bjorn3/browser_wasi_shim": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@bjorn3/browser_wasi_shim/-/browser_wasi_shim-0.2.1.tgz",
|
||||
"integrity": "sha512-QBI2VPoCksV+bN47v1edbFC0td1nXvEhK3i1oTrByKOnLG39RoxZR2KmLByR/6S+8ivAf2E4pWhqRRZsBWItyQ=="
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@bjorn3/browser_wasi_shim/-/browser_wasi_shim-0.2.7.tgz",
|
||||
"integrity": "sha512-ONBGleCpaH5HC4MLLkUmLz69xA28HQIbvwsdA1WTMXvjyhOWXR7jVrC0DkYr/iRqmkNMBZtEVVZWm1L6ZAnJvw=="
|
||||
},
|
||||
"node_modules/@cnakazawa/watch": {
|
||||
"version": "1.0.4",
|
||||
@@ -7618,9 +7618,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz",
|
||||
"integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==",
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
|
||||
"integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"psl": "^1.1.33",
|
||||
@@ -8108,9 +8108,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
|
||||
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -8681,9 +8681,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@bjorn3/browser_wasi_shim": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@bjorn3/browser_wasi_shim/-/browser_wasi_shim-0.2.1.tgz",
|
||||
"integrity": "sha512-QBI2VPoCksV+bN47v1edbFC0td1nXvEhK3i1oTrByKOnLG39RoxZR2KmLByR/6S+8ivAf2E4pWhqRRZsBWItyQ=="
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@bjorn3/browser_wasi_shim/-/browser_wasi_shim-0.2.7.tgz",
|
||||
"integrity": "sha512-ONBGleCpaH5HC4MLLkUmLz69xA28HQIbvwsdA1WTMXvjyhOWXR7jVrC0DkYr/iRqmkNMBZtEVVZWm1L6ZAnJvw=="
|
||||
},
|
||||
"@cnakazawa/watch": {
|
||||
"version": "1.0.4",
|
||||
@@ -13986,9 +13986,9 @@
|
||||
}
|
||||
},
|
||||
"tough-cookie": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz",
|
||||
"integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==",
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
|
||||
"integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"psl": "^1.1.33",
|
||||
@@ -14334,9 +14334,9 @@
|
||||
}
|
||||
},
|
||||
"word-wrap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
|
||||
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
|
||||
"dev": true
|
||||
},
|
||||
"wrap-ansi": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@extism/runtime-browser",
|
||||
"version": "0.2.2",
|
||||
"version": "0.3.0",
|
||||
"description": "Extism runtime in the browser",
|
||||
"scripts": {
|
||||
"build": "node build.js && tsc --emitDeclarationOnly --outDir dist",
|
||||
@@ -34,6 +34,6 @@
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bjorn3/browser_wasi_shim": "^0.2.1"
|
||||
"@bjorn3/browser_wasi_shim": "^0.2.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, config?: PluginConfig) {
|
||||
async newPlugin(manifest: ManifestData, functions: Record<string, any> = {}, 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, config);
|
||||
return new ExtismPlugin(moduleData, functions, config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,6 @@ describe('', () => {
|
||||
// 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)
|
||||
expect(true).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import ExtismContext from './context';
|
||||
import { ExtismFunction, ExtismPlugin } from './plugin';
|
||||
|
||||
export { ExtismContext };
|
||||
export { ExtismContext, ExtismFunction, ExtismPlugin };
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import Allocator from './allocator';
|
||||
import { PluginConfig } from './manifest';
|
||||
//@ts-ignore TODO add types to this library
|
||||
import { WASI, File } from "@bjorn3/browser_wasi_shim";
|
||||
import { WASI, Fd } from '@bjorn3/browser_wasi_shim';
|
||||
|
||||
export default class ExtismPlugin {
|
||||
export type ExtismFunction = any;
|
||||
|
||||
export class ExtismPlugin {
|
||||
moduleData: ArrayBuffer;
|
||||
allocator: Allocator;
|
||||
config?: PluginConfig;
|
||||
@@ -11,14 +12,16 @@ export default class ExtismPlugin {
|
||||
input: Uint8Array;
|
||||
output: Uint8Array;
|
||||
module?: WebAssembly.WebAssemblyInstantiatedSource;
|
||||
functions: Record<string, ExtismFunction>;
|
||||
|
||||
constructor(moduleData: ArrayBuffer, config?: PluginConfig) {
|
||||
constructor(moduleData: ArrayBuffer, functions: Record<string, ExtismFunction> = {}, 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> {
|
||||
@@ -65,26 +68,31 @@ export default class ExtismPlugin {
|
||||
const environment = this.makeEnv();
|
||||
const args: Array<string> = [];
|
||||
const envVars: Array<string> = [];
|
||||
let fds = [
|
||||
new File([]), // stdin
|
||||
new File([]), // stdout
|
||||
new File([]), // stderr
|
||||
let fds: Fd[] = [
|
||||
// new XtermStdio(term), // stdin
|
||||
// new XtermStdio(term), // stdout
|
||||
// new XtermStdio(term), // 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) {
|
||||
wasi.start(this.module.instance);
|
||||
//@ts-ignore
|
||||
this.module.instance.exports._start();
|
||||
}
|
||||
return this.module;
|
||||
}
|
||||
|
||||
makeEnv(): any {
|
||||
const plugin = this;
|
||||
return {
|
||||
var env: any = {
|
||||
extism_alloc(n: bigint): bigint {
|
||||
return plugin.allocator.alloc(n);
|
||||
},
|
||||
@@ -183,5 +191,13 @@ export default class ExtismPlugin {
|
||||
console.error(s);
|
||||
},
|
||||
};
|
||||
|
||||
for (const [name, func] of Object.entries(this.functions)) {
|
||||
env[name] = function () {
|
||||
return func.apply(plugin, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ std::vector<uint8_t> read(const char *filename) {
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
auto wasm = read("../wasm/code-functions.wasm");
|
||||
Context context = Context();
|
||||
std::string tmp = "Testing";
|
||||
|
||||
// A lambda can be used as a host function
|
||||
@@ -34,7 +33,7 @@ int main(int argc, char *argv[]) {
|
||||
[](void *x) { std::cout << "Free user data" << std::endl; }),
|
||||
};
|
||||
|
||||
Plugin plugin = context.plugin(wasm, true, functions);
|
||||
Plugin plugin(wasm, true, functions);
|
||||
|
||||
const char *input = argc > 1 ? argv[1] : "this is a test";
|
||||
ExtismSize length = strlen(input);
|
||||
|
||||
@@ -342,9 +342,10 @@ class Plugin {
|
||||
|
||||
public:
|
||||
// Create a new plugin
|
||||
Plugin(std::shared_ptr<ExtismContext> ctx, const uint8_t *wasm,
|
||||
ExtismSize length, bool with_wasi = false,
|
||||
std::vector<Function> functions = std::vector<Function>())
|
||||
Plugin(const uint8_t *wasm, ExtismSize length, bool with_wasi = false,
|
||||
std::vector<Function> functions = std::vector<Function>(),
|
||||
std::shared_ptr<ExtismContext> ctx = std::shared_ptr<ExtismContext>(
|
||||
extism_context_new(), extism_context_free))
|
||||
: functions(functions) {
|
||||
std::vector<const ExtismFunction *> ptrs;
|
||||
for (auto i : this->functions) {
|
||||
@@ -359,6 +360,19 @@ public:
|
||||
this->context = ctx;
|
||||
}
|
||||
|
||||
Plugin(const std::string &str, bool with_wasi = false,
|
||||
std::vector<Function> functions = {},
|
||||
std::shared_ptr<ExtismContext> ctx = std::shared_ptr<ExtismContext>(
|
||||
extism_context_new(), extism_context_free))
|
||||
: Plugin((const uint8_t *)str.c_str(), str.size(), with_wasi, functions,
|
||||
ctx) {}
|
||||
|
||||
Plugin(const std::vector<uint8_t> &data, bool with_wasi = false,
|
||||
std::vector<Function> functions = {},
|
||||
std::shared_ptr<ExtismContext> ctx = std::shared_ptr<ExtismContext>(
|
||||
extism_context_new(), extism_context_free))
|
||||
: Plugin(data.data(), data.size(), with_wasi, functions, ctx) {}
|
||||
|
||||
CancelHandle cancel_handle() {
|
||||
return CancelHandle(
|
||||
extism_plugin_cancel_handle(this->context.get(), this->id()));
|
||||
@@ -366,8 +380,10 @@ public:
|
||||
|
||||
#ifndef EXTISM_NO_JSON
|
||||
// Create a new plugin from Manifest
|
||||
Plugin(std::shared_ptr<ExtismContext> ctx, const Manifest &manifest,
|
||||
bool with_wasi = false, std::vector<Function> functions = {}) {
|
||||
Plugin(const Manifest &manifest, bool with_wasi = false,
|
||||
std::vector<Function> functions = {},
|
||||
std::shared_ptr<ExtismContext> ctx = std::shared_ptr<ExtismContext>(
|
||||
extism_context_new(), extism_context_free)) {
|
||||
std::vector<const ExtismFunction *> ptrs;
|
||||
for (auto i : this->functions) {
|
||||
ptrs.push_back(i.get());
|
||||
@@ -506,28 +522,28 @@ public:
|
||||
// Create plugin from uint8_t*
|
||||
Plugin plugin(const uint8_t *wasm, size_t length, bool with_wasi = false,
|
||||
std::vector<Function> functions = {}) const {
|
||||
return Plugin(this->pointer, wasm, length, with_wasi, functions);
|
||||
return Plugin(wasm, length, with_wasi, functions, this->pointer);
|
||||
}
|
||||
|
||||
// Create plugin from std::string
|
||||
Plugin plugin(const std::string &str, bool with_wasi = false,
|
||||
std::vector<Function> functions = {}) const {
|
||||
return Plugin(this->pointer, (const uint8_t *)str.c_str(), str.size(),
|
||||
with_wasi, functions);
|
||||
return Plugin((const uint8_t *)str.c_str(), str.size(), with_wasi,
|
||||
functions, this->pointer);
|
||||
}
|
||||
|
||||
// Create plugin from uint8_t vector
|
||||
Plugin plugin(const std::vector<uint8_t> &data, bool with_wasi = false,
|
||||
std::vector<Function> functions = {}) const {
|
||||
return Plugin(this->pointer, data.data(), data.size(), with_wasi,
|
||||
functions);
|
||||
return Plugin(data.data(), data.size(), with_wasi, functions,
|
||||
this->pointer);
|
||||
}
|
||||
|
||||
#ifndef EXTISM_NO_JSON
|
||||
// Create plugin from Manifest
|
||||
Plugin plugin(const Manifest &manifest, bool with_wasi = false,
|
||||
std::vector<Function> functions = {}) const {
|
||||
return Plugin(this->pointer, manifest, with_wasi, functions);
|
||||
return Plugin(manifest, with_wasi, functions, this->pointer);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -21,21 +21,19 @@ TEST(Context, Basic) {
|
||||
}
|
||||
|
||||
TEST(Plugin, Manifest) {
|
||||
Context context;
|
||||
Manifest manifest = Manifest::path(code);
|
||||
manifest.set_config("a", "1");
|
||||
|
||||
ASSERT_NO_THROW(Plugin plugin = context.plugin(manifest));
|
||||
Plugin plugin = context.plugin(manifest);
|
||||
ASSERT_NO_THROW(Plugin plugin(manifest));
|
||||
Plugin 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 = context.plugin(manifest), Error);
|
||||
ASSERT_THROW(Plugin plugin(manifest), Error);
|
||||
}
|
||||
|
||||
TEST(Plugin, Bytes) {
|
||||
@@ -68,7 +66,6 @@ TEST(Plugin, FunctionExists) {
|
||||
}
|
||||
|
||||
TEST(Plugin, HostFunction) {
|
||||
Context context;
|
||||
auto wasm = read("../../wasm/code-functions.wasm");
|
||||
auto t = std::vector<ValType>{ValType::I64};
|
||||
Function hello_world =
|
||||
@@ -82,7 +79,7 @@ TEST(Plugin, HostFunction) {
|
||||
auto functions = std::vector<Function>{
|
||||
hello_world,
|
||||
};
|
||||
Plugin plugin = context.plugin(wasm, true, functions);
|
||||
Plugin plugin(wasm, true, functions);
|
||||
auto buf = plugin.call("count_vowels", "aaa");
|
||||
ASSERT_EQ(buf.length, 4);
|
||||
ASSERT_EQ((std::string)buf, "test");
|
||||
|
||||
@@ -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.4.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.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>
|
||||
|
||||
<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,21 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\..\wasm\code.wasm" Link="code.wasm">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<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>
|
||||
<ProjectReference Include="..\..\src\Extism.Sdk\Extism.Sdk.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Extism.runtime.win-x64" Version="0.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Extism.Sdk\Extism.Sdk.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -1,11 +1,40 @@
|
||||
using Extism.Sdk;
|
||||
using Extism.Sdk.Native;
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
var context = new Context();
|
||||
var wasm = await File.ReadAllBytesAsync("./code.wasm");
|
||||
using var plugin = context.CreatePlugin(wasm, withWasi: true);
|
||||
|
||||
var userData = Marshal.StringToHGlobalAnsi("Hello again!");
|
||||
|
||||
using var helloWorld = new HostFunction(
|
||||
"hello_world",
|
||||
"env",
|
||||
new[] { ExtismValType.I64 },
|
||||
new[] { ExtismValType.I64 },
|
||||
userData,
|
||||
HelloWorld);
|
||||
|
||||
void HelloWorld(CurrentPlugin plugin, Span<ExtismVal> inputs, Span<ExtismVal> outputs, nint data)
|
||||
{
|
||||
Console.WriteLine("Hello from .NET!");
|
||||
|
||||
var text = Marshal.PtrToStringAnsi(data);
|
||||
Console.WriteLine(text);
|
||||
|
||||
var input = plugin.ReadString(new nint(inputs[0].v.i64));
|
||||
Console.WriteLine($"Input: {input}");
|
||||
|
||||
outputs[0].v.i64 = plugin.WriteString(input);
|
||||
}
|
||||
|
||||
var wasm = File.ReadAllBytes("./code-functions.wasm");
|
||||
using var plugin = context.CreatePlugin(wasm, new[] { helloWorld }, withWasi: true);
|
||||
|
||||
var output = Encoding.UTF8.GetString(
|
||||
plugin.CallFunction("count_vowels", Encoding.UTF8.GetBytes("Hello World!"))
|
||||
);
|
||||
Console.WriteLine(output); // prints {"count": 3}
|
||||
|
||||
Console.WriteLine($"Output: {output}");
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
@@ -6,8 +7,10 @@ namespace Extism.Sdk.Native;
|
||||
/// <summary>
|
||||
/// Represents an Extism context through which you can load <see cref="Plugin"/>s.
|
||||
/// </summary>
|
||||
public class Context : IDisposable
|
||||
public unsafe class Context : IDisposable
|
||||
{
|
||||
private readonly ConcurrentDictionary<int, Plugin> _plugins = new ConcurrentDictionary<int, Plugin>();
|
||||
|
||||
private const int DisposedMarker = 1;
|
||||
|
||||
private int _disposed;
|
||||
@@ -17,33 +20,63 @@ public class Context : IDisposable
|
||||
/// </summary>
|
||||
public Context()
|
||||
{
|
||||
NativeHandle = LibExtism.extism_context_new();
|
||||
unsafe
|
||||
{
|
||||
NativeHandle = LibExtism.extism_context_new();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Native pointer to the Extism Context.
|
||||
/// </summary>
|
||||
internal IntPtr NativeHandle { get; }
|
||||
internal LibExtism.ExtismContext* NativeHandle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Loads an Extism <see cref="Plugin"/>.
|
||||
/// </summary>
|
||||
/// <param name="wasm">A WASM module (wat or wasm) or a JSON encoded manifest.</param>
|
||||
/// <param name="functions">List of host functions expected by the plugin.</param>
|
||||
/// <param name="withWasi">Enable/Disable WASI.</param>
|
||||
public Plugin CreatePlugin(ReadOnlySpan<byte> wasm, bool withWasi)
|
||||
public Plugin CreatePlugin(ReadOnlySpan<byte> wasm, HostFunction[] functions, bool withWasi)
|
||||
{
|
||||
CheckNotDisposed();
|
||||
|
||||
var functionHandles = functions.Select(f => f.NativeHandle).ToArray();
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* wasmPtr = wasm)
|
||||
fixed (IntPtr* functionsPtr = functionHandles)
|
||||
{
|
||||
var plugin = LibExtism.extism_plugin_new(NativeHandle, wasmPtr, wasm.Length, null, 0, withWasi);
|
||||
return new Plugin(this, plugin);
|
||||
var index = LibExtism.extism_plugin_new(NativeHandle, wasmPtr, wasm.Length, functionsPtr, functions.Length, withWasi);
|
||||
if (index == -1)
|
||||
{
|
||||
var errorMsg = GetError();
|
||||
if (errorMsg != null)
|
||||
{
|
||||
throw new ExtismException(errorMsg);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ExtismException("Failed to create plugin.");
|
||||
}
|
||||
}
|
||||
|
||||
return _plugins[index] = new Plugin(this, functions, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a plugin by index.
|
||||
/// </summary>
|
||||
/// <param name="index">Index of plugin.</param>
|
||||
/// <returns></returns>
|
||||
public Plugin GetPlugin(int index)
|
||||
{
|
||||
return _plugins[index];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove all plugins from this <see cref="Context"/>'s registry.
|
||||
/// </summary>
|
||||
@@ -110,6 +143,11 @@ public class Context : IDisposable
|
||||
// Free up any managed resources here
|
||||
}
|
||||
|
||||
foreach (var plugin in _plugins.Values)
|
||||
{
|
||||
plugin.Dispose();
|
||||
}
|
||||
|
||||
// Free up unmanaged resources
|
||||
LibExtism.extism_context_free(NativeHandle);
|
||||
}
|
||||
@@ -151,34 +189,3 @@ public class Context : IDisposable
|
||||
return LibExtism.extism_log_file(logPath, logLevel);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extism Log Levels
|
||||
/// </summary>
|
||||
public enum LogLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// Designates very serious errors.
|
||||
/// </summary>
|
||||
Error,
|
||||
|
||||
/// <summary>
|
||||
/// Designates hazardous situations.
|
||||
/// </summary>
|
||||
Warning,
|
||||
|
||||
/// <summary>
|
||||
/// Designates useful information.
|
||||
/// </summary>
|
||||
Info,
|
||||
|
||||
/// <summary>
|
||||
/// Designates lower priority information.
|
||||
/// </summary>
|
||||
Debug,
|
||||
|
||||
/// <summary>
|
||||
/// Designates very low priority, often extremely verbose, information.
|
||||
/// </summary>
|
||||
Trace
|
||||
}
|
||||
|
||||
138
dotnet/src/Extism.Sdk/CurrentPlugin.cs
Normal file
138
dotnet/src/Extism.Sdk/CurrentPlugin.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
<LangVersion>10</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.3.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.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>
|
||||
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -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.
|
||||
146
dotnet/src/Extism.Sdk/HostFunction.cs
Normal file
146
dotnet/src/Extism.Sdk/HostFunction.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
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,24 +2,197 @@ using System.Runtime.InteropServices;
|
||||
|
||||
namespace Extism.Sdk.Native;
|
||||
|
||||
/// <summary>
|
||||
/// A union type for host function argument/return values.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
public struct ExtismValUnion
|
||||
{
|
||||
/// <summary>
|
||||
/// Set this for 32 bit integers
|
||||
/// </summary>
|
||||
[FieldOffset(0)]
|
||||
public int i32;
|
||||
|
||||
/// <summary>
|
||||
/// Set this for 64 bit integers
|
||||
/// </summary>
|
||||
[FieldOffset(0)]
|
||||
public long i64;
|
||||
|
||||
/// <summary>
|
||||
/// Set this for 32 bit floats
|
||||
/// </summary>
|
||||
[FieldOffset(0)]
|
||||
public float f32;
|
||||
|
||||
/// <summary>
|
||||
/// Set this for 64 bit floats
|
||||
/// </summary>
|
||||
[FieldOffset(0)]
|
||||
public double f64;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents Wasm data types that Extism can understand
|
||||
/// </summary>
|
||||
public enum ExtismValType : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Signed 32 bit integer. Equivalent of <see cref="int"/> or <see cref="uint"/>
|
||||
/// </summary>
|
||||
I32,
|
||||
|
||||
/// <summary>
|
||||
/// Signed 64 bit integer. Equivalent of <see cref="long"/> or <see cref="ulong"/>
|
||||
/// </summary>
|
||||
I64,
|
||||
|
||||
/// <summary>
|
||||
/// Floating point 32 bit integer. Equivalent of <see cref="float"/>
|
||||
/// </summary>
|
||||
F32,
|
||||
|
||||
/// <summary>
|
||||
/// Floating point 64 bit integer. Equivalent of <see cref="double"/>
|
||||
/// </summary>
|
||||
F64,
|
||||
|
||||
/// <summary>
|
||||
/// A 128 bit number.
|
||||
/// </summary>
|
||||
V128,
|
||||
|
||||
/// <summary>
|
||||
/// A reference to opaque data in the Wasm instance.
|
||||
/// </summary>
|
||||
FuncRef,
|
||||
|
||||
/// <summary>
|
||||
/// A reference to opaque data in the Wasm instance.
|
||||
/// </summary>
|
||||
ExternRef
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// `ExtismVal` holds the type and value of a function argument/return
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct ExtismVal
|
||||
{
|
||||
/// <summary>
|
||||
/// The type for the argument
|
||||
/// </summary>
|
||||
public ExtismValType t;
|
||||
|
||||
/// <summary>
|
||||
/// The value for the argument
|
||||
/// </summary>
|
||||
public ExtismValUnion v;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Functions exposed by the native Extism library.
|
||||
/// </summary>
|
||||
internal static class LibExtism
|
||||
{
|
||||
/// <summary>
|
||||
/// A `Context` is used to store and manage plugins.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct ExtismContext { }
|
||||
|
||||
/// <summary>
|
||||
/// Host function signature
|
||||
/// </summary>
|
||||
/// <param name="plugin"></param>
|
||||
/// <param name="inputs"></param>
|
||||
/// <param name="n_inputs"></param>
|
||||
/// <param name="outputs"></param>
|
||||
/// <param name="n_outputs"></param>
|
||||
/// <param name="data"></param>
|
||||
unsafe internal delegate void InternalExtismFunction(nint plugin, ExtismVal* inputs, uint n_inputs, ExtismVal* outputs, uint n_outputs, IntPtr data);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a pointer to the memory of the currently running plugin.
|
||||
/// NOTE: this should only be called from host functions.
|
||||
/// </summary>
|
||||
/// <param name="plugin"></param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism", EntryPoint = "extism_current_plugin_memory")]
|
||||
internal static extern IntPtr extism_current_plugin_memory(nint plugin);
|
||||
|
||||
/// <summary>
|
||||
/// Allocate a memory block in the currently running plugin
|
||||
/// </summary>
|
||||
/// <param name="plugin"></param>
|
||||
/// <param name="n"></param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism", EntryPoint = "extism_current_plugin_memory_alloc")]
|
||||
internal static extern IntPtr extism_current_plugin_memory_alloc(nint plugin, long n);
|
||||
|
||||
/// <summary>
|
||||
/// Get the length of an allocated block.
|
||||
/// NOTE: this should only be called from host functions.
|
||||
/// </summary>
|
||||
/// <param name="plugin"></param>
|
||||
/// <param name="n"></param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism", EntryPoint = "extism_current_plugin_memory_length")]
|
||||
internal static extern long extism_current_plugin_memory_length(nint plugin, long n);
|
||||
|
||||
/// <summary>
|
||||
/// Get the length of an allocated block.
|
||||
/// NOTE: this should only be called from host functions.
|
||||
/// </summary>
|
||||
/// <param name="plugin"></param>
|
||||
/// <param name="ptr"></param>
|
||||
[DllImport("extism", EntryPoint = "extism_current_plugin_memory_free")]
|
||||
internal static extern void extism_current_plugin_memory_free(nint plugin, IntPtr ptr);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new host function.
|
||||
/// </summary>
|
||||
/// <param name="name">function name, this should be valid UTF-8</param>
|
||||
/// <param name="inputs">argument types</param>
|
||||
/// <param name="nInputs">number of argument types</param>
|
||||
/// <param name="outputs">return types</param>
|
||||
/// <param name="nOutputs">number of return types</param>
|
||||
/// <param name="func">the function to call</param>
|
||||
/// <param name="userData">a pointer that will be passed to the function when it's called this value should live as long as the function exists</param>
|
||||
/// <param name="freeUserData">a callback to release the `user_data` value when the resulting `ExtismFunction` is freed.</param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism", EntryPoint = "extism_function_new")]
|
||||
unsafe internal static extern IntPtr extism_function_new(string name, ExtismValType* inputs, long nInputs, ExtismValType* outputs, long nOutputs, InternalExtismFunction func, IntPtr userData, IntPtr freeUserData);
|
||||
|
||||
/// <summary>
|
||||
/// Set the namespace of an <see cref="ExtismFunction"/>
|
||||
/// </summary>
|
||||
/// <param name="ptr"></param>
|
||||
/// <param name="namespace"></param>
|
||||
[DllImport("extism", EntryPoint = "extism_function_set_namespace")]
|
||||
internal static extern void extism_function_set_namespace(IntPtr ptr, string @namespace);
|
||||
|
||||
/// <summary>
|
||||
/// Free an <see cref="ExtismFunction"/>
|
||||
/// </summary>
|
||||
/// <param name="ptr"></param>
|
||||
[DllImport("extism", EntryPoint = "extism_function_free")]
|
||||
internal static extern void extism_function_free(IntPtr ptr);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new context.
|
||||
/// </summary>
|
||||
/// <returns>A pointer to the newly created context.</returns>
|
||||
[DllImport("extism")]
|
||||
public static extern IntPtr extism_context_new();
|
||||
unsafe internal static extern ExtismContext* extism_context_new();
|
||||
|
||||
/// <summary>
|
||||
/// Remove a context from the registry and free associated memory.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
[DllImport("extism")]
|
||||
public static extern void extism_context_free(IntPtr context);
|
||||
unsafe internal static extern void extism_context_free(ExtismContext* context);
|
||||
|
||||
/// <summary>
|
||||
/// Load a WASM plugin.
|
||||
@@ -32,7 +205,7 @@ internal static class LibExtism
|
||||
/// <param name="withWasi">Enables/disables WASI.</param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
unsafe public static extern IntPtr extism_plugin_new(IntPtr context, byte* wasm, int wasmSize, IntPtr *functions, int nFunctions, bool withWasi);
|
||||
unsafe internal static extern int extism_plugin_new(ExtismContext* context, byte* wasm, int wasmSize, IntPtr* functions, int nFunctions, bool withWasi);
|
||||
|
||||
/// <summary>
|
||||
/// Update a plugin, keeping the existing ID.
|
||||
@@ -42,13 +215,13 @@ internal static class LibExtism
|
||||
/// <param name="context">Pointer to the context the plugin is associated with.</param>
|
||||
/// <param name="plugin">Pointer to the plugin you want to update.</param>
|
||||
/// <param name="wasm">A WASM module (wat or wasm) or a JSON encoded manifest.</param>
|
||||
/// <param name="wasmLength">The length of the `wasm` parameter.</param>
|
||||
/// <param name="wasmSize">The length of the `wasm` parameter.</param>
|
||||
/// <param name="functions">Array of host function pointers.</param>
|
||||
/// <param name="nFunctions">Number of host functions.</param>
|
||||
/// <param name="withWasi">Enables/disables WASI.</param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
unsafe public static extern bool extism_plugin_update(IntPtr context, IntPtr plugin, byte* wasm, int wasmLength, IntPtr *functions, int nFunctions, bool withWasi);
|
||||
unsafe internal static extern bool extism_plugin_update(ExtismContext* context, int plugin, byte* wasm, long wasmSize, Span<IntPtr> functions, long nFunctions, bool withWasi);
|
||||
|
||||
/// <summary>
|
||||
/// Remove a plugin from the registry and free associated memory.
|
||||
@@ -56,14 +229,14 @@ internal static class LibExtism
|
||||
/// <param name="context">Pointer to the context the plugin is associated with.</param>
|
||||
/// <param name="plugin">Pointer to the plugin you want to free.</param>
|
||||
[DllImport("extism")]
|
||||
public static extern void extism_plugin_free(IntPtr context, IntPtr plugin);
|
||||
unsafe internal static extern void extism_plugin_free(ExtismContext* context, int plugin);
|
||||
|
||||
/// <summary>
|
||||
/// Remove all plugins from the registry.
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
[DllImport("extism")]
|
||||
public static extern void extism_context_reset(IntPtr context);
|
||||
unsafe internal static extern void extism_context_reset(ExtismContext* context);
|
||||
|
||||
/// <summary>
|
||||
/// Update plugin config values, this will merge with the existing values.
|
||||
@@ -74,7 +247,7 @@ internal static class LibExtism
|
||||
/// <param name="jsonLength">The length of the `json` parameter.</param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
unsafe public static extern bool extism_plugin_config(IntPtr context, IntPtr plugin, byte* json, int jsonLength);
|
||||
unsafe internal static extern bool extism_plugin_config(ExtismContext* context, int plugin, byte* json, int jsonLength);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if funcName exists.
|
||||
@@ -84,7 +257,7 @@ internal static class LibExtism
|
||||
/// <param name="funcName"></param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
public static extern bool extism_plugin_function_exists(IntPtr context, IntPtr plugin, string funcName);
|
||||
unsafe internal static extern bool extism_plugin_function_exists(ExtismContext* context, int plugin, string funcName);
|
||||
|
||||
/// <summary>
|
||||
/// Call a function.
|
||||
@@ -96,7 +269,7 @@ internal static class LibExtism
|
||||
/// <param name="dataLen">The length of the `data` parameter.</param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
unsafe public static extern int extism_plugin_call(IntPtr context, IntPtr plugin, string funcName, byte* data, int dataLen);
|
||||
unsafe internal static extern int extism_plugin_call(ExtismContext* context, int plugin, string funcName, byte* data, int dataLen);
|
||||
|
||||
/// <summary>
|
||||
/// Get the error associated with a Context or Plugin, if plugin is -1 then the context error will be returned.
|
||||
@@ -105,7 +278,7 @@ internal static class LibExtism
|
||||
/// <param name="plugin">A plugin pointer, or -1 for the context error.</param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
public static extern IntPtr extism_error(IntPtr context, nint plugin);
|
||||
unsafe internal static extern IntPtr extism_error(ExtismContext* context, nint plugin);
|
||||
|
||||
/// <summary>
|
||||
/// Get the length of a plugin's output data.
|
||||
@@ -114,7 +287,7 @@ internal static class LibExtism
|
||||
/// <param name="plugin"></param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
public static extern long extism_plugin_output_length(IntPtr context, IntPtr plugin);
|
||||
unsafe internal static extern long extism_plugin_output_length(ExtismContext* context, int plugin);
|
||||
|
||||
/// <summary>
|
||||
/// Get the plugin's output data.
|
||||
@@ -123,7 +296,7 @@ internal static class LibExtism
|
||||
/// <param name="plugin"></param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
public static extern IntPtr extism_plugin_output_data(IntPtr context, IntPtr plugin);
|
||||
unsafe internal static extern IntPtr extism_plugin_output_data(ExtismContext* context, int plugin);
|
||||
|
||||
/// <summary>
|
||||
/// Set log file and level.
|
||||
@@ -132,43 +305,43 @@ internal static class LibExtism
|
||||
/// <param name="logLevel"></param>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism")]
|
||||
public static extern bool extism_log_file(string filename, string logLevel);
|
||||
internal static extern bool extism_log_file(string filename, string logLevel);
|
||||
|
||||
/// <summary>
|
||||
/// Get the Extism version string.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DllImport("extism", EntryPoint = "extism_version")]
|
||||
public static extern IntPtr extism_version();
|
||||
internal static extern IntPtr extism_version();
|
||||
|
||||
/// <summary>
|
||||
/// Extism Log Levels
|
||||
/// </summary>
|
||||
public static class LogLevels
|
||||
internal static class LogLevels
|
||||
{
|
||||
/// <summary>
|
||||
/// Designates very serious errors.
|
||||
/// </summary>
|
||||
public const string Error = "Error";
|
||||
internal const string Error = "Error";
|
||||
|
||||
/// <summary>
|
||||
/// Designates hazardous situations.
|
||||
/// </summary>
|
||||
public const string Warn = "Warn";
|
||||
internal const string Warn = "Warn";
|
||||
|
||||
/// <summary>
|
||||
/// Designates useful information.
|
||||
/// </summary>
|
||||
public const string Info = "Info";
|
||||
internal const string Info = "Info";
|
||||
|
||||
/// <summary>
|
||||
/// Designates lower priority information.
|
||||
/// </summary>
|
||||
public const string Debug = "Debug";
|
||||
internal const string Debug = "Debug";
|
||||
|
||||
/// <summary>
|
||||
/// Designates very low priority, often extremely verbose, information.
|
||||
/// </summary>
|
||||
public const string Trace = "Trace";
|
||||
internal const string Trace = "Trace";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
32
dotnet/src/Extism.Sdk/LogLevel.cs
Normal file
32
dotnet/src/Extism.Sdk/LogLevel.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
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
|
||||
}
|
||||
@@ -11,18 +11,32 @@ public class Plugin : IDisposable
|
||||
private const int DisposedMarker = 1;
|
||||
|
||||
private readonly Context _context;
|
||||
private readonly HostFunction[] _functions;
|
||||
private int _disposed;
|
||||
|
||||
internal Plugin(Context context, IntPtr handle)
|
||||
/// <summary>
|
||||
/// Create a and load a plug-in
|
||||
/// Using this constructor will give the plug-in it's own internal Context
|
||||
/// </summary>
|
||||
/// <param name="wasm">A WASM module (wat or wasm) or a JSON encoded manifest.</param>
|
||||
/// <param name="functions">List of host functions expected by the plugin.</param>
|
||||
/// <param name="withWasi">Enable/Disable WASI.</param>
|
||||
public static Plugin Create(ReadOnlySpan<byte> wasm, HostFunction[] functions, bool withWasi) {
|
||||
var context = new Context();
|
||||
return context.CreatePlugin(wasm, functions, withWasi);
|
||||
}
|
||||
|
||||
internal Plugin(Context context, HostFunction[] functions, int index)
|
||||
{
|
||||
_context = context;
|
||||
NativeHandle = handle;
|
||||
_functions = functions;
|
||||
Index = index;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A pointer to the native Plugin struct.
|
||||
/// </summary>
|
||||
internal IntPtr NativeHandle { get; }
|
||||
internal int Index { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Update a plugin, keeping the existing ID.
|
||||
@@ -33,9 +47,10 @@ public class Plugin : IDisposable
|
||||
{
|
||||
CheckNotDisposed();
|
||||
|
||||
var functions = _functions.Select(f => f.NativeHandle).ToArray();
|
||||
fixed (byte* wasmPtr = wasm)
|
||||
{
|
||||
return LibExtism.extism_plugin_update(_context.NativeHandle, NativeHandle, wasmPtr, wasm.Length, null, 0, withWasi);
|
||||
return LibExtism.extism_plugin_update(_context.NativeHandle, Index, wasmPtr, wasm.Length, functions, 0, withWasi);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,18 +64,18 @@ public class Plugin : IDisposable
|
||||
|
||||
fixed (byte* jsonPtr = json)
|
||||
{
|
||||
return LibExtism.extism_plugin_config(_context.NativeHandle, NativeHandle, jsonPtr, json.Length);
|
||||
return LibExtism.extism_plugin_config(_context.NativeHandle, Index, jsonPtr, json.Length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a specific function exists in the current plugin.
|
||||
/// </summary>
|
||||
public bool FunctionExists(string name)
|
||||
unsafe public bool FunctionExists(string name)
|
||||
{
|
||||
CheckNotDisposed();
|
||||
|
||||
return LibExtism.extism_plugin_function_exists(_context.NativeHandle, NativeHandle, name);
|
||||
return LibExtism.extism_plugin_function_exists(_context.NativeHandle, Index, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -78,14 +93,20 @@ public class Plugin : IDisposable
|
||||
|
||||
fixed (byte* dataPtr = data)
|
||||
{
|
||||
int response = LibExtism.extism_plugin_call(_context.NativeHandle, NativeHandle, functionName, dataPtr, data.Length);
|
||||
if (response == 0) {
|
||||
int response = LibExtism.extism_plugin_call(_context.NativeHandle, Index, 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");
|
||||
}
|
||||
}
|
||||
@@ -96,11 +117,11 @@ public class Plugin : IDisposable
|
||||
/// Get the length of a plugin's output data.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
internal int OutputLength()
|
||||
unsafe internal int OutputLength()
|
||||
{
|
||||
CheckNotDisposed();
|
||||
|
||||
return (int)LibExtism.extism_plugin_output_length(_context.NativeHandle, NativeHandle);
|
||||
return (int)LibExtism.extism_plugin_output_length(_context.NativeHandle, Index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -114,7 +135,7 @@ public class Plugin : IDisposable
|
||||
|
||||
unsafe
|
||||
{
|
||||
var ptr = LibExtism.extism_plugin_output_data(_context.NativeHandle, NativeHandle).ToPointer();
|
||||
var ptr = LibExtism.extism_plugin_output_data(_context.NativeHandle, Index).ToPointer();
|
||||
return new Span<byte>(ptr, length);
|
||||
}
|
||||
}
|
||||
@@ -123,11 +144,11 @@ public class Plugin : IDisposable
|
||||
/// Get the error associated with the current plugin.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
internal string? GetError()
|
||||
unsafe internal string? GetError()
|
||||
{
|
||||
CheckNotDisposed();
|
||||
|
||||
var result = LibExtism.extism_error(_context.NativeHandle, NativeHandle);
|
||||
var result = LibExtism.extism_error(_context.NativeHandle, Index);
|
||||
return Marshal.PtrToStringUTF8(result);
|
||||
}
|
||||
|
||||
@@ -168,7 +189,7 @@ public class Plugin : IDisposable
|
||||
/// <summary>
|
||||
/// Frees all resources held by this Plugin.
|
||||
/// </summary>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
unsafe protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
@@ -176,7 +197,7 @@ public class Plugin : IDisposable
|
||||
}
|
||||
|
||||
// Free up unmanaged resources
|
||||
LibExtism.extism_plugin_free(_context.NativeHandle, NativeHandle);
|
||||
LibExtism.extism_plugin_free(_context.NativeHandle, Index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -186,4 +207,4 @@ public class Plugin : IDisposable
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Extism.Sdk.Native;
|
||||
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
using Xunit;
|
||||
@@ -9,6 +10,17 @@ namespace Extism.Sdk.Tests;
|
||||
|
||||
public class BasicTests
|
||||
{
|
||||
[Fact]
|
||||
public void CountHelloWorldVowelsWithoutContext()
|
||||
{
|
||||
var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
|
||||
var wasm = File.ReadAllBytes(Path.Combine(binDirectory, "code.wasm"));
|
||||
using var plugin = Plugin.Create(wasm, Array.Empty<HostFunction>(), withWasi: true);
|
||||
|
||||
var response = plugin.CallFunction("count_vowels", Encoding.UTF8.GetBytes("Hello World"));
|
||||
Assert.Equal("{\"count\": 3}", Encoding.UTF8.GetString(response));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CountHelloWorldVowels()
|
||||
{
|
||||
@@ -16,9 +28,46 @@ public class BasicTests
|
||||
|
||||
var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
|
||||
var wasm = File.ReadAllBytes(Path.Combine(binDirectory, "code.wasm"));
|
||||
using var plugin = context.CreatePlugin(wasm, withWasi: true);
|
||||
using var plugin = context.CreatePlugin(wasm, Array.Empty<HostFunction>(), withWasi: true);
|
||||
|
||||
var response = plugin.CallFunction("count_vowels", Encoding.UTF8.GetBytes("Hello World"));
|
||||
Assert.Equal("{\"count\": 3}", Encoding.UTF8.GetString(response));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CountVowelsHostFunctions()
|
||||
{
|
||||
using var context = new Context();
|
||||
|
||||
var userData = Marshal.StringToHGlobalAnsi("Hello again!");
|
||||
|
||||
using var helloWorld = new HostFunction(
|
||||
"hello_world",
|
||||
"env",
|
||||
new[] { ExtismValType.I64 },
|
||||
new[] { ExtismValType.I64 },
|
||||
userData,
|
||||
HelloWorld);
|
||||
|
||||
var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
|
||||
var wasm = File.ReadAllBytes(Path.Combine(binDirectory, "code-functions.wasm"));
|
||||
using var plugin = context.CreatePlugin(wasm, new[] { helloWorld }, withWasi: true);
|
||||
|
||||
var response = plugin.CallFunction("count_vowels", Encoding.UTF8.GetBytes("Hello World"));
|
||||
Assert.Equal("{\"count\": 3}", Encoding.UTF8.GetString(response));
|
||||
|
||||
void HelloWorld(CurrentPlugin plugin, Span<ExtismVal> inputs, Span<ExtismVal> outputs, nint data)
|
||||
{
|
||||
Console.WriteLine("Hello from .NET!");
|
||||
|
||||
var text = Marshal.PtrToStringAnsi(data);
|
||||
Console.WriteLine(text);
|
||||
|
||||
var input = plugin.ReadString(new nint(inputs[0].v.i64));
|
||||
Console.WriteLine($"Input: {input}");
|
||||
|
||||
var output = new string(input); // clone the string
|
||||
outputs[0].v.i64 = plugin.WriteString(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
<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>
|
||||
|
||||
|
||||
@@ -32,4 +35,8 @@
|
||||
<ProjectReference Include="..\..\src\Extism.Sdk\Extism.Sdk.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Extism.runtime.win-x64" Version="0.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -26,7 +26,7 @@
|
||||
(ctypes-foreign (>= 0.18.0))
|
||||
(bigstringaf (>= 0.9.0))
|
||||
(ppx_yojson_conv (>= v0.15.0))
|
||||
extism-manifest
|
||||
(extism-manifest (= :version))
|
||||
(ppx_inline_test (>= v0.15.0))
|
||||
(cmdliner (>= 1.1.1))
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ defmodule Extism.CancelHandle do
|
||||
"""
|
||||
defstruct [
|
||||
# The actual NIF Resource. PluginIndex and the context
|
||||
handle: nil,
|
||||
handle: nil
|
||||
]
|
||||
|
||||
def wrap_resource(handle) do
|
||||
|
||||
@@ -15,12 +15,25 @@ defmodule Extism.Plugin do
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a new plugin
|
||||
"""
|
||||
def new(manifest, wasi \\ false, context \\ nil) do
|
||||
ctx = context || Extism.Context.new()
|
||||
{:ok, manifest_payload} = JSON.encode(manifest)
|
||||
|
||||
case Extism.Native.plugin_new_with_manifest(ctx.ptr, manifest_payload, wasi) do
|
||||
{:error, err} -> {:error, err}
|
||||
res -> {:ok, Extism.Plugin.wrap_resource(ctx, res)}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Call a plugin's function by name
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
|
||||
iex> {:ok, plugin} = Extism.Plugin.new(manifest, false)
|
||||
iex> {:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
|
||||
# {:ok, "{\"count\": 4}"}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ defmodule Extism.MixProject do
|
||||
def project do
|
||||
[
|
||||
app: :extism,
|
||||
version: "0.3.0",
|
||||
version: "0.5.0",
|
||||
elixir: "~> 1.12",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps(),
|
||||
@@ -23,7 +23,7 @@ defmodule Extism.MixProject do
|
||||
|
||||
defp deps do
|
||||
[
|
||||
{:rustler, "~> 0.27.0"},
|
||||
{:rustler, "~> 0.29.1"},
|
||||
{:json, "~> 1.4"},
|
||||
{:ex_doc, "~> 0.21", only: :dev, runtime: false}
|
||||
]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
%{
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.31", "a93921cdc6b9b869f519213d5bc79d9e218ba768d7270d46fdcf1c01bacff9e2", [:mix], [], "hexpm", "317d367ee0335ef037a87e46c91a2269fef6306413f731e8ec11fc45a7efd059"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.29.2", "dfa97532ba66910b2a3016a4bbd796f41a86fc71dd5227e96f4c8581fdf0fdf0", [: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", "6b5d7139eda18a753e3250e27e4a929f8d2c880dd0d460cb9986305dea3e03af"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.30.5", "aa6da96a5c23389d7dc7c381eba862710e108cee9cfdc629b7ec021313900e9e", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "88a1e115dcb91cefeef7e22df4a6ebbe4634fbf98b38adcbc25c9607d6d9d8e6"},
|
||||
"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.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.27.0", "53ffe86586fd1a2ea60ad07f1506962914eb669dba26c23010cf672662ec8d64", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "d7f5ccaec6e7a96f700330898ff2e9d48818e40789fd2951ba41ecf457986e92"},
|
||||
"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"},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "extism_nif"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
authors = ["Benjamin Eckel <bhelx@simst.im>"]
|
||||
|
||||
@@ -9,7 +9,10 @@ name = "extism_nif"
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
# need this to be here and be empty
|
||||
[workspace]
|
||||
|
||||
[dependencies]
|
||||
rustler = "0.27.0"
|
||||
extism = { version = "0.3.0", path = "../../../rust", package = "extism" }
|
||||
rustler = "0.28.0"
|
||||
extism = "0.5.0"
|
||||
log = "0.4"
|
||||
|
||||
@@ -62,8 +62,8 @@ fn plugin_new_with_manifest(
|
||||
manifest_payload: String,
|
||||
wasi: bool,
|
||||
) -> Result<i32, rustler::Error> {
|
||||
let context = &ctx.ctx.write().unwrap();
|
||||
let result = match Plugin::new(context, manifest_payload, [], wasi) {
|
||||
let context = ctx.ctx.write().unwrap();
|
||||
let result = match Plugin::new(&context, manifest_payload, [], wasi) {
|
||||
Err(e) => Err(to_rustler_error(e)),
|
||||
Ok(plugin) => {
|
||||
let plugin_id = plugin.as_i32();
|
||||
|
||||
22
extism.go
22
extism.go
@@ -173,8 +173,9 @@ func (ctx *Context) Free() {
|
||||
|
||||
// Plugin is used to call WASM functions
|
||||
type Plugin struct {
|
||||
ctx *Context
|
||||
id int32
|
||||
ctx *Context
|
||||
id int32
|
||||
functions []Function
|
||||
}
|
||||
|
||||
type WasmData struct {
|
||||
@@ -272,7 +273,7 @@ func register(ctx *Context, data []byte, functions []Function, wasi bool) (Plugi
|
||||
)
|
||||
}
|
||||
|
||||
return Plugin{id: int32(plugin), ctx: ctx}, nil
|
||||
return Plugin{id: int32(plugin), ctx: ctx, functions: functions}, nil
|
||||
}
|
||||
|
||||
func update(ctx *Context, plugin int32, data []byte, functions []Function, wasi bool) error {
|
||||
@@ -323,6 +324,18 @@ func update(ctx *Context, plugin int32, data []byte, functions []Function, wasi
|
||||
)
|
||||
}
|
||||
|
||||
// NewPlugin creates a plugin in its own context
|
||||
func NewPlugin(module io.Reader, functions []Function, wasi bool) (Plugin, error) {
|
||||
ctx := NewContext()
|
||||
return ctx.Plugin(module, functions, wasi)
|
||||
}
|
||||
|
||||
// NewPlugin creates a plugin in its own context from a manifest
|
||||
func NewPluginFromManifest(manifest Manifest, functions []Function, wasi bool) (Plugin, error) {
|
||||
ctx := NewContext()
|
||||
return ctx.PluginFromManifest(manifest, functions, wasi)
|
||||
}
|
||||
|
||||
// PluginFromManifest creates a plugin from a `Manifest`
|
||||
func (ctx *Context) PluginFromManifest(manifest Manifest, functions []Function, wasi bool) (Plugin, error) {
|
||||
data, err := json.Marshal(manifest)
|
||||
@@ -349,7 +362,7 @@ func (p *Plugin) Update(module io.Reader, functions []Function, wasi bool) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.functions = functions
|
||||
return update(p.ctx, p.id, wasm, functions, wasi)
|
||||
}
|
||||
|
||||
@@ -360,6 +373,7 @@ func (p *Plugin) UpdateManifest(manifest Manifest, functions []Function, wasi bo
|
||||
return err
|
||||
}
|
||||
|
||||
p.functions = functions
|
||||
return update(p.ctx, p.id, data, functions, wasi)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ depends: [
|
||||
"ctypes-foreign" {>= "0.18.0"}
|
||||
"bigstringaf" {>= "0.9.0"}
|
||||
"ppx_yojson_conv" {>= "v0.15.0"}
|
||||
"extism-manifest"
|
||||
"extism-manifest" {= version}
|
||||
"ppx_inline_test" {>= "v0.15.0"}
|
||||
"cmdliner" {>= "1.1.1"}
|
||||
"odoc" {with-doc}
|
||||
|
||||
@@ -36,9 +36,6 @@ 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 {
|
||||
@@ -49,7 +46,7 @@ func main() {
|
||||
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 := ctx.PluginFromManifest(manifest, []extism.Function{f}, true)
|
||||
plugin, err := extism.NewPluginFromManifest(manifest, []extism.Function{f}, true)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
module Main where
|
||||
|
||||
import Extism
|
||||
import Extism.CurrentPlugin
|
||||
import Extism.Manifest(manifest, wasmFile)
|
||||
|
||||
unwrap (Right x) = x
|
||||
unwrap (Left (ExtismError msg)) = do
|
||||
error msg
|
||||
|
||||
hello plugin params msg = do
|
||||
putStrLn "Hello from Haskell!"
|
||||
putStrLn msg
|
||||
offs <- allocBytes plugin (toByteString "{\"count\": 999}")
|
||||
return [toI64 offs]
|
||||
|
||||
main = do
|
||||
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
|
||||
setLogFile "stdout" Error
|
||||
let m = manifest [wasmFile "../wasm/code-functions.wasm"]
|
||||
f <- hostFunction "hello_world" [I64] [I64] hello "Hello, again"
|
||||
plugin <- unwrap <$> createPluginFromManifest m [f] True
|
||||
res <- unwrap <$> call plugin "count_vowels" (toByteString "this is a test")
|
||||
putStrLn (fromByteString res)
|
||||
free plugin
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
cabal-version: 3.0
|
||||
name: extism
|
||||
version: 0.2.0
|
||||
version: 0.5.0
|
||||
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-source-files: CHANGELOG.md
|
||||
extra-doc-files: CHANGELOG.md
|
||||
|
||||
library
|
||||
exposed-modules: Extism
|
||||
exposed-modules: Extism Extism.CurrentPlugin
|
||||
reexported-modules: Extism.Manifest
|
||||
hs-source-dirs: src
|
||||
other-modules: Extism.Bindings
|
||||
@@ -19,10 +19,10 @@ library
|
||||
extra-libraries: extism
|
||||
extra-lib-dirs: /usr/local/lib
|
||||
build-depends:
|
||||
base >= 4.16.1 && < 4.19.0,
|
||||
bytestring >= 0.11.3 && < 0.12,
|
||||
json >= 0.10 && < 0.11,
|
||||
extism-manifest >= 0.0.0 && < 0.3.0
|
||||
base >= 4.16.1 && < 5,
|
||||
bytestring >= 0.11.3 && <= 0.12,
|
||||
json >= 0.10 && <= 0.11,
|
||||
extism-manifest >= 0.0.0 && < 0.4.0
|
||||
|
||||
test-suite extism-example
|
||||
type: exitcode-stdio-1.0
|
||||
|
||||
@@ -16,8 +16,8 @@ isNull JSNull = True
|
||||
isNull _ = False
|
||||
filterNulls obj = [(a, b) | (a, b) <- obj, not (isNull b)]
|
||||
object x = makeObj $ filterNulls x
|
||||
objectWithNulls x = makeObj x
|
||||
nonNull x = NotNull x
|
||||
objectWithNulls = makeObj
|
||||
nonNull = NotNull
|
||||
null' = Null
|
||||
(.=) a b = (a, showJSON b)
|
||||
toNullable (Just x) = NotNull x
|
||||
@@ -40,7 +40,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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
cabal-version: 3.0
|
||||
name: extism-manifest
|
||||
version: 0.2.0
|
||||
version: 0.3.0
|
||||
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-source-files: CHANGELOG.md
|
||||
extra-doc-files: CHANGELOG.md
|
||||
|
||||
library
|
||||
exposed-modules: Extism.Manifest Extism.JSON
|
||||
hs-source-dirs: .
|
||||
default-language: Haskell2010
|
||||
build-depends:
|
||||
base >= 4.16.1 && < 4.19.0,
|
||||
bytestring >= 0.11.3 && < 0.12,
|
||||
json >= 0.10 && < 0.11,
|
||||
base >= 4.16.1 && < 5,
|
||||
bytestring >= 0.11.3 && <= 0.12,
|
||||
json >= 0.10 && <= 0.11,
|
||||
base64-bytestring >= 1.2.1 && < 1.3,
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
module Extism (module Extism, module Extism.Manifest) where
|
||||
module Extism (
|
||||
module Extism,
|
||||
module Extism.Manifest,
|
||||
ValType(..),
|
||||
Val(..)
|
||||
) where
|
||||
|
||||
import Data.Int
|
||||
import Data.Word
|
||||
import Control.Monad (void)
|
||||
import Foreign.ForeignPtr
|
||||
import Foreign.C.String
|
||||
import Foreign.Ptr
|
||||
import Foreign.Marshal.Array
|
||||
import Foreign.Storable
|
||||
import Foreign.StablePtr
|
||||
import Foreign.Concurrent
|
||||
import Foreign.Marshal.Utils (copyBytes, moveBytes)
|
||||
import Data.ByteString as B
|
||||
import Data.ByteString.Internal (c2w, w2c)
|
||||
import Data.ByteString.Unsafe (unsafeUseAsCString)
|
||||
@@ -16,10 +27,17 @@ import Extism.Bindings
|
||||
-- | Context for managing plugins
|
||||
newtype Context = Context (ForeignPtr ExtismContext)
|
||||
|
||||
-- | Plugins can be used to call WASM function
|
||||
data Plugin = Plugin Context Int32
|
||||
-- | Host function
|
||||
data Function = Function (ForeignPtr ExtismFunction) (StablePtr ())
|
||||
|
||||
data CancelHandle = CancelHandle (Ptr ExtismCancelHandle)
|
||||
-- | Plugins can be used to call WASM function
|
||||
data Plugin = Plugin Context Int32 [Function]
|
||||
|
||||
-- | Cancellation handle for Plugins
|
||||
newtype CancelHandle = CancelHandle (Ptr ExtismCancelHandle)
|
||||
|
||||
-- | Access the plugin that is currently executing from inside a host function
|
||||
type CurrentPlugin = Ptr ExtismCurrentPlugin
|
||||
|
||||
-- | Log level
|
||||
data LogLevel = Error | Warn | Info | Debug | Trace deriving (Show)
|
||||
@@ -53,68 +71,94 @@ reset (Context ctx) =
|
||||
newContext :: IO Context
|
||||
newContext = do
|
||||
ptr <- extism_context_new
|
||||
fptr <- newForeignPtr extism_context_free ptr
|
||||
fptr <- Foreign.ForeignPtr.newForeignPtr extism_context_free ptr
|
||||
return (Context fptr)
|
||||
|
||||
|
||||
-- | Execute a function with a new 'Context' that is destroyed when it returns
|
||||
withContext :: (Context -> IO a) -> IO a
|
||||
withContext f = do
|
||||
ctx <- newContext
|
||||
f ctx
|
||||
|
||||
-- | Execute a function with the provided 'Plugin' as a parameter, then frees the 'Plugin'
|
||||
-- | before returning the result.
|
||||
withPlugin :: (Plugin -> IO a) -> Plugin -> IO a
|
||||
withPlugin f plugin = do
|
||||
res <- f plugin
|
||||
free plugin
|
||||
return res
|
||||
|
||||
-- | Create a 'Plugin' from a WASM module, `useWasi` determines if WASI should
|
||||
-- | be linked
|
||||
plugin :: Context -> B.ByteString -> Bool -> IO (Result Plugin)
|
||||
plugin c wasm useWasi =
|
||||
plugin :: Context -> B.ByteString -> [Function] -> Bool -> IO (Result Plugin)
|
||||
plugin c wasm functions useWasi =
|
||||
let nfunctions = fromIntegral (Prelude.length functions) in
|
||||
let length = fromIntegral (B.length wasm) in
|
||||
let wasi = fromInteger (if useWasi then 1 else 0) in
|
||||
let Context ctx = c in
|
||||
do
|
||||
funcs <- Prelude.mapM (\(Function ptr _) -> withForeignPtr ptr (\x -> do return x)) functions
|
||||
withForeignPtr ctx (\ctx -> do
|
||||
p <- unsafeUseAsCString wasm (\s ->
|
||||
extism_plugin_new ctx (castPtr s) length nullPtr 0 wasi )
|
||||
withArray funcs (\funcs ->
|
||||
extism_plugin_new ctx (castPtr s) length funcs nfunctions wasi ))
|
||||
if p < 0 then do
|
||||
err <- extism_error ctx (-1)
|
||||
e <- peekCString err
|
||||
return $ Left (ExtismError e)
|
||||
else
|
||||
return $ Right (Plugin c p))
|
||||
return $ Right (Plugin c p functions))
|
||||
|
||||
-- | Create a 'Plugin' with its own 'Context'
|
||||
createPlugin :: B.ByteString -> [Function] -> Bool -> IO (Result Plugin)
|
||||
createPlugin c functions useWasi = do
|
||||
ctx <- newContext
|
||||
plugin ctx c functions useWasi
|
||||
|
||||
-- | Create a 'Plugin' from a 'Manifest'
|
||||
pluginFromManifest :: Context -> Manifest -> Bool -> IO (Result Plugin)
|
||||
pluginFromManifest ctx manifest useWasi =
|
||||
pluginFromManifest :: Context -> Manifest -> [Function] -> Bool -> IO (Result Plugin)
|
||||
pluginFromManifest ctx manifest functions useWasi =
|
||||
let wasm = toByteString $ toString manifest in
|
||||
plugin ctx wasm useWasi
|
||||
plugin ctx wasm functions useWasi
|
||||
|
||||
-- | Create a 'Plugin' with its own 'Context' from a 'Manifest'
|
||||
createPluginFromManifest :: Manifest -> [Function] -> Bool -> IO (Result Plugin)
|
||||
createPluginFromManifest manifest functions useWasi = do
|
||||
ctx <- newContext
|
||||
pluginFromManifest ctx manifest functions useWasi
|
||||
|
||||
-- | Update a 'Plugin' with a new WASM module
|
||||
update :: Plugin -> B.ByteString -> Bool -> IO (Result ())
|
||||
update (Plugin (Context ctx) id) wasm useWasi =
|
||||
update :: Plugin -> B.ByteString -> [Function] -> Bool -> IO (Result Plugin)
|
||||
update (Plugin (Context ctx) id _) wasm functions useWasi =
|
||||
let nfunctions = fromIntegral (Prelude.length functions) in
|
||||
let length = fromIntegral (B.length wasm) in
|
||||
let wasi = fromInteger (if useWasi then 1 else 0) in
|
||||
do
|
||||
withForeignPtr ctx (\ctx -> do
|
||||
funcs <- Prelude.mapM (\(Function ptr _ ) -> withForeignPtr ptr (\x -> do return x)) functions
|
||||
withForeignPtr ctx (\ctx' -> do
|
||||
b <- unsafeUseAsCString wasm (\s ->
|
||||
extism_plugin_update ctx id (castPtr s) length nullPtr 0 wasi)
|
||||
withArray funcs (\funcs ->
|
||||
extism_plugin_update ctx' id (castPtr s) length funcs nfunctions wasi))
|
||||
if b <= 0 then do
|
||||
err <- extism_error ctx (-1)
|
||||
err <- extism_error ctx' (-1)
|
||||
e <- peekCString err
|
||||
return $ Left (ExtismError e)
|
||||
else
|
||||
return (Right ()))
|
||||
return (Right (Plugin (Context ctx) id functions)))
|
||||
|
||||
-- | Update a 'Plugin' with a new 'Manifest'
|
||||
updateManifest :: Plugin -> Manifest -> Bool -> IO (Result ())
|
||||
updateManifest plugin manifest useWasi =
|
||||
updateManifest :: Plugin -> Manifest -> [Function] -> Bool -> IO (Result Plugin)
|
||||
updateManifest plugin manifest functions useWasi =
|
||||
let wasm = toByteString $ toString manifest in
|
||||
update plugin wasm useWasi
|
||||
update plugin wasm functions useWasi
|
||||
|
||||
-- | Check if a 'Plugin' is valid
|
||||
isValid :: Plugin -> Bool
|
||||
isValid (Plugin _ p) = p >= 0
|
||||
isValid (Plugin _ p _) = p >= 0
|
||||
|
||||
-- | Set configuration values for a plugin
|
||||
setConfig :: Plugin -> [(String, Maybe String)] -> IO Bool
|
||||
setConfig (Plugin (Context ctx) plugin) x =
|
||||
setConfig (Plugin (Context ctx) plugin _) x =
|
||||
if plugin < 0
|
||||
then return False
|
||||
else
|
||||
@@ -143,14 +187,14 @@ setLogFile filename level =
|
||||
|
||||
-- | Check if a function exists in the given plugin
|
||||
functionExists :: Plugin -> String -> IO Bool
|
||||
functionExists (Plugin (Context ctx) plugin) name = do
|
||||
functionExists (Plugin (Context ctx) plugin _) name = do
|
||||
withForeignPtr ctx (\ctx -> do
|
||||
b <- withCString name (extism_plugin_function_exists ctx plugin)
|
||||
if b == 1 then return True else return False)
|
||||
|
||||
--- | Call a function provided by the given plugin
|
||||
call :: Plugin -> String -> B.ByteString -> IO (Result B.ByteString)
|
||||
call (Plugin (Context ctx) plugin) name input =
|
||||
call (Plugin (Context ctx) plugin _) name input =
|
||||
let length = fromIntegral (B.length input) in
|
||||
do
|
||||
withForeignPtr ctx (\ctx -> do
|
||||
@@ -172,15 +216,73 @@ call (Plugin (Context ctx) plugin) name input =
|
||||
-- | Free a 'Plugin', this will automatically be called for every plugin
|
||||
-- | associated with a 'Context' when that 'Context' is freed
|
||||
free :: Plugin -> IO ()
|
||||
free (Plugin (Context ctx) plugin) =
|
||||
free (Plugin (Context ctx) plugin _) =
|
||||
withForeignPtr ctx (`extism_plugin_free` plugin)
|
||||
|
||||
-- | Create a new 'CancelHandle' that can be used to cancel a running plugin
|
||||
-- | from another thread.
|
||||
cancelHandle :: Plugin -> IO CancelHandle
|
||||
cancelHandle (Plugin (Context ctx) plugin) = do
|
||||
handle <- withForeignPtr ctx (\ctx -> extism_plugin_cancel_handle ctx plugin)
|
||||
cancelHandle (Plugin (Context ctx) plugin _) = do
|
||||
handle <- withForeignPtr ctx (`extism_plugin_cancel_handle` plugin)
|
||||
return (CancelHandle handle)
|
||||
|
||||
-- | Cancel a running plugin using a 'CancelHandle'
|
||||
cancel :: CancelHandle -> IO Bool
|
||||
cancel (CancelHandle handle) =
|
||||
cancel (CancelHandle handle) =
|
||||
extism_plugin_cancel handle
|
||||
|
||||
|
||||
-- | Create a new 'Function' that can be called from a 'Plugin'
|
||||
hostFunction :: String -> [ValType] -> [ValType] -> (CurrentPlugin -> [Val] -> a -> IO [Val]) -> a -> IO Function
|
||||
hostFunction name params results f v =
|
||||
let nparams = fromIntegral $ Prelude.length params in
|
||||
let nresults = fromIntegral $ Prelude.length results in
|
||||
do
|
||||
cb <- callbackWrap (callback f :: CCallback)
|
||||
free <- freePtrWrap freePtr
|
||||
userData <- newStablePtr (v, free, cb)
|
||||
let userDataPtr = castStablePtrToPtr userData
|
||||
x <- withCString name (\name -> do
|
||||
withArray params (\params ->
|
||||
withArray results (\results -> do
|
||||
extism_function_new name params nparams results nresults cb userDataPtr free)))
|
||||
let freeFn = extism_function_free x
|
||||
fptr <- Foreign.Concurrent.newForeignPtr x freeFn
|
||||
return $ Function fptr (castPtrToStablePtr userDataPtr)
|
||||
|
||||
|
||||
-- | Create a new I32 'Val'
|
||||
toI32 :: Integral a => a -> Val
|
||||
toI32 x = ValI32 (fromIntegral x)
|
||||
|
||||
-- | Create a new I64 'Val'
|
||||
toI64 :: Integral a => a -> Val
|
||||
toI64 x = ValI64 (fromIntegral x)
|
||||
|
||||
-- | Create a new F32 'Val'
|
||||
toF32 :: Float -> Val
|
||||
toF32 = ValF32
|
||||
|
||||
-- | Create a new F64 'Val'
|
||||
toF64 :: Double -> Val
|
||||
toF64 = ValF64
|
||||
|
||||
-- | Get I32 'Val'
|
||||
fromI32 :: Integral a => Val -> Maybe a
|
||||
fromI32 (ValI32 x) = Just (fromIntegral x)
|
||||
fromI32 _ = Nothing
|
||||
|
||||
-- | Get I64 'Val'
|
||||
fromI64 :: Integral a => Val -> Maybe a
|
||||
fromI64 (ValI64 x) = Just (fromIntegral x)
|
||||
fromI64 _ = Nothing
|
||||
|
||||
-- | Get F32 'Val'
|
||||
fromF32 :: Val -> Maybe Float
|
||||
fromF32 (ValF32 x) = Just x
|
||||
fromF32 _ = Nothing
|
||||
|
||||
-- | Get F64 'Val'
|
||||
fromF64 :: Val -> Maybe Double
|
||||
fromF64 (ValF64 x) = Just x
|
||||
fromF64 _ = Nothing
|
||||
@@ -7,10 +7,77 @@ import Foreign.Ptr
|
||||
import Foreign.C.String
|
||||
import Data.Int
|
||||
import Data.Word
|
||||
import Foreign.Storable
|
||||
import Foreign.Marshal.Array
|
||||
import Foreign.StablePtr
|
||||
|
||||
type FreeCallback = Ptr () -> IO ()
|
||||
|
||||
newtype ExtismContext = ExtismContext () deriving Show
|
||||
newtype ExtismFunction = ExtismFunction () deriving Show
|
||||
newtype ExtismCancelHandle = ExtismCancelHandle () deriving Show
|
||||
newtype ExtismCurrentPlugin = ExtismCurrentPlugin () deriving Show
|
||||
data ValType = I32 | I64 | F32 | F64 | V128 | FuncRef | ExternRef deriving (Show, Eq)
|
||||
data Val = ValI32 Int32 | ValI64 Int64 | ValF32 Float | ValF64 Double deriving (Show, Eq)
|
||||
|
||||
typeOfVal (ValI32 _) = I32
|
||||
typeOfVal (ValI64 _) = I64
|
||||
typeOfVal (ValF32 _) = F32
|
||||
typeOfVal (ValF64 _) = F64
|
||||
|
||||
type CCallback = Ptr ExtismCurrentPlugin -> Ptr Val -> Word64 -> Ptr Val -> Word64 -> Ptr () -> IO ()
|
||||
|
||||
_32Bit = sizeOf (undefined :: Int) == 4
|
||||
|
||||
instance Storable Val where
|
||||
sizeOf _ =
|
||||
if _32Bit then 12 else 16
|
||||
alignment _ = 1
|
||||
peek ptr = do
|
||||
let offs = if _32Bit then 4 else 8
|
||||
t <- valTypeOfInt <$> peekByteOff ptr 0
|
||||
case t of
|
||||
I32 -> ValI32 <$> peekByteOff ptr offs
|
||||
I64 -> ValI64 <$> peekByteOff ptr offs
|
||||
F32 -> ValF32 <$> peekByteOff ptr offs
|
||||
F64 -> ValF64 <$> peekByteOff ptr offs
|
||||
poke ptr x = do
|
||||
let offs = if _32Bit then 4 else 8
|
||||
pokeByteOff ptr 0 (typeOfVal x)
|
||||
case x of
|
||||
ValI32 x -> pokeByteOff ptr offs x
|
||||
ValI64 x -> pokeByteOff ptr offs x
|
||||
ValF32 x -> pokeByteOff ptr offs x
|
||||
ValF64 x -> pokeByteOff ptr offs x
|
||||
|
||||
|
||||
intOfValType :: ValType -> CInt
|
||||
intOfValType I32 = 0
|
||||
intOfValType I64 = 1
|
||||
intOfValType F32 = 2
|
||||
intOfValType F64 = 3
|
||||
intOfValType V128 = 4
|
||||
intOfValType FuncRef = 5
|
||||
intOfValType ExternRef = 6
|
||||
|
||||
valTypeOfInt :: CInt -> ValType
|
||||
valTypeOfInt 0 = I32
|
||||
valTypeOfInt 1 = I64
|
||||
valTypeOfInt 2 = F32
|
||||
valTypeOfInt 3 = F64
|
||||
valTypeOfInt 4 = V128
|
||||
valTypeOfInt 5 = FuncRef
|
||||
valTypeOfInt 6 = ExternRef
|
||||
valTypeOfInt _ = error "Invalid ValType"
|
||||
|
||||
instance Storable ValType where
|
||||
sizeOf _ = 4
|
||||
alignment _ = 1
|
||||
peek ptr = do
|
||||
x <- peekByteOff ptr 0
|
||||
return $ valTypeOfInt (x :: CInt)
|
||||
poke ptr x = do
|
||||
pokeByteOff ptr 0 (intOfValType x)
|
||||
|
||||
foreign import ccall safe "extism.h extism_context_new" extism_context_new :: IO (Ptr ExtismContext)
|
||||
foreign import ccall safe "extism.h &extism_context_free" extism_context_free :: FunPtr (Ptr ExtismContext -> IO ())
|
||||
@@ -28,3 +95,28 @@ foreign import ccall safe "extism.h extism_context_reset" extism_context_reset :
|
||||
foreign import ccall safe "extism.h extism_version" extism_version :: IO CString
|
||||
foreign import ccall safe "extism.h extism_plugin_cancel_handle" extism_plugin_cancel_handle :: Ptr ExtismContext -> Int32 -> IO (Ptr ExtismCancelHandle)
|
||||
foreign import ccall safe "extism.h extism_plugin_cancel" extism_plugin_cancel :: Ptr ExtismCancelHandle -> IO Bool
|
||||
|
||||
foreign import ccall safe "extism.h extism_function_new" extism_function_new :: CString -> Ptr ValType -> Word64 -> Ptr ValType -> Word64 -> FunPtr CCallback -> Ptr () -> FunPtr FreeCallback -> IO (Ptr ExtismFunction)
|
||||
foreign import ccall safe "extism.h extism_function_free" extism_function_free :: Ptr ExtismFunction -> IO ()
|
||||
foreign import ccall safe "extism.h extism_current_plugin_memory" extism_current_plugin_memory :: Ptr ExtismCurrentPlugin -> IO (Ptr Word8)
|
||||
foreign import ccall safe "extism.h extism_current_plugin_memory_alloc" extism_current_plugin_memory_alloc :: Ptr ExtismCurrentPlugin -> Word64 -> IO Word64
|
||||
foreign import ccall safe "extism.h extism_current_plugin_memory_length" extism_current_plugin_memory_length :: Ptr ExtismCurrentPlugin -> Word64 -> IO Word64
|
||||
foreign import ccall safe "extism.h extism_current_plugin_memory_free" extism_current_plugin_memory_free :: Ptr ExtismCurrentPlugin -> Word64 -> IO ()
|
||||
|
||||
freePtr ptr = do
|
||||
let s = castPtrToStablePtr ptr
|
||||
(a, b, c) <- deRefStablePtr s
|
||||
freeHaskellFunPtr b
|
||||
freeHaskellFunPtr c
|
||||
freeStablePtr s
|
||||
|
||||
foreign import ccall "wrapper" freePtrWrap :: FreeCallback -> IO (FunPtr FreeCallback)
|
||||
|
||||
foreign import ccall "wrapper" callbackWrap :: CCallback -> IO (FunPtr CCallback)
|
||||
|
||||
callback :: (Ptr ExtismCurrentPlugin -> [Val] -> a -> IO [Val]) -> (Ptr ExtismCurrentPlugin -> Ptr Val -> Word64 -> Ptr Val -> Word64 -> Ptr () -> IO ())
|
||||
callback f plugin params nparams results nresults ptr = do
|
||||
p <- peekArray (fromIntegral nparams) params
|
||||
(userData, _, _) <- deRefStablePtr (castPtrToStablePtr ptr)
|
||||
res <- f plugin p userData
|
||||
pokeArray results res
|
||||
|
||||
48
haskell/src/Extism/CurrentPlugin.hs
Normal file
48
haskell/src/Extism/CurrentPlugin.hs
Normal file
@@ -0,0 +1,48 @@
|
||||
module Extism.CurrentPlugin where
|
||||
|
||||
import Extism
|
||||
import Extism.Bindings
|
||||
import Data.Word
|
||||
import Data.ByteString as B
|
||||
import Foreign.Ptr
|
||||
import Foreign.Marshal.Array
|
||||
|
||||
-- | Allocate a new handle of the given size
|
||||
memoryAlloc :: CurrentPlugin -> Word64 -> IO Word64
|
||||
memoryAlloc = extism_current_plugin_memory_alloc
|
||||
|
||||
-- | Get the length of a handle, returns 0 if the handle is invalid
|
||||
memoryLength :: CurrentPlugin -> Word64 -> IO Word64
|
||||
memoryLength = extism_current_plugin_memory_length
|
||||
|
||||
-- | Free allocated memory
|
||||
memoryFree :: CurrentPlugin -> Word64 -> IO ()
|
||||
memoryFree = extism_current_plugin_memory_free
|
||||
|
||||
-- | Access a pointer to the entire memory region
|
||||
memory :: CurrentPlugin -> IO (Ptr Word8)
|
||||
memory = extism_current_plugin_memory
|
||||
|
||||
-- | Access a pointer the a specific offset in memory
|
||||
memoryOffset :: CurrentPlugin -> Word64 -> IO (Ptr Word8)
|
||||
memoryOffset plugin offs = do
|
||||
x <- extism_current_plugin_memory plugin
|
||||
return $ plusPtr x (fromIntegral offs)
|
||||
|
||||
-- | Access the data associated with a handle as a 'ByteString'
|
||||
memoryBytes :: CurrentPlugin -> Word64 -> IO B.ByteString
|
||||
memoryBytes plugin offs = do
|
||||
ptr <- memoryOffset plugin offs
|
||||
len <- memoryLength plugin offs
|
||||
arr <- peekArray (fromIntegral len) ptr
|
||||
return $ B.pack arr
|
||||
|
||||
-- | Allocate memory and copy an existing 'ByteString' into it
|
||||
allocBytes :: CurrentPlugin -> B.ByteString -> IO Word64
|
||||
allocBytes plugin s = do
|
||||
let length = B.length s
|
||||
offs <- memoryAlloc plugin (fromIntegral length)
|
||||
ptr <- memoryOffset plugin offs
|
||||
pokeArray ptr (B.unpack s)
|
||||
return offs
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Test.HUnit
|
||||
import Extism
|
||||
import Extism.Manifest
|
||||
import Extism.CurrentPlugin
|
||||
|
||||
|
||||
unwrap (Right x) = return x
|
||||
@@ -8,46 +9,58 @@ unwrap (Left (ExtismError msg)) =
|
||||
assertFailure msg
|
||||
|
||||
defaultManifest = manifest [wasmFile "../../wasm/code.wasm"]
|
||||
hostFunctionManifest = manifest [wasmFile "../../wasm/code-functions.wasm"]
|
||||
|
||||
initPlugin :: Context -> IO Plugin
|
||||
initPlugin context =
|
||||
Extism.pluginFromManifest context defaultManifest False >>= unwrap
|
||||
initPlugin :: Maybe Context -> IO Plugin
|
||||
initPlugin Nothing =
|
||||
Extism.createPluginFromManifest defaultManifest [] False >>= unwrap
|
||||
initPlugin (Just ctx) =
|
||||
Extism.pluginFromManifest ctx defaultManifest [] False >>= unwrap
|
||||
|
||||
pluginFunctionExists = do
|
||||
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'))
|
||||
p <- initPlugin Nothing
|
||||
exists <- functionExists p "count_vowels"
|
||||
assertBool "function exists" exists
|
||||
exists' <- functionExists p "function_doesnt_exist"
|
||||
assertBool "function doesn't exist" (not exists')
|
||||
|
||||
checkCallResult p = do
|
||||
res <- call p "count_vowels" (toByteString "this is a test") >>= unwrap
|
||||
assertEqual "count vowels output" "{\"count\": 4}" (fromByteString res)
|
||||
res <- call p "count_vowels" (toByteString "this is a test") >>= unwrap
|
||||
assertEqual "count vowels output" "{\"count\": 4}" (fromByteString res)
|
||||
|
||||
pluginCall = do
|
||||
withContext (\ctx -> do
|
||||
p <- initPlugin ctx
|
||||
checkCallResult p)
|
||||
p <- initPlugin Nothing
|
||||
checkCallResult p
|
||||
|
||||
|
||||
hello plugin params () = do
|
||||
putStrLn "Hello from Haskell!"
|
||||
offs <- allocBytes plugin (toByteString "{\"count\": 999}")
|
||||
return [toI64 offs]
|
||||
|
||||
pluginCallHostFunction = do
|
||||
p <- Extism.createPluginFromManifest hostFunctionManifest [] False >>= unwrap
|
||||
res <- call p "count_vowels" (toByteString "this is a test") >>= unwrap
|
||||
assertEqual "count vowels output" "{\"count\": 999}" (fromByteString res)
|
||||
|
||||
pluginMultiple = do
|
||||
withContext (\ctx -> do
|
||||
p <- initPlugin ctx
|
||||
withContext(\ctx -> do
|
||||
p <- initPlugin (Just ctx)
|
||||
checkCallResult p
|
||||
q <- initPlugin ctx
|
||||
r <- initPlugin ctx
|
||||
q <- initPlugin (Just ctx)
|
||||
r <- initPlugin (Just ctx)
|
||||
checkCallResult q
|
||||
checkCallResult r)
|
||||
|
||||
pluginUpdate = do
|
||||
withContext (\ctx -> do
|
||||
p <- initPlugin ctx
|
||||
updateManifest p defaultManifest True >>= unwrap
|
||||
p <- initPlugin (Just ctx)
|
||||
updateManifest p defaultManifest [] True >>= unwrap
|
||||
checkCallResult p)
|
||||
|
||||
pluginConfig = do
|
||||
withContext (\ctx -> do
|
||||
p <- initPlugin ctx
|
||||
p <- initPlugin (Just ctx)
|
||||
b <- setConfig p [("a", Just "1"), ("b", Just "2"), ("c", Just "3"), ("d", Nothing)]
|
||||
assertBool "set config" b)
|
||||
|
||||
@@ -62,6 +75,7 @@ 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
|
||||
|
||||
353
java/pom.xml
353
java/pom.xml
@@ -1,193 +1,194 @@
|
||||
<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.3.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.5.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/tree/main/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>11</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>
|
||||
<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>
|
||||
<!--
|
||||
<closeRepository>false</closeRepository>
|
||||
<releaseRepository>false</releaseRepository>
|
||||
-->
|
||||
<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>
|
||||
<stagingRepositories>target/staging-deploy</stagingRepositories>
|
||||
</maven-central>
|
||||
</nexus2>
|
||||
</maven>
|
||||
</deploy>
|
||||
</jreleaser>
|
||||
</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>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<dependency>
|
||||
<groupId>uk.org.webcompere</groupId>
|
||||
<artifactId>model-assert</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<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>
|
||||
</project>
|
||||
|
||||
@@ -62,6 +62,16 @@ public class Plugin implements AutoCloseable {
|
||||
this(context, serialize(manifest), withWASI, functions);
|
||||
}
|
||||
|
||||
|
||||
public Plugin(byte[] manifestBytes, boolean withWASI, HostFunction[] functions) {
|
||||
this(new Context(), manifestBytes, withWASI, functions);
|
||||
}
|
||||
|
||||
|
||||
public Plugin(Manifest manifest, boolean withWASI, HostFunction[] functions) {
|
||||
this(new Context(), serialize(manifest), withWASI, functions);
|
||||
}
|
||||
|
||||
private static byte[] serialize(Manifest manifest) {
|
||||
Objects.requireNonNull(manifest, "manifest");
|
||||
return JsonSerde.toJson(manifest).getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
@@ -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));
|
||||
var output = Extism.invokeFunction(manifest, "count_vowels", "Hello World");
|
||||
assertThat(output).isEqualTo("{\"count\": 3}");
|
||||
assertThrows(ExtismException.class, () -> {
|
||||
Extism.invokeFunction(manifest, "count_vowels", "Hello World");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
2
kernel/.cargo/config
Normal file
2
kernel/.cargo/config
Normal file
@@ -0,0 +1,2 @@
|
||||
[build]
|
||||
target = "wasm32-unknown-unknown"
|
||||
11
kernel/Cargo.toml
Normal file
11
kernel/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "extism-runtime-kernel"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"."
|
||||
]
|
||||
20
kernel/README.md
Normal file
20
kernel/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# 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
|
||||
7
kernel/build.sh
Executable file
7
kernel/build.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/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
|
||||
|
||||
10
kernel/src/bin/extism-runtime.rs
Normal file
10
kernel/src/bin/extism-runtime.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
#![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()
|
||||
}
|
||||
450
kernel/src/lib.rs
Normal file
450
kernel/src/lib.rs
Normal file
@@ -0,0 +1,450 @@
|
||||
//! # 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 npages = nbytes / PAGE_SIZE as u64;
|
||||
let remainder = nbytes % PAGE_SIZE as u64;
|
||||
if remainder != 0 {
|
||||
(npages + 1) as usize
|
||||
} else {
|
||||
npages as usize
|
||||
}
|
||||
}
|
||||
|
||||
// Get the `MemoryRoot` at the correct offset in memory
|
||||
#[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 - core::mem::size_of::<MemoryRoot>() as u64;
|
||||
|
||||
// When the allocation is larger than the number of bytes available
|
||||
// we will need to try to grow the memory
|
||||
if length >= mem_left {
|
||||
// Calculate the number of pages needed to cover the remaining bytes
|
||||
let npages = num_pages(length - mem_left);
|
||||
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)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "libextism"
|
||||
version = "0.3.0"
|
||||
version = "0.5.2"
|
||||
edition = "2021"
|
||||
authors = ["The Extism Authors", "oss@extism.org"]
|
||||
license = "BSD-3-Clause"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "extism-manifest"
|
||||
version = "0.3.0"
|
||||
version = "0.5.0"
|
||||
edition = "2021"
|
||||
authors = ["The Extism Authors", "oss@extism.org"]
|
||||
license = "BSD-3-Clause"
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::path::{Path, PathBuf};
|
||||
#[deprecated]
|
||||
pub type ManifestMemory = MemoryOptions;
|
||||
|
||||
#[derive(Default, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct MemoryOptions {
|
||||
@@ -12,7 +12,7 @@ pub struct MemoryOptions {
|
||||
pub max_pages: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct HttpRequest {
|
||||
@@ -43,7 +43,7 @@ impl HttpRequest {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct WasmMetadata {
|
||||
@@ -81,7 +81,7 @@ impl From<Vec<u8>> for Wasm {
|
||||
#[deprecated]
|
||||
pub type ManifestWasm = Wasm;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -153,7 +153,7 @@ fn base64_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::
|
||||
schema.into()
|
||||
}
|
||||
|
||||
#[derive(Default, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Manifest {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const {
|
||||
withContext,
|
||||
Context,
|
||||
Plugin,
|
||||
HostFunction,
|
||||
ValType,
|
||||
} = require("./dist/index.js");
|
||||
@@ -13,29 +12,31 @@ function f(currentPlugin, inputs, outputs, userData) {
|
||||
outputs[0] = inputs[0];
|
||||
}
|
||||
|
||||
let hello_world = new HostFunction(
|
||||
const hello_world = new HostFunction(
|
||||
"hello_world",
|
||||
[ValType.I64],
|
||||
[ValType.I64],
|
||||
f,
|
||||
"Hello again!"
|
||||
"Hello again!",
|
||||
);
|
||||
|
||||
let functions = [hello_world];
|
||||
async function main() {
|
||||
const functions = [hello_world];
|
||||
|
||||
withContext(async function (context) {
|
||||
let wasm = readFileSync("../wasm/code-functions.wasm");
|
||||
let p = context.plugin(wasm, true, functions);
|
||||
const wasm = readFileSync("../wasm/code-functions.wasm");
|
||||
const p = new Plugin(wasm, true, functions);
|
||||
|
||||
if (!p.functionExists("count_vowels")) {
|
||||
console.log("no function 'count_vowels' in wasm");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let buf = await p.call("count_vowels", process.argv[2] || "this is a test");
|
||||
const buf = await p.call("count_vowels", process.argv[2] || "this is a test");
|
||||
console.log(JSON.parse(buf.toString())["count"]);
|
||||
p.free();
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
// or, use a context like this:
|
||||
// let ctx = new Context();
|
||||
|
||||
795
node/package-lock.json
generated
795
node/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@extism/extism",
|
||||
"version": "0.3.0",
|
||||
"version": "0.5.0",
|
||||
"description": "Extism Host SDK for Node",
|
||||
"keywords": [
|
||||
"extism",
|
||||
@@ -39,12 +39,12 @@
|
||||
"devDependencies": {
|
||||
"@types/ffi-napi": "^4.0.6",
|
||||
"@types/jest": "^29.2.0",
|
||||
"@types/node": "^18.11.4",
|
||||
"@types/node": "^20.1.0",
|
||||
"jest": "^29.2.2",
|
||||
"prettier": "2.8.4",
|
||||
"prettier": "3.0.2",
|
||||
"ts-jest": "^29.0.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"typedoc": "^0.23.18",
|
||||
"typescript": "^4.8.4"
|
||||
"typedoc": "^0.24.1",
|
||||
"typescript": "^5.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ interface LibExtism {
|
||||
data_len: number,
|
||||
functions: Buffer,
|
||||
nfunctions: number,
|
||||
wasi: boolean
|
||||
wasi: boolean,
|
||||
) => number;
|
||||
extism_plugin_update: (
|
||||
ctx: Buffer,
|
||||
@@ -113,7 +113,7 @@ interface LibExtism {
|
||||
data_len: number,
|
||||
functions: Buffer,
|
||||
nfunctions: number,
|
||||
wasi: boolean
|
||||
wasi: boolean,
|
||||
) => boolean;
|
||||
extism_error: (ctx: Buffer, plugin_id: number) => string;
|
||||
extism_plugin_call: (
|
||||
@@ -121,7 +121,7 @@ interface LibExtism {
|
||||
plugin_id: number,
|
||||
func: string,
|
||||
input: string,
|
||||
input_len: number
|
||||
input_len: number,
|
||||
) => number;
|
||||
extism_plugin_output_length: (ctx: Buffer, plugin_id: number) => number;
|
||||
extism_plugin_output_data: (ctx: Buffer, plugin_id: number) => Uint8Array;
|
||||
@@ -129,13 +129,13 @@ interface LibExtism {
|
||||
extism_plugin_function_exists: (
|
||||
ctx: Buffer,
|
||||
plugin_id: number,
|
||||
func: string
|
||||
func: string,
|
||||
) => boolean;
|
||||
extism_plugin_config: (
|
||||
ctx: Buffer,
|
||||
plugin_id: number,
|
||||
data: string | Buffer,
|
||||
data_len: number
|
||||
data_len: number,
|
||||
) => void;
|
||||
extism_plugin_free: (ctx: Buffer, plugin_id: number) => void;
|
||||
extism_context_reset: (ctx: Buffer) => void;
|
||||
@@ -148,7 +148,7 @@ interface LibExtism {
|
||||
nOutputs: number,
|
||||
f: Buffer,
|
||||
user_data: Buffer | null,
|
||||
free: Buffer | null
|
||||
free: Buffer | null,
|
||||
) => Buffer;
|
||||
extism_function_set_namespace: (f: Buffer, s: string) => void;
|
||||
extism_function_free: (f: Buffer) => void;
|
||||
@@ -321,9 +321,9 @@ export class Context {
|
||||
manifest: ManifestData,
|
||||
wasi: boolean = false,
|
||||
functions: HostFunction[] = [],
|
||||
config?: PluginConfig
|
||||
config?: PluginConfig,
|
||||
) {
|
||||
return new Plugin(this, manifest, wasi, functions, config);
|
||||
return new Plugin(manifest, wasi, functions, config, this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -385,7 +385,7 @@ export class CurrentPlugin {
|
||||
return Buffer.from(
|
||||
lib.extism_current_plugin_memory(this.pointer).buffer,
|
||||
offset,
|
||||
length
|
||||
length,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -442,7 +442,7 @@ export class CurrentPlugin {
|
||||
* @param input - The input to read
|
||||
*/
|
||||
inputBytes(input: typeof Val): Buffer {
|
||||
return this.memory(input.v.i64)
|
||||
return this.memory(input.v.i64);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -450,7 +450,7 @@ export class CurrentPlugin {
|
||||
* @param input - The input to read
|
||||
*/
|
||||
inputString(input: typeof Val): string {
|
||||
return this.memory(input.v.i64).toString()
|
||||
return this.memory(input.v.i64).toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,7 +489,7 @@ export class HostFunction {
|
||||
nInputs: number,
|
||||
outputs: Buffer,
|
||||
nOutputs: number,
|
||||
user_data
|
||||
user_data,
|
||||
) => {
|
||||
let inputArr = [];
|
||||
let outputArr = [];
|
||||
@@ -506,13 +506,13 @@ export class HostFunction {
|
||||
new CurrentPlugin(currentPlugin),
|
||||
inputArr,
|
||||
outputArr,
|
||||
...this.userData
|
||||
...this.userData,
|
||||
);
|
||||
|
||||
for (var i = 0; i < nOutputs; i++) {
|
||||
Val.set(outputs, i, outputArr[i]);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
this.name = name;
|
||||
this.inputs = new ValTypeArray(inputs);
|
||||
@@ -525,23 +525,23 @@ export class HostFunction {
|
||||
this.outputs.length,
|
||||
this.callback,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
);
|
||||
this.userData = userData;
|
||||
functionRegistry.register(this, this.pointer, this.pointer);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Set function namespace
|
||||
*/
|
||||
setNamespace(name: string) {
|
||||
if (this.pointer !== null) {
|
||||
lib.extism_function_set_namespace(this.pointer, name)
|
||||
lib.extism_function_set_namespace(this.pointer, name);
|
||||
}
|
||||
}
|
||||
|
||||
withNamespace(name: string) : HostFunction {
|
||||
this.setNamespace(name)
|
||||
withNamespace(name: string): HostFunction {
|
||||
this.setNamespace(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -560,18 +560,18 @@ export class HostFunction {
|
||||
}
|
||||
|
||||
/**
|
||||
* CancelHandle is used to cancel a running Plugin
|
||||
*/
|
||||
* CancelHandle is used to cancel a running Plugin
|
||||
*/
|
||||
export class CancelHandle {
|
||||
handle: Buffer
|
||||
handle: Buffer;
|
||||
|
||||
constructor(handle: Buffer) {
|
||||
this.handle = handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel execution of the Plugin associated with the CancelHandle
|
||||
*/
|
||||
* Cancel execution of the Plugin associated with the CancelHandle
|
||||
*/
|
||||
cancel(): boolean {
|
||||
return lib.extism_plugin_cancel(this.handle);
|
||||
}
|
||||
@@ -589,19 +589,22 @@ export class Plugin {
|
||||
/**
|
||||
* Constructor for a plugin. @see {@link Context#plugin}.
|
||||
*
|
||||
* @param ctx - The context to manage this plugin
|
||||
* @param manifest - The {@link Manifest}
|
||||
* @param wasi - Set to true to enable WASI support
|
||||
* @param functions - An array of {@link HostFunction}
|
||||
* @param config - The plugin config
|
||||
* @param ctx - The context to manage this plugin, or null to use a new context
|
||||
*/
|
||||
constructor(
|
||||
ctx: Context,
|
||||
manifest: ManifestData,
|
||||
wasi: boolean = false,
|
||||
functions: HostFunction[] = [],
|
||||
config?: PluginConfig
|
||||
config?: PluginConfig,
|
||||
ctx: Context | null = null,
|
||||
) {
|
||||
if (ctx == null) {
|
||||
ctx = new Context();
|
||||
}
|
||||
let dataRaw: string | Buffer;
|
||||
if (Buffer.isBuffer(manifest) || typeof manifest === "string") {
|
||||
dataRaw = manifest;
|
||||
@@ -621,7 +624,7 @@ export class Plugin {
|
||||
Buffer.byteLength(dataRaw, "utf-8"),
|
||||
this.functions,
|
||||
functions.length,
|
||||
wasi
|
||||
wasi,
|
||||
);
|
||||
if (plugin < 0) {
|
||||
var err = lib.extism_error(ctx.pointer, -1);
|
||||
@@ -640,7 +643,7 @@ export class Plugin {
|
||||
ctx.pointer,
|
||||
this.id,
|
||||
s,
|
||||
Buffer.byteLength(s, "utf-8")
|
||||
Buffer.byteLength(s, "utf-8"),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -666,7 +669,7 @@ export class Plugin {
|
||||
manifest: ManifestData,
|
||||
wasi: boolean = false,
|
||||
functions: HostFunction[] = [],
|
||||
config?: PluginConfig
|
||||
config?: PluginConfig,
|
||||
) {
|
||||
let dataRaw: string | Buffer;
|
||||
if (Buffer.isBuffer(manifest) || typeof manifest === "string") {
|
||||
@@ -688,7 +691,7 @@ export class Plugin {
|
||||
Buffer.byteLength(dataRaw, "utf-8"),
|
||||
this.functions,
|
||||
functions.length,
|
||||
wasi
|
||||
wasi,
|
||||
);
|
||||
if (!ok) {
|
||||
var err = lib.extism_error(this.ctx.pointer, -1);
|
||||
@@ -704,7 +707,7 @@ export class Plugin {
|
||||
this.ctx.pointer,
|
||||
this.id,
|
||||
s,
|
||||
Buffer.byteLength(s, "utf-8")
|
||||
Buffer.byteLength(s, "utf-8"),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -721,7 +724,7 @@ export class Plugin {
|
||||
return lib.extism_plugin_function_exists(
|
||||
this.ctx.pointer,
|
||||
this.id,
|
||||
functionName
|
||||
functionName,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -739,7 +742,7 @@ export class Plugin {
|
||||
*
|
||||
* @param functionName - The name of the function
|
||||
* @param input - The input data
|
||||
*@returns A Buffer repreesentation of the output
|
||||
* @returns A Buffer repreesentation of the output
|
||||
*/
|
||||
async call(functionName: string, input: string | Buffer): Promise<Buffer> {
|
||||
return new Promise<Buffer>((resolve, reject) => {
|
||||
@@ -749,7 +752,7 @@ export class Plugin {
|
||||
this.id,
|
||||
functionName,
|
||||
input.toString(),
|
||||
Buffer.byteLength(input, "utf-8")
|
||||
Buffer.byteLength(input, "utf-8"),
|
||||
);
|
||||
if (rc !== 0) {
|
||||
var err = lib.extism_error(this.ctx.pointer, this.id);
|
||||
@@ -763,7 +766,7 @@ export class Plugin {
|
||||
var buf = Buffer.from(
|
||||
lib.extism_plugin_output_data(this.ctx.pointer, this.id).buffer,
|
||||
0,
|
||||
out_len
|
||||
out_len,
|
||||
);
|
||||
resolve(buf);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
VERSION?=0.2.0
|
||||
VERSION?=0.4.0
|
||||
TAG?=0.5.0
|
||||
|
||||
build:
|
||||
dune build
|
||||
@@ -12,4 +13,4 @@ prepare:
|
||||
opam install .. --deps-only
|
||||
|
||||
publish:
|
||||
opam publish -v $(VERSION) -t $(VERSION) ..
|
||||
opam publish -v $(VERSION) https://github.com/extism/extism/archive/refs/tags/v$(TAG).tar.gz ..
|
||||
|
||||
@@ -4,10 +4,9 @@ open Cmdliner
|
||||
let read_stdin () = In_channel.input_all stdin
|
||||
|
||||
let main file func_name input =
|
||||
with_context @@ fun ctx ->
|
||||
let input = if String.equal input "-" then read_stdin () else input in
|
||||
let file = In_channel.with_open_bin file In_channel.input_all in
|
||||
let plugin = Plugin.create ctx file ~wasi:true |> Result.get_ok in
|
||||
let plugin = Plugin.create file ~wasi:true |> Result.get_ok in
|
||||
let res = Plugin.call plugin ~name:func_name input |> Result.get_ok in
|
||||
print_endline res
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ module Plugin : sig
|
||||
?config:Manifest.config ->
|
||||
?wasi:bool ->
|
||||
?functions:Function.t list ->
|
||||
Context.t ->
|
||||
?context:Context.t ->
|
||||
string ->
|
||||
(t, Error.t) result
|
||||
(** Make a new plugin from raw WebAssembly or JSON encoded manifest *)
|
||||
@@ -216,7 +216,7 @@ module Plugin : sig
|
||||
val of_manifest :
|
||||
?wasi:bool ->
|
||||
?functions:Function.t list ->
|
||||
Context.t ->
|
||||
?context:Context.t ->
|
||||
Manifest.t ->
|
||||
(t, Error.t) result
|
||||
(** Make a new plugin from a [Manifest] *)
|
||||
|
||||
@@ -26,7 +26,8 @@ let free t =
|
||||
if not (Ctypes.is_null t.ctx.pointer) then
|
||||
Bindings.extism_plugin_free t.ctx.pointer t.id
|
||||
|
||||
let create ?config ?(wasi = false) ?(functions = []) ctx wasm =
|
||||
let create ?config ?(wasi = false) ?(functions = []) ?context wasm =
|
||||
let ctx = match context with Some c -> c | None -> Context.create () in
|
||||
let func_ptrs = List.map (fun x -> x.Function.pointer) functions in
|
||||
let arr = Ctypes.CArray.of_list Ctypes.(ptr void) func_ptrs in
|
||||
let n_funcs = Ctypes.CArray.length arr in
|
||||
@@ -48,16 +49,15 @@ let create ?config ?(wasi = false) ?(functions = []) ctx wasm =
|
||||
let () = Gc.finalise free t in
|
||||
Ok t
|
||||
|
||||
let of_manifest ?wasi ?functions ctx manifest =
|
||||
let of_manifest ?wasi ?functions ?context manifest =
|
||||
let data = Manifest.to_json manifest in
|
||||
create ctx ?wasi ?functions data
|
||||
create ?wasi ?functions ?context data
|
||||
|
||||
let%test "free plugin" =
|
||||
let manifest = Manifest.(create [ Wasm.file "test/code.wasm" ]) in
|
||||
with_context (fun ctx ->
|
||||
let plugin = of_manifest ctx manifest |> Error.unwrap in
|
||||
free plugin;
|
||||
true)
|
||||
let plugin = of_manifest manifest |> Error.unwrap in
|
||||
free plugin;
|
||||
true
|
||||
|
||||
let update plugin ?config ?(wasi = false) ?(functions = []) wasm =
|
||||
let { id; ctx; _ } = plugin in
|
||||
@@ -77,7 +77,9 @@ let update plugin ?config ?(wasi = false) ?(functions = []) wasm =
|
||||
| Some msg -> Error (`Msg msg)
|
||||
else if not (set_config plugin config) then
|
||||
Error (`Msg "call to set_config failed")
|
||||
else Ok ()
|
||||
else
|
||||
let () = plugin.functions <- functions in
|
||||
Ok ()
|
||||
|
||||
let update_manifest plugin ?wasi manifest =
|
||||
let data = Manifest.to_json manifest in
|
||||
@@ -85,11 +87,10 @@ let update_manifest plugin ?wasi manifest =
|
||||
|
||||
let%test "update plugin manifest and config" =
|
||||
let manifest = Manifest.(create [ Wasm.file "test/code.wasm" ]) in
|
||||
with_context (fun ctx ->
|
||||
let config = [ ("a", Some "1") ] in
|
||||
let plugin = of_manifest ctx manifest |> Error.unwrap in
|
||||
let manifest = Manifest.with_config manifest config in
|
||||
update_manifest plugin manifest |> Result.is_ok)
|
||||
let config = [ ("a", Some "1") ] in
|
||||
let plugin = of_manifest manifest |> Error.unwrap in
|
||||
let manifest = Manifest.with_config manifest config in
|
||||
update_manifest plugin manifest |> Result.is_ok
|
||||
|
||||
let call' f { id; ctx; _ } ~name input len =
|
||||
let rc = f ctx.pointer id name input len in
|
||||
@@ -114,11 +115,10 @@ let call_bigstring (t : t) ~name input =
|
||||
|
||||
let%test "call_bigstring" =
|
||||
let manifest = Manifest.(create [ Wasm.file "test/code.wasm" ]) in
|
||||
with_context (fun ctx ->
|
||||
let plugin = of_manifest ctx manifest |> Error.unwrap in
|
||||
call_bigstring plugin ~name:"count_vowels"
|
||||
(Bigstringaf.of_string ~off:0 ~len:14 "this is a test")
|
||||
|> Error.unwrap |> Bigstringaf.to_string = "{\"count\": 4}")
|
||||
let plugin = of_manifest manifest |> Error.unwrap in
|
||||
call_bigstring plugin ~name:"count_vowels"
|
||||
(Bigstringaf.of_string ~off:0 ~len:14 "this is a test")
|
||||
|> Error.unwrap |> Bigstringaf.to_string = "{\"count\": 4}"
|
||||
|
||||
let call (t : t) ~name input =
|
||||
let len = String.length input in
|
||||
@@ -127,10 +127,9 @@ let call (t : t) ~name input =
|
||||
|
||||
let%test "call" =
|
||||
let manifest = Manifest.(create [ Wasm.file "test/code.wasm" ]) in
|
||||
with_context (fun ctx ->
|
||||
let plugin = of_manifest ctx manifest |> Error.unwrap in
|
||||
call plugin ~name:"count_vowels" "this is a test"
|
||||
|> Error.unwrap = "{\"count\": 4}")
|
||||
let plugin = of_manifest manifest |> Error.unwrap in
|
||||
call plugin ~name:"count_vowels" "this is a test"
|
||||
|> Error.unwrap = "{\"count\": 4}"
|
||||
|
||||
let%test "call_functions" =
|
||||
let open Types.Val_type in
|
||||
@@ -147,22 +146,18 @@ let%test "call_functions" =
|
||||
in
|
||||
let functions = [ hello_world ] in
|
||||
let manifest = Manifest.(create [ Wasm.file "test/code-functions.wasm" ]) in
|
||||
with_context (fun ctx ->
|
||||
let plugin =
|
||||
of_manifest ctx manifest ~functions ~wasi:true |> Error.unwrap
|
||||
in
|
||||
call plugin ~name:"count_vowels" "this is a test"
|
||||
|> Error.unwrap = "{\"count\": 4}")
|
||||
let plugin = of_manifest manifest ~functions ~wasi:true |> Error.unwrap in
|
||||
call plugin ~name:"count_vowels" "this is a test"
|
||||
|> Error.unwrap = "{\"count\": 4}"
|
||||
|
||||
let function_exists { id; ctx; _ } name =
|
||||
Bindings.extism_plugin_function_exists ctx.pointer id name
|
||||
|
||||
let%test "function exists" =
|
||||
let manifest = Manifest.(create [ Wasm.file "test/code.wasm" ]) in
|
||||
with_context (fun ctx ->
|
||||
let plugin = of_manifest ctx manifest |> Error.unwrap in
|
||||
function_exists plugin "count_vowels"
|
||||
&& not (function_exists plugin "function_does_not_exist"))
|
||||
let plugin = of_manifest manifest |> Error.unwrap in
|
||||
function_exists plugin "count_vowels"
|
||||
&& not (function_exists plugin "function_does_not_exist")
|
||||
|
||||
module Cancel_handle = struct
|
||||
type t = { inner : unit Ctypes.ptr }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
open Ppx_yojson_conv_lib.Yojson_conv
|
||||
|
||||
type base64 = string
|
||||
|
||||
let yojson_of_base64 x = `String (Base64.encode_exn x)
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
$ctx = new \Extism\Context();
|
||||
$wasm = file_get_contents("../../wasm/code.wasm");
|
||||
$plugin = new \Extism\Plugin($ctx, $wasm);
|
||||
$plugin = new \Extism\Plugin($wasm);
|
||||
|
||||
$output = $plugin->call("count_vowels", "this is an example");
|
||||
$json = json_decode(pack('C*', ...$output));
|
||||
|
||||
@@ -31,8 +31,12 @@ class Plugin
|
||||
|
||||
private $id;
|
||||
|
||||
public function __construct($ctx, $data, $wasi = false, $config = null)
|
||||
public function __construct($data, $wasi = false, $config = null, $ctx = null)
|
||||
{
|
||||
if ($ctx == null) {
|
||||
$ctx = new Context();
|
||||
}
|
||||
|
||||
$this->lib = $ctx->lib;
|
||||
|
||||
$this->wasi = $wasi;
|
||||
@@ -49,7 +53,7 @@ class Plugin
|
||||
$id = $this->lib->extism_plugin_new($ctx->pointer, $data, count($data), null, 0, (int)$wasi);
|
||||
if ($id < 0) {
|
||||
$err = $this->lib->extism_error($ctx->pointer, -1);
|
||||
throw new \Exception("Extism: unable to load plugin: " . $err);
|
||||
throw new \Exception("Extism: unable to load plugin: " . $err->toString());
|
||||
}
|
||||
$this->id = $id;
|
||||
$this->context = $ctx;
|
||||
@@ -91,7 +95,7 @@ class Plugin
|
||||
$msg = "code = " . $rc;
|
||||
$err = $this->lib->extism_error($this->context->pointer, $this->id);
|
||||
if ($err) {
|
||||
$msg = $msg . ", error = " . $err;
|
||||
$msg = $msg . ", error = " . $err->toString();
|
||||
}
|
||||
throw new \Exception("Extism: call to '".$name."' failed with " . $msg);
|
||||
}
|
||||
@@ -121,7 +125,7 @@ class Plugin
|
||||
$ok = $this->lib->extism_plugin_update($this->context->pointer, $this->id, $data, count($data), null, 0, (int)$wasi);
|
||||
if (!$ok) {
|
||||
$err = $this->lib->extism_error($this->context->pointer, -1);
|
||||
throw new \Exception("Extism: unable to update plugin: " . $err);
|
||||
throw new \Exception("Extism: unable to update plugin: " . $err->toString());
|
||||
}
|
||||
|
||||
if ($config != null) {
|
||||
@@ -138,4 +142,4 @@ function string_to_bytes($string) {
|
||||
}
|
||||
|
||||
return $bytes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ lint:
|
||||
poetry run black --check extism/ tests/ example.py
|
||||
|
||||
docs:
|
||||
poetry run pdoc --force --html extism
|
||||
poetry run pycco extism/*.py
|
||||
|
||||
show-docs: docs
|
||||
open html/extism/index.html
|
||||
open docs/extism.html
|
||||
|
||||
@@ -5,11 +5,13 @@ import hashlib
|
||||
import pathlib
|
||||
|
||||
sys.path.append(".")
|
||||
from extism import Context, Function, host_fn, ValType
|
||||
from extism import Function, host_fn, ValType, Plugin, set_log_file
|
||||
|
||||
set_log_file("stderr", "trace")
|
||||
|
||||
|
||||
@host_fn
|
||||
def hello_world(plugin, input_, output, context, a_string):
|
||||
def hello_world(plugin, input_, output, a_string):
|
||||
print("Hello from Python!")
|
||||
print(a_string)
|
||||
print(input_)
|
||||
@@ -26,35 +28,33 @@ def main(args):
|
||||
if len(args) > 1:
|
||||
data = args[1].encode()
|
||||
else:
|
||||
data = b"some data from python!"
|
||||
data = b"a" * 1024
|
||||
|
||||
wasm_file_path = (
|
||||
pathlib.Path(__file__).parent.parent / "wasm" / "code-functions.wasm"
|
||||
)
|
||||
wasm = wasm_file_path.read_bytes()
|
||||
hash = hashlib.sha256(wasm).hexdigest()
|
||||
config = {"wasm": [{"data": wasm, "hash": hash}], "memory": {"max": 5}}
|
||||
manifest = {"wasm": [{"data": wasm, "hash": hash}]}
|
||||
|
||||
# a Context provides a scope for plugins to be managed within. creating multiple contexts
|
||||
# is expected and groups plugins based on source/tenant/lifetime etc.
|
||||
with Context() as context:
|
||||
functions = [
|
||||
Function(
|
||||
"hello_world",
|
||||
[ValType.I64],
|
||||
[ValType.I64],
|
||||
hello_world,
|
||||
context,
|
||||
"Hello again!",
|
||||
)
|
||||
]
|
||||
plugin = context.plugin(config, wasi=True, functions=functions)
|
||||
# Call `count_vowels`
|
||||
wasm_vowel_count = json.loads(plugin.call("count_vowels", data))
|
||||
functions = [
|
||||
Function(
|
||||
"hello_world",
|
||||
[ValType.I64],
|
||||
[ValType.I64],
|
||||
hello_world,
|
||||
"Hello again!",
|
||||
)
|
||||
]
|
||||
plugin = Plugin(manifest, wasi=True, functions=functions)
|
||||
# Call `count_vowels`
|
||||
wasm_vowel_count = plugin.call("count_vowels", data)
|
||||
print(wasm_vowel_count)
|
||||
j = json.loads(wasm_vowel_count)
|
||||
|
||||
print("Number of vowels:", wasm_vowel_count["count"])
|
||||
print("Number of vowels:", j["count"])
|
||||
|
||||
assert wasm_vowel_count["count"] == count_vowels(data)
|
||||
assert j["count"] == count_vowels(data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -193,7 +193,9 @@ class Context:
|
||||
Plugin
|
||||
The created plugin
|
||||
"""
|
||||
return Plugin(self, manifest, wasi, config, functions)
|
||||
return Plugin(
|
||||
manifest, context=self, wasi=wasi, config=config, functions=functions
|
||||
)
|
||||
|
||||
|
||||
class Function:
|
||||
@@ -247,17 +249,21 @@ class Plugin:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Context,
|
||||
plugin: Union[str, bytes, dict],
|
||||
context=None,
|
||||
wasi=False,
|
||||
config=None,
|
||||
functions=None,
|
||||
):
|
||||
"""
|
||||
Construct a Plugin. Please use Context#plugin instead.
|
||||
Construct a Plugin
|
||||
"""
|
||||
|
||||
if context is None:
|
||||
context = Context()
|
||||
|
||||
wasm = _wasm(plugin)
|
||||
self.functions = functions
|
||||
|
||||
# Register plugin
|
||||
if functions is not None:
|
||||
@@ -305,6 +311,7 @@ class Plugin:
|
||||
"""
|
||||
wasm = _wasm(manifest)
|
||||
if functions is not None:
|
||||
self.functions = functions
|
||||
functions = [f.pointer for f in functions]
|
||||
ptr = _ffi.new("ExtismFunction*[]", functions)
|
||||
ok = _lib.extism_plugin_update(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "extism"
|
||||
version = "0.3.0"
|
||||
version = "0.5.0"
|
||||
description = "Extism Host SDK for python"
|
||||
authors = ["The Extism Authors <oss@extism.org>"]
|
||||
license = "BSD-3-Clause"
|
||||
@@ -12,7 +12,7 @@ cffi = "^1.10.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = "^23.1.0"
|
||||
pdoc3 = "^0.10.0"
|
||||
pycco = "^0.6.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
|
||||
@@ -104,23 +104,24 @@ class TestExtism(unittest.TestCase):
|
||||
with extism.Context() as ctx:
|
||||
plugin = ctx.plugin(self._loop_manifest())
|
||||
cancel_handle = plugin.cancel_handle()
|
||||
|
||||
def cancel(handle):
|
||||
time.sleep(0.5)
|
||||
handle.cancel()
|
||||
|
||||
Thread(target=cancel, args=[cancel_handle]).run()
|
||||
self.assertRaises(extism.Error, lambda: plugin.call("infinite_loop", b""))
|
||||
|
||||
def _manifest(self, functions=False):
|
||||
wasm = self._count_vowels_wasm(functions)
|
||||
hash = hashlib.sha256(wasm).hexdigest()
|
||||
return {"wasm": [{"data": wasm, "hash": hash}], "memory": {"max_pages": 5}}
|
||||
return {"wasm": [{"data": wasm, "hash": hash}]}
|
||||
|
||||
def _loop_manifest(self):
|
||||
wasm = self._infinite_loop_wasm()
|
||||
hash = hashlib.sha256(wasm).hexdigest()
|
||||
return {
|
||||
"wasm": [{"data": wasm, "hash": hash}],
|
||||
"memory": {"max_pages": 5},
|
||||
"timeout_ms": 1000,
|
||||
}
|
||||
|
||||
|
||||
@@ -11,5 +11,5 @@ gem "ffi", "~> 1.15.5"
|
||||
group :development do
|
||||
gem "yard", "~> 0.9.28"
|
||||
gem "rufo", "~> 0.13.0"
|
||||
gem "minitest", "~> 5.18.0"
|
||||
gem "minitest", "~> 5.19.0"
|
||||
end
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
require "./lib/extism"
|
||||
require "json"
|
||||
|
||||
# a Context provides a scope for plugins to be managed within. creating multiple contexts
|
||||
# is expected and groups plugins based on source/tenant/lifetime etc.
|
||||
# We recommend you use `Extism.with_context` unless you have a reason to keep your context around.
|
||||
# If you do you can create a context with `Extism#new`, example: `ctx = Extism.new`
|
||||
Extism.with_context do |ctx|
|
||||
manifest = {
|
||||
:wasm => [{ :path => "../wasm/code.wasm" }],
|
||||
}
|
||||
manifest = {
|
||||
:wasm => [{ :path => "../wasm/code.wasm" }],
|
||||
}
|
||||
|
||||
plugin = ctx.plugin(manifest)
|
||||
res = JSON.parse(plugin.call("count_vowels", ARGV[0] || "this is a test"))
|
||||
plugin = Extism::Plugin.new(manifest)
|
||||
res = JSON.parse(plugin.call("count_vowels", ARGV[0] || "this is a test"))
|
||||
|
||||
puts res["count"]
|
||||
end
|
||||
puts res["count"]
|
||||
|
||||
@@ -86,12 +86,13 @@ module Extism
|
||||
|
||||
# Create a new plugin from a WASM module or JSON encoded manifest
|
||||
#
|
||||
# @see Plugin#new
|
||||
# @param wasm [Hash, String] The manifest for the plugin. See https://extism.org/docs/concepts/manifest/.
|
||||
# @param wasi [Boolean] Enable WASI support
|
||||
# @param config [Hash] The plugin config
|
||||
# @return [Plugin]
|
||||
def plugin(wasm, wasi = false, config = nil)
|
||||
Plugin.new(self, wasm, wasi, config)
|
||||
Plugin.new(wasm, wasi, config, self)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -131,12 +132,14 @@ module Extism
|
||||
class Plugin
|
||||
# Intialize a plugin
|
||||
#
|
||||
# @see Extism::Context#plugin
|
||||
# @param context [Context] The context to manager this plugin
|
||||
# @param wasm [Hash, String] The manifest or WASM binary. See https://extism.org/docs/concepts/manifest/.
|
||||
# @param wasi [Boolean] Enable WASI support
|
||||
# @param config [Hash] The plugin config
|
||||
def initialize(context, wasm, wasi = false, config = nil)
|
||||
# @param context [Context] The context to manager this plugin
|
||||
def initialize(wasm, wasi = false, config = nil, context = nil)
|
||||
if context.nil? then
|
||||
context = Context.new
|
||||
end
|
||||
@context = context
|
||||
if wasm.class == Hash
|
||||
wasm = JSON.generate(wasm)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Extism
|
||||
VERSION = "0.3.0"
|
||||
VERSION = '0.5.0'
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "extism-runtime"
|
||||
version = "0.3.0"
|
||||
version = "0.5.2"
|
||||
edition = "2021"
|
||||
authors = ["The Extism Authors", "oss@extism.org"]
|
||||
license = "BSD-3-Clause"
|
||||
@@ -9,9 +9,9 @@ repository = "https://github.com/extism/extism"
|
||||
description = "Extism runtime component"
|
||||
|
||||
[dependencies]
|
||||
wasmtime = "6.0.1"
|
||||
wasmtime-wasi = "6.0.1"
|
||||
wasmtime-wasi-nn = {version = "6.0.1", optional=true}
|
||||
wasmtime = ">= 10.0.0, < 12.0.0"
|
||||
wasmtime-wasi = ">= 10.0.0, < 12.0.0"
|
||||
wasmtime-wasi-nn = {version = ">= 10.0.0, < 12.0.0", optional=true}
|
||||
anyhow = "1"
|
||||
serde = {version = "1", features = ["derive"]}
|
||||
serde_json = "1"
|
||||
@@ -22,8 +22,7 @@ log4rs = "1.1"
|
||||
url = "2"
|
||||
glob = "0.3"
|
||||
ureq = {version = "2.5", optional=true}
|
||||
extism-manifest = { version = "0.3.0", path = "../manifest" }
|
||||
pretty-hex = { version = "0.3" }
|
||||
extism-manifest = { version = "0.5.0", path = "../manifest" }
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
libc = "0.2"
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=src/extism-runtime.wasm");
|
||||
|
||||
let fn_macro = "
|
||||
#define EXTISM_FUNCTION(N) extern void N(ExtismCurrentPlugin*, const ExtismVal*, ExtismSize, ExtismVal*, ExtismSize, void*)
|
||||
#define EXTISM_GO_FUNCTION(N) extern void N(void*, ExtismVal*, ExtismSize, ExtismVal*, ExtismSize, uintptr_t)
|
||||
@@ -16,7 +18,7 @@ fn main() {
|
||||
.rename_item("Context", "ExtismContext")
|
||||
.rename_item("ValType", "ExtismValType")
|
||||
.rename_item("ValUnion", "ExtismValUnion")
|
||||
.rename_item("Plugin", "ExtismCurrentPlugin")
|
||||
.rename_item("Internal", "ExtismCurrentPlugin")
|
||||
.with_style(cbindgen::Style::Type)
|
||||
.generate()
|
||||
{
|
||||
|
||||
@@ -54,7 +54,7 @@ typedef struct ExtismCancelHandle ExtismCancelHandle;
|
||||
typedef struct ExtismFunction ExtismFunction;
|
||||
|
||||
/**
|
||||
* Plugin contains everything needed to execute a WASM function
|
||||
* Internal stores data that is available to the caller in PDK functions
|
||||
*/
|
||||
typedef struct ExtismCurrentPlugin ExtismCurrentPlugin;
|
||||
|
||||
@@ -81,7 +81,12 @@ typedef struct {
|
||||
/**
|
||||
* Host function signature
|
||||
*/
|
||||
typedef void (*ExtismFunctionType)(ExtismCurrentPlugin *plugin, const ExtismVal *inputs, ExtismSize n_inputs, ExtismVal *outputs, ExtismSize n_outputs, void *data);
|
||||
typedef void (*ExtismFunctionType)(ExtismCurrentPlugin *plugin,
|
||||
const ExtismVal *inputs,
|
||||
ExtismSize n_inputs,
|
||||
ExtismVal *outputs,
|
||||
ExtismSize n_outputs,
|
||||
void *data);
|
||||
|
||||
typedef int32_t ExtismPlugin;
|
||||
|
||||
@@ -245,7 +250,7 @@ const char *extism_error(ExtismContext *ctx, ExtismPlugin plugin);
|
||||
ExtismSize extism_plugin_output_length(ExtismContext *ctx, ExtismPlugin plugin);
|
||||
|
||||
/**
|
||||
* Get the length of a plugin's output data
|
||||
* Get a pointer to the output data
|
||||
*/
|
||||
const uint8_t *extism_plugin_output_data(ExtismContext *ctx, ExtismPlugin plugin);
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::cell::UnsafeCell;
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
|
||||
use crate::*;
|
||||
@@ -8,7 +7,7 @@ static mut TIMER: std::sync::Mutex<Option<Timer>> = std::sync::Mutex::new(None);
|
||||
/// A `Context` is used to store and manage plugins
|
||||
pub struct Context {
|
||||
/// Plugin registry
|
||||
pub plugins: BTreeMap<PluginIndex, UnsafeCell<Plugin>>,
|
||||
pub plugins: BTreeMap<PluginIndex, Plugin>,
|
||||
|
||||
/// Error message
|
||||
pub error: Option<std::ffi::CString>,
|
||||
@@ -91,7 +90,7 @@ impl Context {
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
self.plugins.insert(id, UnsafeCell::new(plugin));
|
||||
self.plugins.insert(id, plugin);
|
||||
id
|
||||
}
|
||||
|
||||
@@ -127,7 +126,7 @@ impl Context {
|
||||
/// Get a plugin from the context
|
||||
pub fn plugin(&mut self, id: PluginIndex) -> Option<*mut Plugin> {
|
||||
match self.plugins.get_mut(&id) {
|
||||
Some(x) => Some(x.get_mut()),
|
||||
Some(x) => Some(x),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
BIN
runtime/src/extism-runtime.wasm
Executable file
BIN
runtime/src/extism-runtime.wasm
Executable file
Binary file not shown.
@@ -169,12 +169,12 @@ impl Function {
|
||||
) -> Function
|
||||
where
|
||||
F: 'static
|
||||
+ Fn(&mut crate::Plugin, &[Val], &mut [Val], UserData) -> Result<(), Error>
|
||||
+ Fn(&mut Internal, &[Val], &mut [Val], UserData) -> Result<(), Error>
|
||||
+ Sync
|
||||
+ Send,
|
||||
{
|
||||
let user_data = user_data.unwrap_or_default();
|
||||
let data = UserData::new_pointer(user_data.ptr, None);
|
||||
let data = user_data.make_copy();
|
||||
Function {
|
||||
name: name.into(),
|
||||
ty: wasmtime::FuncType::new(
|
||||
@@ -182,7 +182,7 @@ impl Function {
|
||||
returns.into_iter().map(wasmtime::ValType::from),
|
||||
),
|
||||
f: std::sync::Arc::new(move |mut caller, inp, outp| {
|
||||
f(caller.data_mut().plugin_mut(), inp, outp, data.make_copy())
|
||||
f(caller.data_mut(), inp, outp, data.make_copy())
|
||||
}),
|
||||
namespace: None,
|
||||
_user_data: std::sync::Arc::new(user_data),
|
||||
|
||||
345
runtime/src/internal.rs
Normal file
345
runtime/src/internal.rs
Normal file
@@ -0,0 +1,345 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::*;
|
||||
|
||||
/// WASI context
|
||||
pub struct Wasi {
|
||||
/// wasi
|
||||
pub ctx: wasmtime_wasi::WasiCtx,
|
||||
|
||||
/// wasi-nn
|
||||
#[cfg(feature = "nn")]
|
||||
pub nn: wasmtime_wasi_nn::WasiNnCtx,
|
||||
}
|
||||
|
||||
/// Internal stores data that is available to the caller in PDK functions
|
||||
pub struct Internal {
|
||||
/// Store
|
||||
pub store: *mut Store<Internal>,
|
||||
|
||||
/// Linker
|
||||
pub linker: *mut wasmtime::Linker<Internal>,
|
||||
|
||||
/// WASI context
|
||||
pub wasi: Option<Wasi>,
|
||||
|
||||
/// Keep track of the status from the last HTTP request
|
||||
pub http_status: u16,
|
||||
|
||||
/// Plugin variables
|
||||
pub vars: BTreeMap<String, Vec<u8>>,
|
||||
|
||||
pub manifest: Manifest,
|
||||
|
||||
pub available_pages: Option<u32>,
|
||||
|
||||
pub(crate) memory_limiter: Option<MemoryLimiter>,
|
||||
}
|
||||
|
||||
/// InternalExt provides a unified way of acessing `memory`, `store` and `internal` values
|
||||
pub trait InternalExt {
|
||||
fn store(&self) -> &Store<Internal>;
|
||||
|
||||
fn store_mut(&mut self) -> &mut Store<Internal>;
|
||||
|
||||
fn linker(&self) -> &Linker<Internal>;
|
||||
|
||||
fn linker_mut(&mut self) -> &mut Linker<Internal>;
|
||||
|
||||
fn linker_and_store(&mut self) -> (&mut Linker<Internal>, &mut Store<Internal>);
|
||||
|
||||
fn internal(&self) -> &Internal {
|
||||
self.store().data()
|
||||
}
|
||||
|
||||
fn internal_mut(&mut self) -> &mut Internal {
|
||||
self.store_mut().data_mut()
|
||||
}
|
||||
|
||||
fn memory_ptr(&mut self) -> *mut u8 {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
if let Some(mem) = linker.get(&mut store, "env", "memory") {
|
||||
if let Some(mem) = mem.into_memory() {
|
||||
return mem.data_ptr(&mut store);
|
||||
}
|
||||
}
|
||||
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
|
||||
fn memory(&mut self) -> &mut [u8] {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
let mem = linker
|
||||
.get(&mut store, "env", "memory")
|
||||
.unwrap()
|
||||
.into_memory()
|
||||
.unwrap();
|
||||
let ptr = mem.data_ptr(&store);
|
||||
if ptr.is_null() {
|
||||
return &mut [];
|
||||
}
|
||||
let size = mem.data_size(&store);
|
||||
unsafe { std::slice::from_raw_parts_mut(ptr, size) }
|
||||
}
|
||||
|
||||
fn memory_read(&mut self, offs: u64, len: Size) -> &[u8] {
|
||||
trace!("memory_read: {}, {}", offs, len);
|
||||
let offs = offs as usize;
|
||||
let len = len as usize;
|
||||
let mem = self.memory();
|
||||
&mem[offs..offs + len]
|
||||
}
|
||||
|
||||
fn memory_read_str(&mut self, offs: u64) -> Result<&str, std::str::Utf8Error> {
|
||||
let len = self.memory_length(offs);
|
||||
std::str::from_utf8(self.memory_read(offs, len))
|
||||
}
|
||||
|
||||
fn memory_write(&mut self, offs: u64, bytes: impl AsRef<[u8]>) {
|
||||
trace!("memory_write: {}", offs);
|
||||
let b = bytes.as_ref();
|
||||
let offs = offs as usize;
|
||||
let len = b.len();
|
||||
self.memory()[offs..offs + len].copy_from_slice(bytes.as_ref());
|
||||
}
|
||||
|
||||
fn memory_alloc(&mut self, n: Size) -> Result<u64, Error> {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
let output = &mut [Val::I64(0)];
|
||||
linker
|
||||
.get(&mut store, "env", "extism_alloc")
|
||||
.unwrap()
|
||||
.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[Val::I64(n as i64)], output)?;
|
||||
let offs = output[0].unwrap_i64() as u64;
|
||||
if offs == 0 {
|
||||
anyhow::bail!("out of memory")
|
||||
}
|
||||
trace!("memory_alloc: {}, {}", offs, n);
|
||||
Ok(offs)
|
||||
}
|
||||
|
||||
fn memory_alloc_bytes(&mut self, bytes: impl AsRef<[u8]>) -> Result<u64, Error> {
|
||||
let b = bytes.as_ref();
|
||||
let offs = self.memory_alloc(b.len() as Size)?;
|
||||
self.memory_write(offs, b);
|
||||
Ok(offs)
|
||||
}
|
||||
|
||||
fn memory_free(&mut self, offs: u64) {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
linker
|
||||
.get(&mut store, "env", "extism_free")
|
||||
.unwrap()
|
||||
.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[Val::I64(offs as i64)], &mut [])
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn memory_length(&mut self, offs: u64) -> u64 {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
let output = &mut [Val::I64(0)];
|
||||
linker
|
||||
.get(&mut store, "env", "extism_length")
|
||||
.unwrap()
|
||||
.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[Val::I64(offs as i64)], output)
|
||||
.unwrap();
|
||||
let len = output[0].unwrap_i64() as u64;
|
||||
trace!("memory_length: {}, {}", offs, len);
|
||||
len
|
||||
}
|
||||
|
||||
// A convenience method to set the plugin error and return a value
|
||||
fn error<E>(&mut self, e: impl std::fmt::Debug, x: E) -> E {
|
||||
let s = format!("{e:?}");
|
||||
debug!("Set error: {:?}", s);
|
||||
if let Ok(offs) = self.memory_alloc_bytes(&s) {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
if let Some(f) = linker.get(&mut store, "env", "extism_error_set") {
|
||||
f.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[Val::I64(offs as i64)], &mut [])
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
x
|
||||
}
|
||||
|
||||
fn clear_error(&mut self) {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
if let Some(f) = linker.get(&mut store, "env", "extism_error_set") {
|
||||
f.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[Val::I64(0)], &mut [])
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn has_error(&mut self) -> bool {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
let output = &mut [Val::I64(0)];
|
||||
linker
|
||||
.get(&mut store, "env", "extism_error_get")
|
||||
.unwrap()
|
||||
.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[], output)
|
||||
.unwrap();
|
||||
output[0].unwrap_i64() != 0
|
||||
}
|
||||
|
||||
fn get_error(&mut self) -> Option<&str> {
|
||||
let (linker, mut store) = self.linker_and_store();
|
||||
let output = &mut [Val::I64(0)];
|
||||
linker
|
||||
.get(&mut store, "env", "extism_error_get")
|
||||
.unwrap()
|
||||
.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[], output)
|
||||
.unwrap();
|
||||
let offs = output[0].unwrap_i64() as u64;
|
||||
if offs == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let length = self.memory_length(offs);
|
||||
let data = self.memory_read(offs, length);
|
||||
let s = std::str::from_utf8(data);
|
||||
match s {
|
||||
Ok(s) => Some(s),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Internal {
|
||||
pub(crate) fn new(
|
||||
manifest: Manifest,
|
||||
wasi: bool,
|
||||
available_pages: Option<u32>,
|
||||
) -> Result<Self, Error> {
|
||||
let wasi = if wasi {
|
||||
let auth = wasmtime_wasi::ambient_authority();
|
||||
let mut ctx = wasmtime_wasi::WasiCtxBuilder::new();
|
||||
for (k, v) in manifest.as_ref().config.iter() {
|
||||
ctx = ctx.env(k, v)?;
|
||||
}
|
||||
|
||||
if let Some(a) = &manifest.as_ref().allowed_paths {
|
||||
for (k, v) in a.iter() {
|
||||
let d = wasmtime_wasi::Dir::open_ambient_dir(k, auth)?;
|
||||
ctx = ctx.preopened_dir(d, v)?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "nn")]
|
||||
let nn = wasmtime_wasi_nn::WasiNnCtx::new()?;
|
||||
|
||||
Some(Wasi {
|
||||
ctx: ctx.build(),
|
||||
#[cfg(feature = "nn")]
|
||||
nn,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let memory_limiter = if let Some(pgs) = available_pages {
|
||||
let n = pgs as usize * 65536;
|
||||
Some(MemoryLimiter {
|
||||
max_bytes: n,
|
||||
bytes_left: n,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Internal {
|
||||
wasi,
|
||||
manifest,
|
||||
http_status: 0,
|
||||
vars: BTreeMap::new(),
|
||||
linker: std::ptr::null_mut(),
|
||||
store: std::ptr::null_mut(),
|
||||
available_pages,
|
||||
memory_limiter,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn linker(&self) -> &wasmtime::Linker<Internal> {
|
||||
unsafe { &*self.linker }
|
||||
}
|
||||
|
||||
pub fn linker_mut(&mut self) -> &mut wasmtime::Linker<Internal> {
|
||||
unsafe { &mut *self.linker }
|
||||
}
|
||||
}
|
||||
|
||||
impl InternalExt for Internal {
|
||||
fn store(&self) -> &Store<Internal> {
|
||||
unsafe { &*self.store }
|
||||
}
|
||||
|
||||
fn store_mut(&mut self) -> &mut Store<Internal> {
|
||||
unsafe { &mut *self.store }
|
||||
}
|
||||
|
||||
fn linker(&self) -> &Linker<Internal> {
|
||||
unsafe { &*self.linker }
|
||||
}
|
||||
|
||||
fn linker_mut(&mut self) -> &mut Linker<Internal> {
|
||||
unsafe { &mut *self.linker }
|
||||
}
|
||||
|
||||
fn linker_and_store(&mut self) -> (&mut Linker<Internal>, &mut Store<Internal>) {
|
||||
unsafe { (&mut *self.linker, &mut *self.store) }
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct MemoryLimiter {
|
||||
bytes_left: usize,
|
||||
max_bytes: usize,
|
||||
}
|
||||
|
||||
impl MemoryLimiter {
|
||||
pub(crate) fn reset(&mut self) {
|
||||
self.bytes_left = self.max_bytes;
|
||||
}
|
||||
}
|
||||
|
||||
impl wasmtime::ResourceLimiter for MemoryLimiter {
|
||||
fn memory_growing(
|
||||
&mut self,
|
||||
current: usize,
|
||||
desired: usize,
|
||||
maximum: Option<usize>,
|
||||
) -> Result<bool> {
|
||||
if let Some(max) = maximum {
|
||||
if desired > max {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
let d = desired - current;
|
||||
if d > self.bytes_left {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
self.bytes_left -= d;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn table_growing(&mut self, _current: u32, desired: u32, maximum: Option<u32>) -> Result<bool> {
|
||||
if let Some(max) = maximum {
|
||||
return Ok(desired <= max);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@ pub(crate) use wasmtime::*;
|
||||
|
||||
mod context;
|
||||
mod function;
|
||||
mod internal;
|
||||
pub mod manifest;
|
||||
mod memory;
|
||||
pub(crate) mod pdk;
|
||||
mod plugin;
|
||||
mod plugin_ref;
|
||||
@@ -13,9 +13,9 @@ mod timer;
|
||||
|
||||
pub use context::Context;
|
||||
pub use function::{Function, UserData, Val, ValType};
|
||||
pub use internal::{Internal, InternalExt, Wasi};
|
||||
pub use manifest::Manifest;
|
||||
pub use memory::{MemoryBlock, PluginMemory, ToMemoryBlock};
|
||||
pub use plugin::{Internal, Plugin, Wasi};
|
||||
pub use plugin::Plugin;
|
||||
pub use plugin_ref::PluginRef;
|
||||
pub(crate) use timer::{Timer, TimerAction};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use sha2::Digest;
|
||||
use crate::*;
|
||||
|
||||
/// Manifest wraps the manifest exported by `extism_manifest`
|
||||
#[derive(Default, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Manifest(extism_manifest::Manifest);
|
||||
|
||||
@@ -60,6 +60,8 @@ fn check_hash(hash: &Option<String>, data: &[u8]) -> Result<(), Error> {
|
||||
}
|
||||
}
|
||||
|
||||
const WASM: &[u8] = include_bytes!("extism-runtime.wasm");
|
||||
|
||||
/// Convert from manifest to a wasmtime Module
|
||||
fn to_module(engine: &Engine, wasm: &extism_manifest::Wasm) -> Result<(String, Module), Error> {
|
||||
match wasm {
|
||||
@@ -167,6 +169,7 @@ const WASM_MAGIC: [u8; 4] = [0x00, 0x61, 0x73, 0x6d];
|
||||
impl Manifest {
|
||||
/// Create a new Manifest, returns the manifest and a map of modules
|
||||
pub fn new(engine: &Engine, data: &[u8]) -> Result<(Self, BTreeMap<String, Module>), Error> {
|
||||
let extism_module = Module::new(engine, WASM)?;
|
||||
let has_magic = data.len() >= 4 && data[0..4] == WASM_MAGIC;
|
||||
let is_wast = data.starts_with(b"(module") || data.starts_with(b";;");
|
||||
if !has_magic && !is_wast {
|
||||
@@ -178,12 +181,14 @@ impl Manifest {
|
||||
}
|
||||
|
||||
let t = serde_json::from_slice::<Self>(data)?;
|
||||
let m = t.modules(engine)?;
|
||||
let mut m = t.modules(engine)?;
|
||||
m.insert("env".to_string(), extism_module);
|
||||
return Ok((t, m));
|
||||
}
|
||||
|
||||
let m = Module::new(engine, data)?;
|
||||
let mut modules = BTreeMap::new();
|
||||
modules.insert("env".to_string(), extism_module);
|
||||
modules.insert("main".to_string(), m);
|
||||
Ok((Manifest::default(), modules))
|
||||
}
|
||||
|
||||
@@ -1,331 +0,0 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::*;
|
||||
|
||||
use pretty_hex::PrettyHex;
|
||||
|
||||
/// Handles memory for plugins
|
||||
pub struct PluginMemory {
|
||||
pub store: Store<Internal>,
|
||||
pub memory: Memory,
|
||||
pub live_blocks: BTreeMap<usize, usize>,
|
||||
pub free: Vec<MemoryBlock>,
|
||||
pub position: usize,
|
||||
}
|
||||
|
||||
pub trait ToMemoryBlock {
|
||||
fn to_memory_block(&self, mem: &PluginMemory) -> Result<MemoryBlock, Error>;
|
||||
}
|
||||
|
||||
impl ToMemoryBlock for MemoryBlock {
|
||||
fn to_memory_block(&self, _mem: &PluginMemory) -> Result<MemoryBlock, Error> {
|
||||
Ok(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToMemoryBlock for (usize, usize) {
|
||||
fn to_memory_block(&self, _mem: &PluginMemory) -> Result<MemoryBlock, Error> {
|
||||
Ok(MemoryBlock {
|
||||
offset: self.0,
|
||||
length: self.1,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToMemoryBlock for usize {
|
||||
fn to_memory_block(&self, mem: &PluginMemory) -> Result<MemoryBlock, Error> {
|
||||
match mem.at_offset(*self) {
|
||||
Some(x) => Ok(x),
|
||||
None => Err(Error::msg(format!("Invalid memory offset: {}", self))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const PAGE_SIZE: u32 = 65536;
|
||||
|
||||
// BLOCK_SIZE_THRESHOLD exists to ensure that free blocks are never split up any
|
||||
// smaller than this value
|
||||
const BLOCK_SIZE_THRESHOLD: usize = 32;
|
||||
|
||||
impl PluginMemory {
|
||||
/// Create memory for a plugin
|
||||
pub fn new(store: Store<Internal>, memory: Memory) -> Self {
|
||||
PluginMemory {
|
||||
free: Vec::new(),
|
||||
live_blocks: BTreeMap::new(),
|
||||
store,
|
||||
memory,
|
||||
position: 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Write byte to memory
|
||||
pub(crate) fn store_u8(&mut self, offs: usize, data: u8) -> Result<(), MemoryAccessError> {
|
||||
trace!("store_u8: offset={offs} data={data:#04x}");
|
||||
if offs >= self.size() {
|
||||
// This should raise MemoryAccessError
|
||||
let buf = &mut [0];
|
||||
self.memory.read(&self.store, offs, buf)?;
|
||||
return Ok(());
|
||||
}
|
||||
self.memory.data_mut(&mut self.store)[offs] = data;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read byte from memory
|
||||
pub(crate) fn load_u8(&self, offs: usize) -> Result<u8, MemoryAccessError> {
|
||||
trace!("load_u8: offset={offs}");
|
||||
if offs >= self.size() {
|
||||
// This should raise MemoryAccessError
|
||||
let buf = &mut [0];
|
||||
self.memory.read(&self.store, offs, buf)?;
|
||||
return Ok(0);
|
||||
}
|
||||
Ok(self.memory.data(&self.store)[offs])
|
||||
}
|
||||
|
||||
/// Write u64 to memory
|
||||
pub(crate) fn store_u64(&mut self, offs: usize, data: u64) -> Result<(), Error> {
|
||||
trace!("store_u64: offset={offs} data={data:#18x}");
|
||||
let handle = MemoryBlock {
|
||||
offset: offs,
|
||||
length: 8,
|
||||
};
|
||||
self.write(handle, data.to_ne_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read u64 from memory
|
||||
pub(crate) fn load_u64(&self, offs: usize) -> Result<u64, Error> {
|
||||
trace!("load_u64: offset={offs}");
|
||||
let mut buf = [0; 8];
|
||||
let handle = MemoryBlock {
|
||||
offset: offs,
|
||||
length: 8,
|
||||
};
|
||||
self.read(handle, &mut buf)?;
|
||||
Ok(u64::from_ne_bytes(buf))
|
||||
}
|
||||
|
||||
/// Write slice to memory
|
||||
pub fn write(&mut self, pos: impl ToMemoryBlock, data: impl AsRef<[u8]>) -> Result<(), Error> {
|
||||
let pos = pos.to_memory_block(self)?;
|
||||
assert!(data.as_ref().len() <= pos.length);
|
||||
self.memory
|
||||
.write(&mut self.store, pos.offset, data.as_ref())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read slice from memory
|
||||
pub fn read(&self, pos: impl ToMemoryBlock, mut data: impl AsMut<[u8]>) -> Result<(), Error> {
|
||||
let pos = pos.to_memory_block(self)?;
|
||||
assert!(data.as_mut().len() <= pos.length);
|
||||
self.memory.read(&self.store, pos.offset, data.as_mut())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Size of memory in bytes
|
||||
pub fn size(&self) -> usize {
|
||||
self.memory.data_size(&self.store)
|
||||
}
|
||||
|
||||
/// Size of memory in pages
|
||||
pub fn pages(&self) -> u32 {
|
||||
self.memory.size(&self.store) as u32
|
||||
}
|
||||
|
||||
/// Reserve `n` bytes of memory
|
||||
pub fn alloc(&mut self, n: usize) -> Result<MemoryBlock, Error> {
|
||||
debug!("Allocating {n} bytes");
|
||||
|
||||
for (i, block) in self.free.iter_mut().enumerate() {
|
||||
if block.length == n {
|
||||
let block = self.free.swap_remove(i);
|
||||
self.live_blocks.insert(block.offset, block.length);
|
||||
debug!("Found block with exact size at offset {}", block.offset);
|
||||
return Ok(block);
|
||||
} else if block.length.saturating_sub(n) >= BLOCK_SIZE_THRESHOLD {
|
||||
let handle = MemoryBlock {
|
||||
offset: block.offset,
|
||||
length: n,
|
||||
};
|
||||
debug!(
|
||||
"Using block with size {} at offset {}",
|
||||
block.length, block.offset
|
||||
);
|
||||
|
||||
block.offset += n;
|
||||
block.length -= n;
|
||||
self.live_blocks.insert(handle.offset, handle.length);
|
||||
return Ok(handle);
|
||||
}
|
||||
}
|
||||
|
||||
let new_offset = self.position.saturating_add(n);
|
||||
|
||||
// If there aren't enough bytes, try to grow the memory size
|
||||
if new_offset >= self.size() {
|
||||
debug!("Need more memory");
|
||||
|
||||
let bytes_needed = (new_offset as f64 - self.size() as f64) / PAGE_SIZE as f64;
|
||||
let mut pages_needed = bytes_needed.ceil() as u64;
|
||||
if pages_needed == 0 {
|
||||
pages_needed = 1
|
||||
}
|
||||
|
||||
debug!("Requesting {pages_needed} more pages");
|
||||
// This will fail if we've already allocated the maximum amount of memory allowed
|
||||
self.memory.grow(&mut self.store, pages_needed)?;
|
||||
}
|
||||
|
||||
let mem = MemoryBlock {
|
||||
offset: self.position,
|
||||
length: n,
|
||||
};
|
||||
|
||||
debug!(
|
||||
"Allocated new block: {} bytes at offset {}",
|
||||
mem.length, mem.offset
|
||||
);
|
||||
|
||||
self.live_blocks.insert(mem.offset, mem.length);
|
||||
self.position += n;
|
||||
Ok(mem)
|
||||
}
|
||||
|
||||
/// Allocate and copy `data` into the wasm memory
|
||||
pub fn alloc_bytes(&mut self, data: impl AsRef<[u8]>) -> Result<MemoryBlock, Error> {
|
||||
let handle = self.alloc(data.as_ref().len())?;
|
||||
self.write(handle, data)?;
|
||||
Ok(handle)
|
||||
}
|
||||
|
||||
/// Free the block allocated at `offset`
|
||||
pub fn free(&mut self, offset: usize) {
|
||||
debug!("Freeing block at {offset}");
|
||||
if let Some(length) = self.live_blocks.remove(&offset) {
|
||||
self.free.push(MemoryBlock { offset, length });
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
let free_size: usize = self.free.iter().map(|x| x.length).sum();
|
||||
|
||||
// Perform compaction if there is at least 1kb of free memory available
|
||||
if free_size >= 1024 {
|
||||
let mut last: Option<MemoryBlock> = None;
|
||||
let mut free = Vec::new();
|
||||
for block in self.free.iter() {
|
||||
match last {
|
||||
None => {
|
||||
free.push(*block);
|
||||
}
|
||||
Some(last) => {
|
||||
if last.offset + last.length == block.offset {
|
||||
free.push(MemoryBlock {
|
||||
offset: last.offset,
|
||||
length: last.length + block.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
last = Some(*block);
|
||||
}
|
||||
self.free = free;
|
||||
}
|
||||
}
|
||||
|
||||
/// Log entire memory as hexdump using the `trace` log level
|
||||
pub fn dump(&self) {
|
||||
let data = self.memory.data(&self.store);
|
||||
|
||||
trace!("{:?}", data[..self.position].hex_dump());
|
||||
}
|
||||
|
||||
/// Reset memory - clears free-list and live blocks and resets position
|
||||
pub fn reset(&mut self) {
|
||||
self.free.clear();
|
||||
self.live_blocks.clear();
|
||||
self.position = 1;
|
||||
}
|
||||
|
||||
/// Get memory as a slice of bytes
|
||||
pub fn data(&self) -> &[u8] {
|
||||
self.memory.data(&self.store)
|
||||
}
|
||||
|
||||
/// Get memory as a mutable slice of bytes
|
||||
pub fn data_mut(&mut self) -> &mut [u8] {
|
||||
self.memory.data_mut(&mut self.store)
|
||||
}
|
||||
|
||||
/// Get bytes occupied by the provided memory handle
|
||||
pub fn get(&self, handle: impl ToMemoryBlock) -> Result<&[u8], Error> {
|
||||
let handle = handle.to_memory_block(self)?;
|
||||
Ok(&self.memory.data(&self.store)[handle.offset..handle.offset + handle.length])
|
||||
}
|
||||
|
||||
/// Get mutable bytes occupied by the provided memory handle
|
||||
pub fn get_mut(&mut self, handle: impl ToMemoryBlock) -> Result<&mut [u8], Error> {
|
||||
let handle = handle.to_memory_block(self)?;
|
||||
Ok(
|
||||
&mut self.memory.data_mut(&mut self.store)
|
||||
[handle.offset..handle.offset + handle.length],
|
||||
)
|
||||
}
|
||||
|
||||
/// Get str occupied by the provided memory handle
|
||||
pub fn get_str(&self, handle: impl ToMemoryBlock) -> Result<&str, Error> {
|
||||
let handle = handle.to_memory_block(self)?;
|
||||
Ok(std::str::from_utf8(
|
||||
&self.memory.data(&self.store)[handle.offset..handle.offset + handle.length],
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Get mutable str occupied by the provided memory handle
|
||||
pub fn get_mut_str(&mut self, handle: impl ToMemoryBlock) -> Result<&mut str, Error> {
|
||||
let handle = handle.to_memory_block(self)?;
|
||||
Ok(std::str::from_utf8_mut(
|
||||
&mut self.memory.data_mut(&mut self.store)
|
||||
[handle.offset..handle.offset + handle.length],
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Pointer to the provided memory handle
|
||||
pub fn ptr(&self, handle: impl ToMemoryBlock) -> Result<*mut u8, Error> {
|
||||
let handle = handle.to_memory_block(self)?;
|
||||
Ok(unsafe { self.memory.data_ptr(&self.store).add(handle.offset) })
|
||||
}
|
||||
|
||||
/// Get the length of the block starting at `offs`
|
||||
pub fn block_length(&self, offs: usize) -> Option<usize> {
|
||||
self.live_blocks.get(&offs).cloned()
|
||||
}
|
||||
|
||||
/// Get the block at the specified offset
|
||||
pub fn at_offset(&self, offset: usize) -> Option<MemoryBlock> {
|
||||
let block_length = self.block_length(offset);
|
||||
block_length.map(|length| MemoryBlock { offset, length })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct MemoryBlock {
|
||||
pub offset: usize,
|
||||
pub length: usize,
|
||||
}
|
||||
|
||||
impl From<(usize, usize)> for MemoryBlock {
|
||||
fn from(x: (usize, usize)) -> Self {
|
||||
MemoryBlock {
|
||||
offset: x.0,
|
||||
length: x.1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoryBlock {
|
||||
pub fn new(offset: usize, length: usize) -> Self {
|
||||
MemoryBlock { offset, length }
|
||||
}
|
||||
}
|
||||
@@ -18,178 +18,6 @@ macro_rules! args {
|
||||
};
|
||||
}
|
||||
|
||||
/// Get the input length
|
||||
/// Params: none
|
||||
/// Returns: i64 (length)
|
||||
pub(crate) fn input_length(
|
||||
caller: Caller<Internal>,
|
||||
_input: &[Val],
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
let data: &Internal = caller.data();
|
||||
output[0] = Val::I64(data.input_length as i64);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load a byte from input
|
||||
/// Params: i64 (offset)
|
||||
/// Returns: i32 (byte)
|
||||
pub(crate) fn input_load_u8(
|
||||
caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
let data: &Internal = caller.data();
|
||||
if data.input.is_null() {
|
||||
return Ok(());
|
||||
}
|
||||
output[0] = unsafe { Val::I32(*data.input.add(input[0].unwrap_i64() as usize) as i32) };
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load an unsigned 64 bit integer from input
|
||||
/// Params: i64 (offset)
|
||||
/// Returns: i64 (int)
|
||||
pub(crate) fn input_load_u64(
|
||||
caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
let data: &Internal = caller.data();
|
||||
if data.input.is_null() {
|
||||
return Ok(());
|
||||
}
|
||||
let offs = args!(input, 0, i64) as usize;
|
||||
let slice = unsafe { std::slice::from_raw_parts(data.input.add(offs), 8) };
|
||||
let byte = u64::from_ne_bytes(slice.try_into().unwrap());
|
||||
output[0] = Val::I64(byte as i64);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Store a byte in memory
|
||||
/// Params: i64 (offset), i32 (byte)
|
||||
/// Returns: none
|
||||
pub(crate) fn store_u8(
|
||||
mut caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let (offset, byte) = args!(input, (0, i64), (1, i32));
|
||||
data.memory_mut().store_u8(offset as usize, byte as u8)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load a byte from memory
|
||||
/// Params: i64 (offset)
|
||||
/// Returns: i32 (byte)
|
||||
pub(crate) fn load_u8(
|
||||
caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
let data: &Internal = caller.data();
|
||||
let offset = args!(input, 0, i64) as usize;
|
||||
let byte = data.memory().load_u8(offset)?;
|
||||
output[0] = Val::I32(byte as i32);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Store an unsigned 64 bit integer in memory
|
||||
/// Params: i64 (offset), i64 (int)
|
||||
/// Returns: none
|
||||
pub(crate) fn store_u64(
|
||||
mut caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let (offset, b) = args!(input, (0, i64), (1, i64));
|
||||
data.memory_mut().store_u64(offset as usize, b as u64)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load an unsigned 64 bit integer from memory
|
||||
/// Params: i64 (offset)
|
||||
/// Returns: i64 (int)
|
||||
pub(crate) fn load_u64(
|
||||
caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
let data: &Internal = caller.data();
|
||||
let offset = args!(input, 0, i64) as usize;
|
||||
let byte = data.memory().load_u64(offset)?;
|
||||
output[0] = Val::I64(byte as i64);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set output offset and length
|
||||
/// Params: i64 (offset), i64 (length)
|
||||
/// Returns: none
|
||||
pub(crate) fn output_set(
|
||||
mut caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let (offset, length) = args!(input, (0, i64), (1, i64));
|
||||
data.output_offset = offset as usize;
|
||||
data.output_length = length as usize;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Allocate bytes
|
||||
/// Params: i64 (length)
|
||||
/// Returns: i64 (offset)
|
||||
pub(crate) fn alloc(
|
||||
mut caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let offs = data.memory_mut().alloc(input[0].unwrap_i64() as _)?;
|
||||
output[0] = Val::I64(offs.offset as i64);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Free memory
|
||||
/// Params: i64 (offset)
|
||||
/// Returns: none
|
||||
pub(crate) fn free(
|
||||
mut caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let offset = args!(input, 0, i64) as usize;
|
||||
data.memory_mut().free(offset);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the error message, this can be checked by the host program
|
||||
/// Params: i64 (offset)
|
||||
/// Returns: none
|
||||
pub(crate) fn error_set(
|
||||
mut caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let offset = args!(input, 0, i64) as usize;
|
||||
|
||||
if offset == 0 {
|
||||
data.plugin_mut().clear_error();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let plugin = data.plugin_mut();
|
||||
let s = plugin.memory.get_str(offset)?;
|
||||
plugin.set_error(s);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a configuration value
|
||||
/// Params: i64 (offset)
|
||||
/// Returns: i64 (offset)
|
||||
@@ -199,19 +27,25 @@ pub(crate) fn config_get(
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let plugin = data.plugin_mut();
|
||||
|
||||
let offset = args!(input, 0, i64) as usize;
|
||||
let key = plugin.memory.get_str(offset)?;
|
||||
let val = plugin.manifest.as_ref().config.get(key);
|
||||
let mem = match val {
|
||||
Some(f) => plugin.memory.alloc_bytes(f)?,
|
||||
let offset = args!(input, 0, i64) as u64;
|
||||
let key = data.memory_read_str(offset)?;
|
||||
let key = unsafe {
|
||||
std::str::from_utf8_unchecked(std::slice::from_raw_parts(key.as_ptr(), key.len()))
|
||||
};
|
||||
let val = data.internal().manifest.as_ref().config.get(key);
|
||||
let ptr = val.map(|x| (x.len(), x.as_ptr()));
|
||||
let mem = match ptr {
|
||||
Some((len, ptr)) => {
|
||||
let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
|
||||
data.memory_alloc_bytes(bytes)?
|
||||
}
|
||||
None => {
|
||||
output[0] = Val::I64(0);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
output[0] = Val::I64(mem.offset as i64);
|
||||
output[0] = Val::I64(mem as i64);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -224,21 +58,25 @@ pub(crate) fn var_get(
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let plugin = data.plugin_mut();
|
||||
|
||||
let offset = args!(input, 0, i64) as usize;
|
||||
let key = plugin.memory.get_str(offset)?;
|
||||
let val = plugin.vars.get(key);
|
||||
|
||||
let mem = match val {
|
||||
Some(f) => plugin.memory.alloc_bytes(f)?,
|
||||
let offset = args!(input, 0, i64) as u64;
|
||||
let key = data.memory_read_str(offset)?;
|
||||
let key = unsafe {
|
||||
std::str::from_utf8_unchecked(std::slice::from_raw_parts(key.as_ptr(), key.len()))
|
||||
};
|
||||
let val = data.internal().vars.get(key);
|
||||
let ptr = val.map(|x| (x.len(), x.as_ptr()));
|
||||
let mem = match ptr {
|
||||
Some((len, ptr)) => {
|
||||
let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
|
||||
data.memory_alloc_bytes(bytes)?
|
||||
}
|
||||
None => {
|
||||
output[0] = Val::I64(0);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
output[0] = Val::I64(mem.offset as i64);
|
||||
output[0] = Val::I64(mem as i64);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -251,33 +89,38 @@ pub(crate) fn var_set(
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let plugin = data.plugin_mut();
|
||||
|
||||
let mut size = 0;
|
||||
for v in plugin.vars.values() {
|
||||
for v in data.vars.values() {
|
||||
size += v.len();
|
||||
}
|
||||
|
||||
let voffset = args!(input, 1, i64) as usize;
|
||||
let voffset = args!(input, 1, i64) as u64;
|
||||
|
||||
// If the store is larger than 100MB then stop adding things
|
||||
if size > 1024 * 1024 * 100 && voffset != 0 {
|
||||
return Err(Error::msg("Variable store is full"));
|
||||
}
|
||||
|
||||
let key_offs = args!(input, 0, i64) as usize;
|
||||
let key = plugin.memory.get_str(key_offs)?;
|
||||
let key_offs = args!(input, 0, i64) as u64;
|
||||
let key = {
|
||||
let key = data.memory_read_str(key_offs)?;
|
||||
let key_len = key.len();
|
||||
let key_ptr = key.as_ptr();
|
||||
unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(key_ptr, key_len)) }
|
||||
};
|
||||
|
||||
// Remove if the value offset is 0
|
||||
if voffset == 0 {
|
||||
plugin.vars.remove(key);
|
||||
data.vars.remove(key);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let value = plugin.memory.get(voffset)?;
|
||||
let vlen = data.memory_length(voffset);
|
||||
let value = data.memory_read(voffset, vlen).to_vec();
|
||||
|
||||
// Insert the value from memory into the `vars` map
|
||||
plugin.vars.insert(key.to_string(), value.to_vec());
|
||||
data.vars.insert(key.to_string(), value);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -303,34 +146,38 @@ pub(crate) fn http_request(
|
||||
{
|
||||
use std::io::Read;
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let http_req_offset = args!(input, 0, i64) as usize;
|
||||
let http_req_offset = args!(input, 0, i64) as u64;
|
||||
|
||||
let http_req_len = data.memory_length(http_req_offset);
|
||||
let req: extism_manifest::HttpRequest =
|
||||
serde_json::from_slice(data.memory().get(http_req_offset)?)?;
|
||||
serde_json::from_slice(data.memory_read(http_req_offset, http_req_len))?;
|
||||
|
||||
let body_offset = args!(input, 1, i64) as usize;
|
||||
let body_offset = args!(input, 1, i64) as u64;
|
||||
|
||||
let url = match url::Url::parse(&req.url) {
|
||||
Ok(u) => u,
|
||||
Err(e) => return Err(Error::msg(format!("Invalid URL: {e:?}"))),
|
||||
};
|
||||
let allowed_hosts = &data.plugin().manifest.as_ref().allowed_hosts;
|
||||
let allowed_hosts = &data.internal().manifest.as_ref().allowed_hosts;
|
||||
let host_str = url.host_str().unwrap_or_default();
|
||||
if let Some(allowed_hosts) = allowed_hosts {
|
||||
let host_matches_allowed = allowed_hosts.iter().any(|url| {
|
||||
let host_matches = if let Some(allowed_hosts) = allowed_hosts {
|
||||
allowed_hosts.iter().any(|url| {
|
||||
let pat = match glob::Pattern::new(url) {
|
||||
Ok(x) => x,
|
||||
Err(_) => return url == host_str,
|
||||
};
|
||||
|
||||
pat.matches(host_str)
|
||||
});
|
||||
if !host_matches_allowed {
|
||||
return Err(Error::msg(format!(
|
||||
"HTTP request to {} is not allowed",
|
||||
req.url
|
||||
)));
|
||||
}
|
||||
})
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if !host_matches {
|
||||
return Err(Error::msg(format!(
|
||||
"HTTP request to {} is not allowed",
|
||||
req.url
|
||||
)));
|
||||
}
|
||||
|
||||
let mut r = ureq::request(req.method.as_deref().unwrap_or("GET"), &req.url);
|
||||
@@ -340,23 +187,40 @@ pub(crate) fn http_request(
|
||||
}
|
||||
|
||||
let res = if body_offset > 0 {
|
||||
let buf = data.memory().get(body_offset)?;
|
||||
let res = r.send_bytes(buf)?;
|
||||
data.http_status = res.status();
|
||||
res.into_reader()
|
||||
let len = data.memory_length(body_offset);
|
||||
let buf = data.memory_read(body_offset, len);
|
||||
r.send_bytes(buf)
|
||||
} else {
|
||||
let res = r.call()?;
|
||||
data.http_status = res.status();
|
||||
res.into_reader()
|
||||
r.call()
|
||||
};
|
||||
|
||||
let mut buf = Vec::new();
|
||||
res.take(1024 * 1024 * 50) // TODO: make this limit configurable
|
||||
.read_to_end(&mut buf)?;
|
||||
let reader = match res {
|
||||
Ok(res) => {
|
||||
data.http_status = res.status();
|
||||
Some(res.into_reader())
|
||||
}
|
||||
Err(e) => {
|
||||
if let Some(res) = e.into_response() {
|
||||
data.http_status = res.status();
|
||||
Some(res.into_reader())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mem = data.memory_mut().alloc_bytes(buf)?;
|
||||
if let Some(reader) = reader {
|
||||
let mut buf = Vec::new();
|
||||
reader
|
||||
.take(1024 * 1024 * 50) // TODO: make this limit configurable
|
||||
.read_to_end(&mut buf)?;
|
||||
|
||||
let mem = data.memory_alloc_bytes(buf)?;
|
||||
output[0] = Val::I64(mem as i64);
|
||||
} else {
|
||||
output[0] = Val::I64(0);
|
||||
}
|
||||
|
||||
output[0] = Val::I64(mem.offset as i64);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -374,39 +238,17 @@ pub(crate) fn http_status_code(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the length of an allocated block given the offset
|
||||
/// Params: i64 (offset)
|
||||
/// Returns: i64 (length or 0)
|
||||
pub(crate) fn length(
|
||||
mut caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let offset = args!(input, 0, i64) as usize;
|
||||
if offset == 0 {
|
||||
output[0] = Val::I64(0);
|
||||
return Ok(());
|
||||
}
|
||||
let length = match data.memory().block_length(offset) {
|
||||
Some(x) => x,
|
||||
None => return Err(Error::msg("Unable to find length for offset")),
|
||||
};
|
||||
output[0] = Val::I64(length as i64);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn log(
|
||||
level: log::Level,
|
||||
caller: Caller<Internal>,
|
||||
mut caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
let data: &Internal = caller.data();
|
||||
let offset = args!(input, 0, i64) as usize;
|
||||
let buf = data.memory().get(offset)?;
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let offset = args!(input, 0, i64) as u64;
|
||||
let buf = data.memory_read_str(offset);
|
||||
|
||||
match std::str::from_utf8(buf) {
|
||||
match buf {
|
||||
Ok(buf) => log::log!(level, "{}", buf),
|
||||
Err(_) => log::log!(level, "{:?}", buf),
|
||||
}
|
||||
|
||||
@@ -4,97 +4,121 @@ use crate::*;
|
||||
|
||||
/// Plugin contains everything needed to execute a WASM function
|
||||
pub struct Plugin {
|
||||
pub module: Module,
|
||||
/// All modules that were provided to the linker
|
||||
pub modules: BTreeMap<String, Module>,
|
||||
|
||||
/// Used to define functions and create new instances
|
||||
pub linker: Linker<Internal>,
|
||||
pub instance: Instance,
|
||||
pub last_error: std::cell::RefCell<Option<std::ffi::CString>>,
|
||||
pub memory: PluginMemory,
|
||||
pub manifest: Manifest,
|
||||
pub vars: BTreeMap<String, Vec<u8>>,
|
||||
pub should_reinstantiate: bool,
|
||||
pub store: Store<Internal>,
|
||||
|
||||
/// Instance provides the ability to call functions in a module
|
||||
pub instance: Option<Instance>,
|
||||
pub instance_pre: InstancePre<Internal>,
|
||||
|
||||
/// Keep track of the number of times we're instantiated, this exists
|
||||
/// to avoid issues with memory piling up since `Instance`s are only
|
||||
/// actually cleaned up along with a `Store`
|
||||
instantiations: usize,
|
||||
|
||||
/// The ID used to identify this plugin with the `Timer`
|
||||
pub timer_id: uuid::Uuid,
|
||||
|
||||
/// A handle used to cancel execution of a plugin
|
||||
pub(crate) cancel_handle: sdk::ExtismCancelHandle,
|
||||
|
||||
/// Runtime determines any initialization functions needed
|
||||
/// to run a module
|
||||
pub(crate) runtime: Option<Runtime>,
|
||||
}
|
||||
|
||||
pub struct Internal {
|
||||
pub input_length: usize,
|
||||
pub input: *const u8,
|
||||
pub output_offset: usize,
|
||||
pub output_length: usize,
|
||||
pub plugin: *mut Plugin,
|
||||
pub wasi: Option<Wasi>,
|
||||
pub http_status: u16,
|
||||
}
|
||||
|
||||
pub struct Wasi {
|
||||
pub ctx: wasmtime_wasi::WasiCtx,
|
||||
#[cfg(feature = "nn")]
|
||||
pub nn: wasmtime_wasi_nn::WasiNnCtx,
|
||||
#[cfg(not(feature = "nn"))]
|
||||
pub nn: (),
|
||||
}
|
||||
|
||||
impl Internal {
|
||||
fn new(manifest: &Manifest, wasi: bool) -> Result<Self, Error> {
|
||||
let wasi = if wasi {
|
||||
let auth = wasmtime_wasi::ambient_authority();
|
||||
let mut ctx = wasmtime_wasi::WasiCtxBuilder::new();
|
||||
for (k, v) in manifest.as_ref().config.iter() {
|
||||
ctx = ctx.env(k, v)?;
|
||||
}
|
||||
|
||||
if let Some(a) = &manifest.as_ref().allowed_paths {
|
||||
for (k, v) in a.iter() {
|
||||
let d = wasmtime_wasi::Dir::open_ambient_dir(k, auth)?;
|
||||
ctx = ctx.preopened_dir(d, v)?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "nn")]
|
||||
let nn = wasmtime_wasi_nn::WasiNnCtx::new()?;
|
||||
|
||||
#[cfg(not(feature = "nn"))]
|
||||
#[allow(clippy::let_unit_value)]
|
||||
let nn = ();
|
||||
|
||||
Some(Wasi {
|
||||
ctx: ctx.build(),
|
||||
nn,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Internal {
|
||||
input_length: 0,
|
||||
output_offset: 0,
|
||||
output_length: 0,
|
||||
input: std::ptr::null(),
|
||||
wasi,
|
||||
plugin: std::ptr::null_mut(),
|
||||
http_status: 0,
|
||||
})
|
||||
impl InternalExt for Plugin {
|
||||
fn store(&self) -> &Store<Internal> {
|
||||
&self.store
|
||||
}
|
||||
|
||||
pub fn plugin(&self) -> &Plugin {
|
||||
unsafe { &*self.plugin }
|
||||
fn store_mut(&mut self) -> &mut Store<Internal> {
|
||||
&mut self.store
|
||||
}
|
||||
|
||||
pub fn plugin_mut(&mut self) -> &mut Plugin {
|
||||
unsafe { &mut *self.plugin }
|
||||
fn linker(&self) -> &Linker<Internal> {
|
||||
&self.linker
|
||||
}
|
||||
|
||||
pub fn memory(&self) -> &PluginMemory {
|
||||
&self.plugin().memory
|
||||
fn linker_mut(&mut self) -> &mut Linker<Internal> {
|
||||
&mut self.linker
|
||||
}
|
||||
|
||||
pub fn memory_mut(&mut self) -> &mut PluginMemory {
|
||||
&mut self.plugin_mut().memory
|
||||
fn linker_and_store(&mut self) -> (&mut Linker<Internal>, &mut Store<Internal>) {
|
||||
(&mut self.linker, &mut self.store)
|
||||
}
|
||||
}
|
||||
|
||||
const EXPORT_MODULE_NAME: &str = "env";
|
||||
|
||||
fn profiling_strategy() -> ProfilingStrategy {
|
||||
match std::env::var("EXTISM_PROFILE").as_deref() {
|
||||
Ok("perf") => ProfilingStrategy::PerfMap,
|
||||
Ok(x) => {
|
||||
log::warn!("Invalid value for EXTISM_PROFILE: {x}");
|
||||
ProfilingStrategy::None
|
||||
}
|
||||
Err(_) => ProfilingStrategy::None,
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_available_memory(
|
||||
available_pages: &mut Option<u32>,
|
||||
modules: &BTreeMap<String, Module>,
|
||||
) -> anyhow::Result<()> {
|
||||
let available_pages = match available_pages {
|
||||
Some(p) => p,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let max_pages = *available_pages;
|
||||
let mut fail_memory_check = false;
|
||||
let mut total_memory_needed = 0;
|
||||
for (name, module) in modules.iter() {
|
||||
if name == "env" {
|
||||
continue;
|
||||
}
|
||||
let mut memories = 0;
|
||||
for export in module.exports() {
|
||||
if let Some(memory) = export.ty().memory() {
|
||||
memories += 1;
|
||||
let memory_max = memory.maximum();
|
||||
match memory_max {
|
||||
None => anyhow::bail!("Unbounded memory in module {name}, when `memory.max_pages` is set in the manifest all modules \
|
||||
must have a maximum bound set on an exported memory"),
|
||||
Some(m) => {
|
||||
total_memory_needed += m;
|
||||
if !fail_memory_check {
|
||||
continue;
|
||||
}
|
||||
|
||||
*available_pages = available_pages.saturating_sub(m as u32);
|
||||
if *available_pages == 0 {
|
||||
fail_memory_check = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if memories == 0 {
|
||||
anyhow::bail!("No memory exported from module {name}, when `memory.max_pages` is set in the manifest all modules must \
|
||||
have a maximum bound set on an exported memory");
|
||||
}
|
||||
}
|
||||
|
||||
if fail_memory_check {
|
||||
anyhow::bail!("Not enough memory configured to run the provided plugin, `memory.max_pages` is set to {max_pages} in the manifest \
|
||||
but {total_memory_needed} pages are needed by the plugin");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Plugin {
|
||||
/// Create a new plugin from the given WASM code
|
||||
pub fn new<'a>(
|
||||
@@ -102,26 +126,37 @@ impl Plugin {
|
||||
imports: impl IntoIterator<Item = &'a Function>,
|
||||
with_wasi: bool,
|
||||
) -> Result<Plugin, Error> {
|
||||
// Create a new engine, if the `EXITSM_DEBUG` environment variable is set
|
||||
// then we enable debug info
|
||||
let engine = Engine::new(
|
||||
Config::new()
|
||||
.epoch_interruption(true)
|
||||
.debug_info(std::env::var("EXTISM_DEBUG").is_ok()),
|
||||
.debug_info(std::env::var("EXTISM_DEBUG").is_ok())
|
||||
.profiler(profiling_strategy()),
|
||||
)?;
|
||||
let mut imports = imports.into_iter();
|
||||
let (manifest, modules) = Manifest::new(&engine, wasm.as_ref())?;
|
||||
let mut store = Store::new(&engine, Internal::new(&manifest, with_wasi)?);
|
||||
|
||||
store.epoch_deadline_callback(|_internal| Err(Error::msg("timeout")));
|
||||
// Calculate how much memory is available based on the value of `max_pages` and the exported
|
||||
// memory of the modules. An error will be returned if a module doesn't have an exported memory
|
||||
// or there is no maximum set for a module's exported memory.
|
||||
let mut available_pages = manifest.as_ref().memory.max_pages;
|
||||
calculate_available_memory(&mut available_pages, &modules)?;
|
||||
log::trace!("Available pages: {available_pages:?}");
|
||||
|
||||
let memory = Memory::new(
|
||||
&mut store,
|
||||
MemoryType::new(4, manifest.as_ref().memory.max_pages),
|
||||
)?;
|
||||
let mut memory = PluginMemory::new(store, memory);
|
||||
let mut store = Store::new(
|
||||
&engine,
|
||||
Internal::new(manifest, with_wasi, available_pages)?,
|
||||
);
|
||||
store.epoch_deadline_callback(|_internal| Ok(wasmtime::UpdateDeadline::Continue(1)));
|
||||
|
||||
if available_pages.is_some() {
|
||||
store.limiter(|internal| internal.memory_limiter.as_mut().unwrap());
|
||||
}
|
||||
let mut linker = Linker::new(&engine);
|
||||
linker.allow_shadowing(true);
|
||||
|
||||
// If wasi is enabled then add it to the linker
|
||||
if with_wasi {
|
||||
wasmtime_wasi::add_to_linker(&mut linker, |x: &mut Internal| {
|
||||
&mut x.wasi.as_mut().unwrap().ctx
|
||||
@@ -138,14 +173,14 @@ impl Plugin {
|
||||
(entry.0.as_str(), entry.1)
|
||||
});
|
||||
|
||||
// Define PDK functions
|
||||
macro_rules! define_funcs {
|
||||
($m:expr, { $($name:ident($($args:expr),*) $(-> $($r:expr),*)?);* $(;)?}) => {
|
||||
match $m {
|
||||
$(
|
||||
concat!("extism_", stringify!($name)) => {
|
||||
let t = FuncType::new([$($args),*], [$($($r),*)?]);
|
||||
let f = Func::new(&mut memory.store, t, pdk::$name);
|
||||
linker.define(&mut memory.store, EXPORT_MODULE_NAME, concat!("extism_", stringify!($name)), Extern::Func(f))?;
|
||||
linker.func_new(EXPORT_MODULE_NAME, concat!("extism_", stringify!($name)), t, pdk::$name)?;
|
||||
continue
|
||||
}
|
||||
)*
|
||||
@@ -155,7 +190,10 @@ impl Plugin {
|
||||
}
|
||||
|
||||
// Add builtins
|
||||
for (_name, module) in modules.iter() {
|
||||
for (name, module) in modules.iter() {
|
||||
if name != main_name {
|
||||
linker.module(&mut store, name, module)?;
|
||||
}
|
||||
for import in module.imports() {
|
||||
let module_name = import.module();
|
||||
let name = import.name();
|
||||
@@ -163,249 +201,291 @@ impl Plugin {
|
||||
|
||||
if module_name == EXPORT_MODULE_NAME {
|
||||
define_funcs!(name, {
|
||||
alloc(I64) -> I64;
|
||||
free(I64);
|
||||
load_u8(I64) -> I32;
|
||||
load_u64(I64) -> I64;
|
||||
store_u8(I64, I32);
|
||||
store_u64(I64, I64);
|
||||
input_length() -> I64;
|
||||
input_load_u8(I64) -> I32;
|
||||
input_load_u64(I64) -> I64;
|
||||
output_set(I64, I64);
|
||||
error_set(I64);
|
||||
config_get(I64) -> I64;
|
||||
var_get(I64) -> I64;
|
||||
var_set(I64, I64);
|
||||
http_request(I64, I64) -> I64;
|
||||
http_status_code() -> I32;
|
||||
length(I64) -> I64;
|
||||
log_warn(I64);
|
||||
log_info(I64);
|
||||
log_debug(I64);
|
||||
log_error(I64);
|
||||
});
|
||||
|
||||
for f in &mut imports {
|
||||
let name = f.name().to_string();
|
||||
let ns = f.namespace().unwrap_or(EXPORT_MODULE_NAME);
|
||||
let func = Func::new(&mut memory.store, f.ty().clone(), unsafe {
|
||||
&*std::sync::Arc::as_ptr(&f.f)
|
||||
});
|
||||
linker.define(&mut memory.store, ns, &name, func)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add modules to linker
|
||||
for (name, module) in modules.iter() {
|
||||
if name != main_name {
|
||||
linker.module(&mut memory.store, name, module)?;
|
||||
linker.alias_module(name, "env")?;
|
||||
}
|
||||
for f in &mut imports {
|
||||
let name = f.name().to_string();
|
||||
let ns = f.namespace().unwrap_or(EXPORT_MODULE_NAME);
|
||||
linker.func_new(ns, &name, f.ty().clone(), unsafe {
|
||||
&*std::sync::Arc::as_ptr(&f.f)
|
||||
})?;
|
||||
}
|
||||
|
||||
let instance = linker.instantiate(&mut memory.store, main)?;
|
||||
let instance_pre = linker.instantiate_pre(&main)?;
|
||||
let timer_id = uuid::Uuid::new_v4();
|
||||
let mut plugin = Plugin {
|
||||
module: main.clone(),
|
||||
modules,
|
||||
linker,
|
||||
memory,
|
||||
instance,
|
||||
last_error: std::cell::RefCell::new(None),
|
||||
manifest,
|
||||
vars: BTreeMap::new(),
|
||||
should_reinstantiate: false,
|
||||
instance: None,
|
||||
instance_pre,
|
||||
store,
|
||||
runtime: None,
|
||||
timer_id,
|
||||
cancel_handle: sdk::ExtismCancelHandle {
|
||||
id: timer_id,
|
||||
epoch_timer_tx: None,
|
||||
},
|
||||
instantiations: 0,
|
||||
};
|
||||
|
||||
plugin.initialize_runtime()?;
|
||||
|
||||
plugin.internal_mut().store = &mut plugin.store;
|
||||
plugin.internal_mut().linker = &mut plugin.linker;
|
||||
Ok(plugin)
|
||||
}
|
||||
|
||||
/// Get a function by name
|
||||
pub fn get_func(&mut self, function: impl AsRef<str>) -> Option<Func> {
|
||||
self.instance
|
||||
.get_func(&mut self.memory.store, function.as_ref())
|
||||
}
|
||||
pub(crate) fn reset_store(&mut self) -> Result<(), Error> {
|
||||
self.instance = None;
|
||||
if self.instantiations > 5 {
|
||||
let (main_name, main) = self
|
||||
.modules
|
||||
.get("main")
|
||||
.map(|x| ("main", x))
|
||||
.unwrap_or_else(|| {
|
||||
let entry = self.modules.iter().last().unwrap();
|
||||
(entry.0.as_str(), entry.1)
|
||||
});
|
||||
|
||||
/// Set `last_error` field
|
||||
pub fn set_error(&self, e: impl std::fmt::Debug) {
|
||||
debug!("Set error: {:?}", e);
|
||||
*self.last_error.borrow_mut() = Some(error_string(e));
|
||||
}
|
||||
let engine = self.store.engine().clone();
|
||||
let internal = self.internal();
|
||||
self.store = Store::new(
|
||||
&engine,
|
||||
Internal::new(
|
||||
internal.manifest.clone(),
|
||||
internal.wasi.is_some(),
|
||||
internal.available_pages,
|
||||
)?,
|
||||
);
|
||||
self.store
|
||||
.epoch_deadline_callback(|_internal| Ok(UpdateDeadline::Continue(1)));
|
||||
|
||||
pub fn error<E>(&self, e: impl std::fmt::Debug, x: E) -> E {
|
||||
self.set_error(e);
|
||||
x
|
||||
}
|
||||
if self.internal().available_pages.is_some() {
|
||||
self.store
|
||||
.limiter(|internal| internal.memory_limiter.as_mut().unwrap());
|
||||
}
|
||||
|
||||
/// Unset `last_error` field
|
||||
pub fn clear_error(&self) {
|
||||
*self.last_error.borrow_mut() = None;
|
||||
}
|
||||
for (name, module) in self.modules.iter() {
|
||||
if name != main_name {
|
||||
self.linker.module(&mut self.store, name, module)?;
|
||||
}
|
||||
}
|
||||
self.instantiations = 0;
|
||||
self.instance_pre = self.linker.instantiate_pre(&main)?;
|
||||
|
||||
/// Store input in memory and initialize `Internal` pointer
|
||||
pub fn set_input(&mut self, input: *const u8, mut len: usize) {
|
||||
if input.is_null() {
|
||||
len = 0;
|
||||
let store = &mut self.store as *mut _;
|
||||
let linker = &mut self.linker as *mut _;
|
||||
let internal = self.internal_mut();
|
||||
internal.store = store;
|
||||
internal.linker = linker;
|
||||
}
|
||||
let ptr = self as *mut _;
|
||||
let internal = self.memory.store.data_mut();
|
||||
internal.input = input;
|
||||
internal.input_length = len;
|
||||
internal.plugin = ptr;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn dump_memory(&self) {
|
||||
self.memory.dump();
|
||||
}
|
||||
|
||||
pub fn reinstantiate(&mut self) -> Result<(), Error> {
|
||||
let instance = self
|
||||
.linker
|
||||
.instantiate(&mut self.memory.store, &self.module)?;
|
||||
self.instance = instance;
|
||||
pub(crate) fn instantiate(&mut self) -> Result<(), Error> {
|
||||
self.instance = Some(self.instance_pre.instantiate(&mut self.store)?);
|
||||
self.instantiations += 1;
|
||||
if let Some(limiter) = &mut self.internal_mut().memory_limiter {
|
||||
limiter.reset();
|
||||
}
|
||||
self.detect_runtime();
|
||||
self.initialize_runtime()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn has_wasi(&self) -> bool {
|
||||
self.memory.store.data().wasi.is_some()
|
||||
}
|
||||
|
||||
fn detect_runtime(&mut self) -> Option<Runtime> {
|
||||
// Check for Haskell runtime initialization functions
|
||||
// Initialize Haskell runtime if `hs_init` and `hs_exit` are present,
|
||||
// by calling the `hs_init` export
|
||||
if let Some(init) = self.get_func("hs_init") {
|
||||
if let Some(cleanup) = self.get_func("hs_exit") {
|
||||
if init.typed::<(i32, i32), ()>(&self.memory.store).is_err() {
|
||||
trace!(
|
||||
"hs_init function found with type {:?}",
|
||||
init.ty(&self.memory.store)
|
||||
);
|
||||
return None;
|
||||
}
|
||||
return Some(Runtime::Haskell { init, cleanup });
|
||||
/// Get a function by name
|
||||
pub fn get_func(&mut self, function: impl AsRef<str>) -> Option<Func> {
|
||||
if let None = &self.instance {
|
||||
if let Err(e) = self.instantiate() {
|
||||
error!("Unable to instantiate: {e}");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
trace!("No runtime detected");
|
||||
None
|
||||
if let Some(instance) = &mut self.instance {
|
||||
instance.get_func(&mut self.store, function.as_ref())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize_runtime(&mut self) -> Result<(), Error> {
|
||||
if let Some(runtime) = self.detect_runtime() {
|
||||
if let Some(timer) = Context::timer().as_ref() {
|
||||
self.memory.store.set_epoch_deadline(1);
|
||||
self.start_timer(&timer.tx)?;
|
||||
let x = runtime.init(self);
|
||||
self.stop_timer()?;
|
||||
self.memory.store.set_epoch_deadline(0);
|
||||
return x;
|
||||
/// Store input in memory and initialize `Internal` pointer
|
||||
pub(crate) fn set_input(&mut self, input: *const u8, mut len: usize) -> Result<(), Error> {
|
||||
if input.is_null() {
|
||||
len = 0;
|
||||
}
|
||||
|
||||
{
|
||||
let store = &mut self.store as *mut _;
|
||||
let linker = &mut self.linker as *mut _;
|
||||
let internal = self.internal_mut();
|
||||
internal.store = store;
|
||||
internal.linker = linker;
|
||||
}
|
||||
|
||||
let bytes = unsafe { std::slice::from_raw_parts(input, len) };
|
||||
trace!("Input size: {}", bytes.len());
|
||||
|
||||
if let Some(f) = self.linker.get(&mut self.store, "env", "extism_reset") {
|
||||
f.into_func().unwrap().call(&mut self.store, &[], &mut [])?;
|
||||
} else {
|
||||
error!("Call to extism_reset failed");
|
||||
}
|
||||
|
||||
let offs = self.memory_alloc_bytes(bytes)?;
|
||||
|
||||
if let Some(f) = self.linker.get(&mut self.store, "env", "extism_input_set") {
|
||||
f.into_func().unwrap().call(
|
||||
&mut self.store,
|
||||
&[Val::I64(offs as i64), Val::I64(len as i64)],
|
||||
&mut [],
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Determine if wasi is enabled
|
||||
pub fn has_wasi(&self) -> bool {
|
||||
self.internal().wasi.is_some()
|
||||
}
|
||||
|
||||
fn detect_runtime(&mut self) {
|
||||
// Check for Haskell runtime initialization functions
|
||||
// Initialize Haskell runtime if `hs_init` is present,
|
||||
// by calling the `hs_init` export
|
||||
if let Some(init) = self.get_func("hs_init") {
|
||||
let reactor_init = if let Some(init) = self.get_func("_initialize") {
|
||||
if init.typed::<(), ()>(&self.store()).is_err() {
|
||||
trace!(
|
||||
"_initialize function found with type {:?}",
|
||||
init.ty(self.store())
|
||||
);
|
||||
None
|
||||
} else {
|
||||
trace!("WASI reactor module detected");
|
||||
Some(init)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.runtime = Some(Runtime::Haskell { init, reactor_init });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for `__wasm_call_ctors` or `_initialize`, this is used by WASI to
|
||||
// initialize certain interfaces.
|
||||
let init = if let Some(init) = self.get_func("__wasm_call_ctors") {
|
||||
if init.typed::<(), ()>(&self.store()).is_err() {
|
||||
trace!(
|
||||
"__wasm_call_ctors function found with type {:?}",
|
||||
init.ty(self.store())
|
||||
);
|
||||
return;
|
||||
}
|
||||
trace!("WASI runtime detected");
|
||||
init
|
||||
} else if let Some(init) = self.get_func("_initialize") {
|
||||
if init.typed::<(), ()>(&self.store()).is_err() {
|
||||
trace!(
|
||||
"_initialize function found with type {:?}",
|
||||
init.ty(self.store())
|
||||
);
|
||||
return;
|
||||
}
|
||||
trace!("Reactor module detected");
|
||||
init
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.runtime = Some(Runtime::Wasi { init });
|
||||
|
||||
trace!("No runtime detected");
|
||||
}
|
||||
|
||||
pub(crate) fn initialize_runtime(&mut self) -> Result<(), Error> {
|
||||
let mut store = &mut self.store;
|
||||
if let Some(runtime) = &self.runtime {
|
||||
trace!("Plugin::initialize_runtime");
|
||||
match runtime {
|
||||
Runtime::Haskell { init, reactor_init } => {
|
||||
if let Some(reactor_init) = reactor_init {
|
||||
reactor_init.call(&mut store, &[], &mut [])?;
|
||||
}
|
||||
let mut results = vec![Val::null(); init.ty(&store).results().len()];
|
||||
init.call(
|
||||
&mut store,
|
||||
&[Val::I32(0), Val::I32(0)],
|
||||
results.as_mut_slice(),
|
||||
)?;
|
||||
debug!("Initialized Haskell language runtime");
|
||||
}
|
||||
Runtime::Wasi { init } => {
|
||||
init.call(&mut store, &[], &mut [])?;
|
||||
debug!("Initialied WASI runtime");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start the timer for a Plugin - this is used for both timeouts
|
||||
/// and cancellation
|
||||
pub(crate) fn start_timer(
|
||||
&mut self,
|
||||
tx: &std::sync::mpsc::SyncSender<TimerAction>,
|
||||
) -> Result<(), Error> {
|
||||
let duration = self
|
||||
.internal()
|
||||
.manifest
|
||||
.as_ref()
|
||||
.timeout_ms
|
||||
.map(std::time::Duration::from_millis);
|
||||
self.cancel_handle.epoch_timer_tx = Some(tx.clone());
|
||||
self.memory.store.set_epoch_deadline(1);
|
||||
let engine: Engine = self.memory.store.engine().clone();
|
||||
self.store_mut().set_epoch_deadline(1);
|
||||
self.store
|
||||
.epoch_deadline_callback(|_internal| Err(Error::msg("timeout")));
|
||||
let engine: Engine = self.store().engine().clone();
|
||||
tx.send(TimerAction::Start {
|
||||
id: self.timer_id,
|
||||
duration,
|
||||
engine,
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send TimerAction::Stop
|
||||
pub(crate) fn stop_timer(&mut self) -> Result<(), Error> {
|
||||
if let Some(tx) = &self.cancel_handle.epoch_timer_tx {
|
||||
tx.send(TimerAction::Stop { id: self.timer_id })?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cancel(&self) -> Result<(), Error> {
|
||||
if let Some(tx) = &self.cancel_handle.epoch_timer_tx {
|
||||
tx.send(TimerAction::Cancel { id: self.timer_id })?;
|
||||
}
|
||||
|
||||
self.store
|
||||
.epoch_deadline_callback(|_internal| Ok(wasmtime::UpdateDeadline::Continue(1)));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Enumerates the supported PDK language runtimes
|
||||
enum Runtime {
|
||||
Haskell { init: Func, cleanup: Func },
|
||||
}
|
||||
|
||||
impl Runtime {
|
||||
fn init(&self, plugin: &mut Plugin) -> Result<(), Error> {
|
||||
match self {
|
||||
Runtime::Haskell { init, cleanup: _ } => {
|
||||
let mut results = vec![Val::null(); init.ty(&plugin.memory.store).results().len()];
|
||||
init.call(
|
||||
&mut plugin.memory.store,
|
||||
&[Val::I32(0), Val::I32(0)],
|
||||
results.as_mut_slice(),
|
||||
)?;
|
||||
debug!("Initialized Haskell language runtime");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cleanup(&self, plugin: &mut Plugin) -> Result<(), Error> {
|
||||
match self {
|
||||
// Cleanup Haskell runtime if `hs_exit` and `hs_exit` are present,
|
||||
// by calling the `hs_exit` export
|
||||
Runtime::Haskell { init: _, cleanup } => {
|
||||
let mut results =
|
||||
vec![Val::null(); cleanup.ty(&plugin.memory.store).results().len()];
|
||||
cleanup.call(&mut plugin.memory.store, &[], results.as_mut_slice())?;
|
||||
debug!("Cleaned up Haskell language runtime");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Plugin {
|
||||
fn drop(&mut self) {
|
||||
if let Some(runtime) = self.detect_runtime() {
|
||||
self.memory.store.set_epoch_deadline(1);
|
||||
if let Some(timer) = Context::timer().as_ref() {
|
||||
if self.start_timer(&timer.tx).is_ok() {
|
||||
if let Err(e) = runtime.cleanup(self) {
|
||||
error!("Unable to cleanup runtime: {e:?}");
|
||||
}
|
||||
|
||||
if let Err(e) = self.stop_timer() {
|
||||
error!("Unable to stop timer in Plugin::drop: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum Runtime {
|
||||
Haskell {
|
||||
init: Func,
|
||||
reactor_init: Option<Func>,
|
||||
},
|
||||
Wasi {
|
||||
init: Func,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::*;
|
||||
// PluginRef is used to access a plugin from a context-scoped plugin registry
|
||||
pub struct PluginRef<'a> {
|
||||
pub id: PluginIndex,
|
||||
running: bool,
|
||||
pub(crate) epoch_timer_tx: std::sync::mpsc::SyncSender<TimerAction>,
|
||||
plugin: *mut Plugin,
|
||||
_t: std::marker::PhantomData<&'a ()>,
|
||||
@@ -10,14 +11,25 @@ pub struct PluginRef<'a> {
|
||||
|
||||
impl<'a> PluginRef<'a> {
|
||||
/// Initialize the plugin for a new call
|
||||
///
|
||||
/// - Resets memory offsets
|
||||
/// - Updates `input` pointer
|
||||
pub fn init(mut self, data: *const u8, data_len: usize) -> Self {
|
||||
trace!("PluginRef::init: {}", self.id,);
|
||||
self.as_mut().memory.reset();
|
||||
self.as_mut().set_input(data, data_len);
|
||||
pub(crate) fn start_call(mut self, is_start: bool) -> Self {
|
||||
trace!("PluginRef::start_call: {}", self.id,);
|
||||
|
||||
let plugin = unsafe { &mut *self.plugin };
|
||||
if is_start {
|
||||
if let Err(e) = plugin.reset_store() {
|
||||
error!("Call to Plugin::reset_store failed: {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
if plugin.instance.is_none() {
|
||||
trace!("Plugin::instance is none, instantiating");
|
||||
if let Err(e) = plugin.instantiate() {
|
||||
error!("Plugin::instantiate failed: {e:?}");
|
||||
plugin.error(e, ());
|
||||
}
|
||||
}
|
||||
|
||||
self.running = true;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -29,36 +41,20 @@ impl<'a> PluginRef<'a> {
|
||||
|
||||
let epoch_timer_tx = ctx.epoch_timer_tx.clone();
|
||||
|
||||
if !ctx.plugin_exists(plugin_id) {
|
||||
let plugin = if let Some(plugin) = ctx.plugin(plugin_id) {
|
||||
plugin
|
||||
} else {
|
||||
error!("Plugin does not exist: {plugin_id}");
|
||||
return ctx.error(format!("Plugin does not exist: {plugin_id}"), None);
|
||||
}
|
||||
};
|
||||
|
||||
let plugin = unsafe { &mut *plugin };
|
||||
|
||||
if clear_error {
|
||||
trace!("Clearing context error");
|
||||
ctx.error = None;
|
||||
}
|
||||
|
||||
// `unwrap` is okay here because we already checked with `ctx.plugin_exists` above
|
||||
let plugin = ctx.plugin(plugin_id).unwrap();
|
||||
|
||||
{
|
||||
let plugin = unsafe { &mut *plugin };
|
||||
if clear_error {
|
||||
trace!("Clearing plugin error: {plugin_id}");
|
||||
plugin.clear_error();
|
||||
}
|
||||
|
||||
// Reinstantiate plugin after calling _start because according to the WASI
|
||||
// applicate ABI _start should be called "at most once":
|
||||
// https://github.com/WebAssembly/WASI/blob/main/legacy/application-abi.md
|
||||
if plugin.should_reinstantiate {
|
||||
plugin.should_reinstantiate = false;
|
||||
if let Err(e) = plugin.reinstantiate() {
|
||||
error!("Failed to reinstantiate: {e:?}");
|
||||
return plugin.error(format!("Failed to reinstantiate: {e:?}"), None);
|
||||
}
|
||||
}
|
||||
trace!("Clearing plugin error: {plugin_id}");
|
||||
plugin.clear_error();
|
||||
}
|
||||
|
||||
Some(PluginRef {
|
||||
@@ -66,6 +62,7 @@ impl<'a> PluginRef<'a> {
|
||||
plugin,
|
||||
epoch_timer_tx,
|
||||
_t: std::marker::PhantomData,
|
||||
running: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -85,6 +82,14 @@ impl<'a> AsMut<Plugin> for PluginRef<'a> {
|
||||
impl<'a> Drop for PluginRef<'a> {
|
||||
fn drop(&mut self) {
|
||||
trace!("Dropping PluginRef {}", self.id);
|
||||
// Cleanup?
|
||||
if self.running {
|
||||
let plugin = self.as_mut();
|
||||
|
||||
// Stop timer
|
||||
if let Err(e) = plugin.stop_timer() {
|
||||
let id = plugin.timer_id;
|
||||
error!("Failed to stop timeout manager for {id}: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ impl From<Function> for ExtismFunction {
|
||||
|
||||
/// Host function signature
|
||||
pub type ExtismFunctionType = extern "C" fn(
|
||||
plugin: *mut Plugin,
|
||||
plugin: *mut Internal,
|
||||
inputs: *const ExtismVal,
|
||||
n_inputs: Size,
|
||||
outputs: *mut ExtismVal,
|
||||
@@ -93,60 +93,52 @@ pub unsafe extern "C" fn extism_context_free(ctx: *mut Context) {
|
||||
/// Returns a pointer to the memory of the currently running plugin
|
||||
/// NOTE: this should only be called from host functions.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_current_plugin_memory(plugin: *mut Plugin) -> *mut u8 {
|
||||
pub unsafe extern "C" fn extism_current_plugin_memory(plugin: *mut Internal) -> *mut u8 {
|
||||
if plugin.is_null() {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
|
||||
let plugin = &mut *plugin;
|
||||
plugin.memory.data_mut().as_mut_ptr()
|
||||
plugin.memory_ptr()
|
||||
}
|
||||
|
||||
/// Allocate a memory block in the currently running plugin
|
||||
/// NOTE: this should only be called from host functions.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_current_plugin_memory_alloc(plugin: *mut Plugin, n: Size) -> u64 {
|
||||
pub unsafe extern "C" fn extism_current_plugin_memory_alloc(plugin: *mut Internal, n: Size) -> u64 {
|
||||
if plugin.is_null() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let plugin = &mut *plugin;
|
||||
|
||||
let mem = match plugin.memory.alloc(n as usize) {
|
||||
Ok(x) => x,
|
||||
Err(e) => return plugin.error(e, 0),
|
||||
};
|
||||
|
||||
mem.offset as u64
|
||||
plugin.memory_alloc(n as u64).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get the length of an allocated block
|
||||
/// NOTE: this should only be called from host functions.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_current_plugin_memory_length(plugin: *mut Plugin, n: Size) -> Size {
|
||||
pub unsafe extern "C" fn extism_current_plugin_memory_length(
|
||||
plugin: *mut Internal,
|
||||
n: Size,
|
||||
) -> Size {
|
||||
if plugin.is_null() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let plugin = &mut *plugin;
|
||||
|
||||
match plugin.memory.block_length(n as usize) {
|
||||
Some(x) => x as Size,
|
||||
None => 0,
|
||||
}
|
||||
plugin.memory_length(n)
|
||||
}
|
||||
|
||||
/// Free an allocated memory block
|
||||
/// NOTE: this should only be called from host functions.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_current_plugin_memory_free(plugin: *mut Plugin, ptr: u64) {
|
||||
pub unsafe extern "C" fn extism_current_plugin_memory_free(plugin: *mut Internal, ptr: u64) {
|
||||
if plugin.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
let plugin = &mut *plugin;
|
||||
|
||||
plugin.memory.free(ptr as usize);
|
||||
plugin.memory_free(ptr);
|
||||
}
|
||||
|
||||
/// Create a new host function
|
||||
@@ -339,8 +331,7 @@ pub unsafe extern "C" fn extism_plugin_update(
|
||||
return false;
|
||||
}
|
||||
|
||||
ctx.plugins
|
||||
.insert(index, std::cell::UnsafeCell::new(plugin));
|
||||
ctx.plugins.insert(index, plugin);
|
||||
|
||||
debug!("Plugin updated: {index}");
|
||||
true
|
||||
@@ -412,44 +403,49 @@ pub unsafe extern "C" fn extism_plugin_config(
|
||||
json_size: Size,
|
||||
) -> bool {
|
||||
let ctx = &mut *ctx;
|
||||
let mut plugin = match PluginRef::new(ctx, plugin, true) {
|
||||
let mut plugin_ref = match PluginRef::new(ctx, plugin, true) {
|
||||
None => return false,
|
||||
Some(p) => p,
|
||||
};
|
||||
|
||||
trace!(
|
||||
"Call to extism_plugin_config for {} with json pointer {:?}",
|
||||
plugin.id,
|
||||
plugin_ref.id,
|
||||
json
|
||||
);
|
||||
let plugin = plugin_ref.as_mut();
|
||||
|
||||
let data = std::slice::from_raw_parts(json, json_size as usize);
|
||||
let json: std::collections::BTreeMap<String, Option<String>> =
|
||||
match serde_json::from_slice(data) {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
return plugin.as_mut().error(e, false);
|
||||
return plugin.error(e, false);
|
||||
}
|
||||
};
|
||||
|
||||
let plugin = plugin.as_mut();
|
||||
let wasi = &mut plugin.internal_mut().wasi;
|
||||
if let Some(Wasi { ctx, .. }) = wasi {
|
||||
for (k, v) in json.iter() {
|
||||
match v {
|
||||
Some(v) => {
|
||||
let _ = ctx.push_env(k, v);
|
||||
}
|
||||
None => {
|
||||
let _ = ctx.push_env(k, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let wasi = &mut plugin.memory.store.data_mut().wasi;
|
||||
let config = &mut plugin.manifest.as_mut().config;
|
||||
let config = &mut plugin.internal_mut().manifest.as_mut().config;
|
||||
for (k, v) in json.into_iter() {
|
||||
match v {
|
||||
Some(v) => {
|
||||
trace!("Config, adding {k}");
|
||||
if let Some(Wasi { ctx, .. }) = wasi {
|
||||
let _ = ctx.push_env(&k, &v);
|
||||
}
|
||||
config.insert(k, v);
|
||||
}
|
||||
None => {
|
||||
trace!("Config, removing {k}");
|
||||
if let Some(Wasi { ctx, .. }) = wasi {
|
||||
let _ = ctx.push_env(&k, "");
|
||||
}
|
||||
config.remove(&k);
|
||||
}
|
||||
}
|
||||
@@ -499,77 +495,60 @@ pub unsafe extern "C" fn extism_plugin_call(
|
||||
) -> i32 {
|
||||
let ctx = &mut *ctx;
|
||||
|
||||
// Get function name
|
||||
let name = std::ffi::CStr::from_ptr(func_name);
|
||||
let name = match name.to_str() {
|
||||
Ok(name) => name,
|
||||
Err(e) => return ctx.error(e, -1),
|
||||
};
|
||||
let is_start = name == "_start";
|
||||
|
||||
// Get a `PluginRef` and call `init` to set up the plugin input and memory, this is only
|
||||
// needed before a new call
|
||||
let mut plugin_ref = match PluginRef::new(ctx, plugin_id, true) {
|
||||
None => return -1,
|
||||
Some(p) => p.init(data, data_len as usize),
|
||||
Some(p) => p.start_call(is_start),
|
||||
};
|
||||
let tx = plugin_ref.epoch_timer_tx.clone();
|
||||
let plugin = plugin_ref.as_mut();
|
||||
|
||||
// Find function
|
||||
let name = std::ffi::CStr::from_ptr(func_name);
|
||||
let name = match name.to_str() {
|
||||
Ok(name) => name,
|
||||
Err(e) => return plugin_ref.as_ref().error(e, -1),
|
||||
};
|
||||
|
||||
debug!("Calling function: {name} in plugin {plugin_id}");
|
||||
|
||||
let func = match plugin_ref.as_mut().get_func(name) {
|
||||
let func = match plugin.get_func(name) {
|
||||
Some(x) => x,
|
||||
None => {
|
||||
return plugin_ref
|
||||
.as_ref()
|
||||
.error(format!("Function not found: {name}"), -1)
|
||||
}
|
||||
None => return plugin.error(format!("Function not found: {name}"), -1),
|
||||
};
|
||||
|
||||
// Check the number of results, reject functions with more than 1 result
|
||||
let n_results = func.ty(&plugin_ref.as_ref().memory.store).results().len();
|
||||
let n_results = func.ty(plugin.store()).results().len();
|
||||
if n_results > 1 {
|
||||
return plugin_ref.as_ref().error(
|
||||
return plugin.error(
|
||||
format!("Function {name} has {n_results} results, expected 0 or 1"),
|
||||
-1,
|
||||
);
|
||||
}
|
||||
|
||||
// Start timer
|
||||
let tx = plugin_ref.epoch_timer_tx.clone();
|
||||
if let Err(e) = plugin_ref.as_mut().start_timer(&tx) {
|
||||
let id = plugin_ref.as_ref().timer_id;
|
||||
return plugin_ref.as_ref().error(
|
||||
format!("Unable to start timeout manager for {id}: {e:?}"),
|
||||
-1,
|
||||
);
|
||||
if let Err(e) = plugin.set_input(data, data_len as usize) {
|
||||
return plugin.error(e, -1);
|
||||
}
|
||||
|
||||
if plugin.has_error() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Start timer, this will be stopped when PluginRef goes out of scope
|
||||
if let Err(e) = plugin.start_timer(&tx) {
|
||||
return plugin.error(e, -1);
|
||||
}
|
||||
|
||||
debug!("Calling function: {name} in plugin {plugin_id}");
|
||||
|
||||
// Call the function
|
||||
let mut results = vec![wasmtime::Val::null(); n_results];
|
||||
let res = func.call(
|
||||
&mut plugin_ref.as_mut().memory.store,
|
||||
&[],
|
||||
results.as_mut_slice(),
|
||||
);
|
||||
|
||||
plugin_ref.as_ref().dump_memory();
|
||||
|
||||
if plugin_ref.as_ref().has_wasi() && name == "_start" {
|
||||
plugin_ref.as_mut().should_reinstantiate = true;
|
||||
}
|
||||
|
||||
// Stop timer
|
||||
if let Err(e) = plugin_ref.as_mut().stop_timer() {
|
||||
let id = plugin_ref.as_ref().timer_id;
|
||||
return plugin_ref.as_ref().error(
|
||||
format!("Failed to stop timeout manager for {id}: {e:?}"),
|
||||
-1,
|
||||
);
|
||||
}
|
||||
let res = func.call(plugin.store_mut(), &[], results.as_mut_slice());
|
||||
|
||||
match res {
|
||||
Ok(()) => (),
|
||||
Err(e) => {
|
||||
let plugin = plugin_ref.as_ref();
|
||||
plugin.store.set_epoch_deadline(1);
|
||||
if let Some(exit) = e.downcast_ref::<wasmtime_wasi::I32Exit>() {
|
||||
trace!("WASI return code: {}", exit.0);
|
||||
if exit.0 != 0 {
|
||||
@@ -619,19 +598,27 @@ pub unsafe extern "C" fn extism_error(ctx: *mut Context, plugin: PluginIndex) ->
|
||||
return get_context_error(ctx);
|
||||
}
|
||||
|
||||
let plugin = match PluginRef::new(ctx, plugin, false) {
|
||||
let mut plugin_ref = match PluginRef::new(ctx, plugin, false) {
|
||||
None => return std::ptr::null(),
|
||||
Some(p) => p,
|
||||
};
|
||||
|
||||
let err = plugin.as_ref().last_error.borrow();
|
||||
match err.as_ref() {
|
||||
Some(e) => e.as_ptr() as *const _,
|
||||
None => {
|
||||
trace!("Error is NULL");
|
||||
std::ptr::null()
|
||||
}
|
||||
let plugin = plugin_ref.as_mut();
|
||||
let output = &mut [Val::I64(0)];
|
||||
if let Some(f) = plugin
|
||||
.linker
|
||||
.get(&mut plugin.store, "env", "extism_error_get")
|
||||
{
|
||||
f.into_func()
|
||||
.unwrap()
|
||||
.call(&mut plugin.store, &[], output)
|
||||
.unwrap();
|
||||
}
|
||||
if output[0].unwrap_i64() == 0 {
|
||||
trace!("Error is NULL");
|
||||
return std::ptr::null();
|
||||
}
|
||||
|
||||
plugin.memory_ptr().add(output[0].unwrap_i64() as usize) as *const _
|
||||
}
|
||||
|
||||
/// Get the length of a plugin's output data
|
||||
@@ -643,17 +630,25 @@ pub unsafe extern "C" fn extism_plugin_output_length(
|
||||
trace!("Call to extism_plugin_output_length for plugin {plugin}");
|
||||
|
||||
let ctx = &mut *ctx;
|
||||
let plugin = match PluginRef::new(ctx, plugin, true) {
|
||||
let mut plugin_ref = match PluginRef::new(ctx, plugin, true) {
|
||||
None => return 0,
|
||||
Some(p) => p,
|
||||
};
|
||||
|
||||
let len = plugin.as_ref().memory.store.data().output_length as Size;
|
||||
let plugin = plugin_ref.as_mut();
|
||||
let out = &mut [Val::I64(0)];
|
||||
let _ = plugin
|
||||
.linker
|
||||
.get(&mut plugin.store, "env", "extism_output_length")
|
||||
.unwrap()
|
||||
.into_func()
|
||||
.unwrap()
|
||||
.call(&mut plugin.store_mut(), &[], out);
|
||||
let len = out[0].unwrap_i64() as Size;
|
||||
trace!("Output length: {len}");
|
||||
len
|
||||
}
|
||||
|
||||
/// Get the length of a plugin's output data
|
||||
/// Get a pointer to the output data
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_plugin_output_data(
|
||||
ctx: *mut Context,
|
||||
@@ -662,18 +657,26 @@ pub unsafe extern "C" fn extism_plugin_output_data(
|
||||
trace!("Call to extism_plugin_output_data for plugin {plugin}");
|
||||
|
||||
let ctx = &mut *ctx;
|
||||
let plugin = match PluginRef::new(ctx, plugin, true) {
|
||||
let mut plugin_ref = match PluginRef::new(ctx, plugin, true) {
|
||||
None => return std::ptr::null(),
|
||||
Some(p) => p,
|
||||
};
|
||||
let data = plugin.as_ref().memory.store.data();
|
||||
|
||||
let plugin = plugin_ref.as_mut();
|
||||
let ptr = plugin.memory_ptr();
|
||||
let out = &mut [Val::I64(0)];
|
||||
let mut store = &mut *(plugin.store_mut() as *mut Store<_>);
|
||||
plugin
|
||||
.as_ref()
|
||||
.memory
|
||||
.ptr(MemoryBlock::new(data.output_offset, data.output_length))
|
||||
.map(|x| x as *const _)
|
||||
.unwrap_or(std::ptr::null())
|
||||
.linker
|
||||
.get(&mut store, "env", "extism_output_offset")
|
||||
.unwrap()
|
||||
.into_func()
|
||||
.unwrap()
|
||||
.call(&mut store, &[], out)
|
||||
.unwrap();
|
||||
|
||||
let offs = out[0].unwrap_i64() as usize;
|
||||
trace!("Output offset: {}", offs);
|
||||
ptr.add(offs)
|
||||
}
|
||||
|
||||
/// Set log file and level
|
||||
@@ -700,9 +703,7 @@ pub unsafe extern "C" fn extism_log_file(
|
||||
"stderr"
|
||||
};
|
||||
|
||||
let level = if log_level.is_null() {
|
||||
"error"
|
||||
} else {
|
||||
let level = if !log_level.is_null() {
|
||||
let level = std::ffi::CStr::from_ptr(log_level);
|
||||
match level.to_str() {
|
||||
Ok(x) => x,
|
||||
@@ -710,6 +711,8 @@ pub unsafe extern "C" fn extism_log_file(
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
"error"
|
||||
};
|
||||
|
||||
let level = match LevelFilter::from_str(level) {
|
||||
|
||||
@@ -20,6 +20,7 @@ pub(crate) struct Timer {
|
||||
pub thread: Option<std::thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "windows"))]
|
||||
extern "C" fn cleanup_timer() {
|
||||
drop(Context::timer().take())
|
||||
}
|
||||
@@ -90,6 +91,7 @@ impl Timer {
|
||||
tx: tx.clone(),
|
||||
});
|
||||
|
||||
#[cfg(not(target_family = "windows"))]
|
||||
unsafe {
|
||||
libc::atexit(cleanup_timer);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "extism"
|
||||
version = "0.3.0"
|
||||
version = "0.5.1"
|
||||
edition = "2021"
|
||||
authors = ["The Extism Authors", "oss@extism.org"]
|
||||
license = "BSD-3-Clause"
|
||||
@@ -9,8 +9,8 @@ repository = "https://github.com/extism/extism"
|
||||
description = "Extism Host SDK for Rust"
|
||||
|
||||
[dependencies]
|
||||
extism-manifest = { version = "0.3.0", path = "../manifest" }
|
||||
extism-runtime = { version = "0.3.0", path = "../runtime"}
|
||||
extism-manifest = { version = "0.5.0", path = "../manifest" }
|
||||
extism-runtime = { version = "0.5.2", path = "../runtime"}
|
||||
serde_json = "1"
|
||||
log = "0.4"
|
||||
anyhow = "1"
|
||||
|
||||
@@ -1,45 +1,260 @@
|
||||
/* automatically generated by rust-bindgen 0.60.1 */
|
||||
/* automatically generated by rust-bindgen 0.65.1 */
|
||||
|
||||
pub type __uint8_t = ::std::os::raw::c_uchar;
|
||||
pub type __int32_t = ::std::os::raw::c_int;
|
||||
pub type __uint64_t = ::std::os::raw::c_ulong;
|
||||
#[doc = " Signed 32 bit integer."]
|
||||
pub const ExtismValType_I32: ExtismValType = 0;
|
||||
#[doc = " Signed 64 bit integer."]
|
||||
pub const ExtismValType_I64: ExtismValType = 1;
|
||||
#[doc = " Floating point 32 bit integer."]
|
||||
pub const ExtismValType_F32: ExtismValType = 2;
|
||||
#[doc = " Floating point 64 bit integer."]
|
||||
pub const ExtismValType_F64: ExtismValType = 3;
|
||||
#[doc = " A 128 bit number."]
|
||||
pub const ExtismValType_V128: ExtismValType = 4;
|
||||
#[doc = " A reference to a Wasm function."]
|
||||
pub const ExtismValType_FuncRef: ExtismValType = 5;
|
||||
#[doc = " A reference to opaque data in the Wasm instance."]
|
||||
pub const ExtismValType_ExternRef: ExtismValType = 6;
|
||||
#[doc = " A list of all possible value types in WebAssembly."]
|
||||
pub type ExtismValType = ::std::os::raw::c_uint;
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ExtismContext {
|
||||
_unused: [u8; 0],
|
||||
}
|
||||
pub type ExtismPlugin = i32;
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ExtismCancelHandle {
|
||||
_unused: [u8; 0],
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ExtismFunction {
|
||||
_unused: [u8; 0],
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ExtismCurrentPlugin {
|
||||
_unused: [u8; 0],
|
||||
}
|
||||
pub type ExtismSize = u64;
|
||||
#[doc = " A union type for host function argument/return values"]
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub union ExtismValUnion {
|
||||
pub i32_: i32,
|
||||
pub i64_: i64,
|
||||
pub f32_: f32,
|
||||
pub f64_: f64,
|
||||
}
|
||||
#[test]
|
||||
fn bindgen_test_layout_ExtismValUnion() {
|
||||
const UNINIT: ::std::mem::MaybeUninit<ExtismValUnion> = ::std::mem::MaybeUninit::uninit();
|
||||
let ptr = UNINIT.as_ptr();
|
||||
assert_eq!(
|
||||
::std::mem::size_of::<ExtismValUnion>(),
|
||||
8usize,
|
||||
concat!("Size of: ", stringify!(ExtismValUnion))
|
||||
);
|
||||
assert_eq!(
|
||||
::std::mem::align_of::<ExtismValUnion>(),
|
||||
8usize,
|
||||
concat!("Alignment of ", stringify!(ExtismValUnion))
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { ::std::ptr::addr_of!((*ptr).i32_) as usize - ptr as usize },
|
||||
0usize,
|
||||
concat!(
|
||||
"Offset of field: ",
|
||||
stringify!(ExtismValUnion),
|
||||
"::",
|
||||
stringify!(i32_)
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { ::std::ptr::addr_of!((*ptr).i64_) as usize - ptr as usize },
|
||||
0usize,
|
||||
concat!(
|
||||
"Offset of field: ",
|
||||
stringify!(ExtismValUnion),
|
||||
"::",
|
||||
stringify!(i64_)
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { ::std::ptr::addr_of!((*ptr).f32_) as usize - ptr as usize },
|
||||
0usize,
|
||||
concat!(
|
||||
"Offset of field: ",
|
||||
stringify!(ExtismValUnion),
|
||||
"::",
|
||||
stringify!(f32_)
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { ::std::ptr::addr_of!((*ptr).f64_) as usize - ptr as usize },
|
||||
0usize,
|
||||
concat!(
|
||||
"Offset of field: ",
|
||||
stringify!(ExtismValUnion),
|
||||
"::",
|
||||
stringify!(f64_)
|
||||
)
|
||||
);
|
||||
}
|
||||
#[doc = " `ExtismVal` holds the type and value of a function argument/return"]
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct ExtismVal {
|
||||
pub t: ExtismValType,
|
||||
pub v: ExtismValUnion,
|
||||
}
|
||||
#[test]
|
||||
fn bindgen_test_layout_ExtismVal() {
|
||||
const UNINIT: ::std::mem::MaybeUninit<ExtismVal> = ::std::mem::MaybeUninit::uninit();
|
||||
let ptr = UNINIT.as_ptr();
|
||||
assert_eq!(
|
||||
::std::mem::size_of::<ExtismVal>(),
|
||||
16usize,
|
||||
concat!("Size of: ", stringify!(ExtismVal))
|
||||
);
|
||||
assert_eq!(
|
||||
::std::mem::align_of::<ExtismVal>(),
|
||||
8usize,
|
||||
concat!("Alignment of ", stringify!(ExtismVal))
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { ::std::ptr::addr_of!((*ptr).t) as usize - ptr as usize },
|
||||
0usize,
|
||||
concat!(
|
||||
"Offset of field: ",
|
||||
stringify!(ExtismVal),
|
||||
"::",
|
||||
stringify!(t)
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { ::std::ptr::addr_of!((*ptr).v) as usize - ptr as usize },
|
||||
8usize,
|
||||
concat!(
|
||||
"Offset of field: ",
|
||||
stringify!(ExtismVal),
|
||||
"::",
|
||||
stringify!(v)
|
||||
)
|
||||
);
|
||||
}
|
||||
#[doc = " Host function signature"]
|
||||
pub type ExtismFunctionType = ::std::option::Option<
|
||||
unsafe extern "C" fn(
|
||||
plugin: *mut ExtismCurrentPlugin,
|
||||
inputs: *const ExtismVal,
|
||||
n_inputs: ExtismSize,
|
||||
outputs: *mut ExtismVal,
|
||||
n_outputs: ExtismSize,
|
||||
data: *mut ::std::os::raw::c_void,
|
||||
),
|
||||
>;
|
||||
pub type ExtismPlugin = i32;
|
||||
extern "C" {
|
||||
#[doc = " Create a new context"]
|
||||
pub fn extism_context_new() -> *mut ExtismContext;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Free a context"]
|
||||
pub fn extism_context_free(ctx: *mut ExtismContext);
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Returns a pointer to the memory of the currently running plugin\n NOTE: this should only be called from host functions."]
|
||||
pub fn extism_current_plugin_memory(plugin: *mut ExtismCurrentPlugin) -> *mut u8;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Allocate a memory block in the currently running plugin\n NOTE: this should only be called from host functions."]
|
||||
pub fn extism_current_plugin_memory_alloc(
|
||||
plugin: *mut ExtismCurrentPlugin,
|
||||
n: ExtismSize,
|
||||
) -> u64;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Get the length of an allocated block\n NOTE: this should only be called from host functions."]
|
||||
pub fn extism_current_plugin_memory_length(
|
||||
plugin: *mut ExtismCurrentPlugin,
|
||||
n: ExtismSize,
|
||||
) -> ExtismSize;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Free an allocated memory block\n NOTE: this should only be called from host functions."]
|
||||
pub fn extism_current_plugin_memory_free(plugin: *mut ExtismCurrentPlugin, ptr: u64);
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Create a new host function\n\n Arguments\n - `name`: function name, this should be valid UTF-8\n - `inputs`: argument types\n - `n_inputs`: number of argument types\n - `outputs`: return types\n - `n_outputs`: number of return types\n - `func`: the function to call\n - `user_data`: a pointer that will be passed to the function when it's called\n this value should live as long as the function exists\n - `free_user_data`: a callback to release the `user_data` value when the resulting\n `ExtismFunction` is freed.\n\n Returns a new `ExtismFunction` or `null` if the `name` argument is invalid."]
|
||||
pub fn extism_function_new(
|
||||
name: *const ::std::os::raw::c_char,
|
||||
inputs: *const ExtismValType,
|
||||
n_inputs: ExtismSize,
|
||||
outputs: *const ExtismValType,
|
||||
n_outputs: ExtismSize,
|
||||
func: ExtismFunctionType,
|
||||
user_data: *mut ::std::os::raw::c_void,
|
||||
free_user_data: ::std::option::Option<
|
||||
unsafe extern "C" fn(__: *mut ::std::os::raw::c_void),
|
||||
>,
|
||||
) -> *mut ExtismFunction;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Set the namespace of an `ExtismFunction`"]
|
||||
pub fn extism_function_set_namespace(
|
||||
ptr: *mut ExtismFunction,
|
||||
namespace_: *const ::std::os::raw::c_char,
|
||||
);
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Free an `ExtismFunction`"]
|
||||
pub fn extism_function_free(ptr: *mut ExtismFunction);
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Create a new plugin with additional host functions\n\n `wasm`: is a WASM module (wat or wasm) or a JSON encoded manifest\n `wasm_size`: the length of the `wasm` parameter\n `functions`: an array of `ExtismFunction*`\n `n_functions`: the number of functions provided\n `with_wasi`: enables/disables WASI"]
|
||||
pub fn extism_plugin_new(
|
||||
ctx: *mut ExtismContext,
|
||||
wasm: *const u8,
|
||||
wasm_size: ExtismSize,
|
||||
functions: *mut *const ExtismFunction,
|
||||
n_functions: ExtismSize,
|
||||
with_wasi: bool,
|
||||
) -> ExtismPlugin;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Update a plugin, keeping the existing ID\n\n Similar to `extism_plugin_new` but takes an `index` argument to specify\n which plugin to update\n\n Memory for this plugin will be reset upon update"]
|
||||
pub fn extism_plugin_update(
|
||||
ctx: *mut ExtismContext,
|
||||
index: ExtismPlugin,
|
||||
wasm: *const u8,
|
||||
wasm_size: ExtismSize,
|
||||
functions: *mut *const ExtismFunction,
|
||||
nfunctions: ExtismSize,
|
||||
with_wasi: bool,
|
||||
) -> bool;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Remove a plugin from the registry and free associated memory"]
|
||||
pub fn extism_plugin_free(ctx: *mut ExtismContext, plugin: ExtismPlugin);
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Get plugin ID for cancellation"]
|
||||
pub fn extism_plugin_cancel_handle(
|
||||
ctx: *mut ExtismContext,
|
||||
plugin: ExtismPlugin,
|
||||
) -> *const ExtismCancelHandle;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Cancel a running plugin"]
|
||||
pub fn extism_plugin_cancel(handle: *const ExtismCancelHandle) -> bool;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Remove all plugins from the registry"]
|
||||
pub fn extism_context_reset(ctx: *mut ExtismContext);
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Update plugin config values, this will merge with the existing values"]
|
||||
pub fn extism_plugin_config(
|
||||
ctx: *mut ExtismContext,
|
||||
plugin: ExtismPlugin,
|
||||
@@ -48,6 +263,7 @@ extern "C" {
|
||||
) -> bool;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Returns true if `func_name` exists"]
|
||||
pub fn extism_plugin_function_exists(
|
||||
ctx: *mut ExtismContext,
|
||||
plugin: ExtismPlugin,
|
||||
@@ -55,6 +271,7 @@ extern "C" {
|
||||
) -> bool;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Call a function\n\n `func_name`: is the function to call\n `data`: is the input data\n `data_len`: is the length of `data`"]
|
||||
pub fn extism_plugin_call(
|
||||
ctx: *mut ExtismContext,
|
||||
plugin_id: ExtismPlugin,
|
||||
@@ -64,26 +281,29 @@ extern "C" {
|
||||
) -> i32;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Get the error associated with a `Context` or `Plugin`, if `plugin` is `-1` then the context\n error will be returned"]
|
||||
pub fn extism_error(
|
||||
ctx: *mut ExtismContext,
|
||||
plugin: ExtismPlugin,
|
||||
) -> *const ::std::os::raw::c_char;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Get the length of a plugin's output data"]
|
||||
pub fn extism_plugin_output_length(ctx: *mut ExtismContext, plugin: ExtismPlugin)
|
||||
-> ExtismSize;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Get a pointer to the output data"]
|
||||
pub fn extism_plugin_output_data(ctx: *mut ExtismContext, plugin: ExtismPlugin) -> *const u8;
|
||||
}
|
||||
extern "C" {
|
||||
#[doc = " Set log file and level"]
|
||||
pub fn extism_log_file(
|
||||
filename: *const ::std::os::raw::c_char,
|
||||
log_level: *const ::std::os::raw::c_char,
|
||||
) -> bool;
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
pub fn extism_version(
|
||||
) -> *const ::std::os::raw::c_char;
|
||||
#[doc = " Get the Extism version string"]
|
||||
pub fn extism_version() -> *const ::std::os::raw::c_char;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Context(pub(crate) std::sync::Arc<std::sync::Mutex<extism_runtime::Context>>);
|
||||
|
||||
impl Default for Context {
|
||||
@@ -8,6 +9,9 @@ impl Default for Context {
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Sync for Context {}
|
||||
unsafe impl Send for Context {}
|
||||
|
||||
impl Context {
|
||||
/// Create a new context
|
||||
pub fn new() -> Context {
|
||||
|
||||
166
rust/src/lib.rs
166
rust/src/lib.rs
@@ -1,6 +1,6 @@
|
||||
pub use extism_manifest::{self as manifest, Manifest};
|
||||
pub use extism_runtime::{
|
||||
sdk as bindings, Function, MemoryBlock, Plugin as CurrentPlugin, UserData, Val, ValType,
|
||||
sdk as bindings, Function, Internal as CurrentPlugin, InternalExt, UserData, Val, ValType,
|
||||
};
|
||||
|
||||
mod context;
|
||||
@@ -21,13 +21,20 @@ pub fn extism_version() -> String {
|
||||
|
||||
/// Set the log file and level, this is a global setting
|
||||
pub fn set_log_file(filename: impl AsRef<std::path::Path>, log_level: Option<log::Level>) -> bool {
|
||||
let log_level = log_level.map(|x| x.as_str());
|
||||
unsafe {
|
||||
return bindings::extism_log_file(
|
||||
filename.as_ref().as_os_str().to_string_lossy().as_ptr() as *const _,
|
||||
log_level.map(|x| x.as_ptr()).unwrap_or(std::ptr::null()) as *const _,
|
||||
);
|
||||
if let Ok(filename) = std::ffi::CString::new(filename.as_ref().to_string_lossy().as_bytes()) {
|
||||
let log_level_s = log_level.map(|x| x.as_str());
|
||||
let log_level_c = log_level_s.map(|x| std::ffi::CString::new(x));
|
||||
if let Some(Ok(log_level_c)) = log_level_c {
|
||||
unsafe {
|
||||
return bindings::extism_log_file(filename.as_ptr(), log_level_c.as_ptr());
|
||||
}
|
||||
} else {
|
||||
unsafe {
|
||||
return bindings::extism_log_file(filename.as_ptr(), std::ptr::null());
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -37,14 +44,20 @@ mod tests {
|
||||
|
||||
const WASM: &[u8] = include_bytes!("../../wasm/code-functions.wasm");
|
||||
const WASM_LOOP: &[u8] = include_bytes!("../../wasm/loop.wasm");
|
||||
const WASM_GLOBALS: &[u8] = include_bytes!("../../wasm/globals.wasm");
|
||||
const REFLECT_WASM: &[u8] = include_bytes!("../../wasm/reflect.wasm");
|
||||
|
||||
fn hello_world(
|
||||
_plugin: &mut CurrentPlugin,
|
||||
plugin: &mut CurrentPlugin,
|
||||
inputs: &[Val],
|
||||
outputs: &mut [Val],
|
||||
_user_data: UserData,
|
||||
) -> Result<(), Error> {
|
||||
outputs[0] = inputs[0].clone();
|
||||
let input_offs = inputs[0].unwrap_i64() as u64;
|
||||
let length = plugin.memory_length(input_offs);
|
||||
let input = plugin.memory_read(input_offs, length).to_vec();
|
||||
let output = plugin.memory_alloc_bytes(&input).unwrap();
|
||||
outputs[0] = Val::I64(output as i64);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -60,7 +73,7 @@ mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let wasm_start = Instant::now();
|
||||
set_log_file("test.log", Some(log::Level::Info));
|
||||
assert!(set_log_file("test.log", Some(log::Level::Trace)));
|
||||
let context = Context::new();
|
||||
let f = Function::new(
|
||||
"hello_world",
|
||||
@@ -79,8 +92,7 @@ mod tests {
|
||||
)
|
||||
.with_namespace("test");
|
||||
|
||||
let functions = [&f, &g];
|
||||
let mut plugin = Plugin::new(&context, WASM, functions, true).unwrap();
|
||||
let mut plugin = Plugin::new(&context, WASM, [f, g], true).unwrap();
|
||||
println!("register loaded plugin: {:?}", wasm_start.elapsed());
|
||||
|
||||
let repeat = 1182;
|
||||
@@ -168,7 +180,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_threads() {
|
||||
fn test_context_threads() {
|
||||
use std::io::Write;
|
||||
std::thread::spawn(|| {
|
||||
let context = Context::new();
|
||||
@@ -179,7 +191,7 @@ mod tests {
|
||||
None,
|
||||
hello_world,
|
||||
);
|
||||
let mut plugin = Plugin::new(&context, WASM, [&f], true).unwrap();
|
||||
let mut plugin = Plugin::new(&context, WASM, [f], true).unwrap();
|
||||
let output = plugin.call("count_vowels", "this is a test").unwrap();
|
||||
std::io::stdout().write_all(output).unwrap();
|
||||
});
|
||||
@@ -192,22 +204,63 @@ mod tests {
|
||||
hello_world,
|
||||
);
|
||||
|
||||
let g = f.clone();
|
||||
std::thread::spawn(move || {
|
||||
let context = Context::new();
|
||||
let mut plugin = PluginBuilder::new_with_module(WASM)
|
||||
.with_function(&g)
|
||||
.with_wasi(true)
|
||||
.build(&context)
|
||||
.unwrap();
|
||||
let output = plugin.call("count_vowels", "this is a test aaa").unwrap();
|
||||
std::io::stdout().write_all(output).unwrap();
|
||||
});
|
||||
|
||||
// One context shared between two threads
|
||||
let context = Context::new();
|
||||
let mut plugin = Plugin::new(&context, WASM, [&f], true).unwrap();
|
||||
let output = plugin.call("count_vowels", "abc123").unwrap();
|
||||
std::io::stdout().write_all(output).unwrap();
|
||||
let mut threads = vec![];
|
||||
for _ in 0..3 {
|
||||
let ctx = context.clone();
|
||||
let g = f.clone();
|
||||
let a = std::thread::spawn(move || {
|
||||
let mut plugin = PluginBuilder::new_with_module(WASM)
|
||||
.with_function(g)
|
||||
.with_wasi(true)
|
||||
.build(Some(&ctx))
|
||||
.unwrap();
|
||||
for _ in 0..10 {
|
||||
let output = plugin.call("count_vowels", "this is a test aaa").unwrap();
|
||||
assert_eq!(b"{\"count\": 7}", output);
|
||||
}
|
||||
});
|
||||
threads.push(a);
|
||||
}
|
||||
for thread in threads {
|
||||
thread.join().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plugin_threads() {
|
||||
let f = Function::new(
|
||||
"hello_world",
|
||||
[ValType::I64],
|
||||
[ValType::I64],
|
||||
None,
|
||||
hello_world,
|
||||
);
|
||||
|
||||
let p = std::sync::Arc::new(std::sync::Mutex::new(
|
||||
PluginBuilder::new_with_module(WASM)
|
||||
.with_function(f)
|
||||
.with_wasi(true)
|
||||
.build(None)
|
||||
.unwrap(),
|
||||
));
|
||||
|
||||
let mut threads = vec![];
|
||||
for _ in 0..3 {
|
||||
let plugin = p.clone();
|
||||
let a = std::thread::spawn(move || {
|
||||
let mut plugin = plugin.lock().unwrap();
|
||||
for _ in 0..10 {
|
||||
let output = plugin.call("count_vowels", "this is a test aaa").unwrap();
|
||||
assert_eq!(b"{\"count\": 7}", output);
|
||||
}
|
||||
});
|
||||
threads.push(a);
|
||||
}
|
||||
for thread in threads {
|
||||
thread.join().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -221,7 +274,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let context = Context::new();
|
||||
let mut plugin = Plugin::new(&context, WASM_LOOP, [&f], true).unwrap();
|
||||
let mut plugin = Plugin::new(&context, WASM_LOOP, [f], true).unwrap();
|
||||
let handle = plugin.cancel_handle();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
@@ -236,4 +289,57 @@ mod tests {
|
||||
println!("Cancelled plugin ran for {:?}", time);
|
||||
// std::io::stdout().write_all(output).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_instantiations() {
|
||||
let f = Function::new(
|
||||
"hello_world",
|
||||
[ValType::I64],
|
||||
[ValType::I64],
|
||||
None,
|
||||
hello_world,
|
||||
);
|
||||
|
||||
let context = Context::new();
|
||||
let mut plugin = Plugin::new(&context, WASM, [f], true).unwrap();
|
||||
|
||||
// This is 10,001 because the wasmtime store limit is 10,000 - we want to test
|
||||
// that our reinstantiation process is working and that limit is never hit.
|
||||
for _ in 0..10001 {
|
||||
let _output = plugin.call("count_vowels", "abc123").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_globals() {
|
||||
let context = Context::new();
|
||||
let mut plugin = Plugin::new(&context, WASM_GLOBALS, [], true).unwrap();
|
||||
for i in 0..1000 {
|
||||
let output = plugin.call("globals", "").unwrap();
|
||||
let count: serde_json::Value = serde_json::from_slice(&output).unwrap();
|
||||
assert_eq!(count.get("count").unwrap().as_i64().unwrap(), i);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzz_reflect_plugin() {
|
||||
// assert!(set_log_file("stdout", Some(log::Level::Trace)));
|
||||
let f = Function::new(
|
||||
"host_reflect",
|
||||
[ValType::I64],
|
||||
[ValType::I64],
|
||||
None,
|
||||
hello_world,
|
||||
);
|
||||
|
||||
let context = Context::new();
|
||||
let mut plugin = Plugin::new(&context, REFLECT_WASM, [f], true).unwrap();
|
||||
|
||||
for i in 1..65540 {
|
||||
let input = "a".repeat(i);
|
||||
let output = plugin.call("reflect", input.clone());
|
||||
let output = std::str::from_utf8(output.unwrap()).unwrap();
|
||||
assert_eq!(output, input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,36 @@
|
||||
use crate::*;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
enum RefOrOwned<'a, T> {
|
||||
Ref(&'a T),
|
||||
Owned(T),
|
||||
}
|
||||
|
||||
pub struct Plugin<'a> {
|
||||
id: extism_runtime::PluginIndex,
|
||||
context: &'a Context,
|
||||
context: RefOrOwned<'a, Context>,
|
||||
functions: Vec<Function>,
|
||||
}
|
||||
|
||||
impl<'a, T> From<&'a T> for RefOrOwned<'a, T> {
|
||||
fn from(value: &'a T) -> Self {
|
||||
RefOrOwned::Ref(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for RefOrOwned<'a, T> {
|
||||
fn from(value: T) -> Self {
|
||||
RefOrOwned::Owned(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> AsRef<T> for RefOrOwned<'a, T> {
|
||||
fn as_ref(&self) -> &T {
|
||||
match self {
|
||||
RefOrOwned::Ref(x) => x,
|
||||
RefOrOwned::Owned(x) => x,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CancelHandle(pub(crate) *const extism_runtime::sdk::ExtismCancelHandle);
|
||||
@@ -23,32 +50,44 @@ impl<'a> Plugin<'a> {
|
||||
/// # Safety
|
||||
/// This function does not check to ensure the provided ID is valid
|
||||
pub unsafe fn from_id(id: i32, context: &'a Context) -> Plugin<'a> {
|
||||
Plugin { id, context }
|
||||
let context = RefOrOwned::Ref(context);
|
||||
Plugin {
|
||||
id,
|
||||
context,
|
||||
functions: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn context(&self) -> &Context {
|
||||
match &self.context {
|
||||
RefOrOwned::Ref(x) => x,
|
||||
RefOrOwned::Owned(x) => x,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_i32(&self) -> i32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// Create a new plugin from the given manifest
|
||||
pub fn new_with_manifest(
|
||||
ctx: &'a Context,
|
||||
/// Create a new plugin from the given manifest in its own context
|
||||
pub fn create_with_manifest(
|
||||
manifest: &Manifest,
|
||||
functions: impl IntoIterator<Item = &'a extism_runtime::Function>,
|
||||
functions: impl IntoIterator<Item = Function>,
|
||||
wasi: bool,
|
||||
) -> Result<Plugin<'a>, Error> {
|
||||
let data = serde_json::to_vec(manifest)?;
|
||||
Self::new(ctx, data, functions, wasi)
|
||||
Self::create(data, functions, wasi)
|
||||
}
|
||||
|
||||
/// Create a new plugin from a WASM module
|
||||
pub fn new(
|
||||
ctx: &'a Context,
|
||||
/// Create a new plugin from a WASM module in its own context
|
||||
pub fn create(
|
||||
data: impl AsRef<[u8]>,
|
||||
functions: impl IntoIterator<Item = &'a Function>,
|
||||
functions: impl IntoIterator<Item = Function>,
|
||||
wasi: bool,
|
||||
) -> Result<Plugin, Error> {
|
||||
let plugin = ctx.lock().new_plugin(data, functions, wasi);
|
||||
) -> Result<Plugin<'a>, Error> {
|
||||
let ctx = Context::new();
|
||||
let functions = functions.into_iter().collect();
|
||||
let plugin = ctx.lock().new_plugin(data, &functions, wasi);
|
||||
|
||||
if plugin < 0 {
|
||||
let err = unsafe { bindings::extism_error(&mut *ctx.lock(), -1) };
|
||||
@@ -59,7 +98,43 @@ impl<'a> Plugin<'a> {
|
||||
|
||||
Ok(Plugin {
|
||||
id: plugin,
|
||||
context: ctx,
|
||||
context: ctx.into(),
|
||||
functions,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new plugin from the given manifest
|
||||
pub fn new_with_manifest(
|
||||
ctx: &'a Context,
|
||||
manifest: &Manifest,
|
||||
functions: impl IntoIterator<Item = Function>,
|
||||
wasi: bool,
|
||||
) -> Result<Plugin<'a>, Error> {
|
||||
let data = serde_json::to_vec(manifest)?;
|
||||
Self::new(ctx, data, functions, wasi)
|
||||
}
|
||||
|
||||
/// Create a new plugin from a WASM module
|
||||
pub fn new(
|
||||
ctx: &'a Context,
|
||||
data: impl AsRef<[u8]>,
|
||||
functions: impl IntoIterator<Item = Function>,
|
||||
wasi: bool,
|
||||
) -> Result<Plugin<'a>, Error> {
|
||||
let functions = functions.into_iter().collect();
|
||||
let plugin = ctx.lock().new_plugin(data, &functions, wasi);
|
||||
|
||||
if plugin < 0 {
|
||||
let err = unsafe { bindings::extism_error(&mut *ctx.lock(), -1) };
|
||||
let buf = unsafe { std::ffi::CStr::from_ptr(err) };
|
||||
let buf = buf.to_str().unwrap();
|
||||
return Err(Error::msg(buf));
|
||||
}
|
||||
|
||||
Ok(Plugin {
|
||||
id: plugin,
|
||||
context: ctx.into(),
|
||||
functions,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -67,7 +142,7 @@ impl<'a> Plugin<'a> {
|
||||
pub fn update_with_manifest(
|
||||
&mut self,
|
||||
manifest: &Manifest,
|
||||
functions: impl IntoIterator<Item = &'a Function>,
|
||||
functions: impl IntoIterator<Item = Function>,
|
||||
wasi: bool,
|
||||
) -> Result<(), Error> {
|
||||
let data = serde_json::to_vec(manifest)?;
|
||||
@@ -78,11 +153,13 @@ impl<'a> Plugin<'a> {
|
||||
pub fn update(
|
||||
&mut self,
|
||||
data: impl AsRef<[u8]>,
|
||||
functions: impl IntoIterator<Item = &'a Function>,
|
||||
functions: impl IntoIterator<Item = Function>,
|
||||
wasi: bool,
|
||||
) -> Result<(), Error> {
|
||||
let functions = functions
|
||||
.into_iter()
|
||||
self.functions = functions.into_iter().collect();
|
||||
let functions = self
|
||||
.functions
|
||||
.iter()
|
||||
.map(|x| bindings::ExtismFunction::from(x.clone()));
|
||||
let mut functions = functions
|
||||
.into_iter()
|
||||
@@ -90,7 +167,7 @@ impl<'a> Plugin<'a> {
|
||||
.collect::<Vec<_>>();
|
||||
let b = unsafe {
|
||||
bindings::extism_plugin_update(
|
||||
&mut *self.context.lock(),
|
||||
&mut *self.context.as_ref().lock(),
|
||||
self.id,
|
||||
data.as_ref().as_ptr(),
|
||||
data.as_ref().len() as u64,
|
||||
@@ -103,7 +180,7 @@ impl<'a> Plugin<'a> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let err = unsafe { bindings::extism_error(&mut *self.context.lock(), -1) };
|
||||
let err = unsafe { bindings::extism_error(&mut *self.context.as_ref().lock(), -1) };
|
||||
if !err.is_null() {
|
||||
let s = unsafe { std::ffi::CStr::from_ptr(err) };
|
||||
return Err(Error::msg(s.to_str().unwrap()));
|
||||
@@ -117,7 +194,7 @@ impl<'a> Plugin<'a> {
|
||||
let encoded = serde_json::to_vec(config)?;
|
||||
unsafe {
|
||||
bindings::extism_plugin_config(
|
||||
&mut *self.context.lock(),
|
||||
&mut *self.context.as_ref().lock(),
|
||||
self.id,
|
||||
encoded.as_ptr() as *const _,
|
||||
encoded.len() as u64,
|
||||
@@ -137,7 +214,7 @@ impl<'a> Plugin<'a> {
|
||||
let name = std::ffi::CString::new(name.as_ref()).expect("Invalid function name");
|
||||
unsafe {
|
||||
bindings::extism_plugin_function_exists(
|
||||
&mut *self.context.lock(),
|
||||
&mut *self.context.as_ref().lock(),
|
||||
self.id,
|
||||
name.as_ptr() as *const _,
|
||||
)
|
||||
@@ -145,18 +222,27 @@ impl<'a> Plugin<'a> {
|
||||
}
|
||||
|
||||
pub fn cancel_handle(&self) -> CancelHandle {
|
||||
let ptr =
|
||||
unsafe { bindings::extism_plugin_cancel_handle(&mut *self.context.lock(), self.id) };
|
||||
let ptr = unsafe {
|
||||
bindings::extism_plugin_cancel_handle(&mut *self.context.as_ref().lock(), self.id)
|
||||
};
|
||||
|
||||
CancelHandle(ptr)
|
||||
}
|
||||
|
||||
/// Call a function with the given input
|
||||
pub fn call(&mut self, name: impl AsRef<str>, input: impl AsRef<[u8]>) -> Result<&[u8], Error> {
|
||||
/// Call a function with the given input and call a callback with the output, this should be preferred when
|
||||
/// a single plugin may be acessed from multiple threads because the lock on the plugin is held during the
|
||||
/// callback, ensuring the output value is protected from modification.
|
||||
pub fn call_map<'b, T, F: FnOnce(&'b [u8]) -> Result<T, Error>>(
|
||||
&'b mut self,
|
||||
name: impl AsRef<str>,
|
||||
input: impl AsRef<[u8]>,
|
||||
f: F,
|
||||
) -> Result<T, Error> {
|
||||
let context = &mut *self.context.as_ref().lock();
|
||||
let name = std::ffi::CString::new(name.as_ref()).expect("Invalid function name");
|
||||
let rc = unsafe {
|
||||
bindings::extism_plugin_call(
|
||||
&mut *self.context.lock(),
|
||||
context,
|
||||
self.id,
|
||||
name.as_ptr() as *const _,
|
||||
input.as_ref().as_ptr() as *const _,
|
||||
@@ -165,7 +251,7 @@ impl<'a> Plugin<'a> {
|
||||
};
|
||||
|
||||
if rc != 0 {
|
||||
let err = unsafe { bindings::extism_error(&mut *self.context.lock(), self.id) };
|
||||
let err = unsafe { bindings::extism_error(context, self.id) };
|
||||
if !err.is_null() {
|
||||
let s = unsafe { std::ffi::CStr::from_ptr(err) };
|
||||
return Err(Error::msg(s.to_str().unwrap()));
|
||||
@@ -174,17 +260,25 @@ impl<'a> Plugin<'a> {
|
||||
return Err(Error::msg("extism_call failed"));
|
||||
}
|
||||
|
||||
let out_len =
|
||||
unsafe { bindings::extism_plugin_output_length(&mut *self.context.lock(), self.id) };
|
||||
let out_len = unsafe { bindings::extism_plugin_output_length(context, self.id) };
|
||||
unsafe {
|
||||
let ptr = bindings::extism_plugin_output_data(&mut *self.context.lock(), self.id);
|
||||
Ok(std::slice::from_raw_parts(ptr, out_len as usize))
|
||||
let ptr = bindings::extism_plugin_output_data(context, self.id);
|
||||
f(std::slice::from_raw_parts(ptr, out_len as usize))
|
||||
}
|
||||
}
|
||||
|
||||
/// Call a function with the given input
|
||||
pub fn call<'b>(
|
||||
&'b mut self,
|
||||
name: impl AsRef<str>,
|
||||
input: impl AsRef<[u8]>,
|
||||
) -> Result<&'b [u8], Error> {
|
||||
self.call_map(name, input, |x| Ok(x))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Drop for Plugin<'a> {
|
||||
fn drop(&mut self) {
|
||||
unsafe { bindings::extism_plugin_free(&mut *self.context.lock(), self.id) }
|
||||
unsafe { bindings::extism_plugin_free(&mut *self.context.as_ref().lock(), self.id) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@ enum Source {
|
||||
}
|
||||
|
||||
/// PluginBuilder is used to configure and create `Plugin` instances
|
||||
pub struct PluginBuilder<'a> {
|
||||
pub struct PluginBuilder {
|
||||
source: Source,
|
||||
wasi: bool,
|
||||
functions: Vec<&'a Function>,
|
||||
functions: Vec<Function>,
|
||||
}
|
||||
|
||||
impl<'a> PluginBuilder<'a> {
|
||||
impl PluginBuilder {
|
||||
/// Create a new `PluginBuilder` with the given WebAssembly module
|
||||
pub fn new_with_module(data: impl Into<Vec<u8>>) -> Self {
|
||||
PluginBuilder {
|
||||
@@ -38,23 +38,29 @@ impl<'a> PluginBuilder<'a> {
|
||||
}
|
||||
|
||||
/// Add a single host function
|
||||
pub fn with_function(mut self, f: &'a Function) -> Self {
|
||||
pub fn with_function(mut self, f: Function) -> Self {
|
||||
self.functions.push(f);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add multiple host functions
|
||||
pub fn with_functions(mut self, f: impl IntoIterator<Item = &'a Function>) -> Self {
|
||||
pub fn with_functions(mut self, f: impl IntoIterator<Item = Function>) -> Self {
|
||||
self.functions.extend(f);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self, context: &'a Context) -> Result<Plugin<'a>, Error> {
|
||||
match self.source {
|
||||
Source::Manifest(m) => {
|
||||
Plugin::new_with_manifest(context, &m, self.functions, self.wasi)
|
||||
}
|
||||
Source::Data(d) => Plugin::new(context, d, self.functions, self.wasi),
|
||||
pub fn build<'a>(self, context: Option<&'a Context>) -> Result<Plugin<'a>, Error> {
|
||||
match context {
|
||||
Some(context) => match self.source {
|
||||
Source::Manifest(m) => {
|
||||
Plugin::new_with_manifest(context, &m, self.functions, self.wasi)
|
||||
}
|
||||
Source::Data(d) => Plugin::new(context, d, self.functions, self.wasi),
|
||||
},
|
||||
None => match self.source {
|
||||
Source::Manifest(m) => Plugin::create_with_manifest(&m, self.functions, self.wasi),
|
||||
Source::Data(d) => Plugin::create(d, self.functions, self.wasi),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
BIN
wasm/globals.wasm
Executable file
BIN
wasm/globals.wasm
Executable file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user