Compare commits

..

7 Commits

Author SHA1 Message Date
Benjamin Eckel
b5db12e667 Merge branch 'main' into symlink-plugin-in-example-files 2023-03-16 09:54:02 -05:00
Benjamin Eckel
5b58cd2580 user_code not used 2023-01-23 14:08:49 -06:00
Benjamin Eckel
890760f06d include other languages 2023-01-19 15:33:16 -06:00
Benjamin Eckel
d29e9f8f93 add symlinks 2023-01-19 13:43:05 -06:00
Benjamin Eckel
52cc95bf5a go and python 2023-01-19 13:42:18 -06:00
Benjamin Eckel
4b5796b92b drop unix prefix 2023-01-19 13:33:12 -06:00
Benjamin Eckel
fee2f03651 docs: Symlink plugin in example files 2023-01-19 13:30:04 -06:00
121 changed files with 2361 additions and 4164 deletions

View File

@@ -10,6 +10,13 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install extism shared library
shell: bash
run: |
mkdir -p /home/runner/.local/bin/
export PATH="/home/runner/.local/bin/:$PATH"
curl https://raw.githubusercontent.com/extism/cli/main/install.sh | sh
extism --sudo --prefix /usr/local install
- name: Setup Elixir Host SDK
uses: erlef/setup-beam@v1
with:

View File

@@ -19,22 +19,16 @@ jobs:
override: true
target: ${{ matrix.target }}
- name: Release Rust Manifest Crate
if: always()
- name: Release Rust Host SDK
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_API_TOKEN }}
run: |
# order of crate publication matter: manifest, runtime, rust
cargo publish --manifest-path manifest/Cargo.toml
# allow for crates.io to update so dependant crates can locate extism-manifest
sleep 5
- 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
View File

@@ -30,7 +30,6 @@ rust/test.log
duniverse
_build
php/Extism.php
python/docs
dist-newstyle
.stack-work
vendor

View File

@@ -4,5 +4,5 @@ members = [
"runtime",
"rust",
"libextism",
"elixir/native/extism_nif"
]
exclude = ["kernel"]

View File

@@ -21,10 +21,6 @@ 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

View File

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

View File

