mirror of
https://github.com/extism/extism.git
synced 2026-04-23 03:00:11 -04:00
Compare commits
40 Commits
fix-releas
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37b8e5bd12 | ||
|
|
c6b8429c67 | ||
|
|
ad114f44d2 | ||
|
|
45180ad473 | ||
|
|
9546dac689 | ||
|
|
197e934258 | ||
|
|
339556597b | ||
|
|
64927d9bcd | ||
|
|
86f1117ad5 | ||
|
|
34be80a7ad | ||
|
|
3e6a0071e9 | ||
|
|
d17b693c4b | ||
|
|
18fceec8f8 | ||
|
|
f5cf4f184e | ||
|
|
821661d391 | ||
|
|
f28e01125e | ||
|
|
c0faa53df5 | ||
|
|
be7961bbf6 | ||
|
|
4af4f7c41a | ||
|
|
0003246ff7 | ||
|
|
886e01b959 | ||
|
|
b30bcc3601 | ||
|
|
b57d54e63e | ||
|
|
ba2516650d | ||
|
|
97b4582aa4 | ||
|
|
762feb1056 | ||
|
|
6267682266 | ||
|
|
b1ca2f398d | ||
|
|
58ad4ce6e2 | ||
|
|
d0296de9a7 | ||
|
|
4db1303273 | ||
|
|
28d16f2fa8 | ||
|
|
e3dd34e59f | ||
|
|
bb6026976c | ||
|
|
33c0f8a4c8 | ||
|
|
b57acde149 | ||
|
|
7e8031fcdc | ||
|
|
e6499cab72 | ||
|
|
e44800f7f6 | ||
|
|
d6b403e112 |
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@@ -3,8 +3,8 @@ on: [pull_request, workflow_dispatch]
|
||||
name: CI
|
||||
|
||||
env:
|
||||
RUNTIME_MANIFEST: runtime/Cargo.toml
|
||||
RUNTIME_CRATE: extism-runtime
|
||||
LIBEXTISM_CRATE: libextism
|
||||
RUST_SDK_CRATE: extism
|
||||
|
||||
jobs:
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
- name: Build
|
||||
if: steps.cache-libextism.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: cargo build --release -p ${{ env.RUNTIME_CRATE }}
|
||||
run: cargo build --release -p ${{ env.LIBEXTISM_CRATE }}
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -170,6 +170,7 @@ jobs:
|
||||
- name: Test Python Host SDK
|
||||
run: |
|
||||
cd python
|
||||
cp ../README.md .
|
||||
poetry install
|
||||
poetry run python example.py
|
||||
poetry run python -m unittest discover
|
||||
@@ -225,6 +226,12 @@ jobs:
|
||||
LD_LIBRARY_PATH=/usr/local/lib npm run example
|
||||
LD_LIBRARY_PATH=/usr/local/lib npm run test
|
||||
|
||||
- name: Test Browser Runtime
|
||||
run: |
|
||||
cd browser
|
||||
npm i
|
||||
npm run test
|
||||
|
||||
ocaml:
|
||||
name: OCaml
|
||||
needs: lib
|
||||
|
||||
8
.github/workflows/release-elixir.yaml
vendored
8
.github/workflows/release-elixir.yaml
vendored
@@ -10,7 +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:
|
||||
|
||||
8
.github/workflows/release-python.yaml
vendored
8
.github/workflows/release-python.yaml
vendored
@@ -16,19 +16,19 @@ jobs:
|
||||
with:
|
||||
python-version: "3.9"
|
||||
check-latest: true
|
||||
- name: Run image
|
||||
uses: abatilo/actions-poetry@v2
|
||||
|
||||
- name: Build Python Host SDK
|
||||
run: |
|
||||
cd python
|
||||
cp ../LICENSE .
|
||||
cp ../README.md .
|
||||
make clean
|
||||
make build
|
||||
make prepare
|
||||
poetry build
|
||||
|
||||
- name: Release Python Host SDK
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
env:
|
||||
INPUT_VERIFY_METADATA: false
|
||||
with:
|
||||
user: ${{ secrets.PYPI_API_USER }}
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
|
||||
2
.github/workflows/release-ruby.yaml
vendored
2
.github/workflows/release-ruby.yaml
vendored
@@ -21,5 +21,5 @@ jobs:
|
||||
RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_TOKEN }}
|
||||
run: |
|
||||
cd ruby
|
||||
make publish
|
||||
make publish RUBYGEMS_API_KEY=$RUBYGEMS_API_KEY
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -7,7 +7,7 @@ name: Release
|
||||
|
||||
env:
|
||||
RUNTIME_MANIFEST: runtime/Cargo.toml
|
||||
RUNTIME_CRATE: extism-runtime
|
||||
RUNTIME_CRATE: libextism
|
||||
RUSTFLAGS: -C target-feature=-crt-static
|
||||
ARTIFACT_DIR: release-artifacts
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ members = [
|
||||
"manifest",
|
||||
"runtime",
|
||||
"rust",
|
||||
"libextism"
|
||||
]
|
||||
exclude = [
|
||||
"elixir/native/extism_nif"
|
||||
]
|
||||
|
||||
|
||||
2
Makefile
2
Makefile
@@ -25,7 +25,7 @@ lint:
|
||||
cargo clippy --release --no-deps --manifest-path runtime/Cargo.toml
|
||||
|
||||
build:
|
||||
cargo build --release $(FEATURE_FLAGS) --manifest-path runtime/Cargo.toml
|
||||
cargo build --release $(FEATURE_FLAGS) --manifest-path libextism/Cargo.toml
|
||||
|
||||
install:
|
||||
install runtime/extism.h $(DEST)/include
|
||||
|
||||
130
browser/.gitignore
vendored
Normal file
130
browser/.gitignore
vendored
Normal file
@@ -0,0 +1,130 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
5
browser/.prettierrc
Normal file
5
browser/.prettierrc
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true
|
||||
}
|
||||
25
browser/Makefile
Normal file
25
browser/Makefile
Normal file
@@ -0,0 +1,25 @@
|
||||
.PHONY: test
|
||||
|
||||
prepare:
|
||||
npm install
|
||||
|
||||
test: prepare
|
||||
npm run test
|
||||
|
||||
clean:
|
||||
echo "No clean implemented"
|
||||
|
||||
publish: clean prepare
|
||||
npm publish
|
||||
|
||||
format:
|
||||
npx prettier --write src
|
||||
|
||||
lint:
|
||||
npx prettier --check src
|
||||
|
||||
docs:
|
||||
npx typedoc --out doc src
|
||||
|
||||
show-docs: docs
|
||||
open doc/index.html
|
||||
23
browser/build.js
Normal file
23
browser/build.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const { build } = require("esbuild");
|
||||
const { dependencies, peerDependencies } = require('./package.json')
|
||||
|
||||
const sharedConfig = {
|
||||
entryPoints: ["src/index.ts"],
|
||||
bundle: true,
|
||||
minify: false,
|
||||
drop: [], // preseve debugger statements
|
||||
external: Object.keys(dependencies || {}).concat(Object.keys(peerDependencies || {})),
|
||||
};
|
||||
|
||||
build({
|
||||
...sharedConfig,
|
||||
platform: 'node', // for CJS
|
||||
outfile: "dist/index.js",
|
||||
});
|
||||
|
||||
build({
|
||||
...sharedConfig,
|
||||
outfile: "dist/index.esm.js",
|
||||
platform: 'neutral', // for ESM
|
||||
format: "esm",
|
||||
});
|
||||
BIN
browser/data/code.wasm
Executable file
BIN
browser/data/code.wasm
Executable file
Binary file not shown.
238
browser/index.html
Normal file
238
browser/index.html
Normal file
@@ -0,0 +1,238 @@
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
|
||||
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
|
||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
|
||||
<style>
|
||||
#main {
|
||||
width: 100%;
|
||||
}
|
||||
.manifest {
|
||||
display: flex; /* or inline-flex */
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
.urlInput {
|
||||
width: 600px;
|
||||
}
|
||||
.funcName {
|
||||
width: 150px;
|
||||
}
|
||||
.textAreas {
|
||||
display: flex; /* or inline-flex */
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
}
|
||||
.inputBox {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.inputBox > textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.outputBox {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.outputBox > textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.space {
|
||||
height: 80px;
|
||||
}
|
||||
.dragAreas {
|
||||
display: flex; /* or inline-flex */
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
.dragInput {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-style: dotted;
|
||||
border-color: #000;
|
||||
}
|
||||
.dragOutput {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.dropZone {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.outputImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script type="text/babel">
|
||||
function getBase64(file, cb) {
|
||||
var reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = function () {
|
||||
cb(reader.result)
|
||||
};
|
||||
reader.onerror = function (error) {
|
||||
console.log("error")
|
||||
};
|
||||
}
|
||||
|
||||
function arrayTob64(buffer) {
|
||||
var binary = '';
|
||||
var bytes = [].slice.call(buffer);
|
||||
bytes.forEach((b) => binary += String.fromCharCode(b));
|
||||
return window.btoa(binary);
|
||||
}
|
||||
|
||||
|
||||
class App extends React.Component {
|
||||
state = {
|
||||
url: "https://raw.githubusercontent.com/extism/extism/main/wasm/code.wasm",
|
||||
input: new Uint8Array(),
|
||||
output: new Uint8Array(),
|
||||
func_name: "count_vowels",
|
||||
functions: []
|
||||
}
|
||||
|
||||
async loadFunctions(url) {
|
||||
let plugin = await this.extismContext.newPlugin({ "wasm": [ { "path": url } ] })
|
||||
let functions = await plugin.getExportedFunctions()
|
||||
console.log("funcs ", functions)
|
||||
this.setState({functions})
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadFunctions(this.state.url)
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.extismContext = props.extismContext
|
||||
}
|
||||
|
||||
handleInputChange(e) {
|
||||
e.preventDefault();
|
||||
this.setState({ [e.target.name]: e.target.value })
|
||||
if (e.target.name === "url") {
|
||||
this.loadFunctions(e.target.value)
|
||||
}
|
||||
}
|
||||
|
||||
onInputKeyPress(e) {
|
||||
if (e.keyCode == 13 && e.shiftKey == true) {
|
||||
e.preventDefault()
|
||||
this.handleOnRun()
|
||||
}
|
||||
}
|
||||
|
||||
async handleOnRun(e) {
|
||||
e && e.preventDefault && e.preventDefault();
|
||||
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})
|
||||
}
|
||||
|
||||
nop = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
handleDrop = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
let files = [...e.dataTransfer.files];
|
||||
if (files && files.length == 1) {
|
||||
let file = files[0]
|
||||
console.log(file)
|
||||
file.arrayBuffer().then(b => {
|
||||
this.setState({input: new Uint8Array(b)})
|
||||
this.handleOnRun()
|
||||
})
|
||||
} else {
|
||||
throw Error("Only one file please")
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const funcOptions = this.state.functions.map(f => <option value={f}>{f}</option>)
|
||||
let image = null
|
||||
if (this.state.output) {
|
||||
image = <img src={`data:image/png;base64,${arrayTob64(this.state.output)}`}/>
|
||||
}
|
||||
|
||||
return <div className="app">
|
||||
<div className="manifest">
|
||||
<div>
|
||||
<label>WASM Url: </label>
|
||||
<input type="text" name="url" className="urlInput" value={this.state.url} onChange={this.handleInputChange.bind(this)} />
|
||||
</div>
|
||||
<div>
|
||||
<label>Function: </label>
|
||||
<select type="text" name="func_name" className="funcName" value={this.state.func_name} onChange={this.handleInputChange.bind(this)}>
|
||||
{funcOptions}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<button onClick={this.handleOnRun.bind(this)}>Run</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="textAreas">
|
||||
<div className="inputBox">
|
||||
<h3>Text Input</h3>
|
||||
<textarea name="input" value={this.state.input} onChange={this.handleInputChange.bind(this)} onKeyDown={this.onInputKeyPress.bind(this)}></textarea>
|
||||
</div>
|
||||
<div className="outputBox">
|
||||
<h3>Text Output</h3>
|
||||
<textarea name="output" value={new TextDecoder().decode(this.state.output)} ></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space" />
|
||||
<div className="dragAreas">
|
||||
<div className="dragInput">
|
||||
<h3>Image Input</h3>
|
||||
<div className="dropZone"
|
||||
onDrop={this.handleDrop.bind(this)}
|
||||
onDragOver={this.nop.bind(this)}
|
||||
onDragEnter={this.nop.bind(this)}
|
||||
onDragLeave={this.nop.bind(this)}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dragOutput">
|
||||
<h3>Image Output</h3>
|
||||
<div className="outputImage">
|
||||
{image}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
window.App = App
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
import {ExtismContext} from './dist/index.esm.js'
|
||||
const e = React.createElement;
|
||||
|
||||
window.onload = () => {
|
||||
const domContainer = document.getElementById('main');
|
||||
console.log(domContainer)
|
||||
const root = ReactDOM.createRoot(domContainer);
|
||||
const extismContext = new ExtismContext()
|
||||
root.render(e(App, {extismContext}));
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
</body>
|
||||
</html>
|
||||
5
browser/jest.config.js
Normal file
5
browser/jest.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
};
|
||||
9554
browser/package-lock.json
generated
Normal file
9554
browser/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
browser/package.json
Normal file
34
browser/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@extism/runtime-browser",
|
||||
"version": "0.1.0",
|
||||
"description": "Extism runtime in the browser",
|
||||
"scripts": {
|
||||
"build": "node build.js && tsc --emitDeclarationOnly --outDir dist",
|
||||
"format": "prettier --write \"src/**/*.ts\"",
|
||||
"lint": "tslint -p tsconfig.json",
|
||||
"test": "jest --config jest.config.js"
|
||||
},
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"dist/*"
|
||||
],
|
||||
"module": "dist/index.esm.js",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"author": "The Extism Authors <oss@extism.org>",
|
||||
"license": "BSD-3-Clause",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.2.2",
|
||||
"esbuild": "^0.15.13",
|
||||
"jest": "^29.2.2",
|
||||
"prettier": "^2.7.1",
|
||||
"ts-jest": "^29.0.3",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"typedoc": "^0.23.20",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
104
browser/src/allocator.ts
Normal file
104
browser/src/allocator.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
type MemoryBlock = { offset: bigint; length: bigint };
|
||||
|
||||
export default class Allocator {
|
||||
currentIndex: bigint;
|
||||
active: Record<number, MemoryBlock>;
|
||||
freed: MemoryBlock[];
|
||||
memory: Uint8Array;
|
||||
|
||||
constructor(n: number) {
|
||||
this.currentIndex = BigInt(1);
|
||||
this.active = {};
|
||||
this.freed = [];
|
||||
this.memory = new Uint8Array(n);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.currentIndex = BigInt(1);
|
||||
this.active = {};
|
||||
this.freed = [];
|
||||
}
|
||||
|
||||
alloc(length: bigint): bigint {
|
||||
for (var i = 0; i < this.freed.length; i++) {
|
||||
let block = this.freed[i];
|
||||
if (block.length === length) {
|
||||
this.active[Number(block.offset)] = block;
|
||||
this.freed.splice(i, 1);
|
||||
return block.offset;
|
||||
} else if (block.length > length + BigInt(64)) {
|
||||
const newBlock = { offset: block.offset, length };
|
||||
block.offset += length;
|
||||
block.length -= length;
|
||||
return newBlock.offset;
|
||||
}
|
||||
}
|
||||
|
||||
// Resize memory if needed
|
||||
// TODO: put a limit on the memory size
|
||||
if (BigInt(this.memory.length) < this.currentIndex + length) {
|
||||
const tmp = new Uint8Array(Number(this.currentIndex + length + BigInt(64)));
|
||||
tmp.set(this.memory);
|
||||
this.memory = tmp;
|
||||
}
|
||||
|
||||
const offset = this.currentIndex;
|
||||
this.currentIndex += length;
|
||||
this.active[Number(offset)] = { offset, length };
|
||||
return offset;
|
||||
}
|
||||
|
||||
getBytes(offset: bigint): Uint8Array | null {
|
||||
const block = this.active[Number(offset)];
|
||||
if (!block) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Uint8Array(this.memory.buffer, Number(offset), Number(block.length));
|
||||
}
|
||||
|
||||
getString(offset: bigint): string | null {
|
||||
const bytes = this.getBytes(offset);
|
||||
if (bytes === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new TextDecoder().decode(bytes);
|
||||
}
|
||||
|
||||
allocBytes(data: Uint8Array): bigint {
|
||||
const offs = this.alloc(BigInt(data.length));
|
||||
const bytes = this.getBytes(offs);
|
||||
if (bytes === null) {
|
||||
this.free(offs);
|
||||
return BigInt(0);
|
||||
}
|
||||
|
||||
bytes.set(data);
|
||||
return offs;
|
||||
}
|
||||
|
||||
allocString(data: string): bigint {
|
||||
const bytes = new TextEncoder().encode(data);
|
||||
return this.allocBytes(bytes);
|
||||
}
|
||||
|
||||
getLength(offset: bigint): bigint {
|
||||
const block = this.active[Number(offset)];
|
||||
if (!block) {
|
||||
return BigInt(0);
|
||||
}
|
||||
|
||||
return block.length;
|
||||
}
|
||||
|
||||
free(offset: bigint) {
|
||||
const block = this.active[Number(offset)];
|
||||
if (!block) {
|
||||
return;
|
||||
}
|
||||
|
||||
delete this.active[Number(offset)];
|
||||
this.freed.push(block);
|
||||
}
|
||||
}
|
||||
45
browser/src/context.ts
Normal file
45
browser/src/context.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Manifest, PluginConfig, ManifestWasmFile, ManifestWasmData } from './manifest';
|
||||
import ExtismPlugin from './plugin';
|
||||
|
||||
/**
|
||||
* Can be a {@link Manifest} or just the raw bytes of the WASM module as an ArrayBuffer.
|
||||
* We recommend using {@link Manifest}
|
||||
*/
|
||||
type ManifestData = Manifest | ArrayBuffer;
|
||||
|
||||
/**
|
||||
* A Context is needed to create plugins. The Context
|
||||
* is where your plugins live. Freeing the context
|
||||
* frees all of the plugins in its scope.
|
||||
*/
|
||||
export default class ExtismContext {
|
||||
/**
|
||||
* Create a plugin managed by this context
|
||||
*
|
||||
* @param manifest - The {@link ManifestData} describing the plugin code and config
|
||||
* @param config - Config details for the plugin
|
||||
* @returns A new Plugin scoped to this Context
|
||||
*/
|
||||
async newPlugin(manifest: ManifestData, config?: PluginConfig) {
|
||||
let moduleData: ArrayBuffer | null = null;
|
||||
if (manifest instanceof ArrayBuffer) {
|
||||
moduleData = manifest;
|
||||
} else if ((manifest as Manifest).wasm) {
|
||||
const wasmData = (manifest as Manifest).wasm;
|
||||
if (wasmData.length > 1) throw Error('This runtime only supports one module in Manifest.wasm');
|
||||
const wasm = wasmData[0];
|
||||
if ((wasm as ManifestWasmData).data) {
|
||||
moduleData = (wasm as ManifestWasmData).data;
|
||||
} else if ((wasm as ManifestWasmFile).path) {
|
||||
const response = await fetch((wasm as ManifestWasmFile).path);
|
||||
moduleData = await response.arrayBuffer();
|
||||
console.dir(moduleData);
|
||||
}
|
||||
}
|
||||
if (!moduleData) {
|
||||
throw Error(`Unsure how to interpret manifest ${manifest}`);
|
||||
}
|
||||
|
||||
return new ExtismPlugin(moduleData, config);
|
||||
}
|
||||
}
|
||||
23
browser/src/index.test.ts
Normal file
23
browser/src/index.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ExtismContext } from './';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
function parse(bytes: Uint8Array): any {
|
||||
return JSON.parse(new TextDecoder().decode(bytes));
|
||||
}
|
||||
|
||||
describe('', () => {
|
||||
it('can load and call a plugin', async () => {
|
||||
const data = fs.readFileSync(path.join(__dirname, '..', 'data', 'code.wasm'));
|
||||
const ctx = new ExtismContext();
|
||||
const plugin = await ctx.newPlugin({ wasm: [{ data: data }] });
|
||||
const functions = await plugin.getExports();
|
||||
expect(Object.keys(functions).filter((x) => !x.startsWith('__') && x !== 'memory')).toEqual(['count_vowels']);
|
||||
let output = await plugin.call('count_vowels', 'this is a test');
|
||||
expect(parse(output)).toEqual({ count: 4 });
|
||||
output = await plugin.call('count_vowels', 'this is a test again');
|
||||
expect(parse(output)).toEqual({ count: 7 });
|
||||
output = await plugin.call('count_vowels', 'this is a test thrice');
|
||||
expect(parse(output)).toEqual({ count: 6 });
|
||||
});
|
||||
});
|
||||
3
browser/src/index.ts
Normal file
3
browser/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import ExtismContext from './context';
|
||||
|
||||
export { ExtismContext };
|
||||
40
browser/src/manifest.ts
Normal file
40
browser/src/manifest.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Represents a path or url to a WASM module
|
||||
*/
|
||||
export type ManifestWasmFile = {
|
||||
path: string;
|
||||
name?: string;
|
||||
hash?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents the raw bytes of a WASM file loaded into memory
|
||||
*/
|
||||
export type ManifestWasmData = {
|
||||
data: Uint8Array;
|
||||
name?: string;
|
||||
hash?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* {@link ExtismPlugin} Config
|
||||
*/
|
||||
export type PluginConfig = Map<string, string>;
|
||||
|
||||
/**
|
||||
* The WASM to load as bytes or a path
|
||||
*/
|
||||
export type ManifestWasm = ManifestWasmFile | ManifestWasmData;
|
||||
|
||||
/**
|
||||
* The manifest which describes the {@link ExtismPlugin} code and
|
||||
* runtime constraints.
|
||||
*
|
||||
* @see [Extism > Concepts > Manifest](https://extism.org/docs/concepts/manifest)
|
||||
*/
|
||||
export type Manifest = {
|
||||
wasm: Array<ManifestWasm>;
|
||||
//memory?: ManifestMemory;
|
||||
config?: PluginConfig;
|
||||
allowed_hosts?: Array<string>;
|
||||
};
|
||||
170
browser/src/plugin.ts
Normal file
170
browser/src/plugin.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import Allocator from './allocator';
|
||||
import { PluginConfig } from './manifest';
|
||||
|
||||
export default class ExtismPlugin {
|
||||
moduleData: ArrayBuffer;
|
||||
allocator: Allocator;
|
||||
config?: PluginConfig;
|
||||
vars: Record<string, Uint8Array>;
|
||||
input: Uint8Array;
|
||||
output: Uint8Array;
|
||||
module?: WebAssembly.WebAssemblyInstantiatedSource;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
async getExports(): Promise<WebAssembly.Exports> {
|
||||
const module = await this._instantiateModule();
|
||||
return module.instance.exports;
|
||||
}
|
||||
|
||||
async getImports(): Promise<WebAssembly.ModuleImportDescriptor[]> {
|
||||
const module = await this._instantiateModule();
|
||||
return WebAssembly.Module.imports(module.module);
|
||||
}
|
||||
|
||||
async getInstance(): Promise<WebAssembly.Instance> {
|
||||
const module = await this._instantiateModule();
|
||||
return module.instance;
|
||||
}
|
||||
|
||||
async call(func_name: string, input: Uint8Array | string): Promise<Uint8Array> {
|
||||
const module = await this._instantiateModule();
|
||||
|
||||
if (typeof input === 'string') {
|
||||
this.input = new TextEncoder().encode(input);
|
||||
} else if (input instanceof Uint8Array) {
|
||||
this.input = input;
|
||||
} else {
|
||||
throw new Error('input should be string or Uint8Array');
|
||||
}
|
||||
|
||||
this.allocator.reset();
|
||||
|
||||
let func = module.instance.exports[func_name];
|
||||
if (!func) {
|
||||
throw Error(`function does not exist ${func_name}`);
|
||||
}
|
||||
//@ts-ignore
|
||||
func();
|
||||
return this.output;
|
||||
}
|
||||
|
||||
async _instantiateModule(): Promise<WebAssembly.WebAssemblyInstantiatedSource> {
|
||||
if (this.module) {
|
||||
return this.module;
|
||||
}
|
||||
const environment = this.makeEnv();
|
||||
this.module = await WebAssembly.instantiate(this.moduleData, { env: environment });
|
||||
return this.module;
|
||||
}
|
||||
|
||||
makeEnv(): any {
|
||||
const plugin = this;
|
||||
return {
|
||||
extism_alloc(n: bigint): bigint {
|
||||
return plugin.allocator.alloc(n);
|
||||
},
|
||||
extism_free(n: bigint) {
|
||||
plugin.allocator.free(n);
|
||||
},
|
||||
extism_load_u8(n: bigint): number {
|
||||
return plugin.allocator.memory[Number(n)];
|
||||
},
|
||||
extism_load_u64(n: bigint): bigint {
|
||||
let cast = new DataView(plugin.allocator.memory.buffer, Number(n));
|
||||
return cast.getBigUint64(0, true);
|
||||
},
|
||||
extism_store_u8(offset: bigint, n: number) {
|
||||
plugin.allocator.memory[Number(offset)] = Number(n);
|
||||
},
|
||||
extism_store_u64(offset: bigint, n: bigint) {
|
||||
const tmp = new DataView(plugin.allocator.memory.buffer, Number(offset));
|
||||
tmp.setBigUint64(0, n, true);
|
||||
},
|
||||
extism_input_length(): bigint {
|
||||
return BigInt(plugin.input.length);
|
||||
},
|
||||
extism_input_load_u8(i: bigint): number {
|
||||
return plugin.input[Number(i)];
|
||||
},
|
||||
extism_input_load_u64(idx: bigint): bigint {
|
||||
let cast = new DataView(plugin.input.buffer, Number(idx));
|
||||
return cast.getBigUint64(0, true);
|
||||
},
|
||||
extism_output_set(offset: bigint, length: bigint) {
|
||||
const offs = Number(offset);
|
||||
const len = Number(length);
|
||||
plugin.output = plugin.allocator.memory.slice(offs, offs + len);
|
||||
},
|
||||
extism_error_set(i: bigint) {
|
||||
throw plugin.allocator.getString(i);
|
||||
},
|
||||
extism_config_get(i: bigint): bigint {
|
||||
if (typeof plugin.config === 'undefined') {
|
||||
return BigInt(0);
|
||||
}
|
||||
const key = plugin.allocator.getString(i);
|
||||
if (key === null) {
|
||||
return BigInt(0);
|
||||
}
|
||||
const value = plugin.config.get(key);
|
||||
if (typeof value === 'undefined') {
|
||||
return BigInt(0);
|
||||
}
|
||||
return plugin.allocator.allocString(value);
|
||||
},
|
||||
extism_var_get(i: bigint): bigint {
|
||||
const key = plugin.allocator.getString(i);
|
||||
if (key === null) {
|
||||
return BigInt(0);
|
||||
}
|
||||
const value = plugin.vars[key];
|
||||
if (typeof value === 'undefined') {
|
||||
return BigInt(0);
|
||||
}
|
||||
return plugin.allocator.allocBytes(value);
|
||||
},
|
||||
extism_var_set(n: bigint, i: bigint) {
|
||||
const key = plugin.allocator.getString(n);
|
||||
if (key === null) {
|
||||
return;
|
||||
}
|
||||
const value = plugin.allocator.getBytes(i);
|
||||
if (value === null) {
|
||||
return;
|
||||
}
|
||||
plugin.vars[key] = value;
|
||||
},
|
||||
extism_http_request(n: bigint, i: bigint): number {
|
||||
debugger;
|
||||
return 0;
|
||||
},
|
||||
extism_length(i: bigint): bigint {
|
||||
return plugin.allocator.getLength(i);
|
||||
},
|
||||
extism_log_warn(i: bigint) {
|
||||
const s = plugin.allocator.getString(i);
|
||||
console.warn(s);
|
||||
},
|
||||
extism_log_info(i: bigint) {
|
||||
const s = plugin.allocator.getString(i);
|
||||
console.log(s);
|
||||
},
|
||||
extism_log_debug(i: bigint) {
|
||||
const s = plugin.allocator.getString(i);
|
||||
console.debug(s);
|
||||
},
|
||||
extism_log_error(i: bigint) {
|
||||
const s = plugin.allocator.getString(i);
|
||||
console.error(s);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
12
browser/tsconfig.json
Normal file
12
browser/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
}
|
||||
6
browser/tslint.json
Normal file
6
browser/tslint.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": [
|
||||
"tslint:recommended",
|
||||
"tslint-config-prettier"
|
||||
]
|
||||
}
|
||||
@@ -1,50 +1,50 @@
|
||||
{
|
||||
"name": "extism/extism",
|
||||
"description": "Make your software programmable. Run WebAssembly extensions in your app using the first off-the-shelf, universal plug-in system.",
|
||||
"license": "BSD-3-Clause",
|
||||
"type": "library",
|
||||
"keywords": [
|
||||
"WebAssembly",
|
||||
"plugin-system",
|
||||
"runtime",
|
||||
"plug-in"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "The Extism Authors",
|
||||
"email": "oss@extism.org",
|
||||
"homepage": "https://extism.org"
|
||||
},
|
||||
{
|
||||
"name": "Dylibso, Inc.",
|
||||
"email": "oss@dylib.so",
|
||||
"homepage": "https://dylib.so"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.4 || ^8",
|
||||
"ircmaxell/ffime": "dev-master"
|
||||
"name": "extism/extism",
|
||||
"description": "Make your software programmable. Run WebAssembly extensions in your app using the first off-the-shelf, universal plug-in system.",
|
||||
"license": "BSD-3-Clause",
|
||||
"type": "library",
|
||||
"keywords": [
|
||||
"WebAssembly",
|
||||
"plugin-system",
|
||||
"runtime",
|
||||
"plug-in"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "The Extism Authors",
|
||||
"email": "oss@extism.org",
|
||||
"homepage": "https://extism.org"
|
||||
},
|
||||
"suggest": {},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Extism\\": "php/src/"
|
||||
},
|
||||
"files": [
|
||||
"php/src/Plugin.php",
|
||||
"php/src/generate.php",
|
||||
"php/src/extism.h"
|
||||
]
|
||||
{
|
||||
"name": "Dylibso, Inc.",
|
||||
"email": "oss@dylib.so",
|
||||
"homepage": "https://dylib.so"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.4 || ^8",
|
||||
"ircmaxell/ffime": "dev-master"
|
||||
},
|
||||
"suggest": {},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Extism\\": "php/src/"
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {}
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
},
|
||||
"extra": {},
|
||||
"scripts": {},
|
||||
"scripts-descriptions": {}
|
||||
"files": [
|
||||
"php/src/Context.php",
|
||||
"php/src/Plugin.php",
|
||||
"php/src/extism.h"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {}
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
},
|
||||
"extra": {},
|
||||
"scripts": {},
|
||||
"scripts-descriptions": {}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ defmodule Extism.MixProject do
|
||||
def project do
|
||||
[
|
||||
app: :extism,
|
||||
version: "0.0.1-rc.6",
|
||||
elixir: "~> 1.14",
|
||||
version: "0.1.0",
|
||||
elixir: "~> 1.12",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps(),
|
||||
package: package(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
%{
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.29.0", "4a1cb903ce746aceef9c1f9ae8a6c12b742a5461e6959b9d3b24d813ffbea146", [: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", "f096adb8bbca677d35d278223361c7792d496b3fc0d0224c9d4bc2f651af5db1"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.29.1", "b1c652fa5f92ee9cf15c75271168027f92039b3877094290a75abcaac82a9f77", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "b7745fa6374a36daf484e2a2012274950e084815b936b1319aeebcf7809574f6"},
|
||||
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
|
||||
"json": {:hex, :json, "1.4.1", "8648f04a9439765ad449bc56a3ff7d8b11dd44ff08ffcdefc4329f7c93843dfa", [:mix], [], "hexpm", "9abf218dbe4ea4fcb875e087d5f904ef263d012ee5ed21d46e9dbca63f053d16"},
|
||||
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "extism_nif"
|
||||
version = "0.0.1-rc.6"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Benjamin Eckel <bhelx@simst.im>"]
|
||||
|
||||
@@ -11,5 +11,5 @@ crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
rustler = "0.26.0"
|
||||
extism = { version = "0.0.1-rc.5" }
|
||||
extism = { version = "0.1.0" }
|
||||
log = "0.4"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use rustler::{Atom, Env, Term, ResourceArc};
|
||||
use extism::{Plugin, Context};
|
||||
use std::str;
|
||||
use extism::{Context, Plugin};
|
||||
use rustler::{Atom, Env, ResourceArc, Term};
|
||||
use std::mem;
|
||||
use std::path::Path;
|
||||
use std::str;
|
||||
use std::str::FromStr;
|
||||
use std::sync::RwLock;
|
||||
use std::mem;
|
||||
|
||||
mod atoms {
|
||||
rustler::atoms! {
|
||||
@@ -15,9 +15,12 @@ mod atoms {
|
||||
}
|
||||
|
||||
struct ExtismContext {
|
||||
ctx: RwLock<Context>
|
||||
ctx: RwLock<Context>,
|
||||
}
|
||||
|
||||
unsafe impl Sync for ExtismContext {}
|
||||
unsafe impl Send for ExtismContext {}
|
||||
|
||||
fn load(env: Env, _: Term) -> bool {
|
||||
rustler::resource!(ExtismContext, env);
|
||||
true
|
||||
@@ -27,15 +30,16 @@ fn to_rustler_error(extism_error: extism::Error) -> rustler::Error {
|
||||
match extism_error {
|
||||
extism::Error::UnableToLoadPlugin(msg) => rustler::Error::Term(Box::new(msg)),
|
||||
extism::Error::Message(msg) => rustler::Error::Term(Box::new(msg)),
|
||||
extism::Error::Json(json_err) => rustler::Error::Term(Box::new(json_err.to_string()))
|
||||
extism::Error::Json(json_err) => rustler::Error::Term(Box::new(json_err.to_string())),
|
||||
extism::Error::Runtime(e) => rustler::Error::Term(Box::new(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
fn context_new() -> ResourceArc<ExtismContext> {
|
||||
ResourceArc::new(
|
||||
ExtismContext { ctx: RwLock::new(Context::new()) }
|
||||
)
|
||||
ResourceArc::new(ExtismContext {
|
||||
ctx: RwLock::new(Context::new()),
|
||||
})
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
@@ -51,7 +55,11 @@ fn context_free(ctx: ResourceArc<ExtismContext>) {
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
fn plugin_new_with_manifest(ctx: ResourceArc<ExtismContext>, manifest_payload: String, wasi: bool) -> Result<i32, rustler::Error> {
|
||||
fn plugin_new_with_manifest(
|
||||
ctx: ResourceArc<ExtismContext>,
|
||||
manifest_payload: String,
|
||||
wasi: bool,
|
||||
) -> Result<i32, rustler::Error> {
|
||||
let context = &ctx.ctx.write().unwrap();
|
||||
let result = match Plugin::new(context, manifest_payload, wasi) {
|
||||
Err(e) => Err(to_rustler_error(e)),
|
||||
@@ -67,17 +75,22 @@ fn plugin_new_with_manifest(ctx: ResourceArc<ExtismContext>, manifest_payload: S
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
fn plugin_call(ctx: ResourceArc<ExtismContext>, plugin_id: i32, name: String, input: String) -> Result<String, rustler::Error> {
|
||||
fn plugin_call(
|
||||
ctx: ResourceArc<ExtismContext>,
|
||||
plugin_id: i32,
|
||||
name: String,
|
||||
input: String,
|
||||
) -> Result<String, rustler::Error> {
|
||||
let context = &ctx.ctx.read().unwrap();
|
||||
let plugin = unsafe { Plugin::from_id(plugin_id, context) };
|
||||
let mut plugin = unsafe { Plugin::from_id(plugin_id, context) };
|
||||
let result = match plugin.call(name, input) {
|
||||
Err(e) => Err(to_rustler_error(e)),
|
||||
Ok(result) => {
|
||||
match str::from_utf8(&result) {
|
||||
Ok(output) => Ok(output.to_string()),
|
||||
Err(_e) => Err(rustler::Error::Term(Box::new("Could not read output from plugin")))
|
||||
}
|
||||
}
|
||||
Ok(result) => match str::from_utf8(&result) {
|
||||
Ok(output) => Ok(output.to_string()),
|
||||
Err(_e) => Err(rustler::Error::Term(Box::new(
|
||||
"Could not read output from plugin",
|
||||
))),
|
||||
},
|
||||
};
|
||||
// this forget should be safe because the context will clean up
|
||||
// all it's plugins when it is dropped
|
||||
@@ -86,14 +99,17 @@ fn plugin_call(ctx: ResourceArc<ExtismContext>, plugin_id: i32, name: String, in
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
fn plugin_update_manifest(ctx: ResourceArc<ExtismContext>, plugin_id: i32, manifest_payload: String, wasi: bool) -> Result<(), rustler::Error> {
|
||||
fn plugin_update_manifest(
|
||||
ctx: ResourceArc<ExtismContext>,
|
||||
plugin_id: i32,
|
||||
manifest_payload: String,
|
||||
wasi: bool,
|
||||
) -> Result<(), rustler::Error> {
|
||||
let context = &ctx.ctx.read().unwrap();
|
||||
let mut plugin = unsafe { Plugin::from_id(plugin_id, context) };
|
||||
let result = match plugin.update(manifest_payload, wasi) {
|
||||
Ok(()) => {
|
||||
Ok(())
|
||||
},
|
||||
Err(e) => Err(to_rustler_error(e))
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) => Err(to_rustler_error(e)),
|
||||
};
|
||||
// this forget should be safe because the context will clean up
|
||||
// all it's plugins when it is dropped
|
||||
@@ -113,16 +129,28 @@ fn plugin_free(ctx: ResourceArc<ExtismContext>, plugin_id: i32) -> Result<(), ru
|
||||
fn set_log_file(filename: String, log_level: String) -> Result<Atom, rustler::Error> {
|
||||
let path = Path::new(&filename);
|
||||
match log::Level::from_str(&log_level) {
|
||||
Err(_e) => Err(rustler::Error::Term(Box::new(format!("{} not a valid log level", log_level)))),
|
||||
Err(_e) => Err(rustler::Error::Term(Box::new(format!(
|
||||
"{} not a valid log level",
|
||||
log_level
|
||||
)))),
|
||||
Ok(level) => {
|
||||
extism::set_log_file(path, Some(level));
|
||||
Ok(atoms::ok())
|
||||
if extism::set_log_file(path, Some(level)) {
|
||||
Ok(atoms::ok())
|
||||
} else {
|
||||
Err(rustler::Error::Term(Box::new(
|
||||
"Did not set log file, received false from the API.",
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rustler::nif]
|
||||
fn plugin_has_function(ctx: ResourceArc<ExtismContext>, plugin_id: i32, function_name: String) -> Result<bool, rustler::Error> {
|
||||
fn plugin_has_function(
|
||||
ctx: ResourceArc<ExtismContext>,
|
||||
plugin_id: i32,
|
||||
function_name: String,
|
||||
) -> Result<bool, rustler::Error> {
|
||||
let context = &ctx.ctx.read().unwrap();
|
||||
let plugin = unsafe { Plugin::from_id(plugin_id, context) };
|
||||
let has_function = plugin.has_function(function_name);
|
||||
|
||||
@@ -35,6 +35,8 @@ defmodule ExtismTest do
|
||||
assert JSON.decode(output) == {:ok, %{"count" => 7}}
|
||||
{:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "this is a test thrice")
|
||||
assert JSON.decode(output) == {:ok, %{"count" => 6}}
|
||||
{:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "🌎hello🌎world🌎")
|
||||
assert JSON.decode(output) == {:ok, %{"count" => 3}}
|
||||
Extism.Context.free(ctx)
|
||||
end
|
||||
|
||||
|
||||
@@ -18,6 +18,6 @@ handlePlugin plugin = do
|
||||
exitSuccess) res
|
||||
|
||||
main = do
|
||||
context <- Extism.newContext ()
|
||||
context <- Extism.newContext
|
||||
plugin <- Extism.pluginFromManifest context (manifest [wasmFile "../wasm/code.wasm"]) False
|
||||
try handlePlugin plugin
|
||||
@@ -1,6 +1,6 @@
|
||||
cabal-version: 2.4
|
||||
cabal-version: 3.0
|
||||
name: extism
|
||||
version: 0.0.1.0
|
||||
version: 0.0.1
|
||||
|
||||
-- A short (one-line) description of the package.
|
||||
synopsis: Extism bindings
|
||||
@@ -23,22 +23,41 @@ category: Plugins, WebAssembly
|
||||
extra-source-files: CHANGELOG.md
|
||||
|
||||
library
|
||||
exposed-modules: Extism Extism.Manifest
|
||||
exposed-modules: Extism
|
||||
reexported-modules: Extism.Manifest
|
||||
|
||||
-- Modules included in this library but not exported.
|
||||
other-modules: Extism.Bindings
|
||||
|
||||
-- LANGUAGE extensions used by modules in this package.
|
||||
-- other-extensions:
|
||||
build-depends:
|
||||
base >= 1.6.0
|
||||
, bytestring
|
||||
, json
|
||||
, manifest
|
||||
hs-source-dirs: src
|
||||
default-language: Haskell2010
|
||||
extra-libraries: extism
|
||||
extra-lib-dirs: /usr/local/lib
|
||||
|
||||
library manifest
|
||||
exposed-modules: Extism.Manifest
|
||||
|
||||
visibility: public
|
||||
|
||||
-- Modules included in this library but not exported.
|
||||
other-modules:
|
||||
|
||||
-- LANGUAGE extensions used by modules in this package.
|
||||
-- other-extensions:
|
||||
build-depends:
|
||||
base ^>=4.16.1.0
|
||||
base
|
||||
, bytestring
|
||||
, base64-bytestring
|
||||
, json
|
||||
hs-source-dirs: src
|
||||
hs-source-dirs: manifest
|
||||
default-language: Haskell2010
|
||||
extra-libraries: extism
|
||||
extra-lib-dirs: /usr/local/lib
|
||||
|
||||
Test-Suite extism-example
|
||||
type: exitcode-stdio-1.0
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
{-# LANGUAGE FlexibleInstances, UndecidableInstances #-}
|
||||
|
||||
module Extism.Manifest where
|
||||
|
||||
import Text.JSON
|
||||
(
|
||||
JSON,
|
||||
JSValue(JSNull, JSString, JSArray),
|
||||
toJSString, showJSON, makeObj, encode
|
||||
)
|
||||
@@ -9,16 +12,12 @@ import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
import qualified Data.ByteString.Char8 as BS (unpack)
|
||||
|
||||
valueOrNull f Nothing = JSNull
|
||||
valueOrNull f (Just x) = f x
|
||||
makeString s = JSString (toJSString s)
|
||||
stringOrNull = valueOrNull makeString
|
||||
makeArray f [] = JSNull
|
||||
makeArray f x = JSArray [f a | a <- x]
|
||||
filterNulls obj = [(a, b) | (a, b) <- obj, not (isNull b)]
|
||||
mapObj f x = makeObj (filterNulls [(a, f b) | (a, b) <- x])
|
||||
makeArray x = JSArray [toJSONValue a | a <- x]
|
||||
isNull JSNull = True
|
||||
isNull _ = False
|
||||
filterNulls obj = [(a, b) | (a, b) <- obj, not (isNull b)]
|
||||
object x = makeObj $ filterNulls x
|
||||
(.=) a b = (a, toJSONValue b)
|
||||
|
||||
newtype Memory = Memory
|
||||
{
|
||||
@@ -27,32 +26,37 @@ newtype Memory = Memory
|
||||
|
||||
class JSONValue a where
|
||||
toJSONValue :: a -> JSValue
|
||||
|
||||
instance {-# OVERLAPS #-} (JSON a) => (JSONValue a) where
|
||||
toJSONValue j = showJSON j
|
||||
|
||||
instance {-# OVERLAPS #-} (JSONValue a) => (JSONValue (Maybe a)) where
|
||||
toJSONValue Nothing = JSNull
|
||||
toJSONValue (Just x) = toJSONValue x
|
||||
|
||||
instance JSONValue Memory where
|
||||
toJSONValue x =
|
||||
case memoryMax x of
|
||||
Nothing -> makeObj []
|
||||
Just max -> makeObj [("max", showJSON max)]
|
||||
toJSONValue (Memory max) =
|
||||
object [
|
||||
"max" .= max
|
||||
]
|
||||
|
||||
data HttpRequest = HttpRequest
|
||||
data HTTPRequest = HTTPRequest
|
||||
{
|
||||
url :: String
|
||||
, header :: [(String, String)]
|
||||
, header :: Maybe [(String, String)]
|
||||
, method :: Maybe String
|
||||
}
|
||||
|
||||
requestObj x =
|
||||
let meth = stringOrNull $ method x in
|
||||
let h = mapObj makeString $ header x in
|
||||
filterNulls [
|
||||
("url", makeString $ url x),
|
||||
("header", h),
|
||||
("method", meth)
|
||||
requestObj (HTTPRequest url header method) =
|
||||
[
|
||||
"url" .= url ,
|
||||
"header" .= header,
|
||||
"method" .= method
|
||||
]
|
||||
|
||||
instance JSONValue HttpRequest where
|
||||
instance JSONValue HTTPRequest where
|
||||
toJSONValue x =
|
||||
makeObj $ requestObj x
|
||||
object $ requestObj x
|
||||
|
||||
data WasmFile = WasmFile
|
||||
{
|
||||
@@ -62,14 +66,11 @@ data WasmFile = WasmFile
|
||||
}
|
||||
|
||||
instance JSONValue WasmFile where
|
||||
toJSONValue x =
|
||||
let path = makeString $ filePath x in
|
||||
let name = stringOrNull $ fileName x in
|
||||
let hash = stringOrNull $ fileHash x in
|
||||
makeObj $ filterNulls [
|
||||
("path", path),
|
||||
("name", name),
|
||||
("hash", hash)
|
||||
toJSONValue (WasmFile path name hash) =
|
||||
object [
|
||||
"path" .= path,
|
||||
"name" .= name,
|
||||
"hash" .= hash
|
||||
]
|
||||
|
||||
data WasmCode = WasmCode
|
||||
@@ -81,30 +82,26 @@ data WasmCode = WasmCode
|
||||
|
||||
|
||||
instance JSONValue WasmCode where
|
||||
toJSONValue x =
|
||||
let bytes = makeString $ BS.unpack $ B64.encode $ codeBytes x in
|
||||
let name = stringOrNull $ codeName x in
|
||||
let hash = stringOrNull $ codeHash x in
|
||||
makeObj $ filterNulls [
|
||||
("data", bytes),
|
||||
("name", name),
|
||||
("hash", hash)
|
||||
toJSONValue (WasmCode x name hash) =
|
||||
let bytes = BS.unpack $ B64.encode x in
|
||||
object [
|
||||
"data" .= bytes,
|
||||
"name" .= name,
|
||||
"hash" .= hash
|
||||
]
|
||||
|
||||
data WasmURL = WasmURL
|
||||
{
|
||||
req :: HttpRequest
|
||||
req :: HTTPRequest
|
||||
, urlName :: Maybe String
|
||||
, urlHash :: Maybe String
|
||||
}
|
||||
|
||||
|
||||
instance JSONValue WasmURL where
|
||||
toJSONValue x =
|
||||
let request = requestObj $ req x in
|
||||
let name = stringOrNull $ urlName x in
|
||||
let hash = stringOrNull $ urlHash x in
|
||||
makeObj $ filterNulls $ ("name", name) : ("hash", hash) : request
|
||||
toJSONValue (WasmURL req name hash) =
|
||||
let request = requestObj $ req in
|
||||
object $ "name" .= name : "hash" .= hash : request
|
||||
|
||||
data Wasm = File WasmFile | Code WasmCode | URL WasmURL
|
||||
|
||||
@@ -121,7 +118,7 @@ wasmFile path =
|
||||
|
||||
wasmURL :: String -> String -> Wasm
|
||||
wasmURL method url =
|
||||
let r = HttpRequest { url = url, header = [], method = Just method } in
|
||||
let r = HTTPRequest { url = url, header = Nothing, method = Just method } in
|
||||
URL WasmURL { req = r, urlName = Nothing, urlHash = Nothing }
|
||||
|
||||
wasmCode :: B.ByteString -> Wasm
|
||||
@@ -143,8 +140,8 @@ data Manifest = Manifest
|
||||
{
|
||||
wasm :: [Wasm]
|
||||
, memory :: Maybe Memory
|
||||
, config :: [(String, String)]
|
||||
, allowed_hosts :: [String]
|
||||
, config :: Maybe [(String, String)]
|
||||
, allowedHosts :: Maybe [String]
|
||||
}
|
||||
|
||||
manifest :: [Wasm] -> Manifest
|
||||
@@ -152,32 +149,29 @@ manifest wasm =
|
||||
Manifest {
|
||||
wasm = wasm,
|
||||
memory = Nothing,
|
||||
config = [],
|
||||
allowed_hosts = []
|
||||
config = Nothing,
|
||||
allowedHosts = Nothing
|
||||
}
|
||||
|
||||
withConfig :: Manifest -> [(String, String)] -> Manifest
|
||||
withConfig m config =
|
||||
m { config = config }
|
||||
m { config = Just config }
|
||||
|
||||
|
||||
withHosts :: Manifest -> [String] -> Manifest
|
||||
withHosts m hosts =
|
||||
m { allowed_hosts = hosts }
|
||||
m { allowedHosts = Just hosts }
|
||||
|
||||
instance JSONValue Manifest where
|
||||
toJSONValue x =
|
||||
let w = makeArray toJSONValue $ wasm x in
|
||||
let mem = valueOrNull toJSONValue $ memory x in
|
||||
let c = mapObj makeString $ config x in
|
||||
let hosts = makeArray makeString $ allowed_hosts x in
|
||||
makeObj $ filterNulls [
|
||||
("wasm", w),
|
||||
("memory", mem),
|
||||
("config", c),
|
||||
("allowed_hosts", hosts)
|
||||
toJSONValue (Manifest wasm memory config hosts) =
|
||||
let w = makeArray wasm in
|
||||
object [
|
||||
"wasm" .= w,
|
||||
"memory" .= memory,
|
||||
"config" .= config,
|
||||
"allowed_hosts" .= hosts
|
||||
]
|
||||
|
||||
toString :: Manifest -> String
|
||||
toString manifest =
|
||||
encode (toJSONValue manifest)
|
||||
toString :: (JSONValue a) => a -> String
|
||||
toString v =
|
||||
encode (toJSONValue v)
|
||||
@@ -1,36 +1,17 @@
|
||||
{-# LANGUAGE ForeignFunctionInterface #-}
|
||||
|
||||
module Extism (module Extism, module Extism.Manifest) where
|
||||
import GHC.Int
|
||||
import GHC.Word
|
||||
import Foreign.C.Types
|
||||
import Foreign.Ptr
|
||||
import Data.Int
|
||||
import Data.Word
|
||||
import Control.Monad (void)
|
||||
import Foreign.ForeignPtr
|
||||
import Foreign.C.String
|
||||
import Control.Monad (void)
|
||||
import Foreign.Ptr
|
||||
import Data.ByteString as B
|
||||
import Data.ByteString.Internal (c2w, w2c)
|
||||
import Data.ByteString.Unsafe (unsafeUseAsCString)
|
||||
import Data.Bifunctor (second)
|
||||
import Text.JSON (JSON, toJSObject, toJSString, encode, JSValue(JSNull, JSString))
|
||||
import Extism.Manifest (Manifest, toString)
|
||||
|
||||
newtype ExtismContext = ExtismContext () deriving Show
|
||||
|
||||
foreign import ccall unsafe "extism.h extism_context_new" extism_context_new :: IO (Ptr ExtismContext)
|
||||
foreign import ccall unsafe "extism.h &extism_context_free" extism_context_free :: FunPtr (Ptr ExtismContext -> IO ())
|
||||
foreign import ccall unsafe "extism.h extism_plugin_new" extism_plugin_new :: Ptr ExtismContext -> Ptr Word8 -> Word64 -> CBool -> IO Int32
|
||||
foreign import ccall unsafe "extism.h extism_plugin_update" extism_plugin_update :: Ptr ExtismContext -> Int32 -> Ptr Word8 -> Word64 -> CBool -> IO CBool
|
||||
foreign import ccall unsafe "extism.h extism_plugin_call" extism_plugin_call :: Ptr ExtismContext -> Int32 -> CString -> Ptr Word8 -> Word64 -> IO Int32
|
||||
foreign import ccall unsafe "extism.h extism_plugin_function_exists" extism_plugin_function_exists :: Ptr ExtismContext -> Int32 -> CString -> IO CBool
|
||||
foreign import ccall unsafe "extism.h extism_error" extism_error :: Ptr ExtismContext -> Int32 -> IO CString
|
||||
foreign import ccall unsafe "extism.h extism_plugin_output_length" extism_plugin_output_length :: Ptr ExtismContext -> Int32 -> IO Word64
|
||||
foreign import ccall unsafe "extism.h extism_plugin_output_data" extism_plugin_output_data :: Ptr ExtismContext -> Int32 -> IO (Ptr Word8)
|
||||
foreign import ccall unsafe "extism.h extism_log_file" extism_log_file :: CString -> CString -> IO CBool
|
||||
foreign import ccall unsafe "extism.h extism_plugin_config" extism_plugin_config :: Ptr ExtismContext -> Int32 -> Ptr Word8 -> Int64 -> IO CBool
|
||||
foreign import ccall unsafe "extism.h extism_plugin_free" extism_plugin_free :: Ptr ExtismContext -> Int32 -> IO ()
|
||||
foreign import ccall unsafe "extism.h extism_context_reset" extism_context_reset :: Ptr ExtismContext -> IO ()
|
||||
foreign import ccall unsafe "extism.h extism_version" extism_version :: IO CString
|
||||
import Text.JSON (encode, toJSObject)
|
||||
import Extism.Manifest (Manifest, toString, toJSONValue)
|
||||
import Extism.Bindings
|
||||
|
||||
-- Context manages plugins
|
||||
newtype Context = Context (ForeignPtr ExtismContext)
|
||||
@@ -64,8 +45,8 @@ reset (Context ctx) =
|
||||
withForeignPtr ctx extism_context_reset
|
||||
|
||||
-- Create a new context
|
||||
newContext :: () -> IO Context
|
||||
newContext () = do
|
||||
newContext :: IO Context
|
||||
newContext = do
|
||||
ptr <- extism_context_new
|
||||
fptr <- newForeignPtr extism_context_free ptr
|
||||
return (Context fptr)
|
||||
@@ -73,7 +54,7 @@ newContext () = do
|
||||
-- Execute a function with a new context that is destroyed when it returns
|
||||
withContext :: (Context -> IO a) -> IO a
|
||||
withContext f = do
|
||||
ctx <- newContext ()
|
||||
ctx <- newContext
|
||||
f ctx
|
||||
|
||||
-- Create a plugin from a WASM module, `useWasi` determines if WASI should
|
||||
@@ -126,16 +107,13 @@ updateManifest plugin manifest useWasi =
|
||||
isValid :: Plugin -> Bool
|
||||
isValid (Plugin _ p) = p >= 0
|
||||
|
||||
convertMaybeString Nothing = JSNull
|
||||
convertMaybeString (Just s) = JSString (toJSString s)
|
||||
|
||||
-- Set configuration values for a plugin
|
||||
setConfig :: Plugin -> [(String, Maybe String)] -> IO Bool
|
||||
setConfig (Plugin (Context ctx) plugin) x =
|
||||
if plugin < 0
|
||||
then return False
|
||||
else
|
||||
let obj = toJSObject [(k, convertMaybeString v) | (k, v) <- x] in
|
||||
let obj = toJSObject [(k, toJSONValue v) | (k, v) <- x] in
|
||||
let bs = toByteString (encode obj) in
|
||||
let length = fromIntegral (B.length bs) in
|
||||
unsafeUseAsCString bs (\s -> do
|
||||
|
||||
26
haskell/src/Extism/Bindings.hs
Normal file
26
haskell/src/Extism/Bindings.hs
Normal file
@@ -0,0 +1,26 @@
|
||||
{-# LANGUAGE ForeignFunctionInterface #-}
|
||||
|
||||
module Extism.Bindings where
|
||||
|
||||
import Foreign.C.Types
|
||||
import Foreign.Ptr
|
||||
import Foreign.C.String
|
||||
import Data.Int
|
||||
import Data.Word
|
||||
|
||||
newtype ExtismContext = ExtismContext () deriving Show
|
||||
|
||||
foreign import ccall unsafe "extism.h extism_context_new" extism_context_new :: IO (Ptr ExtismContext)
|
||||
foreign import ccall unsafe "extism.h &extism_context_free" extism_context_free :: FunPtr (Ptr ExtismContext -> IO ())
|
||||
foreign import ccall unsafe "extism.h extism_plugin_new" extism_plugin_new :: Ptr ExtismContext -> Ptr Word8 -> Word64 -> CBool -> IO Int32
|
||||
foreign import ccall unsafe "extism.h extism_plugin_update" extism_plugin_update :: Ptr ExtismContext -> Int32 -> Ptr Word8 -> Word64 -> CBool -> IO CBool
|
||||
foreign import ccall unsafe "extism.h extism_plugin_call" extism_plugin_call :: Ptr ExtismContext -> Int32 -> CString -> Ptr Word8 -> Word64 -> IO Int32
|
||||
foreign import ccall unsafe "extism.h extism_plugin_function_exists" extism_plugin_function_exists :: Ptr ExtismContext -> Int32 -> CString -> IO CBool
|
||||
foreign import ccall unsafe "extism.h extism_error" extism_error :: Ptr ExtismContext -> Int32 -> IO CString
|
||||
foreign import ccall unsafe "extism.h extism_plugin_output_length" extism_plugin_output_length :: Ptr ExtismContext -> Int32 -> IO Word64
|
||||
foreign import ccall unsafe "extism.h extism_plugin_output_data" extism_plugin_output_data :: Ptr ExtismContext -> Int32 -> IO (Ptr Word8)
|
||||
foreign import ccall unsafe "extism.h extism_log_file" extism_log_file :: CString -> CString -> IO CBool
|
||||
foreign import ccall unsafe "extism.h extism_plugin_config" extism_plugin_config :: Ptr ExtismContext -> Int32 -> Ptr Word8 -> Int64 -> IO CBool
|
||||
foreign import ccall unsafe "extism.h extism_plugin_free" extism_plugin_free :: Ptr ExtismContext -> Int32 -> IO ()
|
||||
foreign import ccall unsafe "extism.h extism_context_reset" extism_context_reset :: Ptr ExtismContext -> IO ()
|
||||
foreign import ccall unsafe "extism.h extism_version" extism_version :: IO CString
|
||||
@@ -5,6 +5,6 @@ libdir=${exec_prefix}/lib
|
||||
|
||||
Name: extism
|
||||
Description: The Extism universal plug-in system.
|
||||
Version: 0.0.1
|
||||
Version: 0.1.0
|
||||
Cflags: -I${includedir}
|
||||
Libs: -L${libdir} -lextism
|
||||
23
libextism/Cargo.toml
Normal file
23
libextism/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "libextism"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["The Extism Authors", "oss@extism.org"]
|
||||
license = "BSD-3-Clause"
|
||||
homepage = "https://extism.org"
|
||||
repository = "https://github.com/extism/extism"
|
||||
description = "libextism"
|
||||
|
||||
[lib]
|
||||
name = "extism"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
extism-runtime = {path = "../runtime"}
|
||||
|
||||
[features]
|
||||
default = ["http", "register-http", "register-filesystem"]
|
||||
nn = ["extism-runtime/nn"]
|
||||
register-http = ["extism-runtime/register-http"] # enables wasm to be downloaded using http
|
||||
register-filesystem = ["extism-runtime/register-filesystem"] # enables wasm to be loaded from disk
|
||||
http = ["extism-runtime/http"] # enables extism_http_request
|
||||
10
libextism/src/lib.rs
Normal file
10
libextism/src/lib.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! This crate is used to generate `libextism` using `extism-runtime`
|
||||
|
||||
pub use extism_runtime::sdk::*;
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn test_version() {
|
||||
let s = unsafe { std::ffi::CStr::from_ptr(extism_version()) };
|
||||
assert!(s.to_bytes() != b"0.0.0");
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "extism-manifest"
|
||||
version = "0.0.1-rc.6"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["The Extism Authors", "oss@extism.org"]
|
||||
license = "BSD-3-Clause"
|
||||
@@ -9,7 +9,7 @@ repository = "https://github.com/extism/extism"
|
||||
description = "Extism plug-in manifest crate"
|
||||
|
||||
[dependencies]
|
||||
serde = {version = "1", features=["derive"]}
|
||||
serde = {version = "1", features = ["derive"]}
|
||||
base64 = "0.20.0-alpha"
|
||||
schemars = {version = "0.8", optional=true}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { withContext, Context } from "./dist/index.js";
|
||||
import { readFileSync } from "fs";
|
||||
const { withContext, Context } = require('./dist/index.js');
|
||||
const { readFileSync } = require('fs');
|
||||
|
||||
withContext(async function (context) {
|
||||
let wasm = readFileSync("../wasm/code.wasm");
|
||||
1170
node/package-lock.json
generated
1170
node/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@extism/extism",
|
||||
"version": "0.0.1-rc.6",
|
||||
"version": "0.1.0",
|
||||
"description": "Extism Host SDK for Node",
|
||||
"keywords": [
|
||||
"extism",
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "npm run build",
|
||||
"example": "node example.mjs",
|
||||
"example": "node example.js",
|
||||
"build": "tsc",
|
||||
"test": "jest --coverage"
|
||||
},
|
||||
@@ -36,7 +36,7 @@
|
||||
"@types/jest": "^29.2.0",
|
||||
"@types/node": "^18.11.4",
|
||||
"jest": "^29.2.2",
|
||||
"prettier": "2.7.1",
|
||||
"prettier": "2.8.0",
|
||||
"ts-jest": "^29.0.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"typedoc": "^0.23.18",
|
||||
|
||||
@@ -299,7 +299,7 @@ export class Plugin {
|
||||
let plugin = lib.extism_plugin_new(
|
||||
ctx.pointer,
|
||||
dataRaw,
|
||||
dataRaw.length,
|
||||
Buffer.byteLength(dataRaw, 'utf-8'),
|
||||
wasi
|
||||
);
|
||||
if (plugin < 0) {
|
||||
@@ -319,7 +319,7 @@ export class Plugin {
|
||||
|
||||
if (config != null) {
|
||||
let s = JSON.stringify(config);
|
||||
lib.extism_plugin_config(ctx.pointer, this.id, s, s.length);
|
||||
lib.extism_plugin_config(ctx.pointer, this.id, s, Buffer.byteLength(s, 'utf-8'),);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,7 +344,7 @@ export class Plugin {
|
||||
this.ctx.pointer,
|
||||
this.id,
|
||||
dataRaw,
|
||||
dataRaw.length,
|
||||
Buffer.byteLength(dataRaw, 'utf-8'),
|
||||
wasi
|
||||
);
|
||||
if (!ok) {
|
||||
@@ -357,7 +357,7 @@ export class Plugin {
|
||||
|
||||
if (config != null) {
|
||||
let s = JSON.stringify(config);
|
||||
lib.extism_plugin_config(this.ctx.pointer, this.id, s, s.length);
|
||||
lib.extism_plugin_config(this.ctx.pointer, this.id, s, Buffer.byteLength(s, 'utf-8'),);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,7 +401,7 @@ export class Plugin {
|
||||
this.id,
|
||||
functionName,
|
||||
input.toString(),
|
||||
input.length
|
||||
Buffer.byteLength(input, 'utf-8'),
|
||||
);
|
||||
if (rc !== 0) {
|
||||
var err = lib.extism_error(this.ctx.pointer, this.id);
|
||||
|
||||
@@ -31,6 +31,9 @@ describe("test extism", () => {
|
||||
output = await plugin.call("count_vowels", "this is a test thrice");
|
||||
result = JSON.parse(output.toString());
|
||||
expect(result["count"]).toBe(6);
|
||||
output = await plugin.call("count_vowels", "🌎hello🌎world🌎");
|
||||
result = JSON.parse(output.toString());
|
||||
expect(result["count"]).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
{
|
||||
"name": "extism/example",
|
||||
"description": "Example running the Extism PHP Host SDK",
|
||||
"license": "BSD-3-Clause",
|
||||
"type": "project",
|
||||
"authors": [
|
||||
{
|
||||
"name": "The Extism Authors",
|
||||
"email": "oss@extism.org"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"extism/extism": "*"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "path",
|
||||
"url": "../../"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "dev"
|
||||
"name": "extism/example",
|
||||
"description": "Example running the Extism PHP Host SDK",
|
||||
"license": "BSD-3-Clause",
|
||||
"type": "project",
|
||||
"authors": [
|
||||
{
|
||||
"name": "The Extism Authors",
|
||||
"email": "oss@extism.org"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"extism/extism": "*"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "path",
|
||||
"url": "../../"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
35
php/example/composer.lock
generated
35
php/example/composer.lock
generated
@@ -8,11 +8,11 @@
|
||||
"packages": [
|
||||
{
|
||||
"name": "extism/extism",
|
||||
"version": "dev-feat-reuse-plugins",
|
||||
"version": "dev-php-sdk-fix",
|
||||
"dist": {
|
||||
"type": "path",
|
||||
"url": "../..",
|
||||
"reference": "9e5f576acff0aa9c8720fdf6d1103b7c1996bf14"
|
||||
"reference": "6119eae37bcb2e02f7c7b5b203ab9f959819e83d"
|
||||
},
|
||||
"require": {
|
||||
"ircmaxell/ffime": "dev-master",
|
||||
@@ -22,7 +22,12 @@
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Extism\\": "php/src/"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"php/src/Context.php",
|
||||
"php/src/Plugin.php",
|
||||
"php/src/extism.h"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": []
|
||||
@@ -59,12 +64,12 @@
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ircmaxell/FFIMe.git",
|
||||
"reference": "7b9e0bf23adceddd5fde3130d30275a45cfc1867"
|
||||
"reference": "f6911d7a6a7090a9782a21a946819a2efa9a2ff7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/ircmaxell/FFIMe/zipball/7b9e0bf23adceddd5fde3130d30275a45cfc1867",
|
||||
"reference": "7b9e0bf23adceddd5fde3130d30275a45cfc1867",
|
||||
"url": "https://api.github.com/repos/ircmaxell/FFIMe/zipball/f6911d7a6a7090a9782a21a946819a2efa9a2ff7",
|
||||
"reference": "f6911d7a6a7090a9782a21a946819a2efa9a2ff7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -97,7 +102,7 @@
|
||||
"issues": "https://github.com/ircmaxell/FFIMe/issues",
|
||||
"source": "https://github.com/ircmaxell/FFIMe/tree/master"
|
||||
},
|
||||
"time": "2022-09-07T19:50:56+00:00"
|
||||
"time": "2022-09-25T18:13:59+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ircmaxell/php-c-parser",
|
||||
@@ -105,12 +110,12 @@
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ircmaxell/php-c-parser.git",
|
||||
"reference": "55e0a4fdf88d6e955d928860e1e107a68492c1cf"
|
||||
"reference": "fd8f5efefd0fcc6c5119d945694acaa3a6790ada"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/ircmaxell/php-c-parser/zipball/55e0a4fdf88d6e955d928860e1e107a68492c1cf",
|
||||
"reference": "55e0a4fdf88d6e955d928860e1e107a68492c1cf",
|
||||
"url": "https://api.github.com/repos/ircmaxell/php-c-parser/zipball/fd8f5efefd0fcc6c5119d945694acaa3a6790ada",
|
||||
"reference": "fd8f5efefd0fcc6c5119d945694acaa3a6790ada",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -146,7 +151,7 @@
|
||||
"issues": "https://github.com/ircmaxell/php-c-parser/issues",
|
||||
"source": "https://github.com/ircmaxell/php-c-parser/tree/master"
|
||||
},
|
||||
"time": "2022-08-27T17:37:14+00:00"
|
||||
"time": "2022-09-23T19:39:35+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ircmaxell/php-object-symbolresolver",
|
||||
@@ -154,12 +159,12 @@
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ircmaxell/php-object-symbolresolver.git",
|
||||
"reference": "88c918a0f4621ef59dc4a4c21ead7f39bd720337"
|
||||
"reference": "dfe1b1aa6c15b198bdef50fff8485e98e89f2a09"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/ircmaxell/php-object-symbolresolver/zipball/88c918a0f4621ef59dc4a4c21ead7f39bd720337",
|
||||
"reference": "88c918a0f4621ef59dc4a4c21ead7f39bd720337",
|
||||
"url": "https://api.github.com/repos/ircmaxell/php-object-symbolresolver/zipball/dfe1b1aa6c15b198bdef50fff8485e98e89f2a09",
|
||||
"reference": "dfe1b1aa6c15b198bdef50fff8485e98e89f2a09",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -191,7 +196,7 @@
|
||||
"issues": "https://github.com/ircmaxell/php-object-symbolresolver/issues",
|
||||
"source": "https://github.com/ircmaxell/php-object-symbolresolver/tree/master"
|
||||
},
|
||||
"time": "2022-09-07T19:47:04+00:00"
|
||||
"time": "2022-09-15T18:21:50+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
|
||||
@@ -3,12 +3,37 @@ declare(strict_types=1);
|
||||
namespace Extism;
|
||||
|
||||
require_once "vendor/autoload.php";
|
||||
require_once "generate.php";
|
||||
|
||||
function generate_extism_lib() {
|
||||
return (new \FFIMe\FFIMe("libextism.".soext()))
|
||||
->include("extism.h")
|
||||
->showWarnings(false)
|
||||
->codeGen('ExtismLib', __DIR__.'/ExtismLib.php');
|
||||
}
|
||||
|
||||
function soext() {
|
||||
$platform = php_uname("s");
|
||||
switch ($platform) {
|
||||
case "Darwin":
|
||||
return "dylib";
|
||||
case "Linux":
|
||||
return "so";
|
||||
case "Windows":
|
||||
return "dll";
|
||||
default:
|
||||
throw new \Exception("Extism: unsupported platform ".$platform);
|
||||
}
|
||||
}
|
||||
|
||||
if (!file_exists(__DIR__."/ExtismLib.php")) {
|
||||
generate_extism_lib();
|
||||
}
|
||||
|
||||
require_once "ExtismLib.php";
|
||||
|
||||
$lib = new \ExtismLib(\ExtismLib::SOFILE);
|
||||
if ($lib == null) {
|
||||
throw new Exception("Extism: failed to create new runtime instance");
|
||||
throw new \Exception("Extism: failed to create new runtime instance");
|
||||
}
|
||||
|
||||
class Context
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
declare(strict_types=1);
|
||||
namespace Extism;
|
||||
|
||||
require_once "vendor/autoload.php";
|
||||
require_once "generate.php";
|
||||
require_once "ExtismLib.php";
|
||||
|
||||
class Plugin
|
||||
@@ -34,13 +32,13 @@ class Plugin
|
||||
$id = $this->lib->extism_plugin_new($ctx->pointer, $data, count($data), (int)$wasi);
|
||||
if ($id < 0) {
|
||||
$err = $this->lib->extism_error($ctx->pointer, -1);
|
||||
throw new Exception("Extism: unable to load plugin: " . $err);
|
||||
throw new \Exception("Extism: unable to load plugin: " . $err);
|
||||
}
|
||||
$this->id = $id;
|
||||
$this->context = $ctx;
|
||||
|
||||
if ($config != null) {
|
||||
$cfg = string_to_bytes(json_encode(config));
|
||||
if ($this->config != null) {
|
||||
$cfg = string_to_bytes(json_encode($config));
|
||||
$this->lib->extism_plugin_config($ctx->pointer, $this->id, $cfg, count($cfg));
|
||||
}
|
||||
}
|
||||
@@ -73,14 +71,14 @@ class Plugin
|
||||
if ($err) {
|
||||
$msg = $msg . ", error = " . $err;
|
||||
}
|
||||
throw new Execption("Extism: call to '".$name."' failed with " . $msg);
|
||||
throw new \Exception("Extism: call to '".$name."' failed with " . $msg);
|
||||
}
|
||||
|
||||
$length = $this->lib->extism_plugin_output_length($this->context->pointer, $this->id);
|
||||
|
||||
$buf = $this->lib->extism_plugin_output_data($this->context->pointer, $this->id);
|
||||
|
||||
$ouput = [];
|
||||
$output = [];
|
||||
$data = $buf->getData();
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$output[$i] = $data[$i];
|
||||
@@ -101,7 +99,7 @@ class Plugin
|
||||
$ok = $this->lib->extism_plugin_update($this->context->pointer, $this->id, $data, count($data), (int)$wasi);
|
||||
if (!$ok) {
|
||||
$err = $this->lib->extism_error($this->context->pointer, -1);
|
||||
throw new Exception("Extism: unable to update plugin: " . $err);
|
||||
throw new \Exception("Extism: unable to update plugin: " . $err);
|
||||
}
|
||||
|
||||
if ($config != null) {
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<?php
|
||||
require_once "vendor/autoload.php";
|
||||
|
||||
|
||||
function generate() {
|
||||
return (new FFIMe\FFIMe("libextism.".soext()))
|
||||
->include("extism.h")
|
||||
->showWarnings(false)
|
||||
->codeGen('ExtismLib', __DIR__.'/ExtismLib.php');
|
||||
}
|
||||
|
||||
function soext() {
|
||||
$platform = php_uname("s");
|
||||
switch ($platform) {
|
||||
case "Darwin":
|
||||
return "dylib";
|
||||
case "Linux":
|
||||
return "so";
|
||||
case "Windows":
|
||||
return "dll";
|
||||
default:
|
||||
throw new Exeception("Extism: unsupported platform ".$platform);
|
||||
}
|
||||
}
|
||||
|
||||
if (!file_exists(__DIR__."/ExtismLib.php")) {
|
||||
generate();
|
||||
}
|
||||
|
||||
3
python/README.md
Normal file
3
python/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Extism Python Host SDK
|
||||
|
||||
See [https://extism.org/docs/integrate-into-your-codebase/python-host-sdk/](https://extism.org/docs/integrate-into-your-codebase/python-host-sdk/).
|
||||
@@ -1,9 +1,10 @@
|
||||
[tool.poetry]
|
||||
name = "extism"
|
||||
version = "0.0.1-rc.6"
|
||||
description = ""
|
||||
version = "0.1.0"
|
||||
description = "Extism Host SDK for python"
|
||||
authors = ["The Extism Authors <oss@extism.org>"]
|
||||
license = "BSD-3-Clause"
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.7"
|
||||
|
||||
@@ -20,6 +20,8 @@ class TestExtism(unittest.TestCase):
|
||||
self.assertEqual(j["count"], 7)
|
||||
j = json.loads(plugin.call("count_vowels", "this is a test thrice"))
|
||||
self.assertEqual(j["count"], 6)
|
||||
j = json.loads(plugin.call("count_vowels", "🌎hello🌎world🌎"))
|
||||
self.assertEqual(j["count"], 3)
|
||||
|
||||
def test_update_plugin_manifest(self):
|
||||
with extism.Context() as ctx:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
RUBYGEMS_API_KEY ?=
|
||||
|
||||
.PHONY: prepare test
|
||||
|
||||
@@ -9,11 +10,15 @@ test: prepare
|
||||
bundle exec rake test
|
||||
|
||||
clean:
|
||||
rm extism-*.gem
|
||||
rm -f extism-*.gem
|
||||
|
||||
publish-local: clean prepare
|
||||
gem build extism.gemspec
|
||||
gem push extism-*.gem
|
||||
|
||||
publish: clean prepare
|
||||
gem build extism.gemspec
|
||||
gem push extism-*.gem
|
||||
GEM_HOST_API_KEY=$(RUBYGEMS_API_KEY) gem push extism-*.gem
|
||||
|
||||
lint:
|
||||
bundle exec rufo --check .
|
||||
|
||||
@@ -32,7 +32,7 @@ Gem::Specification.new do |spec|
|
||||
spec.require_paths = ["lib"]
|
||||
|
||||
# Uncomment to register a new dependency of your gem
|
||||
# spec.add_dependency "example-gem", "~> 1.0"
|
||||
spec.add_dependency "ffi", ">= 1.0.0"
|
||||
|
||||
# For more information and examples about making a new gem, check out our
|
||||
# guide at: https://bundler.io/guides/creating_gem.html
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Extism
|
||||
VERSION = "0.0.1.rc6"
|
||||
VERSION = "0.1.0"
|
||||
end
|
||||
|
||||
@@ -20,6 +20,8 @@ class TestExtism < Minitest::Test
|
||||
assert_equal res["count"], 7
|
||||
res = JSON.parse(plugin.call("count_vowels", "this is a test thrice"))
|
||||
assert_equal res["count"], 6
|
||||
res = JSON.parse(plugin.call("count_vowels", "🌎hello🌎world🌎"))
|
||||
assert_equal res["count"], 3
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "extism-runtime"
|
||||
version = "0.0.1-rc.6"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["The Extism Authors", "oss@extism.org"]
|
||||
license = "BSD-3-Clause"
|
||||
@@ -8,27 +8,21 @@ homepage = "https://extism.org"
|
||||
repository = "https://github.com/extism/extism"
|
||||
description = "Extism runtime component"
|
||||
|
||||
[lib]
|
||||
name = "extism"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
wasmtime = "2.0.1"
|
||||
wasmtime-wasi = "2.0.1"
|
||||
wasmtime-wasi-nn = {version = "2.0.1", optional=true}
|
||||
wasmtime = "3.0.0"
|
||||
wasmtime-wasi = "3.0.0"
|
||||
wasmtime-wasi-nn = {version = "3.0.0", optional=true}
|
||||
anyhow = "1"
|
||||
serde = { version = "1", features=["derive"] }
|
||||
toml = "0.5"
|
||||
serde = {version = "1", features = ["derive"]}
|
||||
serde_json = "1"
|
||||
toml = "0.5"
|
||||
sha2 = "0.10"
|
||||
log = "0.4"
|
||||
log4rs = "1.1"
|
||||
url = "2.3"
|
||||
url = "2"
|
||||
glob = "0.3"
|
||||
ureq = {version = "2.5", optional=true}
|
||||
extism-manifest = { version = "0.0.1-rc.6", path = "../manifest" }
|
||||
extism-manifest = { version = "0.1.0", path = "../manifest" }
|
||||
pretty-hex = { version = "0.3" }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
|
||||
if let Ok(bindings) = cbindgen::Builder::new()
|
||||
.with_crate(crate_dir)
|
||||
.with_crate(".")
|
||||
.with_language(cbindgen::Language::C)
|
||||
.with_no_includes()
|
||||
.with_sys_include("stdint.h")
|
||||
|
||||
@@ -29,7 +29,7 @@ impl Context {
|
||||
|
||||
/// Get the next valid plugin ID
|
||||
pub fn next_id(&mut self) -> Result<PluginIndex, Error> {
|
||||
// Make sure we haven't exhausted all plugin IDs, it reach this it would require the machine
|
||||
// Make sure we haven't exhausted all plugin IDs, to reach this it would require the machine
|
||||
// running this code to have a lot of memory - no computer I tested on was able to allocate
|
||||
// the max number of plugins.
|
||||
//
|
||||
@@ -37,7 +37,7 @@ impl Context {
|
||||
// try to use one of those before returning an error
|
||||
let exhausted = self.next_id.load(std::sync::atomic::Ordering::SeqCst) == PluginIndex::MAX;
|
||||
|
||||
// If there is a significant number of old IDs we can start to re-use them
|
||||
// If there are a significant number of old IDs we can start to re-use them
|
||||
if self.reclaimed_ids.len() >= START_REUSING_IDS || exhausted {
|
||||
if let Some(x) = self.reclaimed_ids.pop_front() {
|
||||
return Ok(x);
|
||||
@@ -55,6 +55,32 @@ impl Context {
|
||||
.fetch_add(1, std::sync::atomic::Ordering::SeqCst))
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, plugin: Plugin) -> PluginIndex {
|
||||
// Generate a new plugin ID
|
||||
let id: i32 = match self.next_id() {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
error!("Error creating Plugin: {:?}", e);
|
||||
self.set_error(e);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
self.plugins.insert(id, plugin);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn new_plugin(&mut self, data: impl AsRef<[u8]>, with_wasi: bool) -> PluginIndex {
|
||||
let plugin = match Plugin::new(data, with_wasi) {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
error!("Error creating Plugin: {:?}", e);
|
||||
self.set_error(e);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
self.insert(plugin)
|
||||
}
|
||||
|
||||
/// Set the context error
|
||||
pub fn set_error(&mut self, e: impl std::fmt::Debug) {
|
||||
trace!("Set context error: {:?}", e);
|
||||
@@ -78,9 +104,9 @@ impl Context {
|
||||
|
||||
/// Remove a plugin from the context
|
||||
pub fn remove(&mut self, id: PluginIndex) {
|
||||
self.plugins.remove(&id);
|
||||
|
||||
// Collect old IDs in case we need to re-use them
|
||||
self.reclaimed_ids.push_back(id);
|
||||
if self.plugins.remove(&id).is_some() {
|
||||
// Collect old IDs in case we need to re-use them
|
||||
self.reclaimed_ids.push_back(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,30 +84,6 @@ impl PluginMemory {
|
||||
Ok(self.memory.data(&self.store)[offs])
|
||||
}
|
||||
|
||||
/// Write u32 to memory
|
||||
pub(crate) fn store_u32(&mut self, offs: usize, data: u32) -> Result<(), Error> {
|
||||
trace!("store_u32: {data:x} at offset {offs}");
|
||||
let handle = MemoryBlock {
|
||||
offset: offs,
|
||||
length: 4,
|
||||
};
|
||||
self.write(handle, data.to_ne_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read u32 from memory
|
||||
pub(crate) fn load_u32(&self, offs: usize) -> Result<u32, Error> {
|
||||
trace!("load_u32: offset {offs}");
|
||||
let mut buf = [0; 4];
|
||||
|
||||
let handle = MemoryBlock {
|
||||
offset: offs,
|
||||
length: 4,
|
||||
};
|
||||
self.read(handle, &mut buf)?;
|
||||
Ok(u32::from_ne_bytes(buf))
|
||||
}
|
||||
|
||||
/// Write u64 to memory
|
||||
pub(crate) fn store_u64(&mut self, offs: usize, data: u64) -> Result<(), Error> {
|
||||
trace!("store_u64: {data:x} at offset {offs}");
|
||||
|
||||
@@ -7,7 +7,7 @@ macro_rules! args {
|
||||
($input:expr, $index:expr, $ty:ident) => {
|
||||
match $input[$index].$ty() {
|
||||
Some(x) => x,
|
||||
None => return Err(Trap::new("Invalid input type"))
|
||||
None => return Err(Error::msg("Invalid input type"))
|
||||
}
|
||||
};
|
||||
($input:expr, $(($index:expr, $ty:ident)),*$(,)?) => {
|
||||
@@ -24,7 +24,7 @@ pub(crate) fn input_length(
|
||||
caller: Caller<Internal>,
|
||||
_input: &[Val],
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Trap> {
|
||||
) -> Result<(), Error> {
|
||||
let data: &Internal = caller.data();
|
||||
output[0] = Val::I64(data.input_length as i64);
|
||||
Ok(())
|
||||
@@ -37,7 +37,7 @@ pub(crate) fn input_load_u8(
|
||||
caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Trap> {
|
||||
) -> Result<(), Error> {
|
||||
let data: &Internal = caller.data();
|
||||
if data.input.is_null() {
|
||||
return Ok(());
|
||||
@@ -53,7 +53,7 @@ pub(crate) fn input_load_u64(
|
||||
caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Trap> {
|
||||
) -> Result<(), Error> {
|
||||
let data: &Internal = caller.data();
|
||||
if data.input.is_null() {
|
||||
return Ok(());
|
||||
@@ -72,12 +72,10 @@ pub(crate) fn store_u8(
|
||||
mut caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Trap> {
|
||||
) -> 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)
|
||||
.map_err(|_| Trap::new("Write error"))?;
|
||||
data.memory_mut().store_u8(offset as usize, byte as u8)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -88,51 +86,14 @@ pub(crate) fn load_u8(
|
||||
caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Trap> {
|
||||
) -> Result<(), Error> {
|
||||
let data: &Internal = caller.data();
|
||||
let offset = args!(input, 0, i64) as usize;
|
||||
let byte = data
|
||||
.memory()
|
||||
.load_u8(offset)
|
||||
.map_err(|_| Trap::new("Read error"))?;
|
||||
let byte = data.memory().load_u8(offset)?;
|
||||
output[0] = Val::I32(byte as i32);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Store an unsigned 32 bit integer in memory
|
||||
/// Params: i64 (offset), i32 (int)
|
||||
/// Returns: none
|
||||
pub(crate) fn store_u32(
|
||||
mut caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Trap> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let (offset, b) = args!(input, (0, i64), (1, i32));
|
||||
data.memory_mut()
|
||||
.store_u32(offset as usize, b as u32)
|
||||
.map_err(|_| Trap::new("Write error"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load an unsigned 32 bit integer from memory
|
||||
/// Params: i64 (offset)
|
||||
/// Returns: i32 (int)
|
||||
pub(crate) fn load_u32(
|
||||
caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Trap> {
|
||||
let data: &Internal = caller.data();
|
||||
let offset = args!(input, 0, i64) as usize;
|
||||
let b = data
|
||||
.memory()
|
||||
.load_u32(offset)
|
||||
.map_err(|_| Trap::new("Read error"))?;
|
||||
output[0] = Val::I32(b as i32);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Store an unsigned 64 bit integer in memory
|
||||
/// Params: i64 (offset), i64 (int)
|
||||
/// Returns: none
|
||||
@@ -140,12 +101,10 @@ pub(crate) fn store_u64(
|
||||
mut caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Trap> {
|
||||
) -> 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)
|
||||
.map_err(|_| Trap::new("Write error"))?;
|
||||
data.memory_mut().store_u64(offset as usize, b as u64)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -156,13 +115,10 @@ pub(crate) fn load_u64(
|
||||
caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Trap> {
|
||||
) -> Result<(), Error> {
|
||||
let data: &Internal = caller.data();
|
||||
let offset = args!(input, 0, i64) as usize;
|
||||
let byte = data
|
||||
.memory()
|
||||
.load_u64(offset)
|
||||
.map_err(|_| Trap::new("Read error"))?;
|
||||
let byte = data.memory().load_u64(offset)?;
|
||||
output[0] = Val::I64(byte as i64);
|
||||
Ok(())
|
||||
}
|
||||
@@ -174,7 +130,7 @@ pub(crate) fn output_set(
|
||||
mut caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Trap> {
|
||||
) -> Result<(), Error> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let (offset, length) = args!(input, (0, i64), (1, i64));
|
||||
data.output_offset = offset as usize;
|
||||
@@ -189,7 +145,7 @@ pub(crate) fn alloc(
|
||||
mut caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Trap> {
|
||||
) -> 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);
|
||||
@@ -204,7 +160,7 @@ pub(crate) fn free(
|
||||
mut caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Trap> {
|
||||
) -> Result<(), Error> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let offset = args!(input, 0, i64) as usize;
|
||||
data.memory_mut().free(offset);
|
||||
@@ -218,7 +174,7 @@ pub(crate) fn error_set(
|
||||
mut caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Trap> {
|
||||
) -> Result<(), Error> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let offset = args!(input, 0, i64) as usize;
|
||||
|
||||
@@ -240,7 +196,7 @@ pub(crate) fn config_get(
|
||||
mut caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Trap> {
|
||||
) -> Result<(), Error> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let plugin = data.plugin_mut();
|
||||
|
||||
@@ -265,7 +221,7 @@ pub(crate) fn var_get(
|
||||
mut caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Trap> {
|
||||
) -> Result<(), Error> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let plugin = data.plugin_mut();
|
||||
|
||||
@@ -292,7 +248,7 @@ pub(crate) fn var_set(
|
||||
mut caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Trap> {
|
||||
) -> Result<(), Error> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let plugin = data.plugin_mut();
|
||||
|
||||
@@ -305,7 +261,7 @@ pub(crate) fn var_set(
|
||||
|
||||
// If the store is larger than 100MB then stop adding things
|
||||
if size > 1024 * 1024 * 100 && voffset != 0 {
|
||||
return Err(Trap::new("Variable store is full"));
|
||||
return Err(Error::msg("Variable store is full"));
|
||||
}
|
||||
|
||||
let key_offs = args!(input, 0, i64) as usize;
|
||||
@@ -332,12 +288,12 @@ pub(crate) fn http_request(
|
||||
#[allow(unused_mut)] mut caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Trap> {
|
||||
) -> Result<(), Error> {
|
||||
#[cfg(not(feature = "http"))]
|
||||
{
|
||||
let _ = (caller, input);
|
||||
|
||||
output[0] = Val::I64(0 as i64);
|
||||
output[0] = Val::I64(0);
|
||||
error!("http_request is not enabled");
|
||||
return Ok(());
|
||||
}
|
||||
@@ -349,14 +305,13 @@ pub(crate) fn http_request(
|
||||
let http_req_offset = args!(input, 0, i64) as usize;
|
||||
|
||||
let req: extism_manifest::HttpRequest =
|
||||
serde_json::from_slice(data.memory().get(http_req_offset)?)
|
||||
.map_err(|_| Trap::new("Invalid http request"))?;
|
||||
serde_json::from_slice(data.memory().get(http_req_offset)?)?;
|
||||
|
||||
let body_offset = args!(input, 1, i64) as usize;
|
||||
|
||||
let url = match url::Url::parse(&req.url) {
|
||||
Ok(u) => u,
|
||||
Err(e) => return Err(Trap::new(format!("Invalid URL: {e:?}"))),
|
||||
Err(e) => return Err(Error::msg(format!("Invalid URL: {e:?}"))),
|
||||
};
|
||||
let allowed_hosts = &data.plugin().manifest.as_ref().allowed_hosts;
|
||||
let host_str = url.host_str().unwrap_or_default();
|
||||
@@ -370,7 +325,7 @@ pub(crate) fn http_request(
|
||||
pat.matches(host_str)
|
||||
});
|
||||
if !allowed_hosts.is_empty() && !host_matches_allowed {
|
||||
return Err(Trap::new(format!(
|
||||
return Err(Error::msg(format!(
|
||||
"HTTP request to {} is not allowed",
|
||||
req.url
|
||||
)));
|
||||
@@ -383,20 +338,20 @@ pub(crate) fn http_request(
|
||||
r = r.set(k, v);
|
||||
}
|
||||
|
||||
let mut res = if body_offset > 0 {
|
||||
let res = if body_offset > 0 {
|
||||
let buf = data.memory().get(body_offset)?;
|
||||
r.send_bytes(buf)
|
||||
.map_err(|e| Trap::new(&format!("Request error: {e:?}")))?
|
||||
.into_reader()
|
||||
let res = r.send_bytes(buf)?;
|
||||
data.http_status = res.status();
|
||||
res.into_reader()
|
||||
} else {
|
||||
r.call()
|
||||
.map_err(|e| Trap::new(format!("{:?}", e)))?
|
||||
.into_reader()
|
||||
let res = r.call()?;
|
||||
data.http_status = res.status();
|
||||
res.into_reader()
|
||||
};
|
||||
|
||||
let mut buf = Vec::new();
|
||||
res.read_to_end(&mut buf)
|
||||
.map_err(|e| Trap::new(format!("{:?}", e)))?;
|
||||
res.take(1024 * 1024 * 50) // TODO: make this limit configurable
|
||||
.read_to_end(&mut buf)?;
|
||||
|
||||
let mem = data.memory_mut().alloc_bytes(buf)?;
|
||||
|
||||
@@ -405,6 +360,19 @@ pub(crate) fn http_request(
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the status code of the last HTTP request
|
||||
/// Params: none
|
||||
/// Returns: i32 (status code)
|
||||
pub(crate) fn http_status_code(
|
||||
mut caller: Caller<Internal>,
|
||||
_input: &[Val],
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Error> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
output[0] = Val::I32(data.http_status as i32);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the length of an allocated block given the offset
|
||||
/// Params: i64 (offset)
|
||||
/// Returns: i64 (length or 0)
|
||||
@@ -412,7 +380,7 @@ pub(crate) fn length(
|
||||
mut caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
output: &mut [Val],
|
||||
) -> Result<(), Trap> {
|
||||
) -> Result<(), Error> {
|
||||
let data: &mut Internal = caller.data_mut();
|
||||
let offset = args!(input, 0, i64) as usize;
|
||||
if offset == 0 {
|
||||
@@ -421,7 +389,7 @@ pub(crate) fn length(
|
||||
}
|
||||
let length = match data.memory().block_length(offset) {
|
||||
Some(x) => x,
|
||||
None => return Err(Trap::new("Unable to find length for offset")),
|
||||
None => return Err(Error::msg("Unable to find length for offset")),
|
||||
};
|
||||
output[0] = Val::I64(length as i64);
|
||||
Ok(())
|
||||
@@ -432,7 +400,7 @@ pub fn log(
|
||||
caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Trap> {
|
||||
) -> Result<(), Error> {
|
||||
let data: &Internal = caller.data();
|
||||
let offset = args!(input, 0, i64) as usize;
|
||||
let buf = data.memory().get(offset)?;
|
||||
@@ -451,7 +419,7 @@ pub(crate) fn log_warn(
|
||||
caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Trap> {
|
||||
) -> Result<(), Error> {
|
||||
log(log::Level::Warn, caller, input, _output)
|
||||
}
|
||||
|
||||
@@ -462,7 +430,7 @@ pub(crate) fn log_info(
|
||||
caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Trap> {
|
||||
) -> Result<(), Error> {
|
||||
log(log::Level::Info, caller, input, _output)
|
||||
}
|
||||
|
||||
@@ -473,7 +441,7 @@ pub(crate) fn log_debug(
|
||||
caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Trap> {
|
||||
) -> Result<(), Error> {
|
||||
log(log::Level::Debug, caller, input, _output)
|
||||
}
|
||||
|
||||
@@ -484,6 +452,6 @@ pub(crate) fn log_error(
|
||||
caller: Caller<Internal>,
|
||||
input: &[Val],
|
||||
_output: &mut [Val],
|
||||
) -> Result<(), Trap> {
|
||||
) -> Result<(), Error> {
|
||||
log(log::Level::Error, caller, input, _output)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ pub struct Internal {
|
||||
pub output_length: usize,
|
||||
pub plugin: *mut Plugin,
|
||||
pub wasi: Option<Wasi>,
|
||||
pub http_status: u16,
|
||||
}
|
||||
|
||||
pub struct Wasi {
|
||||
@@ -42,6 +43,7 @@ impl Internal {
|
||||
let nn = wasmtime_wasi_nn::WasiNnCtx::new()?;
|
||||
|
||||
#[cfg(not(feature = "nn"))]
|
||||
#[allow(clippy::let_unit_value)]
|
||||
let nn = ();
|
||||
|
||||
Some(Wasi {
|
||||
@@ -59,6 +61,7 @@ impl Internal {
|
||||
input: std::ptr::null(),
|
||||
wasi,
|
||||
plugin: std::ptr::null_mut(),
|
||||
http_status: 0,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -137,10 +140,8 @@ impl Plugin {
|
||||
alloc(I64) -> I64;
|
||||
free(I64);
|
||||
load_u8(I64) -> I32;
|
||||
load_u32(I64) -> I32;
|
||||
load_u64(I64) -> I64;
|
||||
store_u8(I64, I32);
|
||||
store_u32(I64, I32);
|
||||
store_u64(I64, I64);
|
||||
input_length() -> I64;
|
||||
input_load_u8(I64) -> I32;
|
||||
@@ -151,6 +152,7 @@ impl Plugin {
|
||||
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);
|
||||
|
||||
@@ -36,29 +36,8 @@ pub unsafe extern "C" fn extism_plugin_new(
|
||||
) -> PluginIndex {
|
||||
trace!("Call to extism_plugin_new with wasm pointer {:?}", wasm);
|
||||
let ctx = &mut *ctx;
|
||||
|
||||
let data = std::slice::from_raw_parts(wasm, wasm_size as usize);
|
||||
let plugin = match Plugin::new(data, with_wasi) {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
error!("Error creating Plugin: {:?}", e);
|
||||
ctx.set_error(e);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
// Allocate a new plugin ID
|
||||
let id: i32 = match ctx.next_id() {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
error!("Error creating Plugin: {:?}", e);
|
||||
ctx.set_error(e);
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
ctx.plugins.insert(id, plugin);
|
||||
info!("New plugin added: {id}");
|
||||
id
|
||||
ctx.new_plugin(data, with_wasi)
|
||||
}
|
||||
|
||||
/// Update a plugin, keeping the existing ID
|
||||
@@ -242,12 +221,29 @@ pub unsafe extern "C" fn extism_plugin_call(
|
||||
None => return plugin.error(format!("Function not found: {name}"), -1),
|
||||
};
|
||||
|
||||
// Call function with offset+length pointing to input data.
|
||||
let mut results = vec![Val::I32(0)];
|
||||
// Check the number of results, reject functions with more than 1 result
|
||||
let n_results = func.ty(&plugin.memory.store).results().len();
|
||||
if n_results > 1 {
|
||||
return plugin.error(
|
||||
format!("Function {name} has {n_results} results, expected 0 or 1"),
|
||||
-1,
|
||||
);
|
||||
}
|
||||
|
||||
let mut results = vec![Val::null(); n_results];
|
||||
match func.call(&mut plugin.memory.store, &[], results.as_mut_slice()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
plugin.dump_memory();
|
||||
|
||||
if let Some(exit) = e.downcast_ref::<wasmtime_wasi::I32Exit>() {
|
||||
error!("WASI return code: {}", exit.0);
|
||||
if exit.0 != 0 {
|
||||
return plugin.error(&e, exit.0);
|
||||
}
|
||||
return exit.0;
|
||||
}
|
||||
|
||||
error!("Call: {e:?}");
|
||||
return plugin.error(e.context("Call failed"), -1);
|
||||
}
|
||||
@@ -255,6 +251,12 @@ pub unsafe extern "C" fn extism_plugin_call(
|
||||
|
||||
plugin.dump_memory();
|
||||
|
||||
// If `results` is empty then the return value is expected to be returned
|
||||
// in the error block of the func call above using `I32Exit`
|
||||
if results.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Return result to caller
|
||||
results[0].unwrap_i32()
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
[package]
|
||||
name = "extism"
|
||||
version = "0.0.1-rc.6"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["The Extism Authors", "oss@extism.org"]
|
||||
license = "BSD-3-Clause"
|
||||
links = "extism"
|
||||
homepage = "https://extism.org"
|
||||
repository = "https://github.com/extism/extism"
|
||||
description = "Extism Host SDK for Rust"
|
||||
|
||||
[dependencies]
|
||||
extism-manifest = { version = "0.0.1-rc.6", path = "../manifest" }
|
||||
extism-manifest = { version = "0.1.0", path = "../manifest" }
|
||||
extism-runtime = { version = "0.1.0", path = "../runtime"}
|
||||
serde_json = "1"
|
||||
log = "0.4"
|
||||
log = "0.4"
|
||||
thiserror = "1"
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
fn main() {
|
||||
println!("cargo:rustc-link-search=/usr/local/lib");
|
||||
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
let path = std::path::PathBuf::from(home).join(".local").join("lib");
|
||||
println!("cargo:rustc-link-search={}", path.display());
|
||||
}
|
||||
|
||||
println!("cargo:rustc-link-lib=extism");
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::*;
|
||||
|
||||
pub struct Context {
|
||||
pub(crate) pointer: *mut bindings::ExtismContext,
|
||||
}
|
||||
pub struct Context(pub(crate) std::sync::Arc<std::sync::Mutex<extism_runtime::Context>>);
|
||||
|
||||
impl Default for Context {
|
||||
fn default() -> Context {
|
||||
@@ -13,25 +11,20 @@ impl Default for Context {
|
||||
impl Context {
|
||||
/// Create a new context
|
||||
pub fn new() -> Context {
|
||||
let pointer = unsafe { bindings::extism_context_new() };
|
||||
Context { pointer }
|
||||
Context(std::sync::Arc::new(std::sync::Mutex::new(
|
||||
extism_runtime::Context::new(),
|
||||
)))
|
||||
}
|
||||
|
||||
/// Remove all registered plugins
|
||||
pub fn reset(&mut self) {
|
||||
unsafe { bindings::extism_context_reset(self.pointer) }
|
||||
unsafe { bindings::extism_context_reset(&mut *self.lock()) }
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for Context {}
|
||||
unsafe impl Sync for Context {}
|
||||
|
||||
impl Drop for Context {
|
||||
fn drop(&mut self) {
|
||||
if self.pointer.is_null() {
|
||||
return;
|
||||
pub(crate) fn lock(&self) -> std::sync::MutexGuard<extism_runtime::Context> {
|
||||
match self.0.lock() {
|
||||
Ok(x) => x,
|
||||
Err(x) => x.into_inner(),
|
||||
}
|
||||
unsafe { bindings::extism_context_free(self.pointer) }
|
||||
self.pointer = std::ptr::null_mut();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use extism_manifest::Manifest;
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
pub(crate) mod bindings;
|
||||
pub use extism_manifest::{self as manifest, Manifest};
|
||||
pub use extism_runtime::sdk as bindings;
|
||||
|
||||
mod context;
|
||||
mod plugin;
|
||||
@@ -11,17 +7,16 @@ mod plugin;
|
||||
pub use context::Context;
|
||||
pub use plugin::Plugin;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Unable to load plugin: {0}")]
|
||||
UnableToLoadPlugin(String),
|
||||
#[error("{0}")]
|
||||
Message(String),
|
||||
Json(serde_json::Error),
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
Error::Json(e)
|
||||
}
|
||||
#[error("JSON: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error("Runtime: {0}")]
|
||||
Runtime(#[from] extism_runtime::Error),
|
||||
}
|
||||
|
||||
/// Gets the version of Extism
|
||||
@@ -54,7 +49,7 @@ mod tests {
|
||||
let wasm_start = Instant::now();
|
||||
set_log_file("test.log", Some(log::Level::Info));
|
||||
let context = Context::new();
|
||||
let plugin = Plugin::new(&context, WASM, false).unwrap();
|
||||
let mut plugin = Plugin::new(&context, WASM, false).unwrap();
|
||||
println!("register loaded plugin: {:?}", wasm_start.elapsed());
|
||||
|
||||
let repeat = 1182;
|
||||
@@ -146,20 +141,20 @@ mod tests {
|
||||
use std::io::Write;
|
||||
std::thread::spawn(|| {
|
||||
let context = Context::new();
|
||||
let plugin = Plugin::new(&context, WASM, false).unwrap();
|
||||
let mut plugin = Plugin::new(&context, WASM, false).unwrap();
|
||||
let output = plugin.call("count_vowels", "this is a test").unwrap();
|
||||
std::io::stdout().write_all(output).unwrap();
|
||||
});
|
||||
|
||||
std::thread::spawn(|| {
|
||||
let context = Context::new();
|
||||
let plugin = Plugin::new(&context, WASM, false).unwrap();
|
||||
let mut plugin = Plugin::new(&context, WASM, false).unwrap();
|
||||
let output = plugin.call("count_vowels", "this is a test aaa").unwrap();
|
||||
std::io::stdout().write_all(output).unwrap();
|
||||
});
|
||||
|
||||
let context = Context::new();
|
||||
let plugin = Plugin::new(&context, WASM, false).unwrap();
|
||||
let mut plugin = Plugin::new(&context, WASM, false).unwrap();
|
||||
let output = plugin.call("count_vowels", "abc123").unwrap();
|
||||
std::io::stdout().write_all(output).unwrap();
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
use crate::*;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub struct Plugin<'a> {
|
||||
id: bindings::ExtismPlugin,
|
||||
id: extism_runtime::PluginIndex,
|
||||
context: &'a Context,
|
||||
}
|
||||
|
||||
impl<'a> Plugin<'a> {
|
||||
/// Create plugin from a known-good ID
|
||||
///
|
||||
/// # Safety
|
||||
/// This function does not check to ensure the provided ID is valid
|
||||
pub unsafe fn from_id(id: i32, context: &'a Context) -> Plugin<'a> {
|
||||
Plugin {
|
||||
id,
|
||||
context: context,
|
||||
}
|
||||
Plugin { id, context }
|
||||
}
|
||||
|
||||
pub fn as_i32(&self) -> i32 {
|
||||
@@ -19,7 +21,7 @@ impl<'a> Plugin<'a> {
|
||||
|
||||
/// Create a new plugin from the given manifest
|
||||
pub fn new_with_manifest(
|
||||
ctx: &'a Context,
|
||||
ctx: &'a mut Context,
|
||||
manifest: &Manifest,
|
||||
wasi: bool,
|
||||
) -> Result<Plugin<'a>, Error> {
|
||||
@@ -29,17 +31,10 @@ impl<'a> Plugin<'a> {
|
||||
|
||||
/// Create a new plugin from a WASM module
|
||||
pub fn new(ctx: &'a Context, data: impl AsRef<[u8]>, wasi: bool) -> Result<Plugin, Error> {
|
||||
let plugin = unsafe {
|
||||
bindings::extism_plugin_new(
|
||||
ctx.pointer,
|
||||
data.as_ref().as_ptr(),
|
||||
data.as_ref().len() as u64,
|
||||
wasi,
|
||||
)
|
||||
};
|
||||
let plugin = ctx.lock().new_plugin(data, wasi);
|
||||
|
||||
if plugin < 0 {
|
||||
let err = unsafe { bindings::extism_error(ctx.pointer, -1) };
|
||||
let err = unsafe { bindings::extism_error(&mut *ctx.lock(), -1) };
|
||||
let buf = unsafe { std::ffi::CStr::from_ptr(err) };
|
||||
let buf = buf.to_str().unwrap().to_string();
|
||||
return Err(Error::UnableToLoadPlugin(buf));
|
||||
@@ -55,7 +50,7 @@ impl<'a> Plugin<'a> {
|
||||
pub fn update(&mut self, data: impl AsRef<[u8]>, wasi: bool) -> Result<(), Error> {
|
||||
let b = unsafe {
|
||||
bindings::extism_plugin_update(
|
||||
self.context.pointer,
|
||||
&mut *self.context.lock(),
|
||||
self.id,
|
||||
data.as_ref().as_ptr(),
|
||||
data.as_ref().len() as u64,
|
||||
@@ -66,7 +61,7 @@ impl<'a> Plugin<'a> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let err = unsafe { bindings::extism_error(self.context.pointer, -1) };
|
||||
let err = unsafe { bindings::extism_error(&mut *self.context.lock(), -1) };
|
||||
if !err.is_null() {
|
||||
let s = unsafe { std::ffi::CStr::from_ptr(err) };
|
||||
return Err(Error::Message(s.to_str().unwrap().to_string()));
|
||||
@@ -82,11 +77,11 @@ impl<'a> Plugin<'a> {
|
||||
}
|
||||
|
||||
/// Set configuration values
|
||||
pub fn set_config(&self, config: &BTreeMap<String, Option<String>>) -> Result<(), Error> {
|
||||
pub fn set_config(&mut self, config: &BTreeMap<String, Option<String>>) -> Result<(), Error> {
|
||||
let encoded = serde_json::to_vec(config)?;
|
||||
unsafe {
|
||||
bindings::extism_plugin_config(
|
||||
self.context.pointer,
|
||||
&mut *self.context.lock(),
|
||||
self.id,
|
||||
encoded.as_ptr() as *const _,
|
||||
encoded.len() as u64,
|
||||
@@ -96,7 +91,7 @@ impl<'a> Plugin<'a> {
|
||||
}
|
||||
|
||||
/// Set configuration values, builder-style
|
||||
pub fn with_config(self, config: &BTreeMap<String, Option<String>>) -> Result<Self, Error> {
|
||||
pub fn with_config(mut self, config: &BTreeMap<String, Option<String>>) -> Result<Self, Error> {
|
||||
self.set_config(config)?;
|
||||
Ok(self)
|
||||
}
|
||||
@@ -106,7 +101,7 @@ impl<'a> Plugin<'a> {
|
||||
let name = std::ffi::CString::new(name.as_ref()).expect("Invalid function name");
|
||||
unsafe {
|
||||
bindings::extism_plugin_function_exists(
|
||||
self.context.pointer,
|
||||
&mut *self.context.lock(),
|
||||
self.id,
|
||||
name.as_ptr() as *const _,
|
||||
)
|
||||
@@ -114,11 +109,11 @@ impl<'a> Plugin<'a> {
|
||||
}
|
||||
|
||||
/// Call a function with the given input
|
||||
pub fn call(&self, name: impl AsRef<str>, input: impl AsRef<[u8]>) -> Result<&[u8], Error> {
|
||||
pub fn call(&mut self, name: impl AsRef<str>, input: impl AsRef<[u8]>) -> Result<&[u8], Error> {
|
||||
let name = std::ffi::CString::new(name.as_ref()).expect("Invalid function name");
|
||||
let rc = unsafe {
|
||||
bindings::extism_plugin_call(
|
||||
self.context.pointer,
|
||||
&mut *self.context.lock(),
|
||||
self.id,
|
||||
name.as_ptr() as *const _,
|
||||
input.as_ref().as_ptr() as *const _,
|
||||
@@ -127,7 +122,7 @@ impl<'a> Plugin<'a> {
|
||||
};
|
||||
|
||||
if rc != 0 {
|
||||
let err = unsafe { bindings::extism_error(self.context.pointer, self.id) };
|
||||
let err = unsafe { bindings::extism_error(&mut *self.context.lock(), self.id) };
|
||||
if !err.is_null() {
|
||||
let s = unsafe { std::ffi::CStr::from_ptr(err) };
|
||||
return Err(Error::Message(s.to_str().unwrap().to_string()));
|
||||
@@ -137,9 +132,9 @@ impl<'a> Plugin<'a> {
|
||||
}
|
||||
|
||||
let out_len =
|
||||
unsafe { bindings::extism_plugin_output_length(self.context.pointer, self.id) };
|
||||
unsafe { bindings::extism_plugin_output_length(&mut *self.context.lock(), self.id) };
|
||||
unsafe {
|
||||
let ptr = bindings::extism_plugin_output_data(self.context.pointer, self.id);
|
||||
let ptr = bindings::extism_plugin_output_data(&mut *self.context.lock(), self.id);
|
||||
Ok(std::slice::from_raw_parts(ptr, out_len as usize))
|
||||
}
|
||||
}
|
||||
@@ -147,9 +142,6 @@ impl<'a> Plugin<'a> {
|
||||
|
||||
impl<'a> Drop for Plugin<'a> {
|
||||
fn drop(&mut self) {
|
||||
if self.context.pointer.is_null() {
|
||||
return;
|
||||
}
|
||||
unsafe { bindings::extism_plugin_free(self.context.pointer, self.id) }
|
||||
unsafe { bindings::extism_plugin_free(&mut *self.context.lock(), self.id) }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user