Compare commits

..

2 Commits

Author SHA1 Message Date
Steve Manuel
5c31215ce7 update sdk coverage script to fully include elixir
Co-authored-by: zach <zach@dylib.so>
2022-10-25 20:06:34 -06:00
Benjamin Eckel
fc95a99e40 test(elixir-sdk): Add Elixir to SDK coverage script 2022-10-25 17:34:52 -05:00
117 changed files with 1296 additions and 21003 deletions

View File

@@ -1,18 +0,0 @@
on: [workflow_call]
name: libextism
runs:
using: composite
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Download libextism
uses: actions/download-artifact@v3
with:
name: libextism-${{ matrix.os }}
- name: Install extism shared library
shell: bash
run: |
sudo cp libextism.* /usr/local/lib
sudo cp runtime/extism.h /usr/local/include

View File

@@ -1,41 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "cargo" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "python"
schedule:
interval: "weekly"
- package-ecosystem: "mix"
directory: "elixir"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "node"
schedule:
interval: "weekly"
- package-ecosystem: "composer"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "bundler"
directory: "ruby"
schedule:
interval: "weekly"
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -1,103 +1,15 @@
on: [pull_request, workflow_dispatch]
on: [push, pull_request]
name: CI
env:
RUNTIME_MANIFEST: runtime/Cargo.toml
RUNTIME_CRATE: extism-runtime
LIBEXTISM_CRATE: libextism
RUST_SDK_CRATE: extism
jobs:
lib:
name: Extism runtime lib
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
rust:
- stable
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Cache Rust environment
uses: Swatinem/rust-cache@v1
- name: Cache libextism
id: cache-libextism
uses: actions/cache@v3
with:
path: target/release/libextism.*
key: ${{ runner.os }}-libextism-${{ hashFiles('runtime/**') }}-${{ hashFiles('manifest/**') }}
- name: Cache target
id: cache-target
uses: actions/cache@v3
with:
path: target/**
key: ${{ runner.os }}-target-${{ env.GITHUB_SHA }}
- name: Build
if: steps.cache-libextism.outputs.cache-hit != 'true'
shell: bash
run: cargo build --release -p ${{ env.LIBEXTISM_CRATE }}
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: libextism-${{ matrix.os }}
path: |
target/release/libextism.*
lint_and_test:
name: Extism runtime lint and test
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
rust:
- stable
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Cache Rust environment
uses: Swatinem/rust-cache@v1
- name: Cache target
id: cache-target
uses: actions/cache@v3
with:
path: target/**
key: ${{ runner.os }}-target-${{ env.GITHUB_SHA }}
- name: Format
run: cargo fmt --check -p ${{ env.RUNTIME_CRATE }}
- name: Lint
run: cargo clippy --release --all-features --no-deps -p ${{ env.RUNTIME_CRATE }}
- name: Test
run: cargo test --all-features --release -p ${{ env.RUNTIME_CRATE }}
rust:
name: Rust
needs: lib
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
rust:
- stable
steps:
- name: Checkout sources
uses: actions/checkout@v3
- uses: ./.github/actions/extism
- name: Test Rust Host SDK
run: LD_LIBRARY_PATH=/usr/local/lib cargo test --release -p ${{ env.RUST_SDK_CRATE }}
elixir:
name: Elixir
needs: lib
build_and_test:
name: Build & Test
runs-on: ${{ matrix.os }}
env:
MIX_ENV: test
@@ -109,7 +21,32 @@ jobs:
steps:
- name: Checkout sources
uses: actions/checkout@v3
- uses: ./.github/actions/extism
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
- name: Cache Rust environment
uses: Swatinem/rust-cache@v1
- name: Format
run: cargo fmt --check -p ${{ env.RUNTIME_CRATE }}
- name: Build
run: cargo build --release -p ${{ env.RUNTIME_CRATE }}
- name: Lint
run: cargo clippy --release --no-deps -p ${{ env.RUNTIME_CRATE }}
- name: Test
run: cargo test --all-features --release -p ${{ env.RUNTIME_CRATE }}
- name: Install extism shared library
shell: bash
run: sudo make install
- name: Setup Elixir Host SDK
if: ${{ runner.os != 'macOS' }}
uses: erlef/setup-beam@v1
@@ -124,70 +61,30 @@ jobs:
cd elixir
LD_LIBRARY_PATH=/usr/local/lib mix do deps.get, test
go:
name: Go
needs: lib
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
rust:
- stable
steps:
- name: Checkout sources
uses: actions/checkout@v3
- uses: ./.github/actions/extism
- name: Setup Go env
uses: actions/setup-go@v3
- name: Test Go Host SDK
run: |
go version
cd go
LD_LIBRARY_PATH=/usr/local/lib go run main.go
LD_LIBRARY_PATH=/usr/local/lib go test
cd go && LD_LIBRARY_PATH=/usr/local/lib go run main.go
python:
name: Python
needs: lib
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
rust:
- stable
steps:
- name: Checkout sources
uses: actions/checkout@v3
- uses: ./.github/actions/extism
- name: Setup Python env
uses: actions/setup-python@v4
with:
python-version: "3.9"
check-latest: true
- name: Install Poetry
uses: snok/install-poetry@v1
- name: Test Python Host SDK
run: |
cd python
cp ../README.md .
poetry install
poetry run python example.py
poetry run python -m unittest discover
ruby:
name: Ruby
needs: lib
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
rust:
- stable
steps:
- name: Checkout sources
uses: actions/checkout@v3
- uses: ./.github/actions/extism
- name: Setup Ruby env
uses: ruby/setup-ruby@v1
with:
@@ -200,121 +97,41 @@ jobs:
ruby example.rb
rake test
node:
name: Node
needs: lib
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
rust:
- stable
steps:
- name: Checkout sources
uses: actions/checkout@v3
- uses: ./.github/actions/extism
- name: Setup Node env
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 16
- name: Test Node Host SDK
run: |
cd node
npm i
LD_LIBRARY_PATH=/usr/local/lib npm run build
LD_LIBRARY_PATH=/usr/local/lib npm run example
LD_LIBRARY_PATH=/usr/local/lib npm run test
npm run build
LD_LIBRARY_PATH=/usr/local/lib node example.js
- name: Test Browser Runtime
run: |
cd browser
npm i
npm run test
- name: Test Rust Host SDK
run: LD_LIBRARY_PATH=/usr/local/lib cargo test --release -p ${{ env.RUST_SDK_CRATE }}
ocaml:
name: OCaml
needs: lib
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
rust:
- stable
steps:
- name: Checkout sources
uses: actions/checkout@v3
- uses: ./.github/actions/extism
- name: Setup OCaml env
uses: ocaml/setup-ocaml@v2
with:
ocaml-compiler: ocaml-base-compiler.5.0.0~beta1
- name: Cache OCaml
id: cache-ocaml
uses: actions/cache@v3
with:
path: _build
key: ${{ runner.os }}-ocaml-${{ hashFiles('ocaml/lib/**') }}-${{ hashFiles('ocaml/bin/**') }}-${{ hashFiles('dune-project') }}
- name: Build OCaml Host SDK
if: steps.cache-ocaml.outputs.cache-hit != 'true'
run: |
opam install -y --deps-only .
cd ocaml
LD_LIBRARY_PATH=/usr/local/lib opam exec -- dune build
- name: Test OCaml Host SDK
run: |
opam install -y --deps-only .
cd ocaml
LD_LIBRARY_PATH=/usr/local/lib opam exec -- dune exec ./bin/main.exe
LD_LIBRARY_PATH=/usr/local/lib opam exec -- dune runtest
# - name: Setup OCaml env
# uses: ocaml/setup-ocaml@v2
# - name: Test OCaml Host SDK
# run: |
# opam install -y .
# cd ocaml
# opam exec -- dune exec extism
haskell:
name: Haskell
needs: lib
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
rust:
- stable
steps:
- name: Checkout sources
uses: actions/checkout@v3
- uses: ./.github/actions/extism
- name: Setup Haskell env
uses: haskell/actions/setup@v2
with:
enable-stack: true
stack-version: "latest"
- name: Cache Haskell
id: cache-haskell
uses: actions/cache@v3
with:
path: .stack-work
key: ${{ runner.os }}-haskell-${{ hashFiles('haskell/**') }}
- name: Build Haskell Host SDK
if: steps.cache-haskell.outputs.cache-hit != 'true'
run: |
cd haskell
LD_LIBRARY_PATH=/usr/local/lib stack build
- name: Test Haskell SDK
run: |
cd haskell
LD_LIBRARY_PATH=/usr/local/lib stack test
php:
name: PHP
needs: lib
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
rust:
- stable
steps:
- name: Checkout sources
uses: actions/checkout@v3
- uses: ./.github/actions/extism
- name: Setup PHP env
uses: shivammathur/setup-php@v2
with:
@@ -330,48 +147,24 @@ jobs:
composer install
php index.php
cpp:
name: C++
needs: lib
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
rust:
- stable
steps:
- name: Checkout sources
uses: actions/checkout@v3
- uses: ./.github/actions/extism
- name: Install C++ SDK deps
if: ${{ matrix.os == 'macos-latest' }}
run: |
brew install jsoncpp googletest pkg-config
- name: Install C++ SDK deps
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
sudo apt-get install g++ libjsoncpp-dev libgtest-dev pkg-config
- name: Run C++ tests
run: |
cd cpp
LD_LIBRARY_PATH=/usr/local/lib make example
LD_LIBRARY_PATH=/usr/local/lib make test
sdk_api_coverage:
name: SDK API Coverage Report
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Setup Python env
uses: actions/setup-python@v4
with:
python-version: "3.9"
check-latest: true
- name: Install dependencies
run: |
sudo apt-get install ripgrep
pip3 install pycparser
- name: Run coverage script
id: coverage
run: |

View File

@@ -1,28 +0,0 @@
on:
workflow_dispatch:
name: Release Elixir SDK
jobs:
release-sdks:
name: release-elixir
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Elixir Host SDK
uses: erlef/setup-beam@v1
with:
experimental-otp: true
otp-version: '25.0.4'
elixir-version: '1.14.0'
- name: Publish Elixir Host SDK to hex.pm
env:
HEX_API_KEY: ${{ secrets.HEX_PM_API_TOKEN }}
run: |
cd elixir
cp ../LICENSE .
make publish

View File

@@ -1,30 +0,0 @@
on:
workflow_dispatch:
name: Release Node SDK
jobs:
release-sdks:
name: release-node
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node env
uses: actions/setup-node@v3
with:
node-version: 16
registry-url: "https://registry.npmjs.org"
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_API_TOKEN }}
CI: true
- name: Release Node Host SDK
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_API_TOKEN }}
CI: true
run: |
cd node
make publish

View File

@@ -1,37 +0,0 @@
on:
workflow_dispatch:
name: Release Python SDK
jobs:
release-sdks:
name: release-python
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Python env
uses: actions/setup-python@v4
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 prepare
poetry build
- name: Release Python Host SDK
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: ${{ secrets.PYPI_API_USER }}
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: python/dist/

View File

@@ -1,25 +0,0 @@
on:
workflow_dispatch:
name: Release Ruby SDK
jobs:
release-sdks:
name: release-ruby
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Ruby
uses: actions/setup-ruby@v1
with:
ruby-version: '3.1' # Version range or exact version of a Ruby version to use, using semvers version range syntax.
- name: Publish Ruby Gem
env:
RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_TOKEN }}
run: |
cd ruby
make publish

View File

@@ -1,35 +0,0 @@
on:
workflow_dispatch:
name: Release Rust SDK
jobs:
release-sdks:
name: release-rust
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Rust env
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
target: ${{ matrix.target }}
- name: Release Rust Host SDK
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_API_TOKEN }}
run: |
# order of crate publication matter: manifest, runtime, rust
cargo publish --manifest-path manifest/Cargo.toml
# allow for crates.io to update so dependant crates can locate extism-manifest
sleep 5
cargo publish --manifest-path runtime/Cargo.toml --no-verify
cargo publish --manifest-path rust/Cargo.toml

View File

@@ -1,13 +1,12 @@
on:
release:
types: [created]
workflow_dispatch:
name: Release
env:
RUNTIME_MANIFEST: runtime/Cargo.toml
RUNTIME_CRATE: libextism
RUNTIME_CRATE: extism-runtime
RUSTFLAGS: -C target-feature=-crt-static
ARTIFACT_DIR: release-artifacts
@@ -214,3 +213,112 @@ jobs:
files: |
*.tar.gz
*.txt
release-sdks:
needs: [release-linux, release-macos] # release-windows
name: publish-sdks
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node env
uses: actions/setup-node@v3
with:
node-version: 16
registry-url: "https://registry.npmjs.org"
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_API_TOKEN }}
CI: true
- name: Release Node Host SDK
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_API_TOKEN }}
CI: true
run: |
cd node
npm publish
- name: Setup Rust env
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
target: ${{ matrix.target }}
- name: Release Rust Host SDK
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_API_TOKEN }}
run: |
# order of crate publication matter: manifest, runtime, rust
cargo publish --manifest-path manifest/Cargo.toml
# allow for crates.io to update so dependant crates can locate extism-manifest
sleep 5
cargo publish --manifest-path runtime/Cargo.toml --no-verify
cargo publish --manifest-path rust/Cargo.toml
- name: Setup Elixir Host SDK
uses: erlef/setup-beam@v1
with:
experimental-otp: true
otp-version: '25.0.4'
elixir-version: '1.14.0'
- name: Publish Elixir Host SDK to hex.pm
env:
HEX_API_KEY: ${{ secrets.HEX_PM_API_TOKEN }}
run: |
cd elixir
cp ../LICENSE .
mix do deps.get, deps.compile
mix hex.build
mix hex.publish --yes
- name: Setup Ruby
uses: actions/setup-ruby@v1
with:
ruby-version: '3.1' # Version range or exact version of a Ruby version to use, using semvers version range syntax.
- name: Publish Ruby Gem
env:
RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_TOKEN }}
run: |
cd ruby
gem build extism.gemspec
gem push extism*.gem -k $RUBYGEMS_API_KEY
- name: Publish Elixir Host SDK to hex.pm
env:
HEX_API_KEY: ${{ secrets.HEX_PM_API_TOKEN }}
run: |
cd elixir
mix do deps.get, deps.compile
mix hex.build
mix hex.publish --yes
- name: Setup Python env
uses: actions/setup-python@v4
with:
python-version: "3.9"
check-latest: true
- name: Build Python Host SDK
run: |
pushd python
python3 -m pip install --upgrade build
cp ../LICENSE .
cp ../README.md .
python3 -m poetry build
popd
- 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 }}
packages_dir: python/dist/

3
.gitignore vendored
View File

@@ -13,8 +13,6 @@ __pycache__
python/dist
python/poetry.lock
c/main
cpp/test/test
cpp/example
go/main
ruby/.bundle/
ruby/.yardoc
@@ -25,6 +23,7 @@ ruby/pkg/
ruby/spec/reports/
ruby/tmp/
ruby/Gemfile.lock
cpp/example
rust/target
rust/test.log
ocaml/duniverse

View File

@@ -3,8 +3,8 @@ members = [
"manifest",
"runtime",
"rust",
"libextism"
]
exclude = [
"elixir/native/extism_nif"
]

View File

@@ -25,7 +25,7 @@ lint:
cargo clippy --release --no-deps --manifest-path runtime/Cargo.toml
build:
cargo build --release $(FEATURE_FLAGS) --manifest-path libextism/Cargo.toml
cargo build --release $(FEATURE_FLAGS) --manifest-path runtime/Cargo.toml
install:
install runtime/extism.h $(DEST)/include

View File

@@ -10,16 +10,14 @@ The universal plug-in system. Run WebAssembly extensions inside your app. Use id
[Ruby](https://extism.org/docs/integrate-into-your-codebase/ruby-host-sdk), [Python](https://extism.org/docs/integrate-into-your-codebase/python-host-sdk),
[Node](https://extism.org/docs/integrate-into-your-codebase/node-host-sdk), [Rust](https://extism.org/docs/integrate-into-your-codebase/rust-host-sdk),
[C](https://extism.org/docs/integrate-into-your-codebase/c-host-sdk), [C++](https://extism.org/docs/integrate-into-your-codebase/cpp-host-sdk),
[OCaml](https://extism.org/docs/integrate-into-your-codebase/ocaml-host-sdk), [Haskell](https://extism.org/docs/integrate-into-your-codebase/haskell-host-sdk), [PHP](https://extism.org/docs/integrate-into-your-codebase/php-host-sdk), [Elixir/Erlang](https://extism.org/docs/integrate-into-your-codebase/elixir-or-erlang-host-sdk) &amp; more (others coming soon).
[OCaml](https://extism.org/docs/integrate-into-your-codebase/ocaml-host-sdk), [Haskell](https://extism.org/docs/integrate-into-your-codebase/haskell-host-sdk), [PHP](https://extism.org/docs/integrate-into-your-codebase/php-host-sdk) &amp; more (others coming soon).
Plug-in development kits (PDK) for plug-in authors supported in [Rust](https://github.com/extism/rust-pdk), [AssemblyScript](https://github.com/extism/assemblyscript-pdk), [Go](https://github.com/extism/go-pdk), [C/C++](https://github.com/extism/c-pdk).
<p align="center">
<img style="width: 70%;" src="https://user-images.githubusercontent.com/7517515/200043015-ddfe5833-0252-43a8-bc9e-5b3f829c37d1.png" alt="Extism embedded SDK language support"/>
<img style="width: 70%;" src="https://user-images.githubusercontent.com/7517515/191437220-4030840d-1a9e-47b9-a44a-39a004885308.png"/>
</p>
Add a flexible, secure, and _bLaZiNg FaSt_ plug-in system to your project. Server, desktop, mobile, web, database -- you name it. Enable users to write and execute safe extensions to your software in **3 easy steps:**
### 1. Import
@@ -53,9 +51,8 @@ The easiest way to start would be to join the [Discord](https://discord.gg/cx3us
Extism is an open-source product from the team at:
<p align="left">
<a href="https://dylib.so" _target="blanks"><img width="200px" src="https://user-images.githubusercontent.com/7517515/198204119-5afdebb9-a5d8-4322-bd2a-46179c8d7b24.svg"/></a>
</p>
<a href="https://dylib.so" _target="blanks"><img width="200px" src="https://user-images.githubusercontent.com/7517515/195408048-0f199b26-cba3-4635-b683-f43b1e610c82.svg"/></a>
</p>![dylibso-logo-outline]
_Reach out and tell us what you're building! We'd love to help._

130
browser/.gitignore vendored
View File

@@ -1,130 +0,0 @@
# 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.*

View File

@@ -1,5 +0,0 @@
{
"printWidth": 120,
"trailingComma": "all",
"singleQuote": true
}

View File

@@ -1,23 +0,0 @@
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",
});

Binary file not shown.

View File

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

View File

@@ -1,5 +0,0 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};

10569
browser/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,34 +0,0 @@
{
"name": "@extism/runtime-browser",
"version": "0.0.1",
"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"
}
}

View File

@@ -1,104 +0,0 @@
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);
}
}

View File

@@ -1,45 +0,0 @@
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);
}
}

View File

@@ -1,23 +0,0 @@
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 });
});
});

View File

@@ -1,3 +0,0 @@
import ExtismContext from './context';
export { ExtismContext };

View File

@@ -1,40 +0,0 @@
/**
* 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>;
};

View File

@@ -1,170 +0,0 @@
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);
},
};
}
}

View File

@@ -1,12 +0,0 @@
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"strict": true,
"skipLibCheck": true
},
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}

View File

@@ -1,6 +0,0 @@
{
"extends": [
"tslint:recommended",
"tslint-config-prettier"
]
}

View File

@@ -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": "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": "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/"
"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"
]
},
"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": {}
"autoload-dev": {
"psr-4": {}
},
"config": {
"sort-packages": true
},
"extra": {},
"scripts": {},
"scripts-descriptions": {}
}

View File

@@ -1,15 +1,3 @@
FLAGS=`pkg-config --cflags --libs jsoncpp gtest` -lextism -lpthread
build:
clang++ -std=c++11 -o example example.cpp -lextism -L .
build-example:
$(CXX) -std=c++11 -o example -I. example.cpp $(FLAGS)
.PHONY: example
example: build-example
./example
build-test:
$(CXX) -std=c++11 -o test/test -I. test/test.cpp $(FLAGS)
.PHONY: test
test: build-test
cd test && ./test

View File

@@ -1,4 +1,3 @@
#define EXTISM_NO_JSON
#include "extism.hpp"
#include <cstring>

View File

@@ -1,123 +1,14 @@
#pragma once
#include <map>
#include <memory>
#include <string>
#include <vector>
#ifndef EXTISM_NO_JSON
#if __has_include(<jsoncpp/json/json.h>)
#include <jsoncpp/json/json.h>
#else
#include <json/json.h>
#endif
#endif // EXTISM_NO_JSON
extern "C" {
#include <extism.h>
}
namespace extism {
typedef std::map<std::string, std::string> Config;
class Wasm {
public:
std::string path;
std::string url;
// TODO: add base64 encoded raw data
std::string hash;
#ifndef EXTISM_NO_JSON
Json::Value json() const {
Json::Value doc;
if (!this->path.empty()) {
doc["path"] = this->path;
}
if (!this->url.empty()) {
doc["url"] = this->url;
}
if (!this->hash.empty()) {
doc["hash"] = this->hash;
}
return doc;
}
#endif
};
class Manifest {
public:
Config config;
std::vector<Wasm> wasm;
std::vector<std::string> allowed_hosts;
static Manifest path(std::string s, std::string hash = std::string()) {
Manifest m;
m.add_wasm_path(s, hash);
return m;
}
static Manifest url(std::string s, std::string hash = std::string()) {
Manifest m;
m.add_wasm_url(s, hash);
return m;
}
#ifndef EXTISM_NO_JSON
std::string json() const {
Json::Value doc;
Json::Value wasm;
for (auto w : this->wasm) {
wasm.append(w.json());
}
doc["wasm"] = wasm;
if (!this->config.empty()) {
Json::Value conf;
for (auto k : this->config) {
conf[k.first] = k.second;
}
doc["config"] = conf;
}
if (!this->allowed_hosts.empty()) {
Json::Value h;
for (auto s : this->allowed_hosts) {
h.append(s);
}
doc["allowed_hosts"] = h;
}
Json::FastWriter writer;
return writer.write(doc);
}
#endif
void add_wasm_path(std::string s, std::string hash = std::string()) {
Wasm w;
w.path = s;
w.hash = hash;
this->wasm.push_back(w);
}
void add_wasm_url(std::string u, std::string hash = std::string()) {
Wasm w;
w.url = u;
w.hash = hash;
this->wasm.push_back(w);
}
void allow_host(std::string host) { this->allowed_hosts.push_back(host); }
void set_config(std::string k, std::string v) { this->config[k] = v; }
};
class Error : public std::exception {
private:
std::string message;
@@ -133,10 +24,6 @@ public:
const uint8_t *data;
ExtismSize length;
std::string string() { return (std::string)(*this); }
std::vector<uint8_t> vector() { return (std::vector<uint8_t>)(*this); }
operator std::string() { return std::string((const char *)data, length); }
operator std::vector<uint8_t>() {
return std::vector<uint8_t>(data, data + length);
@@ -158,29 +45,11 @@ public:
this->context = ctx;
}
#ifndef EXTISM_NO_JSON
Plugin(std::shared_ptr<ExtismContext> ctx, const Manifest &manifest,
bool with_wasi = false) {
auto buffer = manifest.json();
this->plugin = extism_plugin_new(ctx.get(), (const uint8_t *)buffer.c_str(),
buffer.size(), with_wasi);
if (this->plugin < 0) {
const char *err = extism_error(ctx.get(), -1);
throw Error(err == nullptr ? "Unable to load plugin from manifest" : err);
}
this->context = ctx;
}
#endif
~Plugin() {
extism_plugin_free(this->context.get(), this->plugin);
this->plugin = -1;
}
ExtismPlugin id() const { return this->plugin; }
ExtismContext *get_context() const { return this->context.get(); }
void update(const uint8_t *wasm, size_t length, bool with_wasi = false) {
bool b = extism_plugin_update(this->context.get(), this->plugin, wasm,
length, with_wasi);
@@ -190,46 +59,9 @@ public:
}
}
#ifndef EXTISM_NO_JSON
void update(const Manifest &manifest, bool with_wasi = false) {
auto buffer = manifest.json();
bool b = extism_plugin_update(this->context.get(), this->plugin,
(const uint8_t *)buffer.c_str(),
buffer.size(), with_wasi);
if (!b) {
const char *err = extism_error(this->context.get(), -1);
throw Error(err == nullptr ? "Unable to update plugin" : err);
}
}
void config(const Config &data) {
Json::Value conf;
for (auto k : data) {
conf[k.first] = k.second;
}
Json::FastWriter writer;
auto s = writer.write(conf);
this->config(s);
}
#endif
void config(const char *json, size_t length) {
bool b = extism_plugin_config(this->context.get(), this->plugin,
(const uint8_t *)json, length);
if (!b) {
const char *err = extism_error(this->context.get(), this->plugin);
throw Error(err == nullptr ? "Unable to update plugin config" : err);
}
}
void config(const std::string &json) {
this->config(json.c_str(), json.size());
}
Buffer call(const std::string &func, const uint8_t *input,
ExtismSize input_length) const {
ExtismSize input_length) {
int32_t rc = extism_plugin_call(this->context.get(), this->plugin,
func.c_str(), input, input_length);
if (rc != 0) {
@@ -248,19 +80,13 @@ public:
return Buffer(ptr, length);
}
Buffer call(const std::string &func,
const std::vector<uint8_t> &input) const {
Buffer call(const std::string &func, const std::vector<uint8_t> &input) {
return this->call(func, input.data(), input.size());
}
Buffer call(const std::string &func, const std::string &input) const {
Buffer call(const std::string &func, const std::string &input) {
return this->call(func, (const uint8_t *)input.c_str(), input.size());
}
bool function_exists(const std::string &func) const {
return extism_plugin_function_exists(this->context.get(), this->plugin,
func.c_str());
}
};
class Context {
@@ -271,33 +97,18 @@ public:
extism_context_free);
}
Plugin plugin(const uint8_t *wasm, size_t length,
bool with_wasi = false) const {
Plugin plugin(const uint8_t *wasm, size_t length, bool with_wasi = false) {
return Plugin(this->pointer, wasm, length, with_wasi);
}
Plugin plugin(const std::string &str, bool with_wasi = false) const {
Plugin plugin(const std::string &str, bool with_wasi = false) {
return Plugin(this->pointer, (const uint8_t *)str.c_str(), str.size(),
with_wasi);
}
Plugin plugin(const std::vector<uint8_t> &data,
bool with_wasi = false) const {
Plugin plugin(const std::vector<uint8_t> &data, bool with_wasi = false) {
return Plugin(this->pointer, data.data(), data.size(), with_wasi);
}
#ifndef EXTISM_NO_JSON
Plugin plugin(const Manifest &manifest, bool with_wasi = false) const {
return Plugin(this->pointer, manifest, with_wasi);
}
#endif
void reset() { extism_context_reset(this->pointer.get()); }
};
inline bool set_log_file(const char *filename, const char *level) {
return extism_log_file(filename, level);
}
inline std::string version() { return std::string(extism_version()); }
} // namespace extism

Binary file not shown.

View File

@@ -1,73 +0,0 @@
#include "../extism.hpp"
#include <fstream>
#include <gtest/gtest.h>
std::vector<uint8_t> read(const char *filename) {
std::ifstream file(filename, std::ios::binary);
return std::vector<uint8_t>((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
}
namespace {
using namespace extism;
TEST(Context, Basic) {
Context context;
ASSERT_NE(context.pointer, nullptr);
}
TEST(Plugin, Manifest) {
Context context;
Manifest manifest = Manifest::path("code.wasm");
manifest.set_config("a", "1");
ASSERT_NO_THROW(Plugin plugin = context.plugin(manifest));
Plugin plugin = context.plugin(manifest);
Buffer buf = plugin.call("count_vowels", "this is a test");
ASSERT_EQ((std::string)buf, "{\"count\": 4}");
}
TEST(Plugin, BadManifest) {
Context context;
Manifest manifest;
ASSERT_THROW(Plugin plugin = context.plugin(manifest), Error);
}
TEST(Plugin, Bytes) {
Context context;
auto wasm = read("code.wasm");
ASSERT_NO_THROW(Plugin plugin = context.plugin(wasm));
Plugin plugin = context.plugin(wasm);
Buffer buf = plugin.call("count_vowels", "this is another test");
ASSERT_EQ(buf.string(), "{\"count\": 6}");
}
TEST(Plugin, UpdateConfig) {
Context context;
auto wasm = read("code.wasm");
Plugin plugin = context.plugin(wasm);
Config config;
config["abc"] = "123";
ASSERT_NO_THROW(plugin.config(config));
}
TEST(Plugin, FunctionExists) {
Context context;
auto wasm = read("code.wasm");
Plugin plugin = context.plugin(wasm);
ASSERT_FALSE(plugin.function_exists("bad_function"));
ASSERT_TRUE(plugin.function_exists("count_vowels"));
}
}; // namespace
int main(int argc, char **argv) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View File

@@ -19,6 +19,6 @@
(name extism)
(synopsis "Extism bindings")
(description "Bindings to Extism, the universal plugin system")
(depends ocaml dune ctypes-foreign bigstringaf ppx_yojson_conv base64 ppx_inline_test)
(depends ocaml dune ctypes-foreign bigstringaf ppx_yojson_conv base64)
(tags
(topics wasm plugin)))

View File

@@ -1,27 +0,0 @@
.PHONY: test
prepare:
mix deps.get
mix compile
test: prepare
mix test
clean:
mix clean
publish: clean prepare
mix hex.build
mix hex.publish --yes
format:
mix format
lint:
mix format --check-formatted
docs:
mix docs
show-docs: docs
open doc/index.html

View File

@@ -2,10 +2,6 @@
Extism Host SDK for Elixir and Erlang
## Docs
Read the [docs on hexdocs.pm](https://hexdocs.pm/extism/).
## Installation
You can find this package on [hex.pm](https://hex.pm/packages/extism).
@@ -13,14 +9,12 @@ You can find this package on [hex.pm](https://hex.pm/packages/extism).
```elixir
def deps do
[
{:extism, "~> 0.0.1-rc.6"}
{:extism, "~> 0.0.1-rc.5"}
]
end
```
## Getting Started
### Example
## Usage
```elixir
# Create a context for which plugins can be allocated and cleaned
@@ -42,32 +36,3 @@ manifest = %{ wasm: [ %{ path: "/Users/ben/code/extism/wasm/code.wasm" } ]}
# free up the context and any plugins we allocated
Extism.Context.free(ctx)
```
### Modules
The two primary modules you should learn are:
* [Extism.Context](Extism.Context.html)
* [Extism.Plugin](Extism.Plugin.html)
#### Context
The [Context](Extism.Context.html) can be thought of as a session. You need a context to interact with the Extism runtime. The context holds your plugins and when you free the context, it frees your plugins. It's important to free up your context and plugins when you are done with them.
```elixir
ctx = Extism.Context.new()
# frees all the plugins
Extism.Context.reset(ctx)
# frees the context and all its plugins
Extism.Context.free(ctx)
```
#### Plugin
The [Plugin](Extism.Plugin.html) represents an instance of your WASM program from the given manifest.
The key method to know here is [Extism.Plugin#call](Extism.Plugin.html#call/3) which takes a function name to invoke and some input data, and returns the results from the plugin.
```elixir
{:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
{:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
```

View File

@@ -1,9 +1,4 @@
defmodule Extism.Context do
@moduledoc """
A Context is needed to create plugins. The Context is where your plugins
live. Freeing the context frees all of the plugins in its scope.
"""
defstruct [
# The actual NIF Resource. A pointer in this case
ptr: nil
@@ -15,47 +10,21 @@ defmodule Extism.Context do
}
end
@doc """
Creates a new context.
"""
def new() do
ptr = Extism.Native.context_new()
Extism.Context.wrap_resource(ptr)
end
@doc """
Resets the context. This has the effect of freeing all the plugins created so far.
"""
def reset(ctx) do
Extism.Native.context_reset(ctx.ptr)
end
@doc """
Frees the context from memory and all of its plugins.
"""
def free(ctx) do
Extism.Native.context_free(ctx.ptr)
end
@doc """
Create a new plugin from a WASM module or manifest
## Examples:
iex> ctx = Extism.Context.new()
iex> manifest = %{ wasm: [ %{ path: "/Users/ben/code/extism/wasm/code.wasm" } ]}
iex> {:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
## Parameters
- ctx: The Context to manage this plugin
- manifest: The String or Map of the WASM module or [manifest](https://extism.org/docs/concepts/manifest)
- wasi: A bool you set to true if you want WASI support
"""
def new_plugin(ctx, manifest, wasi \\ false) do
def new_plugin(ctx, manifest, wasi) do
{:ok, manifest_payload} = JSON.encode(manifest)
case Extism.Native.plugin_new_with_manifest(ctx.ptr, manifest_payload, wasi) do
{:error, err} -> {:error, err}
res -> {:ok, Extism.Plugin.wrap_resource(ctx, res)}

View File

@@ -1,11 +1,7 @@
defmodule Extism.Native do
@moduledoc """
This module represents the Native Extism runtime API and is for internal use.
Do not use or rely on this this module.
"""
use Rustler,
otp_app: :extism,
crate: :extism_nif
otp_app: :extism,
crate: :extism_nif
def context_new(), do: error()
def context_reset(_ctx), do: error()

View File

@@ -1,7 +1,4 @@
defmodule Extism.Plugin do
@moduledoc """
A Plugin represents an instance of your WASM program from the given manifest.
"""
defstruct [
# The actual NIF Resource. PluginIndex and the context
plugin_id: nil,
@@ -15,26 +12,6 @@ defmodule Extism.Plugin do
}
end
@doc """
Call a plugin's function by name
## Examples
iex> {:ok, plugin} = Extism.Context.new_plugin(ctx, manifest, false)
iex> {:ok, output} = Extism.Plugin.call(plugin, "count_vowels", "this is a test")
# {:ok, "{\"count\": 4}"}
## Parameters
- plugin: The plugin
- name: The name of the function as a string
- input: The input data as a string
## Returns
A string representation of the functions output
"""
def call(plugin, name, input) do
case Extism.Native.plugin_call(plugin.ctx.ptr, plugin.plugin_id, name, input) do
{:error, err} -> {:error, err}
@@ -42,44 +19,21 @@ defmodule Extism.Plugin do
end
end
@doc """
Updates the manifest of the given plugin
def update(plugin, manifest, wasi) when is_map(manifest) do
{:ok, manifest_payload} = JSON.encode(manifest)
case Extism.Native.plugin_update_manifest(plugin.ctx.ptr, plugin.plugin_id, manifest_payload, wasi) do
{:error, err} -> {:error, err}
res -> :ok
end
end
## Parameters
def free(plugin) do
Extism.Native.plugin_free(plugin.ctx.ptr, plugin.plugin_id)
end
- ctx: The Context to manage this plugin
- manifest: The String or Map of the WASM module or [manifest](https://extism.org/docs/concepts/manifest)
- wasi: A bool you set to true if you want WASI support
"""
def update(plugin, manifest, wasi) when is_map(manifest) do
{:ok, manifest_payload} = JSON.encode(manifest)
case Extism.Native.plugin_update_manifest(
plugin.ctx.ptr,
plugin.plugin_id,
manifest_payload,
wasi
) do
{:error, err} -> {:error, err}
_ -> :ok
end
end
@doc """
Frees the plugin
"""
def free(plugin) do
Extism.Native.plugin_free(plugin.ctx.ptr, plugin.plugin_id)
end
@doc """
Returns true if the given plugin responds to the given function name
"""
def has_function(plugin, function_name) do
Extism.Native.plugin_has_function(plugin.ctx.ptr, plugin.plugin_id, function_name)
end
def has_function(plugin, function_name) do
Extism.Native.plugin_has_function(plugin.ctx.ptr, plugin.plugin_id, function_name)
end
end
defimpl Inspect, for: Extim.Plugin do

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -4,13 +4,12 @@ defmodule Extism.MixProject do
def project do
[
app: :extism,
version: "0.0.1",
version: "0.0.1-rc.5",
elixir: "~> 1.14",
start_permanent: Mix.env() == :prod,
deps: deps(),
package: package(),
aliases: aliases(),
docs: docs()
aliases: aliases()
]
end
@@ -25,7 +24,7 @@ defmodule Extism.MixProject do
[
{:rustler, "~> 0.26.0"},
{:json, "~> 1.4"},
{:ex_doc, "~> 0.21", only: :dev, runtime: false}
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
]
end
@@ -44,16 +43,7 @@ defmodule Extism.MixProject do
description: "Extism Host SDK for Elixir and Erlang",
name: "extism",
files: ~w(lib native priv .formatter.exs mix.exs README.md LICENSE),
links: %{"GitHub" => "https://github.com/extism/extism"}
]
end
defp docs do
[
main: "Extism",
logo: "./logo.png",
main: "readme",
extras: ["README.md"]
links: %{ "GitHub" => "https://github.com/extism/extism" },
]
end
end

View File

@@ -1,6 +1,6 @@
%{
"earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"},
"ex_doc": {:hex, :ex_doc, "0.29.1", "b1c652fa5f92ee9cf15c75271168027f92039b3877094290a75abcaac82a9f77", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "b7745fa6374a36daf484e2a2012274950e084815b936b1319aeebcf7809574f6"},
"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"},
"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"},

View File

@@ -1,6 +1,6 @@
[package]
name = "extism_nif"
version = "0.0.1-rc.6"
version = "0.0.1-rc.5"
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.6" }
extism = { version = "0.0.1-rc.5" }
log = "0.4"

View File

@@ -115,11 +115,8 @@ fn set_log_file(filename: String, log_level: String) -> Result<Atom, rustler::Er
match log::Level::from_str(&log_level) {
Err(_e) => Err(rustler::Error::Term(Box::new(format!("{} not a valid log level", log_level)))),
Ok(level) => {
if extism::set_log_file(path, Some(level)) {
Ok(atoms::ok())
} else {
Err(rustler::Error::Term(Box::new("Did not set log file, received false from the API.")))
}
extism::set_log_file(path, Some(level));
Ok(atoms::ok())
}
}
}

View File

@@ -35,8 +35,6 @@ 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

View File

@@ -194,7 +194,7 @@ func (plugin Plugin) SetConfig(data map[string][]byte) error {
return nil
}
// FunctionExists returns true when the named function is present in the plugin
/// FunctionExists returns true when the name function is present in the plugin
func (plugin Plugin) FunctionExists(functionName string) bool {
name := C.CString(functionName)
b := C.extism_plugin_function_exists(plugin.ctx.pointer, C.int(plugin.id), name)
@@ -202,7 +202,7 @@ func (plugin Plugin) FunctionExists(functionName string) bool {
return bool(b)
}
// Call a function by name with the given input, returning the output
/// Call a function by name with the given input, returning the output
func (plugin Plugin) Call(functionName string, input []byte) ([]byte, error) {
ptr := makePointer(input)
name := C.CString(functionName)

View File

@@ -16,7 +16,6 @@ depends: [
"bigstringaf"
"ppx_yojson_conv"
"base64"
"ppx_inline_test"
"odoc" {with-doc}
]
build: [

View File

@@ -1,148 +0,0 @@
package extism
import (
"encoding/json"
"fmt"
"testing"
)
func manifest() Manifest {
return Manifest{
Wasm: []Wasm{
WasmFile{
Path: "./wasm/code.wasm",
},
},
}
}
func expectVowelCount(plugin Plugin, input string, count int) error {
out, err := plugin.Call("count_vowels", []byte(input))
if err != nil {
return err
}
var result map[string]int
json.Unmarshal(out, &result)
if result["count"] != count {
return fmt.Errorf("Got count %d but expected %d", result["count"], count)
}
return nil
}
func TestCreateAndFreeContext(t *testing.T) {
ctx := NewContext()
ctx.Free()
}
func TestCallPlugin(t *testing.T) {
ctx := NewContext()
defer ctx.Free()
plugin, err := ctx.PluginFromManifest(manifest(), false)
if err != nil {
t.Error(err)
}
if err := expectVowelCount(plugin, "this is a test", 4); err != nil {
t.Error(err)
}
if err := expectVowelCount(plugin, "this is a test again", 7); err != nil {
t.Error(err)
}
if err := expectVowelCount(plugin, "this is a test thrice", 6); err != nil {
t.Error(err)
}
}
func TestFreePlugin(t *testing.T) {
ctx := NewContext()
defer ctx.Free()
plugin, err := ctx.PluginFromManifest(manifest(), false)
if err != nil {
t.Error(err)
}
if err := expectVowelCount(plugin, "this is a test", 4); err != nil {
t.Error(err)
}
// free this specific plugin
plugin.Free()
if err := expectVowelCount(plugin, "this is a test", 4); err == nil {
t.Fatal("Expected an error after plugin was freed")
}
}
func TestContextReset(t *testing.T) {
ctx := NewContext()
defer ctx.Free()
plugin, err := ctx.PluginFromManifest(manifest(), false)
if err != nil {
t.Error(err)
}
if err := expectVowelCount(plugin, "this is a test", 4); err != nil {
t.Error(err)
}
// reset the context dropping all plugins
ctx.Reset()
if err := expectVowelCount(plugin, "this is a test", 4); err == nil {
t.Fatal("Expected an error after plugin was freed")
}
}
func TestCanUpdateAManifest(t *testing.T) {
ctx := NewContext()
defer ctx.Free()
plugin, err := ctx.PluginFromManifest(manifest(), false)
if err != nil {
t.Error(err)
}
if err := expectVowelCount(plugin, "this is a test", 4); err != nil {
t.Error(err)
}
plugin.UpdateManifest(manifest(), false)
// can still call the plugin
if err := expectVowelCount(plugin, "this is a test", 4); err != nil {
t.Error(err)
}
}
func TestFunctionExists(t *testing.T) {
ctx := NewContext()
defer ctx.Free()
plugin, err := ctx.PluginFromManifest(manifest(), false)
if err != nil {
t.Error(err)
}
if !plugin.FunctionExists("count_vowels") {
t.Fatal("Was expecting to find the function count_vowels")
}
if plugin.FunctionExists("i_dont_exist") {
t.Fatal("Was not expecting to find the function i_dont_exist")
}
}
func TestErrorsOnUnknownFunction(t *testing.T) {
ctx := NewContext()
defer ctx.Free()
plugin, err := ctx.PluginFromManifest(manifest(), false)
if err != nil {
t.Error(err)
}
_, err = plugin.Call("i_dont_exist", []byte("someinput"))
if err == nil {
t.Fatal("Was expecting call to unknown function to fail")
}
}

View File

@@ -6,7 +6,7 @@ import Extism
import Extism.Manifest
try f (Right x) = f x
try f (Left (ErrorMessage msg)) = do
try f (Left (Error msg)) = do
_ <- putStrLn msg
exitFailure

View File

@@ -1,6 +1,6 @@
cabal-version: 2.4
name: extism
version: 0.0.1
version: 0.0.1.0
-- A short (one-line) description of the package.
synopsis: Extism bindings
@@ -19,11 +19,11 @@ maintainer: oss@extism.org
-- A copyright notice.
-- copyright:
category: Plugins, WebAssembly
category: Plugins
extra-source-files: CHANGELOG.md
library
exposed-modules: Extism Extism.Manifest
exposed-modules: Extism Extism.Manifest
-- Modules included in this library but not exported.
other-modules:
@@ -45,10 +45,3 @@ Test-Suite extism-example
main-is: Example.hs
build-depends: base, extism, bytestring
default-language: Haskell2010
Test-Suite extism-test
type: exitcode-stdio-1.0
main-is: Test.hs
hs-source-dirs: test
build-depends: base, extism, bytestring, HUnit
default-language: Haskell2010

View File

@@ -11,8 +11,7 @@ import Control.Monad (void)
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 Text.JSON (JSON, toJSObject, encode)
import Extism.Manifest (Manifest, toString)
newtype ExtismContext = ExtismContext () deriving Show
@@ -38,11 +37,8 @@ newtype Context = Context (ForeignPtr ExtismContext)
-- Plugins can be used to call WASM function
data Plugin = Plugin Context Int32
-- Log level
data LogLevel = Error | Warn | Info | Debug | Trace deriving (Show)
-- Extism error
newtype Error = ErrorMessage String deriving Show
newtype Error = Error String deriving Show
-- Helper function to convert a string to a bytestring
toByteString :: String -> ByteString
@@ -61,7 +57,8 @@ extismVersion () = do
-- Remove all registered plugins in a Context
reset :: Context -> IO ()
reset (Context ctx) =
withForeignPtr ctx extism_context_reset
withForeignPtr ctx (\ctx ->
extism_context_reset ctx)
-- Create a new context
newContext :: () -> IO Context
@@ -69,12 +66,6 @@ newContext () = do
ptr <- extism_context_new
fptr <- newForeignPtr extism_context_free ptr
return (Context fptr)
-- Execute a function with a new context that is destroyed when it returns
withContext :: (Context -> IO a) -> IO a
withContext f = do
ctx <- newContext ()
f ctx
-- Create a plugin from a WASM module, `useWasi` determines if WASI should
-- be linked
@@ -90,7 +81,7 @@ plugin c wasm useWasi =
if p < 0 then do
err <- extism_error ctx (-1)
e <- peekCString err
return $ Left (ErrorMessage e)
return $ Left (Error e)
else
return $ Right (Plugin c p))
@@ -112,7 +103,7 @@ update (Plugin (Context ctx) id) wasm useWasi =
if b <= 0 then do
err <- extism_error ctx (-1)
e <- peekCString err
return $ Left (ErrorMessage e)
return $ Left (Error e)
else
return (Right ()))
@@ -126,37 +117,25 @@ 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 -> [(String, Maybe String)] -> IO ()
setConfig (Plugin (Context ctx) plugin) x =
if plugin < 0
then return False
then return ()
else
let obj = toJSObject [(k, convertMaybeString v) | (k, v) <- x] in
let obj = toJSObject x in
let bs = toByteString (encode obj) in
let length = fromIntegral (B.length bs) in
unsafeUseAsCString bs (\s -> do
withForeignPtr ctx (\ctx -> do
b <- extism_plugin_config ctx plugin (castPtr s) length
return $ b /= 0))
levelStr Error = "error"
levelStr Debug = "debug"
levelStr Warn = "warn"
levelStr Trace = "trace"
levelStr Info = "info"
withForeignPtr ctx (\ctx ->
void $ extism_plugin_config ctx plugin (castPtr s) length))
-- Set the log file and level, this is a global configuration
setLogFile :: String -> LogLevel -> IO Bool
setLogFile :: String -> String -> IO ()
setLogFile filename level =
let s = levelStr level in
withCString filename (\f ->
withCString s (\l -> do
b <- extism_log_file f l
return $ b /= 0))
withCString level (\l -> do
void $ extism_log_file f l))
-- Check if a function exists in the given plugin
functionExists :: Plugin -> String -> IO Bool
@@ -177,16 +156,17 @@ call (Plugin (Context ctx) plugin) name input =
err <- extism_error ctx plugin
if err /= nullPtr
then do e <- peekCString err
return $ Left (ErrorMessage e)
return $ Left (Error e)
else if rc == 0
then do
length <- extism_plugin_output_length ctx plugin
ptr <- extism_plugin_output_data ctx plugin
buf <- packCStringLen (castPtr ptr, fromIntegral length)
return $ Right buf
else return $ Left (ErrorMessage "Call failed"))
else return $ Left (Error "Call failed"))
-- Free a plugin
free :: Plugin -> IO ()
free (Plugin (Context ctx) plugin) =
withForeignPtr ctx (`extism_plugin_free` plugin)
withForeignPtr ctx (\ctx ->
extism_plugin_free ctx plugin)

View File

@@ -1,73 +0,0 @@
import Test.HUnit
import Extism
import Extism.Manifest
unwrap' (Right x) = return x
unwrap' (Left (ErrorMessage msg)) =
assertFailure msg
unwrap io = do
x <- io
unwrap' x
defaultManifest = manifest [wasmFile "test/code.wasm"]
initPlugin context =
unwrap (Extism.pluginFromManifest context defaultManifest False)
pluginFunctionExists = do
withContext (\ctx -> do
p <- initPlugin ctx
exists <- functionExists p "count_vowels"
assertBool "function exists" exists
exists' <- functionExists p "function_doesnt_exist"
assertBool "function doesn't exist" (not exists'))
checkCallResult p = do
res <- unwrap (call p "count_vowels" (toByteString "this is a test"))
assertEqual "count vowels output" "{\"count\": 4}" (fromByteString res)
pluginCall = do
withContext (\ctx -> do
p <- initPlugin ctx
checkCallResult p)
pluginMultiple = do
withContext (\ctx -> do
p <- initPlugin ctx
checkCallResult p
q <- initPlugin ctx
r <- initPlugin ctx
checkCallResult q
checkCallResult r)
pluginUpdate = do
withContext (\ctx -> do
p <- initPlugin ctx
unwrap (updateManifest p defaultManifest True)
checkCallResult p)
pluginConfig = do
withContext (\ctx -> do
p <- initPlugin ctx
b <- setConfig p [("a", Just "1"), ("b", Just "2"), ("c", Just "3"), ("d", Nothing)]
assertBool "set config" b)
testSetLogFile = do
b <- setLogFile "stderr" Error
assertBool "set log file" b
t name f = TestLabel name (TestCase f)
main = do
runTestTT (TestList
[
t "Plugin.FunctionExists" pluginFunctionExists
, t "Plugin.Call" pluginCall
, t "Plugin.Multiple" pluginMultiple
, t "Plugin.Update" pluginUpdate
, t "Plugin.Config" pluginConfig
, t "SetLogFile" testSetLogFile
])

Binary file not shown.

View File

@@ -1,23 +0,0 @@
[package]
name = "libextism"
version = "0.0.1"
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

View File

@@ -1,10 +0,0 @@
//! 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");
}

View File

@@ -1,6 +1,6 @@
[package]
name = "extism-manifest"
version = "0.0.1"
version = "0.0.1-rc.5"
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}

2
node/.gitignore vendored
View File

@@ -1,3 +1 @@
dist/
coverage/
doc/

View File

@@ -1,2 +0,0 @@
dist
coverage

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,26 +0,0 @@
.PHONY: test
prepare:
npm install
test: prepare
npm run example
npm run test
clean:
echo "No clean implemented"
publish: clean prepare
npm publish
format:
npx prettier --write .
lint:
npx prettier --check .
docs:
npx typedoc --out doc src
show-docs: docs
open doc/index.html

22
node/example.js Normal file
View File

@@ -0,0 +1,22 @@
import { withContext, Context } from './dist/index.js';
import { readFileSync } from 'fs';
withContext(async function (context) {
let wasm = readFileSync('../wasm/code.wasm');
let p = context.plugin(wasm);
if (!p.functionExists('count_vowels')) {
console.log("no function 'count_vowels' in wasm");
process.exit(1);
}
let buf = await p.call('count_vowels', process.argv[2] || 'this is a test');
console.log(JSON.parse(buf.toString())['count']);
p.free();
});
// or, use a context like this:
let ctx = new Context();
let wasm = readFileSync('../wasm/code.wasm');
let p = ctx.plugin(wasm);
// ... where the context can be passed around to various functions etc.

View File

@@ -1,22 +0,0 @@
import { withContext, Context } from "./dist/index.js";
import { readFileSync } from "fs";
withContext(async function (context) {
let wasm = readFileSync("../wasm/code.wasm");
let p = context.plugin(wasm);
if (!p.functionExists("count_vowels")) {
console.log("no function 'count_vowels' in wasm");
process.exit(1);
}
let buf = await p.call("count_vowels", process.argv[2] || "this is a test");
console.log(JSON.parse(buf.toString())["count"]);
p.free();
});
// or, use a context like this:
let ctx = new Context();
let wasm = readFileSync("../wasm/code.wasm");
let p = ctx.plugin(wasm);
// ... where the context can be passed around to various functions etc.

View File

@@ -1,5 +0,0 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
};

6520
node/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@extism/extism",
"version": "0.0.1",
"version": "0.0.1-rc.5",
"description": "Extism Host SDK for Node",
"keywords": [
"extism",
@@ -14,16 +14,16 @@
"private": false,
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"homepage": "https://extism.org",
"repository": {
"type": "git",
"url": "https://github.com/extism/extism.git"
},
"scripts": {
"prepare": "npm run build",
"example": "node example.mjs",
"build": "tsc",
"test": "jest --coverage"
"prepare" : "npm run build",
"example": "node example.js",
"build": "tsc"
},
"dependencies": {
"ffi-napi": "^4.0.3"
@@ -33,13 +33,7 @@
},
"devDependencies": {
"@types/ffi-napi": "^4.0.6",
"@types/jest": "^29.2.0",
"@types/node": "^18.11.4",
"jest": "^29.2.2",
"prettier": "2.8.0",
"ts-jest": "^29.0.3",
"ts-node": "^10.9.1",
"typedoc": "^0.23.18",
"typescript": "^4.8.4"
}
}

View File

@@ -1,434 +1,253 @@
import ffi from "ffi-napi";
import path from "path";
import ffi from 'ffi-napi';
import path from 'path';
import url from 'url';
const context = "void*";
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
const context = 'void*';
const _functions = {
extism_context_new: [context, []],
extism_context_free: ["void", [context]],
extism_plugin_new: ["int32", [context, "string", "uint64", "bool"]],
extism_plugin_update: [
"bool",
[context, "int32", "string", "uint64", "bool"],
],
extism_error: ["char*", [context, "int32"]],
extism_plugin_call: [
"int32",
[context, "int32", "string", "string", "uint64"],
],
extism_plugin_output_length: ["uint64", [context, "int32"]],
extism_plugin_output_data: ["uint8*", [context, "int32"]],
extism_log_file: ["bool", ["string", "char*"]],
extism_plugin_function_exists: ["bool", [context, "int32", "string"]],
extism_plugin_config: ["void", [context, "int32", "char*", "uint64"]],
extism_plugin_free: ["void", [context, "int32"]],
extism_context_reset: ["void", [context]],
extism_version: ["char*", []],
extism_context_new: [context, []],
extism_context_free: ['void', [context]],
extism_plugin_new: ['int32', [context, 'string', 'uint64', 'bool']],
extism_plugin_update: ['bool', [context, 'int32', 'string', 'uint64', 'bool']],
extism_error: ['char*', [context, 'int32']],
extism_plugin_call: ['int32', [context, 'int32', 'string', 'string', 'uint64']],
extism_plugin_output_length: ['uint64', [context, 'int32']],
extism_plugin_output_data: ['uint8*', [context, 'int32']],
extism_log_file: ['bool', ['string', 'char*']],
extism_plugin_function_exists: ['bool', [context, 'int32', 'string']],
extism_plugin_config: ['void', [context, 'int32', 'char*', 'uint64']],
extism_plugin_free: ['void', [context, 'int32']],
extism_context_reset: ['void', [context]],
extism_version: ['char*', []],
};
interface LibExtism {
extism_context_new: () => Buffer;
extism_context_free: (ctx: Buffer) => void;
extism_plugin_new: (
ctx: Buffer,
data: string | Buffer,
data_len: number,
wasi: boolean
) => number;
extism_plugin_update: (
ctx: Buffer,
plugin_id: number,
data: string | Buffer,
data_len: number,
wasi: boolean
) => boolean;
extism_error: (ctx: Buffer, plugin_id: number) => Buffer;
extism_plugin_call: (
ctx: Buffer,
plugin_id: number,
func: string,
input: string,
input_len: number
) => number;
extism_plugin_output_length: (ctx: Buffer, plugin_id: number) => number;
extism_plugin_output_data: (ctx: Buffer, plugin_id: Number) => Uint8Array;
extism_log_file: (file: string, level: string) => boolean;
extism_plugin_function_exists: (
ctx: Buffer,
plugin_id: number,
func: string
) => boolean;
extism_plugin_config: (
ctx: Buffer,
plugin_id: number,
data: string | Buffer,
data_len: number
) => void;
extism_plugin_free: (ctx: Buffer, plugin_id: number) => void;
extism_context_reset: (ctx: Buffer) => void;
extism_version: () => Buffer;
extism_context_new: () => Buffer;
extism_context_free: (ctx: Buffer) => void;
extism_plugin_new: (ctx: Buffer, data: string | Buffer, data_len: number, wasi: boolean) => number;
extism_plugin_update: (ctx: Buffer, plugin_id: number, data: string | Buffer, data_len: number, wasi: boolean) => boolean;
extism_error: (ctx: Buffer, plugin_id: number) => string;
extism_plugin_call: (ctx: Buffer, plugin_id: number, func: string, input: string, input_len: number) => number;
extism_plugin_output_length: (ctx: Buffer, plugin_id: number) => number;
extism_plugin_output_data: (ctx: Buffer, plugin_id: Number) => Uint8Array;
extism_log_file: (file: string, level: string) => boolean;
extism_plugin_function_exists: (ctx: Buffer, plugin_id: number, func: string) => boolean;
extism_plugin_config: (ctx: Buffer, plugin_id: number, data: string | Buffer, data_len: number) => void;
extism_plugin_free: (ctx: Buffer, plugin_id: number) => void;
extism_context_reset: (ctx: Buffer) => void;
extism_version: () => string;
}
function locate(paths: string[]): LibExtism {
for (var i = 0; i < paths.length; i++) {
try {
// @ts-ignore
return ffi.Library(path.join(paths[i], "libextism"), _functions);
} catch (exn) {
continue;
for (var i = 0; i < paths.length; i++) {
try {
// @ts-ignore
return ffi.Library(path.join(paths[i], 'libextism'), _functions);
} catch (exn) {
continue;
}
}
}
throw "Unable to locate libextism";
throw 'Unable to locate libextism';
}
const searchPath = [
__dirname,
"/usr/local/lib",
"/usr/lib",
path.join(process.env.HOME as string, ".local", "lib"),
__dirname,
'/usr/local/lib',
'/usr/lib',
path.join(process.env.HOME as string, '.local', 'lib'),
];
if (process.env.EXTISM_PATH) {
searchPath.unshift(path.join(process.env.EXTISM_PATH, "lib"));
searchPath.unshift(path.join(process.env.EXTISM_PATH, 'lib'));
}
const lib = locate(searchPath);
/**
* Sets the logfile and level of the Extism runtime
*
* @param filename - The path to the logfile
* @param level - The level, one of ('debug', 'error', 'info', 'trace')
*/
// Set the log file and level
export function setLogFile(filename: string, level?: string) {
lib.extism_log_file(filename, level || "info");
lib.extism_log_file(filename, level || "info");
}
/**
* Get the version of Extism
*
* @returns The version string of the Extism runtime
*/
// Get the version of Extism
export function extismVersion(): string {
return lib.extism_version().toString();
return lib.extism_version().toString();
}
// @ts-ignore
const pluginRegistry = new FinalizationRegistry(({ id, pointer }) => {
if (id && pointer) lib.extism_plugin_free(pointer, id);
lib.extism_plugin_free(pointer, id);
});
// @ts-ignore
const contextRegistry = new FinalizationRegistry((pointer) => {
if (pointer) lib.extism_context_free(pointer);
lib.extism_context_free(pointer);
});
/**
* Represents a path or url to a WASM module
*/
export type ManifestWasmFile = {
path: string;
name?: string;
hash?: string;
};
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;
};
data: Uint8Array;
name?: string;
hash?: string;
}
/**
* Memory options for the {@link Plugin}
*/
export type ManifestMemory = {
max?: number;
};
max?: number;
}
/**
* {@link Plugin} 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 Plugin} 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>;
};
wasm: Array<ManifestWasm>;
memory: ManifestMemory;
config?: PluginConfig;
allowed_hosts?: Array<string>;
}
/**
* Can be a {@link Manifest} or just the raw bytes of the WASM module.
* We recommend using {@link Manifest}
*/
type ManifestData = Manifest | Buffer | string;
/**
* 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. We recommand managing
* the context with {@link withContext}
*
* @see {@link withContext}
*
* @example
* Use withContext to ensure your memory is cleaned up
* ```
* const output = await withContext(async (ctx) => {
* const plugin = ctx.plugin(manifest)
* return await plugin.call("func", "my-input")
* })
* ```
*
* @example
* You can manage manually if you need a long-lived context
* ```
* const ctx = Context()
* // free all the plugins and reset
* ctx.reset()
* // free everything
* ctx.free()
* ```
*/
// Context manages plugins
export class Context {
pointer: Buffer | null;
pointer: Buffer | null;
/**
* Construct a context
*/
constructor() {
this.pointer = lib.extism_context_new();
contextRegistry.register(this, this.pointer, this);
}
/**
* Create a plugin managed by this context
*
* @param manifest - The {@link Manifest} describing the plugin code and config
* @param wasi - Set to `true` to enable WASI
* @param config - Config details for the plugin
* @returns A new Plugin scoped to this Context
*/
plugin(manifest: ManifestData, wasi: boolean = false, config?: PluginConfig) {
return new Plugin(this, manifest, wasi, config);
}
/**
* Frees the context. Should be called after the context is not needed to reclaim the memory.
*/
free() {
if (this.pointer) {
contextRegistry.unregister(this);
lib.extism_context_free(this.pointer);
}
this.pointer = null;
}
/**
* Resets the context. This clears all the plugins but keeps the context alive.
*/
reset() {
if (this.pointer) lib.extism_context_reset(this.pointer);
}
}
/**
* Creates a context and gives you a scope to use it. This will ensure the context
* and all its plugins are cleaned up for you when you are done.
*
* @param f - The callback function with the context
* @returns Whatever your callback returns
*/
export async function withContext(f: (ctx: Context) => Promise<any>) {
const ctx = new Context();
try {
const x = await f(ctx);
ctx.free();
return x;
} catch (err) {
ctx.free();
throw err;
}
}
/**
* A Plugin represents an instance of your WASM program from the given manifest.
*/
export class Plugin {
id: number;
ctx: Context;
/**
* Constructor for a plugin. @see {@link Context#plugin}.
*
* @param ctx - The context to manage this plugin
* @param manifest - The {@link Manifest}
* @param wasi - Set to true to enable WASI support
* @param config - The plugin config
*/
constructor(
ctx: Context,
manifest: ManifestData,
wasi: boolean = false,
config?: PluginConfig
) {
let dataRaw: string | Buffer;
if (Buffer.isBuffer(manifest) || typeof manifest === "string") {
dataRaw = manifest;
} else if (typeof manifest === "object" && manifest.wasm) {
dataRaw = JSON.stringify(manifest);
} else {
throw Error(`Unknown manifest type ${typeof manifest}`);
}
if (!ctx.pointer) throw Error("No Context set");
let plugin = lib.extism_plugin_new(
ctx.pointer,
dataRaw,
Buffer.byteLength(dataRaw, 'utf-8'),
wasi
);
if (plugin < 0) {
var err = lib.extism_error(ctx.pointer, -1);
if (err.length === 0) {
throw "extism_context_plugin failed";
}
throw `Unable to load plugin: ${err.toString()}`;
}
this.id = plugin;
this.ctx = ctx;
pluginRegistry.register(
this,
{ id: this.id, pointer: this.ctx.pointer },
this
);
if (config != null) {
let s = JSON.stringify(config);
lib.extism_plugin_config(ctx.pointer, this.id, s, Buffer.byteLength(s, 'utf-8'),);
}
}
/**
* Update an existing plugin with new WASM or manifest
*
* @param manifest - The new {@link Manifest} data
* @param wasi - Set to true to enable WASI support
* @param config - The new plugin config
*/
update(manifest: ManifestData, wasi: boolean = false, config?: PluginConfig) {
let dataRaw: string | Buffer;
if (Buffer.isBuffer(manifest) || typeof manifest === "string") {
dataRaw = manifest;
} else if (typeof manifest === "object" && manifest.wasm) {
dataRaw = JSON.stringify(manifest);
} else {
throw Error("Unknown manifest type type");
}
if (!this.ctx.pointer) throw Error("No Context set");
const ok = lib.extism_plugin_update(
this.ctx.pointer,
this.id,
dataRaw,
Buffer.byteLength(dataRaw, 'utf-8'),
wasi
);
if (!ok) {
var err = lib.extism_error(this.ctx.pointer, -1);
if (err.length === 0) {
throw "extism_plugin_update failed";
}
throw `Unable to update plugin: ${err.toString()}`;
constructor() {
this.pointer = lib.extism_context_new();
contextRegistry.register(this, this.pointer, this);
}
if (config != null) {
let s = JSON.stringify(config);
lib.extism_plugin_config(this.ctx.pointer, this.id, s, Buffer.byteLength(s, 'utf-8'),);
// Create a new plugin, optionally enabling WASI
plugin(data: ManifestData, wasi: boolean = false, config?: PluginConfig) {
return new Plugin(this, data, wasi, config);
}
}
/**
* Check if a function exists by name
*
* @param functionName - The name of the function
* @returns true if the function exists, false if not
*/
functionExists(functionName: string) {
if (!this.ctx.pointer) throw Error("No Context set");
return lib.extism_plugin_function_exists(
this.ctx.pointer,
this.id,
functionName
);
}
/**
* Invoke a plugin function with given name and input.
*
* @example
* ```
* const manifest = { wasm: [{ path: "/tmp/code.wasm" }] }
* const plugin = ctx.plugin(manifest)
* const output = await plugin.call("my_function", "some-input")
* output.toString()
* // => "output from the function"
* ```
*
* @param functionName - The name of the function
* @param input - The input data
* @returns A Buffer repreesentation of the output
*/
async call(functionName: string, input: string | Buffer): Promise<Buffer> {
return new Promise<Buffer>((resolve, reject) => {
if (!this.ctx.pointer) throw Error("No Context set");
var rc = lib.extism_plugin_call(
this.ctx.pointer,
this.id,
functionName,
input.toString(),
Buffer.byteLength(input, 'utf-8'),
);
if (rc !== 0) {
var err = lib.extism_error(this.ctx.pointer, this.id);
if (err.length === 0) {
reject(`extism_plugin_call: "${functionName}" failed`);
// Free a context, this should be called when it is
// no longer needed
free() {
if (this.pointer) {
contextRegistry.unregister(this);
lib.extism_context_free(this.pointer);
}
this.pointer = null;
}
// Remove all registered plugins
reset() {
if (this.pointer)
lib.extism_context_reset(this.pointer);
}
}
export async function withContext(f: (ctx: Context) => Promise<any>) {
let ctx = new Context();
try {
let x = await f(ctx);
ctx.free();
return x;
} catch (err) {
ctx.free();
throw err;
}
}
// Plugin provides an interface for calling WASM functions
export class Plugin {
id: number;
ctx: Context;
constructor(ctx: Context, data: ManifestData, wasi: boolean = false, config?: PluginConfig) {
let dataRaw: string | Buffer;
if (Buffer.isBuffer(data) || typeof data === 'string') {
dataRaw = data
} else if (typeof data === 'object' && data.wasm) {
dataRaw = JSON.stringify(data)
} else {
throw Error(`Unknown manifest type ${typeof data}`);
}
if (!ctx.pointer) throw Error("No Context set");
let plugin = lib.extism_plugin_new(ctx.pointer, dataRaw, dataRaw.length, wasi);
if (plugin < 0) {
var err = lib.extism_error(ctx.pointer, -1);
if (err.length === 0) {
throw "extism_context_plugin failed";
}
throw `Unable to load plugin: ${err.toString()}`;
}
this.id = plugin;
this.ctx = ctx;
pluginRegistry.register(this, { id: this.id, pointer: this.ctx.pointer }, this);
if (config != null) {
let s = JSON.stringify(config);
lib.extism_plugin_config(ctx.pointer, this.id, s, s.length);
}
}
// Update an existing plugin with new WASM or manifest
update(data: ManifestData, wasi: boolean = false, config?: PluginConfig) {
let dataRaw: string | Buffer;
if (Buffer.isBuffer(data) || typeof data === 'string') {
dataRaw = data
} else if (typeof data === 'object' && data.wasm) {
dataRaw = JSON.stringify(data)
} else {
throw Error("Unknown manifest type type");
}
if (!this.ctx.pointer) throw Error("No Context set");
const ok = lib.extism_plugin_update(this.ctx.pointer, this.id, dataRaw, dataRaw.length, wasi);
if (!ok) {
var err = lib.extism_error(this.ctx.pointer, -1);
if (err.length === 0) {
throw "extism_plugin_update failed";
}
throw `Unable to update plugin: ${err.toString()}`;
}
if (config != null) {
let s = JSON.stringify(config);
lib.extism_plugin_config(this.ctx.pointer, this.id, s, s.length);
}
}
// Check if a function exists
functionExists(name: string) {
if (!this.ctx.pointer) throw Error("No Context set");
return lib.extism_plugin_function_exists(this.ctx.pointer, this.id, name);
}
// Call a function by name with the given input
async call(name: string, input: string) {
return new Promise((resolve, reject) => {
if (!this.ctx.pointer) throw Error("No Context set");
var rc = lib.extism_plugin_call(this.ctx.pointer, this.id, name, input, input.length);
if (rc !== 0) {
var err = lib.extism_error(this.ctx.pointer, this.id);
if (err.length === 0) {
reject(`extism_plugin_call: "${name}" failed`);
}
reject(`Plugin error: ${err.toString()}, code: ${rc}`);
}
var out_len = lib.extism_plugin_output_length(this.ctx.pointer, this.id);
var buf = Buffer.from(lib.extism_plugin_output_data(this.ctx.pointer, this.id).buffer, 0, out_len);
resolve(buf);
});
}
// Free a plugin, this should be called when the plugin is no longer needed
free() {
if (this.ctx.pointer && this.id !== -1) {
pluginRegistry.unregister(this);
lib.extism_plugin_free(this.ctx.pointer, this.id);
this.id = -1;
}
reject(`Plugin error: ${err.toString()}, code: ${rc}`);
}
var out_len = lib.extism_plugin_output_length(this.ctx.pointer, this.id);
var buf = Buffer.from(
lib.extism_plugin_output_data(this.ctx.pointer, this.id).buffer,
0,
out_len
);
resolve(buf);
});
}
/**
* Free a plugin, this should be called when the plugin is no longer needed
*/
free() {
if (this.ctx.pointer && this.id !== -1) {
pluginRegistry.unregister(this);
lib.extism_plugin_free(this.ctx.pointer, this.id);
this.id = -1;
}
}
}

Binary file not shown.

View File

@@ -1,105 +0,0 @@
import * as extism from "../src/index";
import { readFileSync } from "fs";
import { join } from "path";
function manifest(): extism.Manifest {
return {
wasm: [{ path: join(__dirname, "/code.wasm") }],
};
}
function wasmBuffer(): Buffer {
return readFileSync(join(__dirname, "/code.wasm"));
}
describe("test extism", () => {
test("can create new context", () => {
let ctx = new extism.Context();
expect(ctx).toBeTruthy();
ctx.free();
});
test("can create and call a plugin", async () => {
await extism.withContext(async (ctx: extism.Context) => {
const plugin = ctx.plugin(manifest());
let output = await plugin.call("count_vowels", "this is a test");
let result = JSON.parse(output.toString());
expect(result["count"]).toBe(4);
output = await plugin.call("count_vowels", "this is a test again");
result = JSON.parse(output.toString());
expect(result["count"]).toBe(7);
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);
});
});
test("can free a plugin", async () => {
await extism.withContext(async (ctx: extism.Context) => {
const plugin = ctx.plugin(manifest());
let output = await plugin.call("count_vowels", "this is a test");
plugin.free();
await expect(() =>
plugin.call("count_vowels", "this is a test")
).rejects.toMatch(/Plugin error/);
});
});
test("can update the manifest", async () => {
await extism.withContext(async (ctx: extism.Context) => {
const plugin = ctx.plugin(manifest());
let output = await plugin.call("count_vowels", "this is a test");
let result = JSON.parse(output.toString());
expect(result["count"]).toBe(4);
// let's update the plugin with a manifest of raw module bytes
plugin.update(wasmBuffer());
// can still call it
output = await plugin.call("count_vowels", "this is a test");
result = JSON.parse(output.toString());
expect(result["count"]).toBe(4);
});
});
test("can detect if function exists or not", async () => {
await extism.withContext(async (ctx: extism.Context) => {
const plugin = ctx.plugin(manifest());
expect(plugin.functionExists("count_vowels")).toBe(true);
expect(plugin.functionExists("i_dont_extist")).toBe(false);
});
});
test("withContext returns results", async () => {
const count = await extism.withContext(
async (ctx: extism.Context): Promise<number> => {
const plugin = ctx.plugin(manifest());
let output = await plugin.call("count_vowels", "this is a test");
let result = JSON.parse(output.toString());
return result["count"];
}
);
expect(count).toBe(4);
});
test("errors when function is not known", async () => {
await extism.withContext(async (ctx: extism.Context) => {
const plugin = ctx.plugin(manifest());
await expect(() =>
plugin.call("i_dont_exist", "example-input")
).rejects.toMatch(/Plugin error/);
});
});
test("can result context", async () => {
await extism.withContext(async (ctx: extism.Context) => {
const plugin = ctx.plugin(manifest());
await plugin.call("count_vowels", "this is a test");
ctx.reset();
await expect(() =>
plugin.call("i_dont_exist", "example-input")
).rejects.toMatch(/Plugin error/);
});
});
});

View File

@@ -1,32 +1,107 @@
{
"include": ["./src/**/*"],
"exclude": ["node_modules", "dist"],
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es5" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"lib": [
"es6"
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
"target": "es5", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"lib": ["es6"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "CommonJS" /* Specify what module code is generated. */,
"rootDir": "./src" /* Specify the root folder within your source files. */,
"resolveJsonModule": true /* Enable importing .json files. */,
"module": "NodeNext", /* Specify what module code is generated. */
"rootDir": "./src", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
"resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
"declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true /* Enable all strict type-checking options. */,
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"exclude": [
"node_modules",
"dist"
]
}

View File

@@ -1,3 +1,4 @@
(executable
(public_name extism)
(name main)
(libraries extism))

View File

@@ -1,8 +1,5 @@
(library
(name extism)
(public_name extism)
(inline_tests
(deps test/code.wasm))
(libraries ctypes.foreign bigstringaf base64)
(preprocess
(pps ppx_yojson_conv ppx_inline_test)))
(preprocess (pps ppx_yojson_conv)))

View File

@@ -77,7 +77,8 @@ module Bindings = struct
let extism_log_file =
fn "extism_log_file" (string @-> string_opt @-> returning bool)
let extism_version = fn "extism_version" (void @-> returning string)
let extism_version =
fn "extism_version" (void @-> returning string)
let extism_plugin_free =
fn "extism_plugin_free" (context @-> int32_t @-> returning void)
@@ -123,7 +124,7 @@ module Manifest = struct
}
[@@deriving yojson]
type config = (string * string option) list
type config = (string * string) list
type wasm = File of wasm_file | Data of wasm_data | Url of wasm_url
let yojson_of_wasm = function
@@ -136,38 +137,25 @@ module Manifest = struct
with _ -> (
try Data (wasm_data_of_yojson x) with _ -> Url (wasm_url_of_yojson x))
let is_null = function `Null -> true | _ -> false
let config_of_yojson j =
let assoc = Yojson.Safe.Util.to_assoc j in
List.map
(fun (k, v) ->
(k, if is_null v then None else Some (Yojson.Safe.Util.to_string v)))
assoc
List.map (fun (k, v) -> (k, Yojson.Safe.Util.to_string v)) assoc
let yojson_of_config c =
`Assoc
(List.map
(fun (k, v) -> (k, match v with None -> `Null | Some v -> `String v))
c)
let yojson_of_config c = `Assoc (List.map (fun (k, v) -> (k, `String v)) c)
type t = {
wasm : wasm list;
memory : memory option; [@yojson.option]
config : config option; [@yojson.option]
allowed_hosts : string list option; [@yojson.option]
allowed_hosts: string list option; [@yojson.option]
}
[@@deriving yojson]
let file ?name ?hash path = File { path; name; hash }
let data ?name ?hash data = Data { data; name; hash }
let url ?header ?name ?meth ?hash url = Url { header; name; meth; hash; url }
let v ?config ?memory ?allowed_hosts wasm =
{ config; wasm; memory; allowed_hosts }
let v ?config ?memory ?allowed_hosts wasm = { config; wasm; memory; allowed_hosts }
let json t = yojson_of_t t |> Yojson.Safe.to_string
let with_config t config = { t with config = Some config }
end
module Context = struct
@@ -184,12 +172,6 @@ module Context = struct
ctx.pointer <- Ctypes.null
let reset ctx = Bindings.extism_context_reset ctx.pointer
let%test "test context" =
let ctx = create () in
reset ctx;
free ctx;
true
end
type t = { id : int32; ctx : Context.t }
@@ -205,12 +187,16 @@ let with_context f =
Context.free ctx;
x
let set_config plugin = function
| None -> true
let set_config plugin config =
match config with
| Some config ->
let config = Manifest.yojson_of_config config |> Yojson.Safe.to_string in
Bindings.extism_plugin_config plugin.ctx.pointer plugin.id config
(Unsigned.UInt64.of_int (String.length config))
let _ =
Bindings.extism_plugin_config plugin.ctx.pointer plugin.id config
(Unsigned.UInt64.of_int (String.length config))
in
()
| None -> ()
let free t =
if not (Ctypes.is_null t.ctx.pointer) then
@@ -228,21 +214,13 @@ let plugin ?config ?(wasi = false) ctx wasm =
| Some msg -> Error (`Msg msg)
else
let t = { id; ctx } in
if not (set_config t config) then Error (`Msg "call to set_config failed")
else
let () = Gc.finalise free t in
Ok t
let () = set_config t config in
let () = Gc.finalise free t in
Ok t
let of_manifest ?wasi ctx manifest =
let of_manifest ?config ?wasi ctx manifest =
let data = Manifest.json manifest in
plugin ctx ?wasi data
let%test "free plugin" =
let manifest = Manifest.v [ Manifest.file "test/code.wasm" ] in
with_context (fun ctx ->
let plugin = of_manifest ctx manifest |> Result.get_ok in
free plugin;
true)
plugin ctx ?config ?wasi data
let update plugin ?config ?(wasi = false) wasm =
let { id; ctx } = plugin in
@@ -255,21 +233,13 @@ let update plugin ?config ?(wasi = false) wasm =
match Bindings.extism_error ctx.pointer (-1l) with
| None -> Error (`Msg "extism_plugin_update failed")
| Some msg -> Error (`Msg msg)
else if not (set_config plugin config) then
Error (`Msg "call to set_config failed")
else Ok ()
else
let () = set_config plugin config in
Ok ()
let update_manifest plugin ?wasi manifest =
let update_manifest plugin ?config ?wasi manifest =
let data = Manifest.json manifest in
update plugin ?wasi data
let%test "update plugin manifest and config" =
let manifest = Manifest.v [ Manifest.file "test/code.wasm" ] in
with_context (fun ctx ->
let config = [ ("a", Some "1") ] in
let plugin = of_manifest ctx manifest |> Result.get_ok in
let manifest = Manifest.with_config manifest config in
update_manifest plugin manifest |> Result.is_ok)
update plugin ?config ?wasi data
let call' f { id; ctx } ~name input len =
let rc = f ctx.pointer id name input len in
@@ -292,51 +262,14 @@ let call_bigstring (t : t) ~name input =
let ptr = Ctypes.bigarray_start Ctypes.array1 input in
call' Bindings.extism_plugin_call t ~name ptr len
let%test "call_bigstring" =
let manifest = Manifest.v [ Manifest.file "test/code.wasm" ] in
with_context (fun ctx ->
let plugin = of_manifest ctx manifest |> Result.get_ok in
call_bigstring plugin ~name:"count_vowels"
(Bigstringaf.of_string ~off:0 ~len:14 "this is a test")
|> Result.get_ok |> Bigstringaf.to_string = "{\"count\": 4}")
let call (t : t) ~name input =
let len = String.length input in
call' Bindings.extism_plugin_call_s t ~name input (Unsigned.UInt64.of_int len)
|> Result.map Bigstringaf.to_string
let%test "call" =
let manifest = Manifest.v [ Manifest.file "test/code.wasm" ] in
with_context (fun ctx ->
let plugin = of_manifest ctx manifest |> Result.get_ok in
call plugin ~name:"count_vowels" "this is a test"
|> Result.get_ok = "{\"count\": 4}")
let function_exists { id; ctx } name =
Bindings.extism_plugin_function_exists ctx.pointer id name
let%test "function exists" =
let manifest = Manifest.v [ Manifest.file "test/code.wasm" ] in
with_context (fun ctx ->
let plugin = of_manifest ctx manifest |> Result.get_ok in
function_exists plugin "count_vowels"
&& not (function_exists plugin "function_does_not_exist"))
let set_log_file ?level filename =
let level =
Option.map
(function
| `Error -> "error"
| `Warn -> "warn"
| `Info -> "info"
| `Debug -> "debug"
| `Trace -> "trace")
level
in
Bindings.extism_log_file filename level
let%test _ = set_log_file ~level:`Trace "stderr"
let set_log_file ?level filename = Bindings.extism_log_file filename level
let extism_version = Bindings.extism_version
let%test _ = String.length (extism_version ()) > 0

View File

@@ -1,11 +1,10 @@
type t
type error = [ `Msg of string ]
type error = [`Msg of string]
val extism_version : unit -> string
val extism_version: unit -> string
module Manifest : sig
type memory = { max : int option } [@@deriving yojson]
type wasm_file = {
path : string;
name : string option; [@yojson.option]
@@ -27,79 +26,42 @@ module Manifest : sig
}
type wasm = File of wasm_file | Data of wasm_data | Url of wasm_url
type config = (string * (string option)) list
type config = (string * string) list
type t = {
wasm : wasm list;
memory : memory option;
config : config option;
allowed_hosts : string list option;
config: config option;
allowed_hosts: string list option;
}
val file : ?name:string -> ?hash:string -> string -> wasm
val data : ?name:string -> ?hash:string -> string -> wasm
val url :
?header:(string * string) list ->
?name:string ->
?meth:string ->
?hash:string ->
string ->
wasm
val v :
val file: ?name:string -> ?hash:string -> string -> wasm
val data: ?name:string -> ?hash:string -> string -> wasm
val url: ?header:(string * string) list -> ?name:string -> ?meth:string -> ?hash:string -> string -> wasm
val v:
?config:config ->
?memory:memory ->
?allowed_hosts:string list ->
wasm list ->
t
val json : t -> string
val with_config : t -> config -> t
?allowed_hosts: string list ->
wasm list -> t
val json: t -> string
end
module Context : sig
type t
val create : unit -> t
val free : t -> unit
val reset : t -> unit
val create: unit -> t
val free: t -> unit
val reset: t -> unit
end
val with_context : (Context.t -> 'a) -> 'a
val set_log_file :
?level:[ `Error | `Warn | `Info | `Debug | `Trace ] -> string -> bool
val plugin :
?config:(string * string option) list ->
?wasi:bool ->
Context.t ->
string ->
(t, [ `Msg of string ]) result
val of_manifest :
?wasi:bool ->
Context.t ->
Manifest.t ->
(t, [ `Msg of string ]) result
val update :
t ->
?config:(string * string option) list ->
?wasi:bool ->
string ->
(unit, [ `Msg of string ]) result
val update_manifest :
t ->
?wasi:bool ->
Manifest.t ->
(unit, [ `Msg of string ]) result
val call_bigstring :
t -> name:string -> Bigstringaf.t -> (Bigstringaf.t, error) result
val call : t -> name:string -> string -> (string, error) result
val free : t -> unit
val function_exists : t -> string -> bool
val set_log_file: ?level:string -> string -> bool
val plugin: ?config:(string * string) list -> ?wasi:bool -> Context.t -> string -> (t, [`Msg of string]) result
val of_manifest: ?config:(string * string) list -> ?wasi:bool -> Context.t -> Manifest.t -> (t, [`Msg of string]) result
val update: t -> ?config:(string * string) list -> ?wasi:bool -> string -> (unit, [`Msg of string]) result
val update_manifest: t -> ?config:(string * string) list -> ?wasi:bool -> Manifest.t -> (unit, [`Msg of string]) result
val call_bigstring: t -> name:string -> Bigstringaf.t -> (Bigstringaf.t, error) result
val call: t -> name:string -> string -> (string, error) result
val free: t -> unit
val function_exists: t -> string -> bool