@@ -1,15 +1,15 @@
{
"name": "@extism/runtime-browser",
"version": "0.3.0",
"version": "0.2.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@extism/runtime-browser",
"version": "0.3.0",
"version": "0.2.2",
"license": "BSD-3-Clause",
"dependencies": {
"@bjorn3/browser_wasi_shim": "^0.2.7"
"@bjorn3/browser_wasi_shim": "^0.2.1"
},
"devDependencies": {
"@types/jest": "^29.2.2",
@@ -568,9 +568,9 @@
"dev": true
},
"node_modules/@bjorn3/browser_wasi_shim": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/@bjorn3/browser_wasi_shim/-/browser_wasi_shim-0.2.7.tgz",
"integrity": "sha512-ONBGleCpaH5HC4MLLkUmLz69xA28HQIbvwsdA1WTMXvjyhOWXR7jVrC0DkYr/iRqmkNMBZtEVVZWm1L6ZAnJvw=="
"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=="
},
"node_modules/@cnakazawa/watch": {
"version": "1.0.4",
@@ -7618,9 +7618,9 @@
}
},
"node_modules/tough-cookie": {
"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==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz",
"integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==",
"dev": true,
"dependencies": {
"psl": "^1.1.33",
@@ -8108,9 +8108,9 @@
}
},
"node_modules/word-wrap": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"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==",
"dev": true,
"engines": {
"node": ">=0.10.0"
@@ -8681,9 +8681,9 @@
"dev": true
},
"@bjorn3/browser_wasi_shim": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/@bjorn3/browser_wasi_shim/-/browser_wasi_shim-0.2.7.tgz",
"integrity": "sha512-ONBGleCpaH5HC4MLLkUmLz69xA28HQIbvwsdA1WTMXvjyhOWXR7jVrC0DkYr/iRqmkNMBZtEVVZWm1L6ZAnJvw=="
"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=="
},
"@cnakazawa/watch": {
"version": "1.0.4",
@@ -13986,9 +13986,9 @@
}
},
"tough-cookie": {
"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==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz",
"integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==",
"dev": true,
"requires": {
"psl": "^1.1.33",
@@ -14334,9 +14334,9 @@
}
},
"word-wrap": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"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==",
"dev": true
},
"wrap-ansi": {

View File

@@ -1,6 +1,6 @@
{
"name": "@extism/runtime-browser",
"version": "0.3.0",
"version": "0.2.2",
"description": "Extism runtime in the browser",
"scripts": {
"build": "node build.js && tsc --emitDeclarationOnly --outDir dist",
@@ -34,6 +34,6 @@
"typescript": "^4.8.4"
},
"dependencies": {
"@bjorn3/browser_wasi_shim": "^0.2.7"
"@bjorn3/browser_wasi_shim": "^0.2.1"
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,6 +15,7 @@ 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
@@ -33,7 +34,7 @@ int main(int argc, char *argv[]) {
[](void *x) { std::cout << "Free user data" << std::endl; }),
};
Plugin plugin(wasm, true, functions);
Plugin plugin = context.plugin(wasm, true, functions);
const char *input = argc > 1 ? argv[1] : "this is a test";
ExtismSize length = strlen(input);

View File

@@ -342,10 +342,9 @@ class Plugin {
public:
// Create a new plugin
Plugin(const uint8_t *wasm, ExtismSize length, bool with_wasi = false,
std::vector<Function> functions = std::vector<Function>(),
std::shared_ptr<ExtismContext> ctx = std::shared_ptr<ExtismContext>(
extism_context_new(), extism_context_free))
Plugin(std::shared_ptr<ExtismContext> ctx, const uint8_t *wasm,
ExtismSize length, bool with_wasi = false,
std::vector<Function> functions = std::vector<Function>())
: functions(functions) {
std::vector<const ExtismFunction *> ptrs;
for (auto i : this->functions) {
@@ -360,19 +359,6 @@ 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()));
@@ -380,10 +366,8 @@ public:
#ifndef EXTISM_NO_JSON
// Create a new plugin from Manifest
Plugin(const Manifest &manifest, bool with_wasi = false,
std::vector<Function> functions = {},
std::shared_ptr<ExtismContext> ctx = std::shared_ptr<ExtismContext>(
extism_context_new(), extism_context_free)) {
Plugin(std::shared_ptr<ExtismContext> ctx, const Manifest &manifest,
bool with_wasi = false, std::vector<Function> functions = {}) {
std::vector<const ExtismFunction *> ptrs;
for (auto i : this->functions) {
ptrs.push_back(i.get());
@@ -522,28 +506,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(wasm, length, with_wasi, functions, this->pointer);
return Plugin(this->pointer, wasm, length, with_wasi, functions);
}
// Create plugin from std::string
Plugin plugin(const std::string &str, bool with_wasi = false,
std::vector<Function> functions = {}) const {
return Plugin((const uint8_t *)str.c_str(), str.size(), with_wasi,
functions, this->pointer);
return Plugin(this->pointer, (const uint8_t *)str.c_str(), str.size(),
with_wasi, functions);
}
// Create plugin from uint8_t vector
Plugin plugin(const std::vector<uint8_t> &data, bool with_wasi = false,
std::vector<Function> functions = {}) const {
return Plugin(data.data(), data.size(), with_wasi, functions,
this->pointer);
return Plugin(this->pointer, data.data(), data.size(), with_wasi,
functions);
}
#ifndef EXTISM_NO_JSON
// Create plugin from Manifest
Plugin plugin(const Manifest &manifest, bool with_wasi = false,
std::vector<Function> functions = {}) const {
return Plugin(manifest, with_wasi, functions, this->pointer);
return Plugin(this->pointer, manifest, with_wasi, functions);
}
#endif

View File

@@ -21,19 +21,21 @@ TEST(Context, Basic) {
}
TEST(Plugin, Manifest) {
Context context;
Manifest manifest = Manifest::path(code);
manifest.set_config("a", "1");
ASSERT_NO_THROW(Plugin plugin(manifest));
Plugin plugin(manifest);
ASSERT_NO_THROW(Plugin plugin = context.plugin(manifest));
Plugin plugin = context.plugin(manifest);
Buffer buf = plugin.call("count_vowels", "this is a test");
ASSERT_EQ((std::string)buf, "{\"count\": 4}");
}
TEST(Plugin, BadManifest) {
Context context;
Manifest manifest;
ASSERT_THROW(Plugin plugin(manifest), Error);
ASSERT_THROW(Plugin plugin = context.plugin(manifest), Error);
}
TEST(Plugin, Bytes) {
@@ -66,6 +68,7 @@ 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 =
@@ -79,7 +82,7 @@ TEST(Plugin, HostFunction) {
auto functions = std::vector<Function>{
hello_world,
};
Plugin plugin(wasm, true, functions);
Plugin plugin = context.plugin(wasm, true, functions);
auto buf = plugin.call("count_vowels", "aaa");
ASSERT_EQ(buf.length, 4);
ASSERT_EQ((std::string)buf, "test");

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
@@ -7,10 +6,8 @@ namespace Extism.Sdk.Native;
/// <summary>
/// Represents an Extism context through which you can load <see cref="Plugin"/>s.
/// </summary>
public unsafe class Context : IDisposable
public class Context : IDisposable
{
private readonly ConcurrentDictionary<int, Plugin> _plugins = new ConcurrentDictionary<int, Plugin>();
private const int DisposedMarker = 1;
private int _disposed;
@@ -20,63 +17,33 @@ public unsafe class Context : IDisposable
/// </summary>
public Context()
{
unsafe
{
NativeHandle = LibExtism.extism_context_new();
}
NativeHandle = LibExtism.extism_context_new();
}
/// <summary>
/// Native pointer to the Extism Context.
/// </summary>
internal LibExtism.ExtismContext* NativeHandle { get; }
internal IntPtr NativeHandle { get; }
/// <summary>
/// Loads an Extism <see cref="Plugin"/>.
/// </summary>
/// <param name="wasm">A WASM module (wat or wasm) or a JSON encoded manifest.</param>
/// <param name="functions">List of host functions expected by the plugin.</param>
/// <param name="withWasi">Enable/Disable WASI.</param>
public Plugin CreatePlugin(ReadOnlySpan<byte> wasm, HostFunction[] functions, bool withWasi)
public Plugin CreatePlugin(ReadOnlySpan<byte> wasm, bool withWasi)
{
CheckNotDisposed();
var functionHandles = functions.Select(f => f.NativeHandle).ToArray();
unsafe
{
fixed (byte* wasmPtr = wasm)
fixed (IntPtr* functionsPtr = functionHandles)
{
var index = LibExtism.extism_plugin_new(NativeHandle, wasmPtr, wasm.Length, functionsPtr, functions.Length, withWasi);
if (index == -1)
{
var errorMsg = GetError();
if (errorMsg != null)
{
throw new ExtismException(errorMsg);
}
else
{
throw new ExtismException("Failed to create plugin.");
}
}
return _plugins[index] = new Plugin(this, functions, index);
var plugin = LibExtism.extism_plugin_new(NativeHandle, wasmPtr, wasm.Length, null, 0, withWasi);
return new Plugin(this, plugin);
}
}
}
/// <summary>
/// Get a plugin by index.
/// </summary>
/// <param name="index">Index of plugin.</param>
/// <returns></returns>
public Plugin GetPlugin(int index)
{
return _plugins[index];
}
/// <summary>
/// Remove all plugins from this <see cref="Context"/>'s registry.
/// </summary>
@@ -143,11 +110,6 @@ public unsafe class Context : IDisposable
// Free up any managed resources here
}
foreach (var plugin in _plugins.Values)
{
plugin.Dispose();
}
// Free up unmanaged resources
LibExtism.extism_context_free(NativeHandle);
}
@@ -189,3 +151,34 @@ public unsafe class Context : IDisposable
return LibExtism.extism_log_file(logPath, logLevel);
}
}
/// <summary>
/// Extism Log Levels
/// </summary>
public enum LogLevel
{
/// <summary>
/// Designates very serious errors.
/// </summary>
Error,
/// <summary>
/// Designates hazardous situations.
/// </summary>
Warning,
/// <summary>
/// Designates useful information.
/// </summary>
Info,
/// <summary>
/// Designates lower priority information.
/// </summary>
Debug,
/// <summary>
/// Designates very low priority, often extremely verbose, information.
/// </summary>
Trace
}

View File

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

View File

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

View File

@@ -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.7.0</Version>
<Authors>Extism Contributors</Authors>
<Description>Extism SDK that allows hosting Extism plugins in .NET apps.</Description>
<Tags>extism, wasm, plugin</Tags>
<PackageLicenseExpression>BSD-3-Clause</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<PropertyGroup>
<PackageId>Extism.Sdk</PackageId>
<Version>0.4.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>

View File

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

View File

@@ -2,197 +2,24 @@ using System.Runtime.InteropServices;
namespace Extism.Sdk.Native;
/// <summary>
/// A union type for host function argument/return values.
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public struct ExtismValUnion
{
/// <summary>
/// Set this for 32 bit integers
/// </summary>
[FieldOffset(0)]
public int i32;
/// <summary>
/// Set this for 64 bit integers
/// </summary>
[FieldOffset(0)]
public long i64;
/// <summary>
/// Set this for 32 bit floats
/// </summary>
[FieldOffset(0)]
public float f32;
/// <summary>
/// Set this for 64 bit floats
/// </summary>
[FieldOffset(0)]
public double f64;
}
/// <summary>
/// Represents Wasm data types that Extism can understand
/// </summary>
public enum ExtismValType : byte
{
/// <summary>
/// Signed 32 bit integer. Equivalent of <see cref="int"/> or <see cref="uint"/>
/// </summary>
I32,
/// <summary>
/// Signed 64 bit integer. Equivalent of <see cref="long"/> or <see cref="ulong"/>
/// </summary>
I64,
/// <summary>
/// Floating point 32 bit integer. Equivalent of <see cref="float"/>
/// </summary>
F32,
/// <summary>
/// Floating point 64 bit integer. Equivalent of <see cref="double"/>
/// </summary>
F64,
/// <summary>
/// A 128 bit number.
/// </summary>
V128,
/// <summary>
/// A reference to opaque data in the Wasm instance.
/// </summary>
FuncRef,
/// <summary>
/// A reference to opaque data in the Wasm instance.
/// </summary>
ExternRef
}
/// <summary>
/// `ExtismVal` holds the type and value of a function argument/return
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct ExtismVal
{
/// <summary>
/// The type for the argument
/// </summary>
public ExtismValType t;
/// <summary>
/// The value for the argument
/// </summary>
public ExtismValUnion v;
}
/// <summary>
/// Functions exposed by the native Extism library.
/// </summary>
internal static class LibExtism
{
/// <summary>
/// A `Context` is used to store and manage plugins.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct ExtismContext { }
/// <summary>
/// Host function signature
/// </summary>
/// <param name="plugin"></param>
/// <param name="inputs"></param>
/// <param name="n_inputs"></param>
/// <param name="outputs"></param>
/// <param name="n_outputs"></param>
/// <param name="data"></param>
unsafe internal delegate void InternalExtismFunction(nint plugin, ExtismVal* inputs, uint n_inputs, ExtismVal* outputs, uint n_outputs, IntPtr data);
/// <summary>
/// Returns a pointer to the memory of the currently running plugin.
/// NOTE: this should only be called from host functions.
/// </summary>
/// <param name="plugin"></param>
/// <returns></returns>
[DllImport("extism", EntryPoint = "extism_current_plugin_memory")]
internal static extern IntPtr extism_current_plugin_memory(nint plugin);
/// <summary>
/// Allocate a memory block in the currently running plugin
/// </summary>
/// <param name="plugin"></param>
/// <param name="n"></param>
/// <returns></returns>
[DllImport("extism", EntryPoint = "extism_current_plugin_memory_alloc")]
internal static extern IntPtr extism_current_plugin_memory_alloc(nint plugin, long n);
/// <summary>
/// Get the length of an allocated block.
/// NOTE: this should only be called from host functions.
/// </summary>
/// <param name="plugin"></param>
/// <param name="n"></param>
/// <returns></returns>
[DllImport("extism", EntryPoint = "extism_current_plugin_memory_length")]
internal static extern long extism_current_plugin_memory_length(nint plugin, long n);
/// <summary>
/// Get the length of an allocated block.
/// NOTE: this should only be called from host functions.
/// </summary>
/// <param name="plugin"></param>
/// <param name="ptr"></param>
[DllImport("extism", EntryPoint = "extism_current_plugin_memory_free")]
internal static extern void extism_current_plugin_memory_free(nint plugin, IntPtr ptr);
/// <summary>
/// Create a new host function.
/// </summary>
/// <param name="name">function name, this should be valid UTF-8</param>
/// <param name="inputs">argument types</param>
/// <param name="nInputs">number of argument types</param>
/// <param name="outputs">return types</param>
/// <param name="nOutputs">number of return types</param>
/// <param name="func">the function to call</param>
/// <param name="userData">a pointer that will be passed to the function when it's called this value should live as long as the function exists</param>
/// <param name="freeUserData">a callback to release the `user_data` value when the resulting `ExtismFunction` is freed.</param>
/// <returns></returns>
[DllImport("extism", EntryPoint = "extism_function_new")]
unsafe internal static extern IntPtr extism_function_new(string name, ExtismValType* inputs, long nInputs, ExtismValType* outputs, long nOutputs, InternalExtismFunction func, IntPtr userData, IntPtr freeUserData);
/// <summary>
/// Set the namespace of an <see cref="ExtismFunction"/>
/// </summary>
/// <param name="ptr"></param>
/// <param name="namespace"></param>
[DllImport("extism", EntryPoint = "extism_function_set_namespace")]
internal static extern void extism_function_set_namespace(IntPtr ptr, string @namespace);
/// <summary>
/// Free an <see cref="ExtismFunction"/>
/// </summary>
/// <param name="ptr"></param>
[DllImport("extism", EntryPoint = "extism_function_free")]
internal static extern void extism_function_free(IntPtr ptr);
/// <summary>
/// Create a new context.
/// </summary>
/// <returns>A pointer to the newly created context.</returns>
[DllImport("extism")]
unsafe internal static extern ExtismContext* extism_context_new();
public static extern IntPtr extism_context_new();
/// <summary>
/// Remove a context from the registry and free associated memory.
/// </summary>
/// <param name="context"></param>
[DllImport("extism")]
unsafe internal static extern void extism_context_free(ExtismContext* context);
public static extern void extism_context_free(IntPtr context);
/// <summary>
/// Load a WASM plugin.
@@ -205,7 +32,7 @@ internal static class LibExtism
/// <param name="withWasi">Enables/disables WASI.</param>
/// <returns></returns>
[DllImport("extism")]
unsafe internal static extern int extism_plugin_new(ExtismContext* context, byte* wasm, int wasmSize, IntPtr* functions, int nFunctions, bool withWasi);
unsafe public static extern IntPtr extism_plugin_new(IntPtr context, byte* wasm, int wasmSize, IntPtr *functions, int nFunctions, bool withWasi);
/// <summary>
/// Update a plugin, keeping the existing ID.
@@ -215,13 +42,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="wasmSize">The length of the `wasm` parameter.</param>
/// <param name="wasmLength">The length of the `wasm` parameter.</param>
/// <param name="functions">Array of host function pointers.</param>
/// <param name="nFunctions">Number of host functions.</param>
/// <param name="withWasi">Enables/disables WASI.</param>
/// <returns></returns>
[DllImport("extism")]
unsafe internal static extern bool extism_plugin_update(ExtismContext* context, int plugin, byte* wasm, long wasmSize, Span<IntPtr> functions, long nFunctions, bool withWasi);
unsafe public static extern bool extism_plugin_update(IntPtr context, IntPtr plugin, byte* wasm, int wasmLength, IntPtr *functions, int nFunctions, bool withWasi);
/// <summary>
/// Remove a plugin from the registry and free associated memory.
@@ -229,14 +56,14 @@ internal static class LibExtism
/// <param name="context">Pointer to the context the plugin is associated with.</param>
/// <param name="plugin">Pointer to the plugin you want to free.</param>
[DllImport("extism")]
unsafe internal static extern void extism_plugin_free(ExtismContext* context, int plugin);
public static extern void extism_plugin_free(IntPtr context, IntPtr plugin);
/// <summary>
/// Remove all plugins from the registry.
/// </summary>
/// <param name="context"></param>
[DllImport("extism")]
unsafe internal static extern void extism_context_reset(ExtismContext* context);
public static extern void extism_context_reset(IntPtr context);
/// <summary>
/// Update plugin config values, this will merge with the existing values.
@@ -247,7 +74,7 @@ internal static class LibExtism
/// <param name="jsonLength">The length of the `json` parameter.</param>
/// <returns></returns>
[DllImport("extism")]
unsafe internal static extern bool extism_plugin_config(ExtismContext* context, int plugin, byte* json, int jsonLength);
unsafe public static extern bool extism_plugin_config(IntPtr context, IntPtr plugin, byte* json, int jsonLength);
/// <summary>
/// Returns true if funcName exists.
@@ -257,7 +84,7 @@ internal static class LibExtism
/// <param name="funcName"></param>
/// <returns></returns>
[DllImport("extism")]
unsafe internal static extern bool extism_plugin_function_exists(ExtismContext* context, int plugin, string funcName);
public static extern bool extism_plugin_function_exists(IntPtr context, IntPtr plugin, string funcName);
/// <summary>
/// Call a function.
@@ -269,7 +96,7 @@ internal static class LibExtism
/// <param name="dataLen">The length of the `data` parameter.</param>
/// <returns></returns>
[DllImport("extism")]
unsafe internal static extern int extism_plugin_call(ExtismContext* context, int plugin, string funcName, byte* data, int dataLen);
unsafe public static extern int extism_plugin_call(IntPtr context, IntPtr plugin, string funcName, byte* data, int dataLen);
/// <summary>
/// Get the error associated with a Context or Plugin, if plugin is -1 then the context error will be returned.
@@ -278,7 +105,7 @@ internal static class LibExtism
/// <param name="plugin">A plugin pointer, or -1 for the context error.</param>
/// <returns></returns>
[DllImport("extism")]
unsafe internal static extern IntPtr extism_error(ExtismContext* context, nint plugin);
public static extern IntPtr extism_error(IntPtr context, nint plugin);
/// <summary>
/// Get the length of a plugin's output data.
@@ -287,7 +114,7 @@ internal static class LibExtism
/// <param name="plugin"></param>
/// <returns></returns>
[DllImport("extism")]
unsafe internal static extern long extism_plugin_output_length(ExtismContext* context, int plugin);
public static extern long extism_plugin_output_length(IntPtr context, IntPtr plugin);
/// <summary>
/// Get the plugin's output data.
@@ -296,7 +123,7 @@ internal static class LibExtism
/// <param name="plugin"></param>
/// <returns></returns>
[DllImport("extism")]
unsafe internal static extern IntPtr extism_plugin_output_data(ExtismContext* context, int plugin);
public static extern IntPtr extism_plugin_output_data(IntPtr context, IntPtr plugin);
/// <summary>
/// Set log file and level.
@@ -305,43 +132,43 @@ internal static class LibExtism
/// <param name="logLevel"></param>
/// <returns></returns>
[DllImport("extism")]
internal static extern bool extism_log_file(string filename, string logLevel);
public static extern bool extism_log_file(string filename, string logLevel);
/// <summary>
/// Get the Extism version string.
/// </summary>
/// <returns></returns>
[DllImport("extism", EntryPoint = "extism_version")]
internal static extern IntPtr extism_version();
public static extern IntPtr extism_version();
/// <summary>
/// Extism Log Levels
/// </summary>
internal static class LogLevels
public static class LogLevels
{
/// <summary>
/// Designates very serious errors.
/// </summary>
internal const string Error = "Error";
public const string Error = "Error";
/// <summary>
/// Designates hazardous situations.
/// </summary>
internal const string Warn = "Warn";
public const string Warn = "Warn";
/// <summary>
/// Designates useful information.
/// </summary>
internal const string Info = "Info";
public const string Info = "Info";
/// <summary>
/// Designates lower priority information.
/// </summary>
internal const string Debug = "Debug";
public const string Debug = "Debug";
/// <summary>
/// Designates very low priority, often extremely verbose, information.
/// </summary>
internal const string Trace = "Trace";
public const string Trace = "Trace";
}
}
}

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
using Extism.Sdk.Native;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using Xunit;
@@ -10,17 +9,6 @@ namespace Extism.Sdk.Tests;
public class BasicTests
{
[Fact]
public void CountHelloWorldVowelsWithoutContext()
{
var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
var wasm = File.ReadAllBytes(Path.Combine(binDirectory, "code.wasm"));
using var plugin = Plugin.Create(wasm, Array.Empty<HostFunction>(), withWasi: true);
var response = plugin.CallFunction("count_vowels", Encoding.UTF8.GetBytes("Hello World"));
Assert.Equal("{\"count\": 3}", Encoding.UTF8.GetString(response));
}
[Fact]
public void CountHelloWorldVowels()
{
@@ -28,46 +16,9 @@ public class BasicTests
var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
var wasm = File.ReadAllBytes(Path.Combine(binDirectory, "code.wasm"));
using var plugin = context.CreatePlugin(wasm, Array.Empty<HostFunction>(), withWasi: true);
using var plugin = context.CreatePlugin(wasm, withWasi: true);
var response = plugin.CallFunction("count_vowels", Encoding.UTF8.GetBytes("Hello World"));
Assert.Equal("{\"count\": 3}", Encoding.UTF8.GetString(response));
}
[Fact]
public void CountVowelsHostFunctions()
{
using var context = new Context();
var userData = Marshal.StringToHGlobalAnsi("Hello again!");
using var helloWorld = new HostFunction(
"hello_world",
"env",
new[] { ExtismValType.I64 },
new[] { ExtismValType.I64 },
userData,
HelloWorld);
var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
var wasm = File.ReadAllBytes(Path.Combine(binDirectory, "code-functions.wasm"));
using var plugin = context.CreatePlugin(wasm, new[] { helloWorld }, withWasi: true);
var response = plugin.CallFunction("count_vowels", Encoding.UTF8.GetBytes("Hello World"));
Assert.Equal("{\"count\": 3}", Encoding.UTF8.GetString(response));
void HelloWorld(CurrentPlugin plugin, Span<ExtismVal> inputs, Span<ExtismVal> outputs, nint data)
{
Console.WriteLine("Hello from .NET!");
var text = Marshal.PtrToStringAnsi(data);
Console.WriteLine(text);
var input = plugin.ReadString(new nint(inputs[0].v.i64));
Console.WriteLine($"Input: {input}");
var output = new string(input); // clone the string
outputs[0].v.i64 = plugin.WriteString(output);
}
}
}
}

View File

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

View File

@@ -26,7 +26,7 @@
(ctypes-foreign (>= 0.18.0))
(bigstringaf (>= 0.9.0))
(ppx_yojson_conv (>= v0.15.0))
(extism-manifest (= :version))
extism-manifest
(ppx_inline_test (>= v0.15.0))
(cmdliner (>= 1.1.1))
)

View File

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

View File

@@ -15,25 +15,12 @@ defmodule Extism.Plugin do
}
end
@doc """
Creates a new plugin
"""
def new(manifest, wasi \\ false, context \\ nil) do
ctx = context || Extism.Context.new()
{:ok, manifest_payload} = JSON.encode(manifest)
case Extism.Native.plugin_new_with_manifest(ctx.ptr, manifest_payload, wasi) do
{:error, err} -> {:error, err}
res -> {:ok, Extism.Plugin.wrap_resource(ctx, res)}
end
end
@doc """
Call a plugin's function by name
## Examples
iex> {:ok, plugin} = Extism.Plugin.new(manifest, false)
iex> {:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
iex> {:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
# {:ok, "{\"count\": 4}"}

View File

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

View File

@@ -1,12 +1,12 @@
%{
"earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"},
"ex_doc": {:hex, :ex_doc, "0.30.5", "aa6da96a5c23389d7dc7c381eba862710e108cee9cfdc629b7ec021313900e9e", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "88a1e115dcb91cefeef7e22df4a6ebbe4634fbf98b38adcbc25c9607d6d9d8e6"},
"earmark_parser": {:hex, :earmark_parser, "1.4.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"},
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
"json": {:hex, :json, "1.4.1", "8648f04a9439765ad449bc56a3ff7d8b11dd44ff08ffcdefc4329f7c93843dfa", [:mix], [], "hexpm", "9abf218dbe4ea4fcb875e087d5f904ef263d012ee5ed21d46e9dbca63f053d16"},
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
"rustler": {:hex, :rustler, "0.29.1", "880f20ae3027bd7945def6cea767f5257bc926f33ff50c0d5d5a5315883c084d", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "109497d701861bfcd26eb8f5801fe327a8eef304f56a5b63ef61151ff44ac9b6"},
"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"},
"toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
}

View File

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

View File

@@ -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();

View File

@@ -173,9 +173,8 @@ func (ctx *Context) Free() {
// Plugin is used to call WASM functions
type Plugin struct {
ctx *Context
id int32
functions []Function
ctx *Context
id int32
}
type WasmData struct {
@@ -273,7 +272,7 @@ func register(ctx *Context, data []byte, functions []Function, wasi bool) (Plugi
)
}
return Plugin{id: int32(plugin), ctx: ctx, functions: functions}, nil
return Plugin{id: int32(plugin), ctx: ctx}, nil
}
func update(ctx *Context, plugin int32, data []byte, functions []Function, wasi bool) error {
@@ -324,18 +323,6 @@ 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)
@@ -362,7 +349,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)
}
@@ -373,7 +360,6 @@ func (p *Plugin) UpdateManifest(manifest Manifest, functions []Function, wasi bo
return err
}
p.functions = functions
return update(p.ctx, p.id, data, functions, wasi)
}

View File

@@ -16,7 +16,7 @@ depends: [
"ctypes-foreign" {>= "0.18.0"}
"bigstringaf" {>= "0.9.0"}
"ppx_yojson_conv" {>= "v0.15.0"}
"extism-manifest" {= version}
"extism-manifest"
"ppx_inline_test" {>= "v0.15.0"}
"cmdliner" {>= "1.1.1"}
"odoc" {with-doc}

1
go/code-functions.wasm Symbolic link
View File

@@ -0,0 +1 @@
../wasm/code-functions.wasm

View File

@@ -36,6 +36,9 @@ func main() {
version := extism.ExtismVersion()
fmt.Println("Extism Version: ", version)
ctx := extism.NewContext()
defer ctx.Free() // this will free the context and all associated plugins
// set some input data to provide to the plugin module
var data []byte
if len(os.Args) > 1 {
@@ -43,10 +46,10 @@ func main() {
} else {
data = []byte("testing from go -> wasm shared memory...")
}
manifest := extism.Manifest{Wasm: []extism.Wasm{extism.WasmFile{Path: "../wasm/code-functions.wasm"}}}
manifest := extism.Manifest{Wasm: []extism.Wasm{extism.WasmFile{Path: "code-functions.wasm"}}}
f := extism.NewFunction("hello_world", []extism.ValType{extism.I64}, []extism.ValType{extism.I64}, C.hello_world, "Hello again!")
defer f.Free()
plugin, err := extism.NewPluginFromManifest(manifest, []extism.Function{f}, true)
plugin, err := ctx.PluginFromManifest(manifest, []extism.Function{f}, true)
if err != nil {
fmt.Println(err)
os.Exit(1)

View File

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

1
haskell/code.wasm Symbolic link
View File

@@ -0,0 +1 @@
../wasm/code.wasm

View File

@@ -1,6 +1,6 @@
cabal-version: 3.0
name: extism
version: 0.5.0
version: 0.2.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-doc-files: CHANGELOG.md
extra-source-files: CHANGELOG.md
library
exposed-modules: Extism Extism.CurrentPlugin
exposed-modules: Extism
reexported-modules: Extism.Manifest
hs-source-dirs: src
other-modules: Extism.Bindings
@@ -19,10 +19,10 @@ library
extra-libraries: extism
extra-lib-dirs: /usr/local/lib
build-depends:
base >= 4.16.1 && < 5,
bytestring >= 0.11.3 && <= 0.12,
json >= 0.10 && <= 0.11,
extism-manifest >= 0.0.0 && < 0.4.0
base >= 4.16.1 && < 4.19.0,
bytestring >= 0.11.3 && < 0.12,
json >= 0.10 && < 0.11,
extism-manifest >= 0.0.0 && < 0.3.0
test-suite extism-example
type: exitcode-stdio-1.0

View File

@@ -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 = makeObj
nonNull = NotNull
objectWithNulls x = makeObj x
nonNull x = NotNull x
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

View File

@@ -1,6 +1,6 @@
cabal-version: 3.0
name: extism-manifest
version: 0.3.0
version: 0.2.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-doc-files: CHANGELOG.md
extra-source-files: CHANGELOG.md
library
exposed-modules: Extism.Manifest Extism.JSON
hs-source-dirs: .
default-language: Haskell2010
build-depends:
base >= 4.16.1 && < 5,
bytestring >= 0.11.3 && <= 0.12,
json >= 0.10 && <= 0.11,
base >= 4.16.1 && < 4.19.0,
bytestring >= 0.11.3 && < 0.12,
json >= 0.10 && < 0.11,
base64-bytestring >= 1.2.1 && < 1.3,

View File

@@ -1,21 +1,10 @@
module Extism (
module Extism,
module Extism.Manifest,
ValType(..),
Val(..)
) where
module Extism (module Extism, module Extism.Manifest) where
import Data.Int
import Data.Word
import Control.Monad (void)
import Foreign.ForeignPtr
import Foreign.C.String
import Foreign.Ptr
import Foreign.Marshal.Array
import Foreign.Storable
import Foreign.StablePtr
import Foreign.Concurrent
import Foreign.Marshal.Utils (copyBytes, moveBytes)
import Data.ByteString as B
import Data.ByteString.Internal (c2w, w2c)
import Data.ByteString.Unsafe (unsafeUseAsCString)
@@ -27,17 +16,10 @@ import Extism.Bindings
-- | Context for managing plugins
newtype Context = Context (ForeignPtr ExtismContext)
-- | Host function
data Function = Function (ForeignPtr ExtismFunction) (StablePtr ())
-- | Plugins can be used to call WASM function
data Plugin = Plugin Context Int32 [Function]
data Plugin = Plugin Context Int32
-- | Cancellation handle for Plugins
newtype CancelHandle = CancelHandle (Ptr ExtismCancelHandle)
-- | Access the plugin that is currently executing from inside a host function
type CurrentPlugin = Ptr ExtismCurrentPlugin
data CancelHandle = CancelHandle (Ptr ExtismCancelHandle)
-- | Log level
data LogLevel = Error | Warn | Info | Debug | Trace deriving (Show)
@@ -71,94 +53,68 @@ reset (Context ctx) =
newContext :: IO Context
newContext = do
ptr <- extism_context_new
fptr <- Foreign.ForeignPtr.newForeignPtr extism_context_free ptr
fptr <- newForeignPtr extism_context_free ptr
return (Context fptr)
-- | Execute a function with a new 'Context' that is destroyed when it returns
withContext :: (Context -> IO a) -> IO a
withContext f = do
ctx <- newContext
f ctx
-- | Execute a function with the provided 'Plugin' as a parameter, then frees the 'Plugin'
-- | before returning the result.
withPlugin :: (Plugin -> IO a) -> Plugin -> IO a
withPlugin f plugin = do
res <- f plugin
free plugin
return res
-- | Create a 'Plugin' from a WASM module, `useWasi` determines if WASI should
-- | be linked
plugin :: Context -> B.ByteString -> [Function] -> Bool -> IO (Result Plugin)
plugin c wasm functions useWasi =
let nfunctions = fromIntegral (Prelude.length functions) in
plugin :: Context -> B.ByteString -> Bool -> IO (Result Plugin)
plugin c wasm useWasi =
let length = fromIntegral (B.length wasm) in
let wasi = fromInteger (if useWasi then 1 else 0) in
let Context ctx = c in
do
funcs <- Prelude.mapM (\(Function ptr _) -> withForeignPtr ptr (\x -> do return x)) functions
withForeignPtr ctx (\ctx -> do
p <- unsafeUseAsCString wasm (\s ->
withArray funcs (\funcs ->
extism_plugin_new ctx (castPtr s) length funcs nfunctions wasi ))
extism_plugin_new ctx (castPtr s) length nullPtr 0 wasi )
if p < 0 then do
err <- extism_error ctx (-1)
e <- peekCString err
return $ Left (ExtismError e)
else
return $ Right (Plugin c p functions))
-- | Create a 'Plugin' with its own 'Context'
createPlugin :: B.ByteString -> [Function] -> Bool -> IO (Result Plugin)
createPlugin c functions useWasi = do
ctx <- newContext
plugin ctx c functions useWasi
return $ Right (Plugin c p))
-- | Create a 'Plugin' from a 'Manifest'
pluginFromManifest :: Context -> Manifest -> [Function] -> Bool -> IO (Result Plugin)
pluginFromManifest ctx manifest functions useWasi =
pluginFromManifest :: Context -> Manifest -> Bool -> IO (Result Plugin)
pluginFromManifest ctx manifest useWasi =
let wasm = toByteString $ toString manifest in
plugin ctx wasm functions useWasi
-- | Create a 'Plugin' with its own 'Context' from a 'Manifest'
createPluginFromManifest :: Manifest -> [Function] -> Bool -> IO (Result Plugin)
createPluginFromManifest manifest functions useWasi = do
ctx <- newContext
pluginFromManifest ctx manifest functions useWasi
plugin ctx wasm useWasi
-- | Update a 'Plugin' with a new WASM module
update :: Plugin -> B.ByteString -> [Function] -> Bool -> IO (Result Plugin)
update (Plugin (Context ctx) id _) wasm functions useWasi =
let nfunctions = fromIntegral (Prelude.length functions) in
update :: Plugin -> B.ByteString -> Bool -> IO (Result ())
update (Plugin (Context ctx) id) wasm useWasi =
let length = fromIntegral (B.length wasm) in
let wasi = fromInteger (if useWasi then 1 else 0) in
do
funcs <- Prelude.mapM (\(Function ptr _ ) -> withForeignPtr ptr (\x -> do return x)) functions
withForeignPtr ctx (\ctx' -> do
withForeignPtr ctx (\ctx -> do
b <- unsafeUseAsCString wasm (\s ->
withArray funcs (\funcs ->
extism_plugin_update ctx' id (castPtr s) length funcs nfunctions wasi))
extism_plugin_update ctx id (castPtr s) length nullPtr 0 wasi)
if b <= 0 then do
err <- extism_error ctx' (-1)
err <- extism_error ctx (-1)
e <- peekCString err
return $ Left (ExtismError e)
else
return (Right (Plugin (Context ctx) id functions)))
return (Right ()))
-- | Update a 'Plugin' with a new 'Manifest'
updateManifest :: Plugin -> Manifest -> [Function] -> Bool -> IO (Result Plugin)
updateManifest plugin manifest functions useWasi =
updateManifest :: Plugin -> Manifest -> Bool -> IO (Result ())
updateManifest plugin manifest useWasi =
let wasm = toByteString $ toString manifest in
update plugin wasm functions useWasi
update plugin wasm useWasi
-- | Check if a 'Plugin' is valid
isValid :: Plugin -> Bool
isValid (Plugin _ p _) = p >= 0
isValid (Plugin _ p) = p >= 0
-- | Set configuration values for a plugin
setConfig :: Plugin -> [(String, Maybe String)] -> IO Bool
setConfig (Plugin (Context ctx) plugin _) x =
setConfig (Plugin (Context ctx) plugin) x =
if plugin < 0
then return False
else
@@ -187,14 +143,14 @@ setLogFile filename level =
-- | Check if a function exists in the given plugin
functionExists :: Plugin -> String -> IO Bool
functionExists (Plugin (Context ctx) plugin _) name = do
functionExists (Plugin (Context ctx) plugin) name = do
withForeignPtr ctx (\ctx -> do
b <- withCString name (extism_plugin_function_exists ctx plugin)
if b == 1 then return True else return False)
--- | Call a function provided by the given plugin
call :: Plugin -> String -> B.ByteString -> IO (Result B.ByteString)
call (Plugin (Context ctx) plugin _) name input =
call (Plugin (Context ctx) plugin) name input =
let length = fromIntegral (B.length input) in
do
withForeignPtr ctx (\ctx -> do
@@ -216,73 +172,15 @@ call (Plugin (Context ctx) plugin _) name input =
-- | Free a 'Plugin', this will automatically be called for every plugin
-- | associated with a 'Context' when that 'Context' is freed
free :: Plugin -> IO ()
free (Plugin (Context ctx) plugin _) =
free (Plugin (Context ctx) plugin) =
withForeignPtr ctx (`extism_plugin_free` plugin)
-- | Create a new 'CancelHandle' that can be used to cancel a running plugin
-- | from another thread.
cancelHandle :: Plugin -> IO CancelHandle
cancelHandle (Plugin (Context ctx) plugin _) = do
handle <- withForeignPtr ctx (`extism_plugin_cancel_handle` plugin)
cancelHandle (Plugin (Context ctx) plugin) = do
handle <- withForeignPtr ctx (\ctx -> extism_plugin_cancel_handle ctx 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

View File

@@ -7,77 +7,10 @@ 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 ())
@@ -95,28 +28,3 @@ 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

View File

@@ -1,48 +0,0 @@
module Extism.CurrentPlugin where
import Extism
import Extism.Bindings
import Data.Word
import Data.ByteString as B
import Foreign.Ptr
import Foreign.Marshal.Array
-- | Allocate a new handle of the given size
memoryAlloc :: CurrentPlugin -> Word64 -> IO Word64
memoryAlloc = extism_current_plugin_memory_alloc
-- | Get the length of a handle, returns 0 if the handle is invalid
memoryLength :: CurrentPlugin -> Word64 -> IO Word64
memoryLength = extism_current_plugin_memory_length
-- | Free allocated memory
memoryFree :: CurrentPlugin -> Word64 -> IO ()
memoryFree = extism_current_plugin_memory_free
-- | Access a pointer to the entire memory region
memory :: CurrentPlugin -> IO (Ptr Word8)
memory = extism_current_plugin_memory
-- | Access a pointer the a specific offset in memory
memoryOffset :: CurrentPlugin -> Word64 -> IO (Ptr Word8)
memoryOffset plugin offs = do
x <- extism_current_plugin_memory plugin
return $ plusPtr x (fromIntegral offs)
-- | Access the data associated with a handle as a 'ByteString'
memoryBytes :: CurrentPlugin -> Word64 -> IO B.ByteString
memoryBytes plugin offs = do
ptr <- memoryOffset plugin offs
len <- memoryLength plugin offs
arr <- peekArray (fromIntegral len) ptr
return $ B.pack arr
-- | Allocate memory and copy an existing 'ByteString' into it
allocBytes :: CurrentPlugin -> B.ByteString -> IO Word64
allocBytes plugin s = do
let length = B.length s
offs <- memoryAlloc plugin (fromIntegral length)
ptr <- memoryOffset plugin offs
pokeArray ptr (B.unpack s)
return offs

View File

@@ -1,7 +1,6 @@
import Test.HUnit
import Extism
import Extism.Manifest
import Extism.CurrentPlugin
unwrap (Right x) = return x
@@ -9,58 +8,46 @@ unwrap (Left (ExtismError msg)) =
assertFailure msg
defaultManifest = manifest [wasmFile "../../wasm/code.wasm"]
hostFunctionManifest = manifest [wasmFile "../../wasm/code-functions.wasm"]
initPlugin :: Maybe Context -> IO Plugin
initPlugin Nothing =
Extism.createPluginFromManifest defaultManifest [] False >>= unwrap
initPlugin (Just ctx) =
Extism.pluginFromManifest ctx defaultManifest [] False >>= unwrap
initPlugin :: Context -> IO Plugin
initPlugin context =
Extism.pluginFromManifest context defaultManifest False >>= unwrap
pluginFunctionExists = do
p <- initPlugin Nothing
exists <- functionExists p "count_vowels"
assertBool "function exists" exists
exists' <- functionExists p "function_doesnt_exist"
assertBool "function doesn't exist" (not exists')
withContext (\ctx -> do
p <- initPlugin ctx
exists <- functionExists p "count_vowels"
assertBool "function exists" exists
exists' <- functionExists p "function_doesnt_exist"
assertBool "function doesn't exist" (not exists'))
checkCallResult p = do
res <- call p "count_vowels" (toByteString "this is a test") >>= unwrap
assertEqual "count vowels output" "{\"count\": 4}" (fromByteString res)
res <- call p "count_vowels" (toByteString "this is a test") >>= unwrap
assertEqual "count vowels output" "{\"count\": 4}" (fromByteString res)
pluginCall = do
p <- initPlugin Nothing
checkCallResult p
hello plugin params () = do
putStrLn "Hello from Haskell!"
offs <- allocBytes plugin (toByteString "{\"count\": 999}")
return [toI64 offs]
pluginCallHostFunction = do
p <- Extism.createPluginFromManifest hostFunctionManifest [] False >>= unwrap
res <- call p "count_vowels" (toByteString "this is a test") >>= unwrap
assertEqual "count vowels output" "{\"count\": 999}" (fromByteString res)
withContext (\ctx -> do
p <- initPlugin ctx
checkCallResult p)
pluginMultiple = do
withContext(\ctx -> do
p <- initPlugin (Just ctx)
withContext (\ctx -> do
p <- initPlugin ctx
checkCallResult p
q <- initPlugin (Just ctx)
r <- initPlugin (Just ctx)
q <- initPlugin ctx
r <- initPlugin ctx
checkCallResult q
checkCallResult r)
pluginUpdate = do
withContext (\ctx -> do
p <- initPlugin (Just ctx)
updateManifest p defaultManifest [] True >>= unwrap
p <- initPlugin ctx
updateManifest p defaultManifest True >>= unwrap
checkCallResult p)
pluginConfig = do
withContext (\ctx -> do
p <- initPlugin (Just ctx)
p <- initPlugin ctx
b <- setConfig p [("a", Just "1"), ("b", Just "2"), ("c", Just "3"), ("d", Nothing)]
assertBool "set config" b)
@@ -75,7 +62,6 @@ main = do
[
t "Plugin.FunctionExists" pluginFunctionExists
, t "Plugin.Call" pluginCall
, t "Plugin.CallHostFunction" pluginCallHostFunction
, t "Plugin.Multiple" pluginMultiple
, t "Plugin.Update" pluginUpdate
, t "Plugin.Config" pluginConfig

View File

@@ -1,194 +1,193 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.extism.sdk</groupId>
<artifactId>extism</artifactId>
<packaging>jar</packaging>
<version>0.5.0</version>
<name>extism</name>
<url>https://github.com/extism/extism</url>
<description>Java-SDK for Extism to use webassembly from Java</description>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.extism.sdk</groupId>
<artifactId>extism</artifactId>
<packaging>jar</packaging>
<version>0.3.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>
<stagingRepositories>target/staging-deploy</stagingRepositories>
</maven-central>
</nexus2>
</maven>
</deploy>
</jreleaser>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<release>${java.version}</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration>
<systemPropertyVariables>
<jna.library.path>../target/release</jna.library.path>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</build>
<dependencies>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>${jna.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit-jupiter-engine.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj-core.version}</version>
<scope>test</scope>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<release>${java.version}</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration>
<systemPropertyVariables>
<jna.library.path>../target/release</jna.library.path>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>${jna.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit-jupiter-engine.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj-core.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>uk.org.webcompere</groupId>
<artifactId>model-assert</artifactId>
<version>1.0.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<dependency>
<groupId>uk.org.webcompere</groupId>
<artifactId>model-assert</artifactId>
<version>1.0.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -62,16 +62,6 @@ 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);

View File

@@ -20,10 +20,10 @@ public class PluginTests {
@Test
public void shouldInvokeFunctionWithMemoryOptions() {
//FIXME check whether memory options are effective
var manifest = new Manifest(List.of(CODE.pathWasmSource()), new MemoryOptions(0));
assertThrows(ExtismException.class, () -> {
Extism.invokeFunction(manifest, "count_vowels", "Hello World");
});
var output = Extism.invokeFunction(manifest, "count_vowels", "Hello World");
assertThat(output).isEqualTo("{\"count\": 3}");
}
@Test

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,525 +0,0 @@
//! # Extism kernel
//!
//! - Isolated memory from both host and plugin
//! - An allocator for managing that memory
//! - Input/output handling
//! - Error message handling
//! - Backward compatible `extism_*` functions
//!
//! ## Allocator
//!
//! The Extism allocator is a bump allocator that tracks the `length` of the total number of bytes
//! available to the allocator and `position` to track how much of the data has been used. Things like memory
//! have not really been optimized at all. When a new allocation that is larger than the remaning size is made,
//! the allocator attempts to call `memory.grow` if that fails a `0` offset is returned, which should be interpreted
//! as a failed allocation.
//!
//! ## Input/Output
//!
//! Input and output are just allocated blocks of memory that are marked as either input or output using
//! the `extism_input_set` or `extism_output_set` functions. The MemoryRoot field `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` field 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;
/// 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.
///
/// This means that the offset of a `Block` is the size of `Root` plus the size of all existing `Blocks`
/// including their data.
#[repr(C)]
pub struct MemoryRoot {
/// Set to true after initialization
pub initialized: AtomicBool,
/// Position of the bump allocator, relative to `blocks` field
pub position: AtomicU64,
/// The total size of all data allocated using this allocator
pub length: AtomicU64,
/// Offset of error block
pub error: AtomicU64,
/// Input position in memory
pub input_offset: Pointer,
/// Input length
pub input_length: Length,
/// Output position in memory
pub output_offset: Pointer,
/// Output length
pub output_length: Length,
/// A pointer to the start of the first block
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`, this is always stored at offset 1 in memory
#[inline]
unsafe fn memory_root() -> &'static mut MemoryRoot {
&mut *(1 as *mut MemoryRoot)
}
impl MemoryRoot {
/// Initialize or load the `MemoryRoot` from the correct position in memory
pub unsafe fn new() -> &'static mut MemoryRoot {
let root = memory_root();
// If this fails then `INITIALIZED` is already `true` and we can just return the
// already initialized `MemoryRoot`
if root
.initialized
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
return root;
}
// Ensure that at least one page is allocated to store the `MemoryRoot` data
if core::arch::wasm32::memory_size(0) == 0 {
if core::arch::wasm32::memory_grow(0, 1) == usize::MAX {
core::arch::wasm32::unreachable()
}
}
root.input_offset = 0;
root.input_length = 0;
root.output_offset = 0;
root.output_length = 0;
root.error.store(0, Ordering::Release);
// Initialize the `MemoryRoot` length, position and data
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);
self.error.store(0, Ordering::Release);
self.input_offset = 0;
self.input_length = 0;
self.output_offset = 0;
self.output_length = 0;
}
#[inline(always)]
#[allow(unused)]
fn pointer_in_bounds(&self, p: Pointer) -> bool {
let start_ptr = self.blocks.as_ptr() as Pointer;
p >= start_ptr && p < start_ptr + self.length.load(Ordering::Acquire) as Pointer
}
#[inline(always)]
#[allow(unused)]
fn pointer_in_bounds_fast(p: Pointer) -> bool {
// Similar to `pointer_in_bounds` but less accurate on the upper bound. This uses the total memory size,
// instead of checking `MemoryRoot::length`
let end = core::arch::wasm32::memory_size(0) << 16;
p >= core::mem::size_of::<Self>() as Pointer && p <= end as Pointer
}
// 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;
// Get the block status, this lets us know if we are able to re-use it
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 >= 128 {
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 !Self::pointer_in_bounds_fast(offs) {
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) as *mut MemoryBlock
}
/// Mark a block as free
pub fn free(&mut self) {
self.status
.store(MemoryStatus::Free as u8, Ordering::Release);
}
}
// Extism functions
/// Allocate a block of memory and return the offset
#[no_mangle]
pub unsafe fn extism_alloc(n: Length) -> Pointer {
if n == 0 {
return 0;
}
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) {
if p == 0 {
return;
}
let root = MemoryRoot::new();
let block = root.find_block(p);
if let Some(block) = block {
block.free();
// If the input pointer is freed for some reason, make sure the input length to 0
// since the original data is gone
if p == root.input_offset {
root.input_length = 0;
}
}
}
/// 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 {
#[cfg(feature = "bounds-checking")]
if !MemoryRoot::pointer_in_bounds_fast(p) {
return 0;
}
*(p as *mut u8)
}
/// Load a u64 from Extism-managed memory
#[no_mangle]
pub unsafe fn extism_load_u64(p: Pointer) -> u64 {
#[cfg(feature = "bounds-checking")]
if !MemoryRoot::pointer_in_bounds_fast(p + core::mem::size_of::<u64>() as u64 - 1) {
return 0;
}
*(p as *mut u64)
}
/// Load a byte from the input data
#[no_mangle]
pub unsafe fn extism_input_load_u8(p: Pointer) -> u8 {
let root = MemoryRoot::new();
#[cfg(feature = "bounds-checking")]
if p >= root.input_length {
return 0;
}
*((root.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 {
let root = MemoryRoot::new();
#[cfg(feature = "bounds-checking")]
if p + core::mem::size_of::<u64>() as Pointer > root.input_length {
return 0;
}
*((root.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) {
#[cfg(feature = "bounds-checking")]
if !MemoryRoot::pointer_in_bounds_fast(p) {
return;
}
*(p as *mut u8) = x;
}
/// Write a u64 in Extism-managed memory
#[no_mangle]
pub unsafe fn extism_store_u64(p: Pointer, x: u64) {
#[cfg(feature = "bounds-checking")]
if !MemoryRoot::pointer_in_bounds_fast(p + core::mem::size_of::<u64>() as u64 - 1) {
return;
}
*(p as *mut u64) = x;
}
/// Set the range of the input data in memory
#[no_mangle]
pub unsafe fn extism_input_set(p: Pointer, len: Length) {
let root = MemoryRoot::new();
#[cfg(feature = "bounds-checking")]
{
if !root.pointer_in_bounds(p) || !root.pointer_in_bounds(p + len - 1) {
return;
}
}
root.input_offset = p;
root.input_length = len;
}
/// Set the range of the output data in memory
#[no_mangle]
pub unsafe fn extism_output_set(p: Pointer, len: Length) {
let root = MemoryRoot::new();
#[cfg(feature = "bounds-checking")]
{
if !root.pointer_in_bounds(p) || !root.pointer_in_bounds(p + len - 1) {
return;
}
}
root.output_offset = p;
root.output_length = len;
}
/// Get the input length
#[no_mangle]
pub fn extism_input_length() -> Length {
unsafe { MemoryRoot::new().input_length }
}
/// Get the input offset in Exitsm-managed memory
#[no_mangle]
pub fn extism_input_offset() -> Length {
unsafe { MemoryRoot::new().input_offset }
}
/// Get the output length
#[no_mangle]
pub unsafe fn extism_output_length() -> Length {
unsafe { MemoryRoot::new().output_length }
}
/// Get the output offset in Extism-managed memory
#[no_mangle]
pub unsafe fn extism_output_offset() -> Length {
MemoryRoot::new().output_offset
}
/// Reset the allocator
#[no_mangle]
pub unsafe fn extism_reset() {
MemoryRoot::new().reset()
}
/// Set the error message offset
#[no_mangle]
pub unsafe fn extism_error_set(ptr: Pointer) {
let root = MemoryRoot::new();
// Allow ERROR to be set to 0
if ptr == 0 {
root.error.store(ptr, Ordering::SeqCst);
return;
}
if !root.pointer_in_bounds(ptr) {
return;
}
root.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 {
MemoryRoot::new().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)
}

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ use std::path::{Path, PathBuf};
#[deprecated]
pub type ManifestMemory = MemoryOptions;
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[derive(Default, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct MemoryOptions {
@@ -12,7 +12,7 @@ pub struct MemoryOptions {
pub max_pages: Option<u32>,
}
#[derive(Clone, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct HttpRequest {
@@ -43,7 +43,7 @@ impl HttpRequest {
}
}
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[derive(Default, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct WasmMetadata {
@@ -81,7 +81,7 @@ impl From<Vec<u8>> for Wasm {
#[deprecated]
pub type ManifestWasm = Wasm;
#[derive(Clone, serde::Serialize, serde::Deserialize)]
#[derive(serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
#[serde(untagged)]
#[serde(deny_unknown_fields)]
@@ -153,7 +153,7 @@ fn base64_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::
schema.into()
}
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[derive(Default, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Manifest {

1
node/code-functions.wasm Symbolic link
View File

@@ -0,0 +1 @@
../wasm/code-functions.wasm

View File

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

795
node/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@extism/extism",
"version": "0.5.0",
"version": "0.3.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": "^20.1.0",
"@types/node": "^18.11.4",
"jest": "^29.2.2",
"prettier": "3.0.2",
"prettier": "2.8.4",
"ts-jest": "^29.0.3",
"ts-node": "^10.9.1",
"typedoc": "^0.24.1",
"typescript": "^5.0.4"
"typedoc": "^0.23.18",
"typescript": "^4.8.4"
}
}

View File

@@ -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(manifest, wasi, functions, config, this);
return new Plugin(this, manifest, wasi, functions, config);
}
/**
@@ -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,22 +589,19 @@ 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,
ctx: Context | null = null,
config?: PluginConfig
) {
if (ctx == null) {
ctx = new Context();
}
let dataRaw: string | Buffer;
if (Buffer.isBuffer(manifest) || typeof manifest === "string") {
dataRaw = manifest;
@@ -624,7 +621,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);
@@ -643,7 +640,7 @@ export class Plugin {
ctx.pointer,
this.id,
s,
Buffer.byteLength(s, "utf-8"),
Buffer.byteLength(s, "utf-8")
);
}
}
@@ -669,7 +666,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") {
@@ -691,7 +688,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);
@@ -707,7 +704,7 @@ export class Plugin {
this.ctx.pointer,
this.id,
s,
Buffer.byteLength(s, "utf-8"),
Buffer.byteLength(s, "utf-8")
);
}
}
@@ -724,7 +721,7 @@ export class Plugin {
return lib.extism_plugin_function_exists(
this.ctx.pointer,
this.id,
functionName,
functionName
);
}
@@ -742,7 +739,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) => {
@@ -752,7 +749,7 @@ export class Plugin {
this.id,
functionName,
input.toString(),
Buffer.byteLength(input, "utf-8"),
Buffer.byteLength(input, "utf-8")
);
if (rc !== 0) {
var err = lib.extism_error(this.ctx.pointer, this.id);
@@ -766,7 +763,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);
});

Binary file not shown.

View File

@@ -1,5 +1,4 @@
VERSION?=0.4.0
TAG?=0.5.0
VERSION?=0.2.0
build:
dune build
@@ -13,4 +12,4 @@ prepare:
opam install .. --deps-only
publish:
opam publish -v $(VERSION) https://github.com/extism/extism/archive/refs/tags/v$(TAG).tar.gz ..
opam publish -v $(VERSION) -t $(VERSION) ..

View File

@@ -4,9 +4,10 @@ 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 file ~wasi:true |> Result.get_ok in
let plugin = Plugin.create ctx file ~wasi:true |> Result.get_ok in
let res = Plugin.call plugin ~name:func_name input |> Result.get_ok in
print_endline res

View File

@@ -208,7 +208,7 @@ module Plugin : sig
?config:Manifest.config ->
?wasi:bool ->
?functions:Function.t list ->
?context:Context.t ->
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:Context.t ->
Context.t ->
Manifest.t ->
(t, Error.t) result
(** Make a new plugin from a [Manifest] *)

View File

@@ -26,8 +26,7 @@ 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 = []) ?context wasm =
let ctx = match context with Some c -> c | None -> Context.create () in
let create ?config ?(wasi = false) ?(functions = []) ctx wasm =
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
@@ -49,15 +48,16 @@ let create ?config ?(wasi = false) ?(functions = []) ?context wasm =
let () = Gc.finalise free t in
Ok t
let of_manifest ?wasi ?functions ?context manifest =
let of_manifest ?wasi ?functions ctx manifest =
let data = Manifest.to_json manifest in
create ?wasi ?functions ?context data
create ctx ?wasi ?functions data
let%test "free plugin" =
let manifest = Manifest.(create [ Wasm.file "test/code.wasm" ]) in
let plugin = of_manifest manifest |> Error.unwrap in
free plugin;
true
with_context (fun ctx ->
let plugin = of_manifest ctx manifest |> Error.unwrap in
free plugin;
true)
let update plugin ?config ?(wasi = false) ?(functions = []) wasm =
let { id; ctx; _ } = plugin in
@@ -77,9 +77,7 @@ 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
let () = plugin.functions <- functions in
Ok ()
else Ok ()
let update_manifest plugin ?wasi manifest =
let data = Manifest.to_json manifest in
@@ -87,10 +85,11 @@ let update_manifest plugin ?wasi manifest =
let%test "update plugin manifest and config" =
let manifest = Manifest.(create [ Wasm.file "test/code.wasm" ]) in
let config = [ ("a", Some "1") ] in
let plugin = of_manifest manifest |> Error.unwrap in
let manifest = Manifest.with_config manifest config in
update_manifest plugin manifest |> Result.is_ok
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 call' f { id; ctx; _ } ~name input len =
let rc = f ctx.pointer id name input len in
@@ -115,10 +114,11 @@ let call_bigstring (t : t) ~name input =
let%test "call_bigstring" =
let manifest = Manifest.(create [ Wasm.file "test/code.wasm" ]) in
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}"
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 call (t : t) ~name input =
let len = String.length input in
@@ -127,9 +127,10 @@ let call (t : t) ~name input =
let%test "call" =
let manifest = Manifest.(create [ Wasm.file "test/code.wasm" ]) in
let plugin = of_manifest manifest |> Error.unwrap in
call plugin ~name:"count_vowels" "this is a test"
|> Error.unwrap = "{\"count\": 4}"
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%test "call_functions" =
let open Types.Val_type in
@@ -146,18 +147,22 @@ let%test "call_functions" =
in
let functions = [ hello_world ] in
let manifest = Manifest.(create [ Wasm.file "test/code-functions.wasm" ]) in
let plugin = of_manifest manifest ~functions ~wasi:true |> Error.unwrap in
call plugin ~name:"count_vowels" "this is a test"
|> Error.unwrap = "{\"count\": 4}"
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 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
let plugin = of_manifest manifest |> Error.unwrap in
function_exists plugin "count_vowels"
&& not (function_exists plugin "function_does_not_exist")
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"))
module Cancel_handle = struct
type t = { inner : unit Ctypes.ptr }

View File

@@ -1,5 +1,3 @@
open Ppx_yojson_conv_lib.Yojson_conv
type base64 = string
let yojson_of_base64 x = `String (Base64.encode_exn x)

1
php/example/code.wasm Symbolic link
View File

@@ -0,0 +1 @@
../../wasm/code.wasm

View File

@@ -2,16 +2,17 @@
require_once __DIR__ . '/vendor/autoload.php';
$wasm = file_get_contents("../../wasm/code.wasm");
$plugin = new \Extism\Plugin($wasm);
$ctx = new \Extism\Context();
$wasm = file_get_contents("code.wasm");
$plugin = new \Extism\Plugin($ctx, $wasm);
$output = $plugin->call("count_vowels", "this is an example");
$json = json_decode(pack('C*', ...$output));
echo "Vowels counted = " . $json->{'count'} . PHP_EOL;
$wasm = file_get_contents("../../wasm/code.wasm");
$wasm = file_get_contents("code.wasm");
$ok = $plugin->update($wasm);
if ($ok) {
$id = $plugin->getId();
echo "updated plugin: $id";
}
}

View File

@@ -31,12 +31,8 @@ class Plugin
private $id;
public function __construct($data, $wasi = false, $config = null, $ctx = null)
public function __construct($ctx, $data, $wasi = false, $config = null)
{
if ($ctx == null) {
$ctx = new Context();
}
$this->lib = $ctx->lib;
$this->wasi = $wasi;
@@ -53,7 +49,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->toString());
throw new \Exception("Extism: unable to load plugin: " . $err);
}
$this->id = $id;
$this->context = $ctx;
@@ -95,7 +91,7 @@ class Plugin
$msg = "code = " . $rc;
$err = $this->lib->extism_error($this->context->pointer, $this->id);
if ($err) {
$msg = $msg . ", error = " . $err->toString();
$msg = $msg . ", error = " . $err;
}
throw new \Exception("Extism: call to '".$name."' failed with " . $msg);
}
@@ -125,7 +121,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->toString());
throw new \Exception("Extism: unable to update plugin: " . $err);
}
if ($config != null) {
@@ -142,4 +138,4 @@ function string_to_bytes($string) {
}
return $bytes;
}
}

View File

@@ -20,7 +20,7 @@ lint:
poetry run black --check extism/ tests/ example.py
docs:
poetry run pycco extism/*.py
poetry run pdoc --force --html extism
show-docs: docs
open docs/extism.html
open html/extism/index.html

1
python/code-functions.wasm Symbolic link
View File

@@ -0,0 +1 @@
../wasm/code-functions.wasm

1
python/code.wasm Symbolic link
View File

@@ -0,0 +1 @@
../wasm/code.wasm

View File

@@ -5,13 +5,11 @@ import hashlib
import pathlib
sys.path.append(".")
from extism import Function, host_fn, ValType, Plugin, set_log_file
set_log_file("stderr", "trace")
from extism import Context, Function, host_fn, ValType
@host_fn
def hello_world(plugin, input_, output, a_string):
def hello_world(plugin, input_, output, context, a_string):
print("Hello from Python!")
print(a_string)
print(input_)
@@ -28,33 +26,35 @@ def main(args):
if len(args) > 1:
data = args[1].encode()
else:
data = b"a" * 1024
data = b"some data from python!"
wasm_file_path = (
pathlib.Path(__file__).parent.parent / "wasm" / "code-functions.wasm"
)
wasm = wasm_file_path.read_bytes()
hash = hashlib.sha256(wasm).hexdigest()
manifest = {"wasm": [{"data": wasm, "hash": hash}]}
config = {"wasm": [{"data": wasm, "hash": hash}], "memory": {"max": 5}}
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)
# 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))
print("Number of vowels:", j["count"])
print("Number of vowels:", wasm_vowel_count["count"])
assert j["count"] == count_vowels(data)
assert wasm_vowel_count["count"] == count_vowels(data)
if __name__ == "__main__":

View File

@@ -193,9 +193,7 @@ class Context:
Plugin
The created plugin
"""
return Plugin(
manifest, context=self, wasi=wasi, config=config, functions=functions
)
return Plugin(self, manifest, wasi, config, functions)
class Function:
@@ -249,21 +247,17 @@ class Plugin:
def __init__(
self,
context: Context,
plugin: Union[str, bytes, dict],
context=None,
wasi=False,
config=None,
functions=None,
):
"""
Construct a Plugin
Construct a Plugin. Please use Context#plugin instead.
"""
if context is None:
context = Context()
wasm = _wasm(plugin)
self.functions = functions
# Register plugin
if functions is not None:
@@ -311,7 +305,6 @@ 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(

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "extism"
version = "0.5.0"
version = "0.3.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"
pycco = "^0.6.0"
pdoc3 = "^0.10.0"
[build-system]
requires = ["poetry-core>=1.0.0"]

View File

@@ -104,24 +104,23 @@ 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}]}
return {"wasm": [{"data": wasm, "hash": hash}], "memory": {"max_pages": 5}}
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,
}

View File

@@ -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.19.0"
gem "minitest", "~> 5.18.0"
end

1
ruby/code.wasm Symbolic link
View File

@@ -0,0 +1 @@
../wasm/code.wasm

View File

@@ -1,11 +1,17 @@
require "./lib/extism"
require "json"
manifest = {
:wasm => [{ :path => "../wasm/code.wasm" }],
}
# 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 => "code.wasm" }],
}
plugin = Extism::Plugin.new(manifest)
res = JSON.parse(plugin.call("count_vowels", ARGV[0] || "this is a test"))
plugin = ctx.plugin(manifest)
res = JSON.parse(plugin.call("count_vowels", ARGV[0] || "this is a test"))
puts res["count"]
puts res["count"]
end

View File

@@ -27,12 +27,13 @@ Gem::Specification.new do |spec|
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
end
end
spec.files.reject! { |f| f.end_with?(".wasm") }
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
# Uncomment to register a new dependency of your gem
spec.add_dependency "ffi", ">= 1.0.0"
spec.add_dependency "ffi", "~> 1.0"
# For more information and examples about making a new gem, check out our
# guide at: https://bundler.io/guides/creating_gem.html

View File

@@ -86,13 +86,12 @@ 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(wasm, wasi, config, self)
Plugin.new(self, wasm, wasi, config)
end
end
@@ -132,14 +131,12 @@ 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
# @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
def initialize(context, wasm, wasi = false, config = nil)
@context = context
if wasm.class == Hash
wasm = JSON.generate(wasm)