Binary file not shown.

View File

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

View File

@@ -8,11 +8,11 @@
"packages": [
{
"name": "extism/extism",
"version": "dev-php-sdk-fix",
"version": "dev-feat-reuse-plugins",
"dist": {
"type": "path",
"url": "../..",
"reference": "6119eae37bcb2e02f7c7b5b203ab9f959819e83d"
"reference": "9e5f576acff0aa9c8720fdf6d1103b7c1996bf14"
},
"require": {
"ircmaxell/ffime": "dev-master",
@@ -22,12 +22,7 @@
"autoload": {
"psr-4": {
"Extism\\": "php/src/"
},
"files": [
"php/src/Context.php",
"php/src/Plugin.php",
"php/src/extism.h"
]
}
},
"autoload-dev": {
"psr-4": []
@@ -64,12 +59,12 @@
"source": {
"type": "git",
"url": "https://github.com/ircmaxell/FFIMe.git",
"reference": "f6911d7a6a7090a9782a21a946819a2efa9a2ff7"
"reference": "7b9e0bf23adceddd5fde3130d30275a45cfc1867"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ircmaxell/FFIMe/zipball/f6911d7a6a7090a9782a21a946819a2efa9a2ff7",
"reference": "f6911d7a6a7090a9782a21a946819a2efa9a2ff7",
"url": "https://api.github.com/repos/ircmaxell/FFIMe/zipball/7b9e0bf23adceddd5fde3130d30275a45cfc1867",
"reference": "7b9e0bf23adceddd5fde3130d30275a45cfc1867",
"shasum": ""
},
"require": {
@@ -102,7 +97,7 @@
"issues": "https://github.com/ircmaxell/FFIMe/issues",
"source": "https://github.com/ircmaxell/FFIMe/tree/master"
},
"time": "2022-09-25T18:13:59+00:00"
"time": "2022-09-07T19:50:56+00:00"
},
{
"name": "ircmaxell/php-c-parser",
@@ -110,12 +105,12 @@
"source": {
"type": "git",
"url": "https://github.com/ircmaxell/php-c-parser.git",
"reference": "fd8f5efefd0fcc6c5119d945694acaa3a6790ada"
"reference": "55e0a4fdf88d6e955d928860e1e107a68492c1cf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ircmaxell/php-c-parser/zipball/fd8f5efefd0fcc6c5119d945694acaa3a6790ada",
"reference": "fd8f5efefd0fcc6c5119d945694acaa3a6790ada",
"url": "https://api.github.com/repos/ircmaxell/php-c-parser/zipball/55e0a4fdf88d6e955d928860e1e107a68492c1cf",
"reference": "55e0a4fdf88d6e955d928860e1e107a68492c1cf",
"shasum": ""
},
"require": {
@@ -151,7 +146,7 @@
"issues": "https://github.com/ircmaxell/php-c-parser/issues",
"source": "https://github.com/ircmaxell/php-c-parser/tree/master"
},
"time": "2022-09-23T19:39:35+00:00"
"time": "2022-08-27T17:37:14+00:00"
},
{
"name": "ircmaxell/php-object-symbolresolver",
@@ -159,12 +154,12 @@
"source": {
"type": "git",
"url": "https://github.com/ircmaxell/php-object-symbolresolver.git",
"reference": "dfe1b1aa6c15b198bdef50fff8485e98e89f2a09"
"reference": "88c918a0f4621ef59dc4a4c21ead7f39bd720337"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ircmaxell/php-object-symbolresolver/zipball/dfe1b1aa6c15b198bdef50fff8485e98e89f2a09",
"reference": "dfe1b1aa6c15b198bdef50fff8485e98e89f2a09",
"url": "https://api.github.com/repos/ircmaxell/php-object-symbolresolver/zipball/88c918a0f4621ef59dc4a4c21ead7f39bd720337",
"reference": "88c918a0f4621ef59dc4a4c21ead7f39bd720337",
"shasum": ""
},
"require": {
@@ -196,7 +191,7 @@
"issues": "https://github.com/ircmaxell/php-object-symbolresolver/issues",
"source": "https://github.com/ircmaxell/php-object-symbolresolver/tree/master"
},
"time": "2022-09-15T18:21:50+00:00"
"time": "2022-09-07T19:47:04+00:00"
}
],
"packages-dev": [],

View File

@@ -3,37 +3,12 @@ declare(strict_types=1);
namespace Extism;
require_once "vendor/autoload.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 "generate.php";
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

View File

@@ -2,6 +2,8 @@
declare(strict_types=1);
namespace Extism;
require_once "vendor/autoload.php";
require_once "generate.php";
require_once "ExtismLib.php";
class Plugin
@@ -32,13 +34,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 ($this->config != null) {
$cfg = string_to_bytes(json_encode($config));
if ($config != null) {
$cfg = string_to_bytes(json_encode(config));
$this->lib->extism_plugin_config($ctx->pointer, $this->id, $cfg, count($cfg));
}
}
@@ -71,14 +73,14 @@ class Plugin
if ($err) {
$msg = $msg . ", error = " . $err;
}
throw new \Exception("Extism: call to '".$name."' failed with " . $msg);
throw new Execption("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);
$output = [];
$ouput = [];
$data = $buf->getData();
for ($i = 0; $i < $length; $i++) {
$output[$i] = $data[$i];
@@ -99,7 +101,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) {

29
php/src/generate.php Normal file
View File

@@ -0,0 +1,29 @@
<?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();
}

1
python/.gitignore vendored
View File

@@ -1 +0,0 @@
html

View File

@@ -1,26 +0,0 @@
.PHONY: test
prepare:
poetry install
test: prepare
poetry run python -m unittest discover
clean:
rm -rf dist/*
publish: clean prepare
poetry build
poetry run twine upload dist/extism-*.tar.gz
format:
poetry run black extism/ tests/ example.py
lint:
poetry run black --check extism/ tests/ example.py
docs:
poetry run pdoc --force --html extism
show-docs: docs
open html/extism/index.html

View File

@@ -1,9 +1,10 @@
import sys
import os
import json
import hashlib
sys.path.append(".")
from extism import Context
from extism import Plugin, Context
if len(sys.argv) > 1:
data = sys.argv[1].encode()
@@ -13,7 +14,7 @@ else:
# a Context provides a scope for plugins to be managed within. creating multiple contexts
# is expected and groups plugins based on source/tenant/lifetime etc.
with Context() as context:
wasm = open("../wasm/code.wasm", "rb").read()
wasm = open("../wasm/code.wasm", 'rb').read()
hash = hashlib.sha256(wasm).hexdigest()
config = {"wasm": [{"data": wasm, "hash": hash}], "memory": {"max": 5}}
@@ -27,9 +28,9 @@ with Context() as context:
def count_vowels(data):
count = 0
for c in data:
if c in b"AaEeIiOoUu":
if c in b'AaEeIiOoUu':
count += 1
return count
assert j["count"] == count_vowels(data)
assert (j["count"] == count_vowels(data))

View File

@@ -1,61 +1,66 @@
import sys
import json
import os
from base64 import b64encode
from cffi import FFI
from typing import Union
class Error(Exception):
"""Extism error type"""
'''Extism error type'''
pass
_search_dirs = ["/usr/local", "/usr", os.path.join(os.getenv("HOME"), ".local"), "."]
search_dirs = [
"/usr/local", "/usr",
os.path.join(os.getenv("HOME"), ".local"), "."
]
def _check_for_header_and_lib(p):
def _exists(a, *b):
return os.path.exists(os.path.join(a, *b))
def exists(a, *b):
return os.path.exists(os.path.join(a, *b))
if _exists(p, "extism.h"):
if _exists(p, "libextism.so"):
def check_for_header_and_lib(p):
if exists(p, "extism.h"):
if exists(p, "libextism.so"):
return os.path.join(p, "extism.h"), os.path.join(p, "libextism.so")
if _exists(p, "libextism.dylib"):
return os.path.join(p, "extism.h"), os.path.join(p, "libextism.dylib")
if exists(p, "libextism.dylib"):
return os.path.join(p, "extism.h"), os.path.join(
p, "libextism.dylib")
if _exists(p, "include", "extism.h"):
if _exists(p, "lib", "libextism.so"):
if exists(p, "include", "extism.h"):
if exists(p, "lib", "libextism.so"):
return os.path.join(p, "include", "extism.h"), os.path.join(
p, "lib", "libextism.so"
)
p, "lib", "libextism.so")
if _exists(p, "lib", "libextism.dylib"):
if exists(p, "lib", "libextism.dylib"):
return os.path.join(p, "include", "extism.h"), os.path.join(
p, "lib", "libextism.dylib"
)
p, "lib", "libextism.dylib")
def _locate():
"""Locate extism library and header"""
def locate():
'''Locate extism library and header'''
script_dir = os.path.dirname(__file__)
env = os.getenv("EXTISM_PATH")
if env is not None:
r = _check_for_header_and_lib(env)
r = check_for_header_and_lib(env)
if r is not None:
return r
r = _check_for_header_and_lib(script_dir)
r = check_for_header_and_lib(script_dir)
if r is not None:
return r
r = _check_for_header_and_lib(".")
r = check_for_header_and_lib(".")
if r is not None:
return r
for d in _search_dirs:
r = _check_for_header_and_lib(d)
for d in search_dirs:
r = check_for_header_and_lib(d)
if r is not None:
return r
@@ -63,18 +68,18 @@ def _locate():
# Initialize the C library
_ffi = FFI()
_header, _lib = _locate()
with open(_header) as f:
ffi = FFI()
header, lib = locate()
with open(header) as f:
lines = []
for line in f.readlines():
if line[0] != "#":
if line[0] != '#':
lines.append(line)
_ffi.cdef("".join(lines))
_lib = _ffi.dlopen(_lib)
ffi.cdef(''.join(lines))
lib = ffi.dlopen(lib)
class _Base64Encoder(json.JSONEncoder):
class Base64Encoder(json.JSONEncoder):
# pylint: disable=method-hidden
def default(self, o):
if isinstance(o, bytes):
@@ -82,74 +87,38 @@ class _Base64Encoder(json.JSONEncoder):
return json.JSONEncoder.default(self, o)
def set_log_file(file, level=None):
"""
Sets the log file and level, this is a global configuration
Parameters
----------
file : str
The path to the logfile
level : str
The debug level, one of ('debug', 'error', 'trace', 'warn')
"""
level = level or _ffi.NULL
def set_log_file(file, level=ffi.NULL):
'''Sets the log file and level, this is a global configuration'''
if isinstance(level, str):
level = level.encode()
_lib.extism_log_file(file.encode(), level)
lib.extism_log_file(file.encode(), level)
def extism_version():
"""
Gets the Extism version string
Returns
-------
str
The Extism runtime version string
"""
return _ffi.string(_lib.extism_version()).decode()
'''Gets the Extism version string'''
return ffi.string(lib.extism_version()).decode()
def _wasm(plugin):
if isinstance(plugin, str) and os.path.exists(plugin):
with open(plugin, "rb") as f:
with open(plugin, 'rb') as f:
wasm = f.read()
elif isinstance(plugin, str):
wasm = plugin.encode()
elif isinstance(plugin, dict):
wasm = json.dumps(plugin, cls=_Base64Encoder).encode()
wasm = json.dumps(plugin, cls=Base64Encoder).encode()
else:
wasm = plugin
return wasm
class Context:
"""
Context is used to store and manage plugins. You need a context to create
or call plugins. The best way to interact with the Context is
as a context manager as it can ensure that resources are cleaned up.
Example
-------
with Context() as ctx:
plugin = ctx.plugin(manifest)
print(plugin.call("my_function", "some-input"))
If you need a long lived context, you can use the constructor and the `del` keyword to free.
Example
-------
ctx = Context()
del ctx
"""
'''Context is used to store and manage plugins'''
def __init__(self):
self.pointer = _lib.extism_context_new()
self.pointer = lib.extism_context_new()
def __del__(self):
_lib.extism_context_free(self.pointer)
self.pointer = _ffi.NULL
lib.extism_context_free(self.pointer)
self.pointer = ffi.NULL
def __enter__(self):
return self
@@ -158,151 +127,94 @@ class Context:
self.__del__()
def reset(self):
"""Remove all registered plugins"""
_lib.extism_context_reset(self.pointer)
'''Remove all registered plugins'''
lib.extism_context_reset(self.pointer)
def plugin(self, manifest: Union[str, bytes, dict], wasi=False, config=None):
"""
Register a new plugin from a WASM module or JSON encoded manifest
Parameters
----------
manifest : Union[str, bytes, dict]
A manifest dictionary describing the plugin or the raw bytes for a module. See [Extism > Concepts > Manifest](https://extism.org/docs/concepts/manifest/).
wasi : bool
Set to `True` to enable WASI support
config : dict
The plugin config dictionary
Returns
-------
Plugin
The created plugin
"""
return Plugin(self, manifest, wasi, config)
def plugin(self, plugin: Union[str, bytes, dict], wasi=False, config=None):
'''Register a new plugin from a WASM module or JSON encoded manifest'''
return Plugin(self, plugin, wasi, config)
class Plugin:
"""
Plugin is used to call WASM functions.
Plugins can kept in a context for as long as you need
or be freed with the `del` keyword.
"""
def __init__(
self, context: Context, plugin: Union[str, bytes, dict], wasi=False, config=None
):
"""
Construct a Plugin. Please use Context#plugin instead.
"""
'''Plugin is used to call WASM functions'''
def __init__(self,
context: Context,
plugin: Union[str, bytes, dict],
wasi=False,
config=None):
wasm = _wasm(plugin)
# Register plugin
self.plugin = _lib.extism_plugin_new(context.pointer, wasm, len(wasm), wasi)
self.plugin = lib.extism_plugin_new(context.pointer, wasm, len(wasm),
wasi)
self.ctx = context
if self.plugin < 0:
error = _lib.extism_error(self.ctx.pointer, -1)
if error != _ffi.NULL:
raise Error(_ffi.string(error).decode())
error = lib.extism_error(self.ctx.pointer, -1)
if error != ffi.NULL:
raise Error(ffi.string(error).decode())
raise Error("Unable to register plugin")
if config is not None:
s = json.dumps(config).encode()
_lib.extism_plugin_config(self.ctx.pointer, self.plugin, s, len(s))
lib.extism_plugin_config(self.ctx.pointer, self.plugin, s, len(s))
def update(self, manifest: Union[str, bytes, dict], wasi=False, config=None):
"""
Update a plugin with a new WASM module or manifest
Parameters
----------
plugin : Union[str, bytes, dict]
A manifest dictionary describing the plugin or the raw bytes for a module. See [Extism > Concepts > Manifest](https://extism.org/docs/concepts/manifest/).
wasi : bool
Set to `True` to enable WASI support
config : dict
The plugin config dictionary
"""
wasm = _wasm(manifest)
ok = _lib.extism_plugin_update(
self.ctx.pointer, self.plugin, wasm, len(wasm), wasi
)
def update(self, plugin: Union[str, bytes, dict], wasi=False, config=None):
'''Update a plugin with a new WASM module or manifest'''
wasm = _wasm(plugin)
ok = lib.extism_plugin_update(self.ctx.pointer, self.plugin, wasm,
len(wasm), wasi)
if not ok:
error = _lib.extism_error(self.ctx.pointer, -1)
if error != _ffi.NULL:
raise Error(_ffi.string(error).decode())
error = lib.extism_error(self.ctx.pointer, -1)
if error != ffi.NULL:
raise Error(ffi.string(error).decode())
raise Error("Unable to update plugin")
if config is not None:
s = json.dumps(config).encode()
_lib.extism_plugin_config(self.ctx.pointer, self.plugin, s, len(s))
lib.extism_plugin_config(self.ctx.pointer, self.plugin, s, len(s))
def _check_error(self, rc):
if rc != 0:
error = _lib.extism_error(self.ctx.pointer, self.plugin)
if error != _ffi.NULL:
raise Error(_ffi.string(error).decode())
error = lib.extism_error(self.ctx.pointer, self.plugin)
if error != ffi.NULL:
raise Error(ffi.string(error).decode())
raise Error(f"Error code: {rc}")
def function_exists(self, name: str) -> bool:
"""
Returns true if the given function exists
'''Returns true if the given function exists'''
return lib.extism_plugin_function_exists(self.ctx.pointer, self.plugin,
name.encode())
Parameters
----------
name : str
The function name to check for
Returns
-------
True if the function exists in the plugin, False otherwise
"""
return _lib.extism_plugin_function_exists(
self.ctx.pointer, self.plugin, name.encode()
)
def call(self, function_name: str, data: Union[str, bytes], parse=bytes):
"""
def call(self, name: str, data: Union[str, bytes], parse=bytes):
'''
Call a function by name with the provided input data
Parameters
----------
name : str
The name of the function to invoke
data : Union[str, bytes]
The input data to the function, can be bytes or a string
parse : Func
Can be used to transform the output buffer into
your expected type. It expects a function that takes a buffer as the
only argument.
Return
------
The bytes or parsed data from the plugin function
"""
The `parse` argument can be used to transform the output buffer into
your expected type. It expects a function that takes a buffer as the
only argument
'''
if isinstance(data, str):
data = data.encode()
self._check_error(
_lib.extism_plugin_call(
self.ctx.pointer, self.plugin, function_name.encode(), data, len(data)
)
)
out_len = _lib.extism_plugin_output_length(self.ctx.pointer, self.plugin)
out_buf = _lib.extism_plugin_output_data(self.ctx.pointer, self.plugin)
buf = _ffi.buffer(out_buf, out_len)
lib.extism_plugin_call(self.ctx.pointer, self.plugin,
name.encode(), data, len(data)))
out_len = lib.extism_plugin_output_length(self.ctx.pointer,
self.plugin)
out_buf = lib.extism_plugin_output_data(self.ctx.pointer, self.plugin)
buf = ffi.buffer(out_buf, out_len)
if parse is None:
return buf
return parse(buf)
def __del__(self):
if not hasattr(self, "ctx"):
if not hasattr(self, 'ctx'):
return
if self.ctx.pointer == _ffi.NULL:
if self.ctx.pointer == ffi.NULL:
return
_lib.extism_plugin_free(self.ctx.pointer, self.plugin)
lib.extism_plugin_free(self.ctx.pointer, self.plugin)
self.plugin = -1
def __enter__(self):

View File

@@ -1,18 +1,15 @@
[tool.poetry]
name = "extism"
version = "0.0.1"
description = "Extism Host SDK for python"
version = "0.0.1-rc.5"
description = ""
authors = ["The Extism Authors <oss@extism.org>"]
license = "BSD-3-Clause"
readme = "../README.md"
[tool.poetry.dependencies]
python = "^3.7"
cffi = "^1.10.0"
[tool.poetry.dev-dependencies]
black = "^22.10.0"
pdoc3 = "^0.10.0"
[build-system]
requires = ["poetry-core>=1.0.0"]

View File

@@ -4,7 +4,6 @@ import hashlib
import json
from os.path import join, dirname
class TestExtism(unittest.TestCase):
def test_context_new(self):
ctx = extism.Context()
@@ -15,13 +14,11 @@ class TestExtism(unittest.TestCase):
with extism.Context() as ctx:
plugin = ctx.plugin(self._manifest())
j = json.loads(plugin.call("count_vowels", "this is a test"))
self.assertEqual(j["count"], 4)
self.assertEqual(j['count'], 4)
j = json.loads(plugin.call("count_vowels", "this is a test again"))
self.assertEqual(j["count"], 7)
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)
self.assertEqual(j['count'], 6)
def test_update_plugin_manifest(self):
with extism.Context() as ctx:
@@ -30,7 +27,7 @@ class TestExtism(unittest.TestCase):
plugin.update(self._count_vowels_wasm())
# should still work
j = json.loads(plugin.call("count_vowels", "this is a test"))
self.assertEqual(j["count"], 4)
self.assertEqual(j['count'], 4)
def test_function_exists(self):
with extism.Context() as ctx:
@@ -41,8 +38,8 @@ class TestExtism(unittest.TestCase):
def test_errors_on_unknown_function(self):
with extism.Context() as ctx:
plugin = ctx.plugin(self._manifest())
self.assertRaises(
extism.Error, lambda: plugin.call("i_dont_exist", "someinput")
self.assertRaises(extism.Error,
lambda: plugin.call("i_dont_exist", "someinput")
)
def test_can_free_plugin(self):
@@ -52,12 +49,12 @@ class TestExtism(unittest.TestCase):
def test_errors_on_bad_manifest(self):
with extism.Context() as ctx:
self.assertRaises(
extism.Error, lambda: ctx.plugin({"invalid_manifest": True})
self.assertRaises(extism.Error,
lambda: ctx.plugin({"invalid_manifest": True})
)
plugin = ctx.plugin(self._manifest())
self.assertRaises(
extism.Error, lambda: plugin.update({"invalid_manifest": True})
self.assertRaises(extism.Error,
lambda: plugin.update({"invalid_manifest": True})
)
def test_extism_version(self):
@@ -69,6 +66,6 @@ class TestExtism(unittest.TestCase):
return {"wasm": [{"data": wasm, "hash": hash}], "memory": {"max": 5}}
def _count_vowels_wasm(self):
path = join(dirname(__file__), "code.wasm")
with open(path, "rb") as wasm_file:
path = join(dirname(__file__), 'code.wasm')
with open(path, 'rb') as wasm_file:
return wasm_file.read()

View File

@@ -1,2 +0,0 @@
--readme GETTING_STARTED.md
- GETTING_STARTED.md

View File

@@ -1,35 +0,0 @@
# Extism
## Getting Started
### Example
```ruby
require "extism"
require "json"
Extism.with_context do |ctx|
manifest = {
:wasm => [{ :path => "../wasm/code.wasm" }],
}
plugin = ctx.plugin(manifest)
res = JSON.parse(plugin.call("count_vowels", "this is a test"))
puts res["count"] # => 4
end
```
### API
There are two primary classes you need to understand:
* [Context](Extism/Context.html)
* [Plugin](Extism/Plugin.html)
#### Context
The [Context](Extism/Context.html) can be thought of as a session. You need a context to interact with the Extism runtime. The context holds your plugins and when you free the context, it frees your plugins. We recommend using the [Extism.with_context](Extism.html#with_context-class_method) method to ensure that your plugins are cleaned up. But if you need a long lived context for any reason, you can use the constructor [Extism::Context.new](Extism/Context.html#initialize-instance_method).
#### Plugin
The [Plugin](Extism/Plugin.html) represents an instance of your WASM program from the given manifest.
The key method to know here is [Extism::Plugin#call](Extism/Plugin.html#call-instance_method) which takes a function name to invoke and some input data, and returns the results from the plugin.

View File

@@ -6,10 +6,4 @@ source "https://rubygems.org"
gemspec
gem "rake", "~> 13.0"
gem "ffi", "~> 1.15.5"
group :development do
gem "yard", "~> 0.9.28"
gem "rufo", "~> 0.13.0"
gem "minitest", "~> 5.16.3"
end
gem "ffi"

View File

@@ -1,28 +0,0 @@
.PHONY: prepare test
prepare:
bundle install
bundle binstubs --all
test: prepare
bundle exec rake test
clean:
rm extism-*.gem
publish: clean prepare
gem build extism.gemspec
gem push extism-*.gem
lint:
bundle exec rufo --check .
format:
bundle exec rufo .
docs:
bundle exec yard
show-docs: docs
open doc/index.html

View File

@@ -1,114 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'bundle' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require "rubygems"
m = Module.new do
module_function
def invoked_as_script?
File.expand_path($0) == File.expand_path(__FILE__)
end
def env_var_version
ENV["BUNDLER_VERSION"]
end
def cli_arg_version
return unless invoked_as_script? # don't want to hijack other binstubs
return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update`
bundler_version = nil
update_index = nil
ARGV.each_with_index do |a, i|
if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
bundler_version = a
end
next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
bundler_version = $1
update_index = i
end
bundler_version
end
def gemfile
gemfile = ENV["BUNDLE_GEMFILE"]
return gemfile if gemfile && !gemfile.empty?
File.expand_path("../Gemfile", __dir__)
end
def lockfile
lockfile =
case File.basename(gemfile)
when "gems.rb" then gemfile.sub(/\.rb$/, gemfile)
else "#{gemfile}.lock"
end
File.expand_path(lockfile)
end
def lockfile_version
return unless File.file?(lockfile)
lockfile_contents = File.read(lockfile)
return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
Regexp.last_match(1)
end
def bundler_requirement
@bundler_requirement ||=
env_var_version || cli_arg_version ||
bundler_requirement_for(lockfile_version)
end
def bundler_requirement_for(version)
return "#{Gem::Requirement.default}.a" unless version
bundler_gem_version = Gem::Version.new(version)
requirement = bundler_gem_version.approximate_recommendation
return requirement unless Gem.rubygems_version < Gem::Version.new("2.7.0")
requirement += ".a" if bundler_gem_version.prerelease?
requirement
end
def load_bundler!
ENV["BUNDLE_GEMFILE"] ||= gemfile
activate_bundler
end
def activate_bundler
gem_error = activation_error_handling do
gem "bundler", bundler_requirement
end
return if gem_error.nil?
require_error = activation_error_handling do
require "bundler/version"
end
return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`"
exit 42
end
def activation_error_handling
yield
nil
rescue StandardError, LoadError => e
e
end
end
m.load_bundler!
if m.invoked_as_script?
load Gem.bin_path("bundler", "bundle")
end

15
ruby/bin/console Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require "bundler/setup"
require "extism"
# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.
# (If you use this, don't forget to add pry to your Gemfile!)
# require "pry"
# Pry.start
require "irb"
IRB.start(__FILE__)

View File

@@ -1,27 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'rake' is installed as part of a gem, and
# this file is here to facilitate running it.
#
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
bundle_binstub = File.expand_path("bundle", __dir__)
if File.file?(bundle_binstub)
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
load(bundle_binstub)
else
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
end
end
require "rubygems"
require "bundler/setup"
load Gem.bin_path("rake", "rake")

View File

@@ -1,27 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'rufo' is installed as part of a gem, and
# this file is here to facilitate running it.
#
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
bundle_binstub = File.expand_path("bundle", __dir__)
if File.file?(bundle_binstub)
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
load(bundle_binstub)
else
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
end
end
require "rubygems"
require "bundler/setup"
load Gem.bin_path("rufo", "rufo")

8
ruby/bin/setup Executable file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
set -vx
bundle install
# Do any other automated setup that you need to do here

View File

@@ -1,27 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'yard' is installed as part of a gem, and
# this file is here to facilitate running it.
#
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
bundle_binstub = File.expand_path("bundle", __dir__)
if File.file?(bundle_binstub)
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
load(bundle_binstub)
else
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
end
end
require "rubygems"
require "bundler/setup"
load Gem.bin_path("yard", "yard")

View File

@@ -1,27 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'yardoc' is installed as part of a gem, and
# this file is here to facilitate running it.
#
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
bundle_binstub = File.expand_path("bundle", __dir__)
if File.file?(bundle_binstub)
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
load(bundle_binstub)
else
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
end
end
require "rubygems"
require "bundler/setup"
load Gem.bin_path("yard", "yardoc")

View File

@@ -1,27 +0,0 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'yri' is installed as part of a gem, and
# this file is here to facilitate running it.
#
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
bundle_binstub = File.expand_path("bundle", __dir__)
if File.file?(bundle_binstub)
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
load(bundle_binstub)
else
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
end
end
require "rubygems"
require "bundler/setup"
load Gem.bin_path("yard", "yri")

View File

@@ -1,17 +1,18 @@
require "./lib/extism"
require "json"
require './lib/extism'
require 'json'
# a Context provides a scope for plugins to be managed within. creating multiple contexts
# is expected and groups plugins based on source/tenant/lifetime etc.
# We recommend you use `Extism.with_context` unless you have a reason to keep your context around.
# If you do you can create a context with `Extism#new`, example: `ctx = Extism.new`
Extism.with_context do |ctx|
Extism.with_context do |ctx|
manifest = {
:wasm => [{ :path => "../wasm/code.wasm" }],
:wasm => [{:path => "../wasm/code.wasm"}]
}
plugin = ctx.plugin(manifest)
res = JSON.parse(plugin.call("count_vowels", ARGV[0] || "this is a test"))
puts res["count"]
puts res['count']
end

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