View File

@@ -1,5 +1,5 @@
# frozen_string_literal: true
module Extism
VERSION = '0.5.0'
VERSION = "0.3.0"
end

Binary file not shown.

View File

@@ -1,6 +1,6 @@
[package]
name = "extism-runtime"
version = "0.5.4"
version = "0.3.0"
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 = ">= 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}
wasmtime = "6.0.1"
wasmtime-wasi = "6.0.1"
wasmtime-wasi-nn = {version = "6.0.1", optional=true}
anyhow = "1"
serde = {version = "1", features = ["derive"]}
serde_json = "1"
@@ -22,7 +22,8 @@ log4rs = "1.1"
url = "2"
glob = "0.3"
ureq = {version = "2.5", optional=true}
extism-manifest = { version = "0.5.0", path = "../manifest" }
extism-manifest = { version = "0.3.0", path = "../manifest" }
pretty-hex = { version = "0.3" }
uuid = { version = "1", features = ["v4"] }
libc = "0.2"

View File

@@ -1,6 +1,4 @@
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)
@@ -18,7 +16,7 @@ fn main() {
.rename_item("Context", "ExtismContext")
.rename_item("ValType", "ExtismValType")
.rename_item("ValUnion", "ExtismValUnion")
.rename_item("Internal", "ExtismCurrentPlugin")
.rename_item("Plugin", "ExtismCurrentPlugin")
.with_style(cbindgen::Style::Type)
.generate()
{

View File

@@ -54,7 +54,7 @@ typedef struct ExtismCancelHandle ExtismCancelHandle;
typedef struct ExtismFunction ExtismFunction;
/**
* Internal stores data that is available to the caller in PDK functions
* Plugin contains everything needed to execute a WASM function
*/
typedef struct ExtismCurrentPlugin ExtismCurrentPlugin;
@@ -81,12 +81,7 @@ 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;
@@ -250,7 +245,7 @@ const char *extism_error(ExtismContext *ctx, ExtismPlugin plugin);
ExtismSize extism_plugin_output_length(ExtismContext *ctx, ExtismPlugin plugin);
/**
* Get a pointer to the output data
* Get the length of a plugin's output data
*/
const uint8_t *extism_plugin_output_data(ExtismContext *ctx, ExtismPlugin plugin);

View File

@@ -1,3 +1,4 @@
use std::cell::UnsafeCell;
use std::collections::{BTreeMap, VecDeque};
use crate::*;
@@ -7,7 +8,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, Plugin>,
pub plugins: BTreeMap<PluginIndex, UnsafeCell<Plugin>>,
/// Error message
pub error: Option<std::ffi::CString>,
@@ -90,7 +91,7 @@ impl Context {
return -1;
}
};
self.plugins.insert(id, plugin);
self.plugins.insert(id, UnsafeCell::new(plugin));
id
}
@@ -126,7 +127,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),
Some(x) => Some(x.get_mut()),
None => None,
}
}

Binary file not shown.

View File

@@ -169,12 +169,12 @@ impl Function {
) -> Function
where
F: 'static
+ Fn(&mut Internal, &[Val], &mut [Val], UserData) -> Result<(), Error>
+ Fn(&mut crate::Plugin, &[Val], &mut [Val], UserData) -> Result<(), Error>
+ Sync
+ Send,
{
let user_data = user_data.unwrap_or_default();
let data = user_data.make_copy();
let data = UserData::new_pointer(user_data.ptr, None);
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(), inp, outp, data.make_copy())
f(caller.data_mut().plugin_mut(), inp, outp, data.make_copy())
}),
namespace: None,
_user_data: std::sync::Arc::new(user_data),

View File

@@ -1,348 +0,0 @@
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> {
if n == 0 {
return Ok(0);
}
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 && n > 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)
}
}

View File

@@ -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 plugin::Plugin;
pub use memory::{MemoryBlock, PluginMemory, ToMemoryBlock};
pub use plugin::{Internal, Plugin, Wasi};
pub use plugin_ref::PluginRef;
pub(crate) use timer::{Timer, TimerAction};

View File

@@ -7,7 +7,7 @@ use sha2::Digest;
use crate::*;
/// Manifest wraps the manifest exported by `extism_manifest`
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[derive(Default, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct Manifest(extism_manifest::Manifest);
@@ -60,8 +60,6 @@ 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 {
@@ -169,7 +167,6 @@ 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 {
@@ -181,14 +178,12 @@ impl Manifest {
}
let t = serde_json::from_slice::<Self>(data)?;
let mut m = t.modules(engine)?;
m.insert("env".to_string(), extism_module);
let m = t.modules(engine)?;
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))
}

331
runtime/src/memory.rs Normal file
View File

@@ -0,0 +1,331 @@
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 }
}
}

View File

@@ -18,6 +18,178 @@ 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)
@@ -27,25 +199,19 @@ 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 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)?
}
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)?,
None => {
output[0] = Val::I64(0);
return Ok(());
}
};
output[0] = Val::I64(mem as i64);
output[0] = Val::I64(mem.offset as i64);
Ok(())
}
@@ -58,25 +224,21 @@ 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 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)?
}
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)?,
None => {
output[0] = Val::I64(0);
return Ok(());
}
};
output[0] = Val::I64(mem as i64);
output[0] = Val::I64(mem.offset as i64);
Ok(())
}
@@ -89,38 +251,33 @@ 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 data.vars.values() {
for v in plugin.vars.values() {
size += v.len();
}
let voffset = args!(input, 1, i64) as u64;
let voffset = args!(input, 1, i64) as usize;
// 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 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)) }
};
let key_offs = args!(input, 0, i64) as usize;
let key = plugin.memory.get_str(key_offs)?;
// Remove if the value offset is 0
if voffset == 0 {
data.vars.remove(key);
plugin.vars.remove(key);
return Ok(());
}
let vlen = data.memory_length(voffset);
let value = data.memory_read(voffset, vlen).to_vec();
let value = plugin.memory.get(voffset)?;
// Insert the value from memory into the `vars` map
data.vars.insert(key.to_string(), value);
plugin.vars.insert(key.to_string(), value.to_vec());
Ok(())
}
@@ -146,38 +303,34 @@ 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 u64;
let http_req_offset = args!(input, 0, i64) as usize;
let http_req_len = data.memory_length(http_req_offset);
let req: extism_manifest::HttpRequest =
serde_json::from_slice(data.memory_read(http_req_offset, http_req_len))?;
serde_json::from_slice(data.memory().get(http_req_offset)?)?;
let body_offset = args!(input, 1, i64) as u64;
let body_offset = args!(input, 1, i64) as usize;
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.internal().manifest.as_ref().allowed_hosts;
let allowed_hosts = &data.plugin().manifest.as_ref().allowed_hosts;
let host_str = url.host_str().unwrap_or_default();
let host_matches = if let Some(allowed_hosts) = allowed_hosts {
allowed_hosts.iter().any(|url| {
if let Some(allowed_hosts) = allowed_hosts {
let host_matches_allowed = allowed_hosts.iter().any(|url| {
let pat = match glob::Pattern::new(url) {
Ok(x) => x,
Err(_) => return url == host_str,
};
pat.matches(host_str)
})
} else {
false
};
if !host_matches {
return Err(Error::msg(format!(
"HTTP request to {} is not allowed",
req.url
)));
});
if !host_matches_allowed {
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);
@@ -187,41 +340,23 @@ pub(crate) fn http_request(
}
let res = if body_offset > 0 {
let len = data.memory_length(body_offset);
let buf = data.memory_read(body_offset, len);
r.send_bytes(buf)
let buf = data.memory().get(body_offset)?;
let res = r.send_bytes(buf)?;
data.http_status = res.status();
res.into_reader()
} else {
r.call()
let res = r.call()?;
data.http_status = res.status();
res.into_reader()
};
let reader = match res {
Ok(res) => {
data.http_status = res.status();
Some(res.into_reader())
}
Err(e) => {
log::error!("Unable to make HTTP request: {:?}", e);
if let Some(res) = e.into_response() {
data.http_status = res.status();
Some(res.into_reader())
} else {
None
}
}
};
let mut buf = Vec::new();
res.take(1024 * 1024 * 50) // TODO: make this limit configurable
.read_to_end(&mut 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);
}
let mem = data.memory_mut().alloc_bytes(buf)?;
output[0] = Val::I64(mem.offset as i64);
Ok(())
}
}
@@ -239,17 +374,39 @@ 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,
mut caller: Caller<Internal>,
caller: Caller<Internal>,
input: &[Val],
_output: &mut [Val],
) -> Result<(), Error> {
let data: &mut Internal = caller.data_mut();
let offset = args!(input, 0, i64) as u64;
let buf = data.memory_read_str(offset);
let data: &Internal = caller.data();
let offset = args!(input, 0, i64) as usize;
let buf = data.memory().get(offset)?;
match buf {
match std::str::from_utf8(buf) {
Ok(buf) => log::log!(level, "{}", buf),
Err(_) => log::log!(level, "{:?}", buf),
}

View File

@@ -4,121 +4,97 @@ use crate::*;
/// Plugin contains everything needed to execute a WASM function
pub struct Plugin {
/// All modules that were provided to the linker
pub modules: BTreeMap<String, Module>,
/// Used to define functions and create new instances
pub module: Module,
pub linker: Linker<Internal>,
pub store: Store<Internal>,
/// Instance provides the ability to call functions in a module
pub instance: Option<Instance>,
pub instance_pre: InstancePre<Internal>,
/// Keep track of the number of times we're instantiated, this exists
/// to avoid issues with memory piling up since `Instance`s are only
/// actually cleaned up along with a `Store`
instantiations: usize,
/// The ID used to identify this plugin with the `Timer`
pub 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 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>,
}
impl InternalExt for Plugin {
fn store(&self) -> &Store<Internal> {
&self.store
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,
})
}
fn store_mut(&mut self) -> &mut Store<Internal> {
&mut self.store
pub fn plugin(&self) -> &Plugin {
unsafe { &*self.plugin }
}
fn linker(&self) -> &Linker<Internal> {
&self.linker
pub fn plugin_mut(&mut self) -> &mut Plugin {
unsafe { &mut *self.plugin }
}
fn linker_mut(&mut self) -> &mut Linker<Internal> {
&mut self.linker
pub fn memory(&self) -> &PluginMemory {
&self.plugin().memory
}
fn linker_and_store(&mut self) -> (&mut Linker<Internal>, &mut Store<Internal>) {
(&mut self.linker, &mut self.store)
pub fn memory_mut(&mut self) -> &mut PluginMemory {
&mut self.plugin_mut().memory
}
}
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>(
@@ -126,37 +102,26 @@ 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())
.profiler(profiling_strategy()),
.debug_info(std::env::var("EXTISM_DEBUG").is_ok()),
)?;
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)?);
// 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:?}");
store.epoch_deadline_callback(|_internal| Err(Error::msg("timeout")));
let mut store = Store::new(
&engine,
Internal::new(manifest, with_wasi, available_pages)?,
);
store.epoch_deadline_callback(|_internal| Ok(wasmtime::UpdateDeadline::Continue(1)));
let memory = Memory::new(
&mut store,
MemoryType::new(4, manifest.as_ref().memory.max_pages),
)?;
let mut memory = PluginMemory::new(store, memory);
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
@@ -173,14 +138,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),*)?]);
linker.func_new(EXPORT_MODULE_NAME, concat!("extism_", stringify!($name)), t, pdk::$name)?;
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))?;
continue
}
)*
@@ -190,10 +155,7 @@ impl Plugin {
}
// Add builtins
for (name, module) in modules.iter() {
if name != main_name {
linker.module(&mut store, name, module)?;
}
for (_name, module) in modules.iter() {
for import in module.imports() {
let module_name = import.module();
let name = import.name();
@@ -201,293 +163,249 @@ 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)?;
}
}
}
}
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)
})?;
// 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")?;
}
}
let instance_pre = linker.instantiate_pre(&main)?;
let instance = linker.instantiate(&mut memory.store, main)?;
let timer_id = uuid::Uuid::new_v4();
let mut plugin = Plugin {
modules,
module: main.clone(),
linker,
instance: None,
instance_pre,
store,
runtime: None,
memory,
instance,
last_error: std::cell::RefCell::new(None),
manifest,
vars: BTreeMap::new(),
should_reinstantiate: false,
timer_id,
cancel_handle: sdk::ExtismCancelHandle {
id: timer_id,
epoch_timer_tx: None,
},
instantiations: 0,
};
plugin.internal_mut().store = &mut plugin.store;
plugin.internal_mut().linker = &mut plugin.linker;
plugin.initialize_runtime()?;
Ok(plugin)
}
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)
});
let engine = self.store.engine().clone();
let internal = self.internal();
self.store = Store::new(
&engine,
Internal::new(
internal.manifest.clone(),
internal.wasi.is_some(),
internal.available_pages,
)?,
);
self.store
.epoch_deadline_callback(|_internal| Ok(UpdateDeadline::Continue(1)));
if self.internal().available_pages.is_some() {
self.store
.limiter(|internal| internal.memory_limiter.as_mut().unwrap());
}
for (name, module) in self.modules.iter() {
if name != main_name {
self.linker.module(&mut self.store, name, module)?;
}
}
self.instantiations = 0;
self.instance_pre = self.linker.instantiate_pre(&main)?;
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;
}
Ok(())
}
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(())
}
/// 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;
}
}
self.instance
.get_func(&mut self.memory.store, function.as_ref())
}
if let Some(instance) = &mut self.instance {
instance.get_func(&mut self.store, function.as_ref())
} else {
None
}
/// 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));
}
pub fn error<E>(&self, e: impl std::fmt::Debug, x: E) -> E {
self.set_error(e);
x
}
/// Unset `last_error` field
pub fn clear_error(&self) {
*self.last_error.borrow_mut() = None;
}
/// Store input in memory and initialize `Internal` pointer
pub(crate) fn set_input(&mut self, input: *const u8, mut len: usize) -> Result<(), Error> {
pub fn set_input(&mut self, input: *const u8, mut len: usize) {
if input.is_null() {
len = 0;
}
let ptr = self as *mut _;
let internal = self.memory.store.data_mut();
internal.input = input;
internal.input_length = len;
internal.plugin = ptr;
}
{
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;
}
if len > 0 {
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 [],
)?;
}
}
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;
self.initialize_runtime()?;
Ok(())
}
/// Determine if wasi is enabled
pub fn has_wasi(&self) -> bool {
self.internal().wasi.is_some()
self.memory.store.data().wasi.is_some()
}
fn detect_runtime(&mut self) {
fn detect_runtime(&mut self) -> Option<Runtime> {
// Check for Haskell runtime initialization functions
// Initialize Haskell runtime if `hs_init` is present,
// 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") {
let reactor_init = if let Some(init) = self.get_func("_initialize") {
if init.typed::<(), ()>(&self.store()).is_err() {
if let Some(cleanup) = self.get_func("hs_exit") {
if init.typed::<(i32, i32), ()>(&self.memory.store).is_err() {
trace!(
"_initialize function found with type {:?}",
init.ty(self.store())
"hs_init function found with type {:?}",
init.ty(&self.memory.store)
);
None
} else {
trace!("WASI reactor module detected");
Some(init)
return None;
}
} else {
None
};
self.runtime = Some(Runtime::Haskell { init, reactor_init });
return;
return Some(Runtime::Haskell { init, cleanup });
}
}
// 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");
None
}
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");
}
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;
}
}
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.store_mut().set_epoch_deadline(1);
self.store
.epoch_deadline_callback(|_internal| Err(Error::msg("timeout")));
let engine: Engine = self.store().engine().clone();
self.memory.store.set_epoch_deadline(1);
let engine: Engine = self.memory.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 })?;
}
self.store
.epoch_deadline_callback(|_internal| Ok(wasmtime::UpdateDeadline::Continue(1)));
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 })?;
}
Ok(())
}
}
// Enumerates the supported PDK language runtimes
#[derive(Clone)]
pub(crate) enum Runtime {
Haskell {
init: Func,
reactor_init: Option<Func>,
},
Wasi {
init: Func,
},
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:?}");
}
}
}
}
}
}

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