mirror of
https://github.com/extism/extism.git
synced 2026-01-11 23:08:06 -05:00
Compare commits
38 Commits
v1.9.0
...
kernel-imp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3300fed506 | ||
|
|
b026d30865 | ||
|
|
e8d0397754 | ||
|
|
39685a9038 | ||
|
|
d1ba15484e | ||
|
|
dedd81d90f | ||
|
|
2732ca198d | ||
|
|
30b4a7d2d3 | ||
|
|
b6e5684461 | ||
|
|
36e05e8668 | ||
|
|
9415aaa08a | ||
|
|
168459be0f | ||
|
|
1ea498b5d9 | ||
|
|
f8e16dc875 | ||
|
|
2524707334 | ||
|
|
de65e22f68 | ||
|
|
59acffa8ac | ||
|
|
1f46f9842d | ||
|
|
b249f09b90 | ||
|
|
4e638e14b1 | ||
|
|
d7956ff08c | ||
|
|
87c3384f1e | ||
|
|
07047eaab0 | ||
|
|
1e281e93cd | ||
|
|
f57d987d48 | ||
|
|
9da6d43f05 | ||
|
|
d1a248e19e | ||
|
|
7b2db7588b | ||
|
|
a367bc77a3 | ||
|
|
98800fe8a0 | ||
|
|
7e3665ae8c | ||
|
|
3cfde7966d | ||
|
|
5d18cc71eb | ||
|
|
4f599d4b20 | ||
|
|
4db57de98e | ||
|
|
9134635b37 | ||
|
|
75428f26e2 | ||
|
|
7beeee35f1 |
33
.github/dependabot.yml
vendored
33
.github/dependabot.yml
vendored
@@ -9,33 +9,8 @@ updates:
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
ignore:
|
||||
- dependency-name: "wasmtime"
|
||||
- dependency-name: "wasi-common"
|
||||
- dependency-name: "wiggle"
|
||||
|
||||
- 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"
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -52,7 +52,7 @@ jobs:
|
||||
shell: bash
|
||||
run: cargo build --release -p ${{ env.LIBEXTISM_CRATE }}
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: libextism-${{ matrix.os }}
|
||||
path: |
|
||||
|
||||
13
.github/workflows/release-dotnet-native.yaml
vendored
13
.github/workflows/release-dotnet-native.yaml
vendored
@@ -1,4 +1,6 @@
|
||||
on:
|
||||
release:
|
||||
types: [published, edited]
|
||||
workflow_dispatch:
|
||||
|
||||
name: Release .NET Native NuGet Packages
|
||||
@@ -18,10 +20,13 @@ jobs:
|
||||
uses: actions/setup-dotnet@v3.0.3
|
||||
with:
|
||||
dotnet-version: 7.x
|
||||
- uses: dawidd6/action-download-artifact@v2
|
||||
with:
|
||||
workflow: release.yml
|
||||
name: release-artifacts
|
||||
- name: download release
|
||||
run: |
|
||||
tag='${{ github.ref }}'
|
||||
tag="${tag/refs\/tags\//}"
|
||||
gh release download "$tag" -p 'libextism-*.tar.gz'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract Archive
|
||||
run: |
|
||||
extract_archive() {
|
||||
|
||||
39
.github/workflows/release.yml
vendored
39
.github/workflows/release.yml
vendored
@@ -7,11 +7,6 @@ on:
|
||||
|
||||
name: Release
|
||||
|
||||
env:
|
||||
RUNTIME_MANIFEST: runtime/Cargo.toml
|
||||
RUNTIME_CRATE: libextism
|
||||
RUSTFLAGS: -C target-feature=-crt-static
|
||||
ARTIFACT_DIR: release-artifacts
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -21,6 +16,11 @@ jobs:
|
||||
release:
|
||||
name: ${{ matrix.os }} ${{ matrix.target }}
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
env:
|
||||
RUNTIME_MANIFEST: runtime/Cargo.toml
|
||||
RUNTIME_CRATE: libextism
|
||||
RUSTFLAGS: -C target-feature=-crt-static
|
||||
ARTIFACT_DIR: release-artifacts-${{ matrix.os }}-${{ matrix.target }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -98,13 +98,11 @@ jobs:
|
||||
pyproject="$(cat extism-maturin/pyproject.toml)"
|
||||
<<<"$pyproject" >extism-maturin/pyproject.toml sed -e 's/^version = "0.0.0.replaced-by-ci"/version = "'"$version"'"/g'
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
target: ${{ matrix.target }}
|
||||
override: true
|
||||
toolchain: stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
@@ -113,11 +111,15 @@ jobs:
|
||||
cache-on-failure: "true"
|
||||
|
||||
- name: Build Target (${{ matrix.os }} ${{ matrix.target }})
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
use-cross: ${{ matrix.os != 'windows' }}
|
||||
command: build
|
||||
args: --release --target ${{ matrix.target }} -p ${{ env.RUNTIME_CRATE }}
|
||||
if: ${{ matrix.os != 'windows' }}
|
||||
run: |
|
||||
cargo install cross
|
||||
cross build --release --target ${{ matrix.target }} -p ${{ env.RUNTIME_CRATE }}
|
||||
|
||||
- name: Build Target (${{ matrix.os }} ${{ matrix.target }})
|
||||
if: ${{ matrix.os == 'windows' }}
|
||||
run: |
|
||||
cargo build --release --target ${{ matrix.target }} -p ${{ env.RUNTIME_CRATE }}
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
@@ -190,7 +192,7 @@ jobs:
|
||||
ls -ll ${DEST_DIR}
|
||||
|
||||
- name: Upload Artifact to Summary
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_DIR }}
|
||||
path: ${{ env.ARTIFACT_DIR }}
|
||||
@@ -209,9 +211,10 @@ jobs:
|
||||
needs: [release]
|
||||
if: github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_DIR }}
|
||||
pattern: release-artifacts-*
|
||||
merge-multiple: true
|
||||
|
||||
- uses: "marvinpinto/action-automatic-releases@latest"
|
||||
with:
|
||||
|
||||
88
README.md
88
README.md
@@ -36,6 +36,22 @@ function linking, and more. Extism users build:
|
||||
- web applications
|
||||
- & much more...
|
||||
|
||||
# Supported Targets
|
||||
|
||||
We currently provide releases for the following targets:
|
||||
|
||||
- aarch64-apple-darwin
|
||||
- aarch64-unknown-linux-gnu
|
||||
- aarch64-unknown-linux-musl
|
||||
- x86_64-apple-darwin
|
||||
- x86_64-pc-windows-gnu
|
||||
- x86_64-pc-windows-msvc
|
||||
- x86_64-unknown-linux-gnu
|
||||
- x86_64-unknown-linux-musl
|
||||
|
||||
For Android we suggest taking a look at the [Chicory SDK](https://github.com/extism/chicory-sdk) for a pure Java
|
||||
Extism runtime.
|
||||
|
||||
# Run WebAssembly In Your App
|
||||
|
||||
Pick a SDK to import into your program, and refer to the documentation to get
|
||||
@@ -61,11 +77,11 @@ started:
|
||||
|
||||
# Compile WebAssembly to run in Extism Hosts
|
||||
|
||||
Extism Hosts (running the SDK) must execute WebAssembly code that has a [PDK, or Plug-in Development Kit](https://extism.org/docs/concepts/pdk),
|
||||
library compiled in to the `.wasm` binary. PDKs make it easy for plug-in /
|
||||
extension code authors to read input from the host and return data back, read
|
||||
provided configuration, set/get variables, make outbound HTTP calls if allowed,
|
||||
and more.
|
||||
Extism Hosts (running the SDK) must execute WebAssembly code that has a
|
||||
[PDK, or Plug-in Development Kit](https://extism.org/docs/concepts/pdk), library
|
||||
compiled in to the `.wasm` binary. PDKs make it easy for plug-in / extension
|
||||
code authors to read input from the host and return data back, read provided
|
||||
configuration, set/get variables, make outbound HTTP calls if allowed, and more.
|
||||
|
||||
Pick a PDK to import into your Wasm program, and refer to the documentation to
|
||||
get started:
|
||||
@@ -74,13 +90,73 @@ get started:
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | --------------------------------------------------------- |
|
||||
| Rust PDK | <img alt="Rust PDK" src="https://extism.org/img/sdk-languages/rust.svg" width="50px"/> | https://github.com/extism/rust-pdk | [Crates.io](https://crates.io/crates/extism-pdk) |
|
||||
| JS PDK | <img alt="JS PDK" src="https://extism.org/img/sdk-languages/js.svg" width="50px"/> | https://github.com/extism/js-pdk | N/A |
|
||||
| Python PDK | <img alt="Python PDK" src="https://extism.org/img/sdk-languages/python.svg" width="50px"/> | https://github.com/extism/python-pdk | N/A |
|
||||
| Go PDK | <img alt="Go PDK" src="https://extism.org/img/sdk-languages/go.svg" width="50px"/> | https://github.com/extism/go-pdk | [Go mod](https://pkg.go.dev/github.com/extism/go-pdk) |
|
||||
| Haskell PDK | <img alt="Haskell PDK" src="https://extism.org/img/sdk-languages/haskell.svg" width="50px"/> | https://github.com/extism/haskell-pdk | [Hackage](https://hackage.haskell.org/package/extism-pdk) |
|
||||
| AssemblyScript PDK | <img alt="AssemblyScript PDK" src="https://extism.org/img/sdk-languages/assemblyscript.svg" width="50px"/> | https://github.com/extism/assemblyscript-pdk | [NPM](https://www.npmjs.com/package/@extism/as-pdk) |
|
||||
| .NET PDK | <img alt=".NET PDK" src="https://extism.org/img/sdk-languages/dotnet.svg" width="50px"/> | https://github.com/extism/dotnet-pdk <br/>(supports C# & F#!) | https://www.nuget.org/packages/Extism.Pdk |
|
||||
| .NET PDK | <img alt=".NET PDK" src="https://extism.org/img/sdk-languages/dotnet.svg" width="50px"/> | https://github.com/extism/dotnet-pdk <br/>(supports C# & F#!) | [Nuget](https://www.nuget.org/packages/Extism.Pdk) |
|
||||
| C PDK | <img alt="C PDK" src="https://extism.org/img/sdk-languages/c.svg" width="50px"/> | https://github.com/extism/c-pdk | N/A |
|
||||
| C++ PDK | <img alt="C++ PDK" src="https://extism.org/img/sdk-languages/cpp.svg" width="50px"/> | https://github.com/extism/cpp-pdk | N/A |
|
||||
| Zig PDK | <img alt="Zig PDK" src="https://extism.org/img/sdk-languages/zig.svg" width="50px"/> | https://github.com/extism/zig-pdk | N/A |
|
||||
|
||||
# Generating Bindings
|
||||
|
||||
It's often very useful to define a schema to describe the function signatures
|
||||
and types you want to use between Extism SDK and PDK languages.
|
||||
|
||||
[XTP Bindgen](https://github.com/dylibso/xtp-bindgen) is an open source
|
||||
framework to generate PDK bindings for Extism plug-ins. It's used by the
|
||||
[XTP Platform](https://www.getxtp.com/), but can be used outside of the platform
|
||||
to define any Extism compatible plug-in system.
|
||||
|
||||
## 1. Install the `xtp` CLI.
|
||||
|
||||
See installation instructions
|
||||
[here](https://docs.xtp.dylibso.com/docs/cli#installation).
|
||||
|
||||
## 2. Create a schema using our OpenAPI-inspired IDL:
|
||||
|
||||
```yaml
|
||||
version: v1-draft
|
||||
exports:
|
||||
CountVowels:
|
||||
input:
|
||||
type: string
|
||||
contentType: text/plain; charset=utf-8
|
||||
output:
|
||||
$ref: "#/components/schemas/VowelReport"
|
||||
contentType: application/json
|
||||
# components.schemas defined in example-schema.yaml...
|
||||
```
|
||||
|
||||
> See an example in [example-schema.yaml](./example-schema.yaml), or a full
|
||||
> "kitchen sink" example on
|
||||
> [the docs page](https://docs.xtp.dylibso.com/docs/concepts/xtp-schema/).
|
||||
|
||||
## 3. Generate bindings to use from your plugins:
|
||||
|
||||
```
|
||||
xtp plugin init --schema-file ./example-schema.yaml
|
||||
> 1. TypeScript
|
||||
2. Go
|
||||
3. Rust
|
||||
4. Python
|
||||
5. C#
|
||||
6. Zig
|
||||
7. C++
|
||||
8. GitHub Template
|
||||
9. Local Template
|
||||
```
|
||||
|
||||
This will create an entire boilerplate plugin project for you to get started
|
||||
with. Implement the empty function(s), and run `xtp plugin build` to compile
|
||||
your plugin.
|
||||
|
||||
> For more information about XTP Bindgen, see the
|
||||
> [dylibso/xtp-bindgen](https://github.com/dylibso/xtp-bindgen) repository and
|
||||
> the official
|
||||
> [XTP Schema documentation](https://docs.xtp.dylibso.com/docs/concepts/xtp-schema).
|
||||
|
||||
# Support
|
||||
|
||||
## Discord
|
||||
|
||||
@@ -34,11 +34,11 @@ fn extract_encoding(attrs: &[Attribute]) -> Result<Path> {
|
||||
.iter()
|
||||
.filter(|attr| attr.path().is_ident("encoding"))
|
||||
.collect();
|
||||
ensure!(!encodings.is_empty(), "encoding needs to be specified"; try = "`#[encoding(ToJson)]`");
|
||||
ensure!(!encodings.is_empty(), "encoding needs to be specified"; try = "`#[encoding(Json)]`");
|
||||
ensure!(encodings.len() < 2, encodings[1], "only one encoding can be specified"; try = "remove `{}`", encodings[1].to_token_stream());
|
||||
|
||||
Ok(encodings[0].parse_args().map_err(
|
||||
|e| error_message!(e.span(), "{e}"; note= "expects a path"; try = "`#[encoding(ToJson)]`"),
|
||||
|e| error_message!(e.span(), "{e}"; note= "expects a path"; try = "`#[encoding(Json)]`"),
|
||||
)?)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
error: encoding needs to be specified
|
||||
|
||||
= try: `#[encoding(ToJson)]`
|
||||
= try: `#[encoding(Json)]`
|
||||
--> tests/ui/invalid-encoding.rs:3:10
|
||||
|
|
||||
3 | #[derive(ToBytes)]
|
||||
@@ -11,7 +11,7 @@ error: encoding needs to be specified
|
||||
error: expected attribute arguments in parentheses: #[encoding(...)]
|
||||
|
||||
= note: expects a path
|
||||
= try: `#[encoding(ToJson)]`
|
||||
= try: `#[encoding(Json)]`
|
||||
--> tests/ui/invalid-encoding.rs:7:3
|
||||
|
|
||||
7 | #[encoding]
|
||||
@@ -20,7 +20,7 @@ error: expected attribute arguments in parentheses: #[encoding(...)]
|
||||
error: expected parentheses: #[encoding(...)]
|
||||
|
||||
= note: expects a path
|
||||
= try: `#[encoding(ToJson)]`
|
||||
= try: `#[encoding(Json)]`
|
||||
--> tests/ui/invalid-encoding.rs:11:12
|
||||
|
|
||||
11 | #[encoding = "string"]
|
||||
@@ -29,7 +29,7 @@ error: expected parentheses: #[encoding(...)]
|
||||
error: unexpected token
|
||||
|
||||
= note: expects a path
|
||||
= try: `#[encoding(ToJson)]`
|
||||
= try: `#[encoding(Json)]`
|
||||
--> tests/ui/invalid-encoding.rs:15:21
|
||||
|
|
||||
15 | #[encoding(something, else)]
|
||||
|
||||
@@ -55,7 +55,7 @@ encoding!(pub Json, serde_json::to_vec, serde_json::from_slice);
|
||||
#[cfg(feature = "msgpack")]
|
||||
encoding!(pub Msgpack, rmp_serde::to_vec, rmp_serde::from_slice);
|
||||
|
||||
impl<'a> ToBytes<'a> for serde_json::Value {
|
||||
impl ToBytes<'_> for serde_json::Value {
|
||||
type Bytes = Vec<u8>;
|
||||
|
||||
fn to_bytes(&self) -> Result<Self::Bytes, Error> {
|
||||
@@ -85,7 +85,7 @@ impl<T: AsRef<[u8]>> From<T> for Base64<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: AsRef<[u8]>> ToBytes<'a> for Base64<T> {
|
||||
impl<T: AsRef<[u8]>> ToBytes<'_> for Base64<T> {
|
||||
type Bytes = String;
|
||||
|
||||
fn to_bytes(&self) -> Result<Self::Bytes, Error> {
|
||||
@@ -124,7 +124,7 @@ impl<T: prost::Message> From<T> for Prost<T> {
|
||||
}
|
||||
|
||||
#[cfg(feature = "prost")]
|
||||
impl<'a, T: prost::Message> ToBytes<'a> for Prost<T> {
|
||||
impl<T: prost::Message> ToBytes<'_> for Prost<T> {
|
||||
type Bytes = Vec<u8>;
|
||||
|
||||
fn to_bytes(&self) -> Result<Self::Bytes, Error> {
|
||||
@@ -146,7 +146,7 @@ impl<T: Default + prost::Message> FromBytesOwned for Prost<T> {
|
||||
pub struct Protobuf<T: protobuf::Message>(pub T);
|
||||
|
||||
#[cfg(feature = "protobuf")]
|
||||
impl<'a, T: protobuf::Message> ToBytes<'a> for Protobuf<T> {
|
||||
impl<T: protobuf::Message> ToBytes<'_> for Protobuf<T> {
|
||||
type Bytes = Vec<u8>;
|
||||
|
||||
fn to_bytes(&self) -> Result<Self::Bytes, Error> {
|
||||
|
||||
@@ -141,6 +141,16 @@ impl FromBytesOwned for u32 {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromBytesOwned for bool {
|
||||
fn from_bytes_owned(data: &[u8]) -> Result<Self, Error> {
|
||||
if let Some(x) = data.first() {
|
||||
Ok(*x != 0)
|
||||
} else {
|
||||
Err(Error::msg("Expected one byte to read boolean value"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromBytesOwned for () {
|
||||
fn from_bytes_owned(_: &[u8]) -> Result<Self, Error> {
|
||||
Ok(())
|
||||
|
||||
@@ -61,8 +61,19 @@ fn rountrip_option() {
|
||||
assert_eq!(y.unwrap().0, z.unwrap().0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_bool() {
|
||||
// `None` case
|
||||
let a = true.to_bytes().unwrap();
|
||||
let b = false.to_bytes().unwrap();
|
||||
assert_ne!(a, b);
|
||||
|
||||
assert_eq!(a, [1]);
|
||||
assert_eq!(b, [0]);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "raw", target_endian = "little"))]
|
||||
mod tests {
|
||||
mod raw_tests {
|
||||
use crate::*;
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -61,21 +61,21 @@ pub trait ToBytes<'a> {
|
||||
fn to_bytes(&self) -> Result<Self::Bytes, Error>;
|
||||
}
|
||||
|
||||
impl<'a> ToBytes<'a> for () {
|
||||
impl ToBytes<'_> for () {
|
||||
type Bytes = [u8; 0];
|
||||
fn to_bytes(&self) -> Result<Self::Bytes, Error> {
|
||||
Ok([])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ToBytes<'a> for Vec<u8> {
|
||||
impl ToBytes<'_> for Vec<u8> {
|
||||
type Bytes = Vec<u8>;
|
||||
fn to_bytes(&self) -> Result<Self::Bytes, Error> {
|
||||
Ok(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ToBytes<'a> for String {
|
||||
impl ToBytes<'_> for String {
|
||||
type Bytes = String;
|
||||
fn to_bytes(&self) -> Result<Self::Bytes, Error> {
|
||||
Ok(self.clone())
|
||||
@@ -96,7 +96,7 @@ impl<'a> ToBytes<'a> for &'a str {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ToBytes<'a> for f64 {
|
||||
impl ToBytes<'_> for f64 {
|
||||
type Bytes = [u8; 8];
|
||||
|
||||
fn to_bytes(&self) -> Result<Self::Bytes, Error> {
|
||||
@@ -104,7 +104,7 @@ impl<'a> ToBytes<'a> for f64 {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ToBytes<'a> for f32 {
|
||||
impl ToBytes<'_> for f32 {
|
||||
type Bytes = [u8; 4];
|
||||
|
||||
fn to_bytes(&self) -> Result<Self::Bytes, Error> {
|
||||
@@ -112,7 +112,7 @@ impl<'a> ToBytes<'a> for f32 {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ToBytes<'a> for i64 {
|
||||
impl ToBytes<'_> for i64 {
|
||||
type Bytes = [u8; 8];
|
||||
|
||||
fn to_bytes(&self) -> Result<Self::Bytes, Error> {
|
||||
@@ -120,7 +120,7 @@ impl<'a> ToBytes<'a> for i64 {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ToBytes<'a> for i32 {
|
||||
impl ToBytes<'_> for i32 {
|
||||
type Bytes = [u8; 4];
|
||||
|
||||
fn to_bytes(&self) -> Result<Self::Bytes, Error> {
|
||||
@@ -128,7 +128,7 @@ impl<'a> ToBytes<'a> for i32 {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ToBytes<'a> for u64 {
|
||||
impl ToBytes<'_> for u64 {
|
||||
type Bytes = [u8; 8];
|
||||
|
||||
fn to_bytes(&self) -> Result<Self::Bytes, Error> {
|
||||
@@ -136,7 +136,7 @@ impl<'a> ToBytes<'a> for u64 {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ToBytes<'a> for u32 {
|
||||
impl ToBytes<'_> for u32 {
|
||||
type Bytes = [u8; 4];
|
||||
|
||||
fn to_bytes(&self) -> Result<Self::Bytes, Error> {
|
||||
@@ -144,6 +144,14 @@ impl<'a> ToBytes<'a> for u32 {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToBytes<'_> for bool {
|
||||
type Bytes = [u8; 1];
|
||||
|
||||
fn to_bytes(&self) -> Result<Self::Bytes, Error> {
|
||||
Ok([*self as u8])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: ToBytes<'a>> ToBytes<'a> for &'a T {
|
||||
type Bytes = T::Bytes;
|
||||
|
||||
|
||||
28
example-schema.yaml
Normal file
28
example-schema.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
# yaml-language-server: $schema=https://xtp.dylibso.com/assets/wasm/schema.json
|
||||
# Learn more at https://docs.xtp.dylibso.com/docs/concepts/xtp-schema
|
||||
version: v1-draft
|
||||
exports:
|
||||
CountVowels:
|
||||
input:
|
||||
type: string
|
||||
contentType: text/plain; charset=utf-8
|
||||
output:
|
||||
$ref: "#/components/schemas/VowelReport"
|
||||
contentType: application/json
|
||||
components:
|
||||
schemas:
|
||||
VowelReport:
|
||||
description: The result of counting vowels on the Vowels input.
|
||||
properties:
|
||||
count:
|
||||
type: integer
|
||||
format: int32
|
||||
description: The count of vowels for input string.
|
||||
total:
|
||||
type: integer
|
||||
format: int32
|
||||
description: The cumulative amount of vowels counted, if this keeps state across multiple function calls.
|
||||
nullable: true
|
||||
vowels:
|
||||
type: string
|
||||
description: The set of vowels used to get the count, e.g. "aAeEiIoOuU"
|
||||
@@ -263,24 +263,24 @@ impl MemoryRoot {
|
||||
let mem_left = self_length - self_position - core::mem::size_of::<MemoryRoot>() as u64;
|
||||
let length_with_block = length + core::mem::size_of::<MemoryBlock>() as u64;
|
||||
|
||||
// If the current position is large enough to hold the length of the block being
|
||||
// allocated then check for existing free blocks that can be re-used before
|
||||
// growing memory
|
||||
if length_with_block <= self_position {
|
||||
let b = self.find_free_block(length, self_position);
|
||||
|
||||
// If there's a free block then re-use it
|
||||
if let Some(b) = b {
|
||||
b.used = length as usize;
|
||||
b.status
|
||||
.store(MemoryStatus::Active as u8, Ordering::Release);
|
||||
return Some(b);
|
||||
}
|
||||
}
|
||||
|
||||
// When the allocation is larger than the number of bytes available
|
||||
// we will need to try to grow the memory
|
||||
if length_with_block >= mem_left {
|
||||
// If the current position is large enough to hold the length of the block being
|
||||
// allocated then check for existing free blocks that can be re-used before
|
||||
// growing memory
|
||||
if length_with_block <= self_position {
|
||||
let b = self.find_free_block(length, self_position);
|
||||
|
||||
// If there's a free block then re-use it
|
||||
if let Some(b) = b {
|
||||
b.used = length as usize;
|
||||
b.status
|
||||
.store(MemoryStatus::Active as u8, Ordering::Release);
|
||||
return Some(b);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the number of pages needed to cover the remaining bytes
|
||||
let npages = num_pages(length_with_block - mem_left);
|
||||
let x = core::arch::wasm32::memory_grow(0, npages);
|
||||
|
||||
@@ -269,8 +269,8 @@ pub struct Manifest {
|
||||
/// Config values are made accessible using the PDK `extism_config_get` function
|
||||
#[serde(default)]
|
||||
pub config: BTreeMap<String, String>,
|
||||
#[serde(default)]
|
||||
|
||||
#[serde(default)]
|
||||
/// Specifies which hosts may be accessed via HTTP, if this is empty then
|
||||
/// no hosts may be accessed. Wildcards may be used.
|
||||
pub allowed_hosts: Option<Vec<String>>,
|
||||
@@ -417,14 +417,14 @@ mod wasmdata {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Manifest> for std::borrow::Cow<'a, [u8]> {
|
||||
impl From<Manifest> for std::borrow::Cow<'_, [u8]> {
|
||||
fn from(m: Manifest) -> Self {
|
||||
let s = serde_json::to_vec(&m).unwrap();
|
||||
std::borrow::Cow::Owned(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&Manifest> for std::borrow::Cow<'a, [u8]> {
|
||||
impl From<&Manifest> for std::borrow::Cow<'_, [u8]> {
|
||||
fn from(m: &Manifest) -> Self {
|
||||
let s = serde_json::to_vec(&m).unwrap();
|
||||
std::borrow::Cow::Owned(s)
|
||||
|
||||
@@ -9,9 +9,9 @@ repository.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
wasmtime = {version = ">= 26.0.0, < 27.0.0"}
|
||||
wasi-common = {version = ">= 26.0.0, < 27.0.0"}
|
||||
wiggle = {version = ">= 26.0.0, < 27.0.0"}
|
||||
wasmtime = {version = ">= 27.0.0, < 31.0.0"}
|
||||
wasi-common = {version = ">= 27.0.0, < 31.0.0"}
|
||||
wiggle = {version = ">= 27.0.0, < 31.0.0"}
|
||||
anyhow = "1"
|
||||
serde = {version = "1", features = ["derive"]}
|
||||
serde_json = "1"
|
||||
@@ -21,7 +21,7 @@ tracing = "0.1"
|
||||
tracing-subscriber = {version = "0.3.18", features = ["std", "env-filter", "fmt"]}
|
||||
url = "2"
|
||||
glob = "0.3"
|
||||
ureq = {version = "2.5", optional=true}
|
||||
ureq = {version = "3.0", optional=true}
|
||||
extism-manifest = { workspace = true }
|
||||
extism-convert = { workspace = true, features = ["extism-path"] }
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
@@ -34,12 +34,12 @@ register-filesystem = [] # enables wasm to be loaded from disk
|
||||
http = ["ureq"] # enables extism_http_request
|
||||
|
||||
[build-dependencies]
|
||||
cbindgen = { version = "0.27", default-features = false }
|
||||
cbindgen = { version = "0.29", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.5.1"
|
||||
criterion = "0.6.0"
|
||||
quickcheck = "1"
|
||||
rand = "0.8.5"
|
||||
rand = "0.9.0"
|
||||
|
||||
[[bench]]
|
||||
name = "bench"
|
||||
|
||||
@@ -24,7 +24,7 @@ There are a few environment variables that can be used for debugging purposes:
|
||||
- `EXTISM_COREDUMP=extism.core`: write [coredump](https://github.com/WebAssembly/tool-conventions/blob/main/Coredump.md) to a file when a WebAssembly function traps
|
||||
- `EXTISM_DEBUG=1`: generate debug information
|
||||
- `EXTISM_PROFILE=perf|jitdump|vtune`: enable Wasmtime profiling
|
||||
- `EXTISM_CACHE_CONFIG=path/to/config.toml`: enable Wasmtime cache, see [the docs](https://docs.wasmtime.dev/cli-cache.html) for details about configuration. Setting this to an empty string will disable caching.
|
||||
- `EXTISM_CACHE_CONFIG=path/to/config.toml`: enable Wasmtime cache, details [here](#wasmtime-caching)
|
||||
|
||||
> *Note*: The debug and coredump info will only be written if the plug-in has an error.
|
||||
|
||||
@@ -88,7 +88,7 @@ println!("{:?}", res);
|
||||
|
||||
### Plug-in State
|
||||
|
||||
Plug-ins may be stateful or stateless. Plug-ins can maintain state b/w calls by the use of variables. Our count vowels plug-in remembers the total number of vowels it's ever counted in the "total" key in the result. You can see this by making subsequent calls to the export:
|
||||
Plug-ins may be stateful or stateless. Plug-ins can maintain state between calls by the use of variables. Our count vowels plug-in remembers the total number of vowels it's ever counted in the "total" key in the result. You can see this by making subsequent calls to the export:
|
||||
|
||||
```rust
|
||||
let res = plugin.call::<&str, &str>("count_vowels", "Hello, world!").unwrap();
|
||||
@@ -219,5 +219,52 @@ println!("{}", res);
|
||||
# => {"count": 3, "total": 6, "vowels": "aeiouAEIOU"}
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
Plug-ins can't directly print anything to the console. They can however use Extism's built-in logging functionality, for example the [`log!` macro in the rust-pdk](https://docs.rs/extism-pdk/1.4.0/extism_pdk/macro.log.html) or the [`logInfo` function in the haskell-pdk](https://hackage.haskell.org/package/extism-pdk-1.2.0.0/docs/Extism-PDK.html#v:logInfo).
|
||||
|
||||
Inside your host application, the rust-sdk emits these as [tracing](https://github.com/tokio-rs/tracing) events. The simplest way to make the logged messages visible is by adding the `tracing_subscriber` dependency to your crate and then initializing a tracing subscriber at the top of your main function:
|
||||
|
||||
```rust
|
||||
tracing_subscriber::fmt::init();
|
||||
```
|
||||
|
||||
### Wasmtime Caching
|
||||
|
||||
To enable or disable caching for plugin compilation, you need to provide a configuration file that will be used by the [wasmtime crate](https://github.com/bytecodealliance/wasmtime).
|
||||
|
||||
For more information and values that can be used for configuring caching, take a look at [the docs](https://docs.wasmtime.dev/cli-cache.html).
|
||||
|
||||
> *Note*: As of now extism uses wasmtime [`version = ">= 27.0.0, < 31.0.0"`](https://github.com/extism/extism/blob/v1.11.1/runtime/Cargo.toml#L12), but the `enabled` key requirement [was removed](https://github.com/bytecodealliance/wasmtime/pull/10859) from `wasmtime` and its documentation, this could explain the `failed to parse config file` error you might encounter without it.
|
||||
|
||||
An example configuration for caching would be:
|
||||
|
||||
```toml
|
||||
[cache]
|
||||
enabled = true # This value is required
|
||||
directory = "/some/path"
|
||||
```
|
||||
|
||||
You can :
|
||||
- [Create a global `wasmtime` configuration file](#using-a-configuration-file) in `$HOME/.config/wasmtime/config.toml`.
|
||||
- [Set the `EXTISM_CACHE_CONFIG` environment variable](#using-an-environment-variable)
|
||||
- [Set the configuration file path using `PluginBuilder`](#using-pluginbuilder)
|
||||
|
||||
#### Using a configuration file
|
||||
|
||||
The [wasmtime](https://github.com/bytecodealliance/wasmtime) crate, by default, will look for a configuration file in your systems' default configuration directory (for example on UNIX systems: `$HOME/.config/wasmtime/config.toml`),
|
||||
for more [information on this behaviour](`https://docs.rs/wasmtime/31.0.0/wasmtime/struct.Config.html#method.cache_config_load_default`).
|
||||
|
||||
#### Using an environment variable
|
||||
|
||||
You can set the `EXTISM_CACHE_CONFIG=path/to/config.toml` environment variable to set the path of the configuration file used by [wasmtime](https://github.com/bytecodealliance/wasmtime).
|
||||
Setting the variable to an empty string will disable caching (it won't load any configuration file).
|
||||
|
||||
> *Note*: If the environment variable is not set, `wasmtime` will still try to read from a configuration file that may exist in your system's default configuration folder (e.g. `$HOME/.config/wasmtime/config.toml`).
|
||||
|
||||
The environment variable does not override the path you might have set using `PluginBuilder`. will only be checked for if you did not specify a cache configuration path in `PluginBuilder`.
|
||||
|
||||
#### Using PluginBuilder
|
||||
|
||||
If you use a [PluginBuilder](https://docs.rs/extism/latest/extism/struct.PluginBuilder.html), you can set the `wasmtime` configuration path using the [with_cache_config](https://docs.rs/extism/latest/extism/struct.PluginBuilder.html#method.with_cache_config) method.
|
||||
This will override the `EXTISM_CACHE_CONFIG` environment variable if it's set, so you could have a "global" and per plugin configuration if needed.
|
||||
|
||||
@@ -16,7 +16,7 @@ fn main() {
|
||||
|
||||
let res = plugin.call::<&str, &str>("try_read", "").unwrap();
|
||||
|
||||
println!("{:?}", res);
|
||||
println!("{res:?}");
|
||||
|
||||
println!("-----------------------------------------------------");
|
||||
|
||||
@@ -30,7 +30,7 @@ fn main() {
|
||||
);
|
||||
let res2 = plugin.call::<&str, &str>("try_write", &line).unwrap();
|
||||
|
||||
println!("{:?}", res2);
|
||||
println!("{res2:?}");
|
||||
|
||||
println!("done!");
|
||||
}
|
||||
|
||||
@@ -30,6 +30,6 @@ fn main() {
|
||||
let res = plugin
|
||||
.call::<&str, &str>("reflect", "Hello, world!")
|
||||
.unwrap();
|
||||
println!("{}", res);
|
||||
println!("{res}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,6 @@ fn main() {
|
||||
println!("Dumping logs");
|
||||
|
||||
for line in LOGS.lock().unwrap().iter() {
|
||||
print!("{}", line);
|
||||
print!("{line}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,6 @@ fn main() {
|
||||
let res = plugin
|
||||
.call::<&str, &str>("count_vowels", "Hello, world!")
|
||||
.unwrap();
|
||||
println!("{}", res);
|
||||
println!("{res}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ void extism_current_plugin_memory_free(ExtismCurrentPlugin *plugin, ExtismMemory
|
||||
* - `n_outputs`: number of return types
|
||||
* - `func`: the function to call
|
||||
* - `user_data`: a pointer that will be passed to the function when it's called
|
||||
* this value should live as long as the function exists
|
||||
* this value should live as long as the function exists
|
||||
* - `free_user_data`: a callback to release the `user_data` value when the resulting
|
||||
* `ExtismFunction` is freed.
|
||||
*
|
||||
|
||||
@@ -206,15 +206,16 @@ impl CurrentPlugin {
|
||||
anyhow::bail!("expected extism_context to be an externref value",)
|
||||
};
|
||||
|
||||
match xs
|
||||
.data_mut(&mut *store)?
|
||||
.downcast_mut::<Box<dyn std::any::Any + Send + Sync>>()
|
||||
{
|
||||
Some(xs) => match xs.downcast_mut::<T>() {
|
||||
Some(xs) => Ok(xs),
|
||||
None => anyhow::bail!("could not downcast extism_context inner value"),
|
||||
},
|
||||
None => anyhow::bail!("could not downcast extism_context"),
|
||||
if let Some(d) = xs.data_mut(&mut *store)? {
|
||||
match d.downcast_mut::<Box<dyn std::any::Any + Send + Sync>>() {
|
||||
Some(xs) => match xs.downcast_mut::<T>() {
|
||||
Some(xs) => Ok(xs),
|
||||
None => anyhow::bail!("could not downcast extism_context inner value"),
|
||||
},
|
||||
None => anyhow::bail!("could not downcast extism_context"),
|
||||
}
|
||||
} else {
|
||||
anyhow::bail!("extism_context not found")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,10 +477,7 @@ impl CurrentPlugin {
|
||||
offset: offs,
|
||||
length,
|
||||
});
|
||||
match s {
|
||||
Ok(s) => Some(s),
|
||||
Err(_) => None,
|
||||
}
|
||||
s.ok()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
|
||||
Binary file not shown.
@@ -284,8 +284,8 @@ impl Function {
|
||||
/// A few things worth noting:
|
||||
/// - The function always returns a `Result` that wraps the specified return type
|
||||
/// - If a first parameter and type are passed (`_user_data` above) followed by a semicolon it will be
|
||||
/// the name of the `UserData` parameter and can be used from inside the function
|
||||
// definition.
|
||||
/// the name of the `UserData` parameter and can be used from inside the function
|
||||
// definition.
|
||||
#[macro_export]
|
||||
macro_rules! host_fn {
|
||||
($pub:vis $name: ident ($($arg:ident : $argty:ty),*) $(-> $ret:ty)? $b:block) => {
|
||||
|
||||
@@ -29,6 +29,7 @@ pub(crate) mod manifest;
|
||||
pub(crate) mod pdk;
|
||||
mod plugin;
|
||||
mod plugin_builder;
|
||||
mod pool;
|
||||
mod readonly_dir;
|
||||
mod timer;
|
||||
|
||||
@@ -43,6 +44,7 @@ pub use plugin::{
|
||||
CancelHandle, CompiledPlugin, Plugin, WasmInput, EXTISM_ENV_MODULE, EXTISM_USER_MODULE,
|
||||
};
|
||||
pub use plugin_builder::{DebugOptions, PluginBuilder};
|
||||
pub use pool::{Pool, PoolBuilder, PoolPlugin};
|
||||
|
||||
pub(crate) use internal::{Internal, Wasi};
|
||||
pub(crate) use timer::{Timer, TimerAction};
|
||||
@@ -96,7 +98,7 @@ pub fn set_log_callback<F: 'static + Clone + Fn(&str)>(
|
||||
let x = tracing_subscriber::EnvFilter::builder()
|
||||
.with_default_directive(tracing::Level::ERROR.into());
|
||||
if is_level {
|
||||
x.parse_lossy(format!("extism={}", filter))
|
||||
x.parse_lossy(format!("extism={filter}"))
|
||||
} else {
|
||||
x.parse_lossy(filter)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::*;
|
||||
fn hex(data: &[u8]) -> String {
|
||||
let mut s = String::new();
|
||||
for &byte in data {
|
||||
write!(&mut s, "{:02x}", byte).unwrap();
|
||||
write!(&mut s, "{byte:02x}").unwrap();
|
||||
}
|
||||
s
|
||||
}
|
||||
@@ -86,14 +86,16 @@ fn to_module(engine: &Engine, wasm: &extism_manifest::Wasm) -> Result<(String, M
|
||||
#[cfg(feature = "register-http")]
|
||||
{
|
||||
// Setup request
|
||||
let mut req = ureq::request(method.as_deref().unwrap_or("GET"), url);
|
||||
let mut req = ureq::http::request::Builder::new()
|
||||
.method(method.as_deref().unwrap_or("GET").to_uppercase().as_str())
|
||||
.uri(url);
|
||||
|
||||
for (k, v) in headers.iter() {
|
||||
req = req.set(k, v);
|
||||
req = req.header(k, v);
|
||||
}
|
||||
|
||||
// Fetch WASM code
|
||||
let mut r = req.call()?.into_reader();
|
||||
let mut r = ureq::run(req.body(())?)?.into_body().into_reader();
|
||||
let mut data = Vec::new();
|
||||
r.read_to_end(&mut data)?;
|
||||
|
||||
|
||||
@@ -233,17 +233,22 @@ pub(crate) fn http_request(
|
||||
)));
|
||||
}
|
||||
|
||||
let mut r = ureq::request(req.method.as_deref().unwrap_or("GET"), &req.url);
|
||||
let mut r = ureq::http::request::Builder::new()
|
||||
.method(
|
||||
req.method
|
||||
.as_deref()
|
||||
.unwrap_or("GET")
|
||||
.to_uppercase()
|
||||
.as_str(),
|
||||
)
|
||||
.uri(&req.url);
|
||||
|
||||
for (k, v) in req.headers.iter() {
|
||||
r = r.set(k, v);
|
||||
r = r.header(k, v);
|
||||
}
|
||||
|
||||
// Set HTTP timeout to respect the manifest timeout
|
||||
if let Some(remaining) = data.time_remaining() {
|
||||
r = r.timeout(remaining);
|
||||
}
|
||||
|
||||
let timeout = data.time_remaining();
|
||||
let res = if body_offset > 0 {
|
||||
let handle = match data.memory_handle(body_offset) {
|
||||
Some(h) => h,
|
||||
@@ -252,9 +257,15 @@ pub(crate) fn http_request(
|
||||
}
|
||||
};
|
||||
let buf: &[u8] = data.memory_bytes(handle)?;
|
||||
r.send_bytes(buf)
|
||||
let agent = ureq::agent();
|
||||
let config = agent.configure_request(r.body(buf)?);
|
||||
let req = config.timeout_global(timeout).build();
|
||||
ureq::run(req)
|
||||
} else {
|
||||
r.call()
|
||||
let agent = ureq::agent();
|
||||
let config = agent.configure_request(r.body(())?);
|
||||
let req = config.timeout_global(timeout).build();
|
||||
ureq::run(req)
|
||||
};
|
||||
|
||||
if let Some(handle) = data.memory_handle(body_offset) {
|
||||
@@ -264,26 +275,26 @@ pub(crate) fn http_request(
|
||||
let reader = match res {
|
||||
Ok(res) => {
|
||||
if let Some(headers) = &mut data.http_headers {
|
||||
for name in res.headers_names() {
|
||||
if let Some(h) = res.header(&name) {
|
||||
headers.insert(name, h.to_string());
|
||||
for (name, h) in res.headers() {
|
||||
if let Ok(h) = h.to_str() {
|
||||
headers.insert(name.as_str().to_string(), h.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
data.http_status = res.status();
|
||||
Some(res.into_reader())
|
||||
data.http_status = res.status().as_u16();
|
||||
Some(res.into_body().into_reader())
|
||||
}
|
||||
Err(e) => {
|
||||
// Catch timeout and return
|
||||
if let Some(d) = data.time_remaining() {
|
||||
if e.kind() == ureq::ErrorKind::Io && d.as_nanos() == 0 {
|
||||
if matches!(e, ureq::Error::Timeout(_)) && d.as_nanos() == 0 {
|
||||
anyhow::bail!("timeout");
|
||||
}
|
||||
}
|
||||
let msg = e.to_string();
|
||||
if let Some(res) = e.into_response() {
|
||||
data.http_status = res.status();
|
||||
Some(res.into_reader())
|
||||
if let ureq::Error::StatusCode(res) = e {
|
||||
data.http_status = res;
|
||||
None
|
||||
} else {
|
||||
return Err(Error::msg(msg));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::{
|
||||
any::Any,
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
sync::TryLockError,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
@@ -190,6 +191,7 @@ pub(crate) fn profiling_strategy() -> ProfilingStrategy {
|
||||
/// Defines an input type for Wasm data.
|
||||
///
|
||||
/// Types that implement `Into<WasmInput>` can be passed directly into `Plugin::new`
|
||||
#[derive(Clone)]
|
||||
pub enum WasmInput<'a> {
|
||||
/// Raw Wasm module
|
||||
Data(std::borrow::Cow<'a, [u8]>),
|
||||
@@ -199,7 +201,7 @@ pub enum WasmInput<'a> {
|
||||
ManifestRef(&'a Manifest),
|
||||
}
|
||||
|
||||
impl<'a> From<Manifest> for WasmInput<'a> {
|
||||
impl From<Manifest> for WasmInput<'_> {
|
||||
fn from(value: Manifest) -> Self {
|
||||
WasmInput::Manifest(value)
|
||||
}
|
||||
@@ -229,7 +231,7 @@ impl<'a> From<&'a str> for WasmInput<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Vec<u8>> for WasmInput<'a> {
|
||||
impl From<Vec<u8>> for WasmInput<'_> {
|
||||
fn from(value: Vec<u8>) -> Self {
|
||||
WasmInput::Data(value.into())
|
||||
}
|
||||
@@ -325,6 +327,36 @@ fn relink(
|
||||
get_log_level() -> I32;
|
||||
);
|
||||
|
||||
for (name, module) in modules.iter() {
|
||||
if name == EXTISM_ENV_MODULE {
|
||||
continue;
|
||||
}
|
||||
|
||||
for import in module.imports() {
|
||||
if import.module() == EXTISM_ENV_MODULE
|
||||
&& modules[EXTISM_ENV_MODULE]
|
||||
.get_export(import.name())
|
||||
.is_none()
|
||||
&& linker
|
||||
.get(&mut store, EXTISM_ENV_MODULE, import.name())
|
||||
.is_none()
|
||||
{
|
||||
let (kind, ty) = match import.ty() {
|
||||
ExternType::Func(t) => ("function", t.to_string()),
|
||||
ExternType::Global(t) => ("global", t.content().to_string()),
|
||||
ExternType::Table(t) => ("table", t.element().to_string()),
|
||||
ExternType::Memory(_) => ("memory", String::new()),
|
||||
};
|
||||
anyhow::bail!(
|
||||
"Invalid {kind} import from extism:host/env: {} {ty}\n\n\
|
||||
Note: This may indicate that the PDK that was used to build this plugin has additional features that aren't \
|
||||
available in this version of the SDK, try updating the SDK to the latest version.",
|
||||
import.name(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut linked = BTreeSet::new();
|
||||
linker.module(&mut store, EXTISM_ENV_MODULE, &modules[EXTISM_ENV_MODULE])?;
|
||||
linked.insert(EXTISM_ENV_MODULE.to_string());
|
||||
@@ -530,7 +562,7 @@ impl Plugin {
|
||||
}
|
||||
|
||||
/// Returns `true` if the given function exists, otherwise `false`
|
||||
pub fn function_exists(&mut self, function: impl AsRef<str>) -> bool {
|
||||
pub fn function_exists(&self, function: impl AsRef<str>) -> bool {
|
||||
self.modules[MAIN_KEY]
|
||||
.get_export(function.as_ref())
|
||||
.map(|x| {
|
||||
@@ -825,15 +857,20 @@ impl Plugin {
|
||||
|
||||
// Set host context
|
||||
let r = if let Some(host_context) = host_context {
|
||||
let inner = self
|
||||
if let Some(inner) = self
|
||||
.host_context
|
||||
.data_mut(&mut self.store)
|
||||
.map_err(|x| (x, -1))?;
|
||||
if let Some(inner) = inner.downcast_mut::<Box<dyn std::any::Any + Send + Sync>>() {
|
||||
let x: Box<T> = Box::new(host_context);
|
||||
*inner = x;
|
||||
.map_err(|x| (x, -1))?
|
||||
{
|
||||
if let Some(inner) = inner.downcast_mut::<Box<dyn std::any::Any + Send + Sync>>() {
|
||||
let x: Box<T> = Box::new(host_context);
|
||||
*inner = x;
|
||||
}
|
||||
|
||||
Some(self.host_context)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
Some(self.host_context)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -876,7 +913,7 @@ impl Plugin {
|
||||
let mut res = func.call(self.store_mut(), &[], results.as_mut_slice());
|
||||
|
||||
// Reset host context
|
||||
if let Ok(inner) = self.host_context.data_mut(&mut self.store) {
|
||||
if let Ok(Some(inner)) = self.host_context.data_mut(&mut self.store) {
|
||||
if let Some(inner) = inner.downcast_mut::<Box<dyn std::any::Any + Send + Sync>>() {
|
||||
let x: Box<dyn Any + Send + Sync> = Box::new(());
|
||||
*inner = x;
|
||||
@@ -906,8 +943,7 @@ impl Plugin {
|
||||
}
|
||||
|
||||
// on extism error
|
||||
if output_res.is_ok() && self.output.error_offset != 0 && self.output.error_length != 0
|
||||
{
|
||||
if output_res.is_ok() && self.extism_error_is_set() {
|
||||
let handle = MemoryHandle {
|
||||
offset: self.output.error_offset,
|
||||
length: self.output.error_length,
|
||||
@@ -927,8 +963,7 @@ impl Plugin {
|
||||
}
|
||||
Err(msg) => {
|
||||
res = Err(Error::msg(format!(
|
||||
"Call to Extism plugin function {name} encountered an error: {}",
|
||||
msg,
|
||||
"unable to load error message from memory: {msg}",
|
||||
)));
|
||||
}
|
||||
}
|
||||
@@ -994,11 +1029,12 @@ impl Plugin {
|
||||
plugin = self.id.to_string(),
|
||||
"WASI exit code: {}", exit_code
|
||||
);
|
||||
if exit_code == 0 {
|
||||
|
||||
if exit_code == 0 && !self.extism_error_is_set() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
return Err((e.context("WASI exit code"), exit_code));
|
||||
return Err((e, exit_code));
|
||||
}
|
||||
|
||||
// Handle timeout interrupts
|
||||
@@ -1026,6 +1062,10 @@ impl Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
fn extism_error_is_set(&self) -> bool {
|
||||
self.output.error_offset != 0 && self.output.error_length != 0
|
||||
}
|
||||
|
||||
/// Call a function by name with the given input, the return value is
|
||||
/// the output data returned by the plugin. The return type can be anything that implements
|
||||
/// [FromBytes]. This data will be invalidated next time the plugin is called.
|
||||
@@ -1049,7 +1089,12 @@ impl Plugin {
|
||||
input: T,
|
||||
) -> Result<U, Error> {
|
||||
let lock = self.instance.clone();
|
||||
let mut lock = lock.lock().unwrap();
|
||||
let mut lock = lock.try_lock().map_err(|e| match e {
|
||||
TryLockError::Poisoned(_) => anyhow::anyhow!(
|
||||
"instance lock was poisoned; previous thread panicked while calling into wasm"
|
||||
),
|
||||
TryLockError::WouldBlock => anyhow::anyhow!("cannot make reentrant calls into plugin"),
|
||||
})?;
|
||||
let data = input.to_bytes()?;
|
||||
self.raw_call(&mut lock, name, data, None::<()>)
|
||||
.map_err(|e| e.0)
|
||||
@@ -1074,7 +1119,12 @@ impl Plugin {
|
||||
C: Any + Send + Sync + 'static,
|
||||
{
|
||||
let lock = self.instance.clone();
|
||||
let mut lock = lock.lock().unwrap();
|
||||
let mut lock = lock.try_lock().map_err(|e| match e {
|
||||
TryLockError::Poisoned(_) => anyhow::anyhow!(
|
||||
"instance lock was poisoned; previous thread panicked while calling into wasm"
|
||||
),
|
||||
TryLockError::WouldBlock => anyhow::anyhow!("cannot make reentrant calls into plugin"),
|
||||
})?;
|
||||
let data = input.to_bytes()?;
|
||||
self.raw_call(&mut lock, name, data, Some(host_context))
|
||||
.map_err(|e| e.0)
|
||||
@@ -1093,7 +1143,18 @@ impl Plugin {
|
||||
input: T,
|
||||
) -> Result<U, (Error, i32)> {
|
||||
let lock = self.instance.clone();
|
||||
let mut lock = lock.lock().unwrap();
|
||||
let mut lock = lock.try_lock().map_err(|e| match e {
|
||||
TryLockError::Poisoned(_) => (
|
||||
anyhow::anyhow!(
|
||||
"instance lock was poisoned; previous thread panicked while calling into wasm"
|
||||
),
|
||||
-1,
|
||||
),
|
||||
TryLockError::WouldBlock => (
|
||||
anyhow::anyhow!("cannot make reentrant calls into plugin"),
|
||||
-1,
|
||||
),
|
||||
})?;
|
||||
let data = input.to_bytes().map_err(|e| (e, -1))?;
|
||||
self.raw_call(&mut lock, name, data, None::<()>)
|
||||
.and_then(move |_| self.output().map_err(|e| (e, -1)))
|
||||
@@ -1121,6 +1182,25 @@ impl Plugin {
|
||||
anyhow::bail!("Plugin::clear_error failed, extism:host/env::error_set not found")
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the amount of fuel consumed by the plugin.
|
||||
///
|
||||
/// This function calculates the difference between the initial fuel and the remaining fuel.
|
||||
/// If either the initial fuel or the remaining fuel is not set, it returns `None`.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Some(u64)` - The amount of fuel consumed.
|
||||
/// * `None` - If the initial fuel or remaining fuel is not set.
|
||||
pub fn fuel_consumed(&self) -> Option<u64> {
|
||||
self.fuel.map(|x| {
|
||||
x.saturating_sub(
|
||||
self.store
|
||||
.get_fuel()
|
||||
.expect("fuel support should be enabled to use fuel"),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Enumerates the PDK languages that need some additional initialization
|
||||
@@ -1164,7 +1244,7 @@ macro_rules! typed_plugin {
|
||||
|
||||
impl TryFrom<$crate::Plugin> for $name {
|
||||
type Error = $crate::Error;
|
||||
fn try_from(mut x: $crate::Plugin) -> Result<Self, Self::Error> {
|
||||
fn try_from(x: $crate::Plugin) -> Result<Self, Self::Error> {
|
||||
$(
|
||||
if !x.function_exists(stringify!($f)) {
|
||||
return Err($crate::Error::msg(format!("Invalid function: {}", stringify!($f))));
|
||||
|
||||
@@ -33,6 +33,7 @@ impl Default for DebugOptions {
|
||||
}
|
||||
|
||||
/// PluginBuilder is used to configure and create `Plugin` instances
|
||||
#[derive(Clone)]
|
||||
pub struct PluginBuilder<'a> {
|
||||
pub(crate) source: WasmInput<'a>,
|
||||
pub(crate) config: Option<wasmtime::Config>,
|
||||
|
||||
174
runtime/src/pool.rs
Normal file
174
runtime/src/pool.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
use crate::{Error, FromBytesOwned, Plugin, ToBytes};
|
||||
|
||||
// `PoolBuilder` is used to configure and create `Pool`s
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PoolBuilder {
|
||||
/// Max number of concurrent instances for a plugin - by default this is set to
|
||||
/// the output of `std::thread::available_parallelism`
|
||||
pub max_instances: usize,
|
||||
}
|
||||
|
||||
impl PoolBuilder {
|
||||
/// Create a `PoolBuilder` with default values
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Set the max number of parallel instances
|
||||
pub fn with_max_instances(mut self, n: usize) -> Self {
|
||||
self.max_instances = n;
|
||||
self
|
||||
}
|
||||
|
||||
/// Create a new `Pool` with the given configuration
|
||||
pub fn build<F: 'static + Fn() -> Result<Plugin, Error>>(self, source: F) -> Pool {
|
||||
Pool::new_from_builder(source, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PoolBuilder {
|
||||
fn default() -> Self {
|
||||
PoolBuilder {
|
||||
max_instances: std::thread::available_parallelism()
|
||||
.expect("available parallelism")
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `PoolPlugin` is used by the pool to track the number of live instances of a particular plugin
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PoolPlugin(std::rc::Rc<std::cell::RefCell<Plugin>>);
|
||||
|
||||
impl PoolPlugin {
|
||||
fn new(plugin: Plugin) -> Self {
|
||||
Self(std::rc::Rc::new(std::cell::RefCell::new(plugin)))
|
||||
}
|
||||
|
||||
/// Access the underlying plugin
|
||||
pub fn plugin(&self) -> std::cell::RefMut<Plugin> {
|
||||
self.0.borrow_mut()
|
||||
}
|
||||
|
||||
/// Helper to call a plugin function on the underlying plugin
|
||||
pub fn call<'a, Input: ToBytes<'a>, Output: FromBytesOwned>(
|
||||
&self,
|
||||
name: impl AsRef<str>,
|
||||
input: Input,
|
||||
) -> Result<Output, Error> {
|
||||
self.plugin().call(name.as_ref(), input)
|
||||
}
|
||||
|
||||
/// Helper to get the underlying plugin's ID
|
||||
pub fn id(&self) -> uuid::Uuid {
|
||||
self.plugin().id
|
||||
}
|
||||
}
|
||||
|
||||
type PluginSource = dyn Fn() -> Result<Plugin, Error>;
|
||||
|
||||
struct PoolInner {
|
||||
plugin_source: Box<PluginSource>,
|
||||
instances: Vec<PoolPlugin>,
|
||||
}
|
||||
|
||||
unsafe impl Send for PoolInner {}
|
||||
unsafe impl Sync for PoolInner {}
|
||||
|
||||
/// `Pool` manages threadsafe access to a limited number of instances of multiple plugins
|
||||
#[derive(Clone)]
|
||||
pub struct Pool {
|
||||
config: PoolBuilder,
|
||||
inner: std::sync::Arc<std::sync::Mutex<PoolInner>>,
|
||||
}
|
||||
|
||||
unsafe impl Send for Pool {}
|
||||
unsafe impl Sync for Pool {}
|
||||
|
||||
impl Pool {
|
||||
/// Create a new pool with the default configuration
|
||||
pub fn new<F: 'static + Fn() -> Result<Plugin, Error>>(source: F) -> Self {
|
||||
Pool {
|
||||
config: Default::default(),
|
||||
inner: std::sync::Arc::new(std::sync::Mutex::new(PoolInner {
|
||||
plugin_source: Box::new(source),
|
||||
instances: Default::default(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new pool configured using a `PoolBuilder`
|
||||
pub fn new_from_builder<F: 'static + Fn() -> Result<Plugin, Error>>(
|
||||
source: F,
|
||||
builder: PoolBuilder,
|
||||
) -> Self {
|
||||
Pool {
|
||||
config: builder,
|
||||
inner: std::sync::Arc::new(std::sync::Mutex::new(PoolInner {
|
||||
plugin_source: Box::new(source),
|
||||
instances: Default::default(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
fn find_available(&self) -> Result<Option<PoolPlugin>, Error> {
|
||||
let pool = self.inner.lock().unwrap();
|
||||
for instance in pool.instances.iter() {
|
||||
if std::rc::Rc::strong_count(&instance.0) == 1 {
|
||||
return Ok(Some(instance.clone()));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Get the number of live instances for a plugin
|
||||
pub fn count(&self) -> usize {
|
||||
self.inner.lock().unwrap().instances.len()
|
||||
}
|
||||
|
||||
/// Get access to a plugin, this will create a new instance if needed (and allowed by the specified
|
||||
/// max_instances). `Ok(None)` is returned if the timeout is reached before an available plugin could be
|
||||
/// acquired
|
||||
pub fn get(&self, timeout: std::time::Duration) -> Result<Option<PoolPlugin>, Error> {
|
||||
let start = std::time::Instant::now();
|
||||
let max = self.config.max_instances;
|
||||
if let Some(avail) = self.find_available()? {
|
||||
return Ok(Some(avail));
|
||||
}
|
||||
|
||||
{
|
||||
let mut pool = self.inner.lock().unwrap();
|
||||
if pool.instances.len() < max {
|
||||
let plugin = (*pool.plugin_source)()?;
|
||||
let instance = PoolPlugin::new(plugin);
|
||||
pool.instances.push(instance);
|
||||
return Ok(Some(pool.instances.last().unwrap().clone()));
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
if let Ok(Some(x)) = self.find_available() {
|
||||
return Ok(Some(x));
|
||||
}
|
||||
if std::time::Instant::now() - start > timeout {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
|
||||
/// Access a plugin in a callback function. This calls `Pool::get` then the provided
|
||||
/// callback. `Ok(None)` is returned if the timeout is reached before an available
|
||||
/// plugin could be acquired
|
||||
pub fn with_plugin<T>(
|
||||
&self,
|
||||
timeout: std::time::Duration,
|
||||
f: impl FnOnce(&mut Plugin) -> Result<T, Error>,
|
||||
) -> Result<Option<T>, Error> {
|
||||
if let Some(plugin) = self.get(timeout)? {
|
||||
return f(&mut plugin.plugin()).map(Some);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
@@ -180,7 +180,7 @@ pub unsafe extern "C" fn extism_current_plugin_memory_free(
|
||||
/// - `n_outputs`: number of return types
|
||||
/// - `func`: the function to call
|
||||
/// - `user_data`: a pointer that will be passed to the function when it's called
|
||||
/// this value should live as long as the function exists
|
||||
/// this value should live as long as the function exists
|
||||
/// - `free_user_data`: a callback to release the `user_data` value when the resulting
|
||||
/// `ExtismFunction` is freed.
|
||||
///
|
||||
@@ -314,30 +314,53 @@ pub unsafe extern "C" fn extism_compiled_plugin_new(
|
||||
) -> *mut CompiledPlugin {
|
||||
trace!("Call to extism_plugin_new with wasm pointer {:?}", wasm);
|
||||
let data = std::slice::from_raw_parts(wasm, wasm_size as usize);
|
||||
|
||||
let mut builder = PluginBuilder::new(data).with_wasi(with_wasi);
|
||||
|
||||
if !functions.is_null() {
|
||||
for i in 0..n_functions {
|
||||
unsafe {
|
||||
let f = *functions.add(i as usize);
|
||||
if f.is_null() {
|
||||
continue;
|
||||
let funcs = (0..n_functions)
|
||||
.map(|i| unsafe { *functions.add(i as usize) })
|
||||
.map(|ptr| {
|
||||
if ptr.is_null() {
|
||||
return Err("Cannot pass null pointer");
|
||||
}
|
||||
if let Some(f) = (*f).0.take() {
|
||||
builder.options.functions.push(f);
|
||||
} else {
|
||||
let e = std::ffi::CString::new(
|
||||
"Function cannot be registered with multiple different Plugins",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let ExtismFunction(func) = &*ptr;
|
||||
let Some(func) = func.take() else {
|
||||
return Err("Function cannot be registered with multiple different Plugins");
|
||||
};
|
||||
|
||||
Ok(func)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.unwrap_or_else(|e| {
|
||||
if !errmsg.is_null() {
|
||||
let e = std::ffi::CString::new(e.to_string()).unwrap();
|
||||
*errmsg = e.into_raw();
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
}
|
||||
Vec::new()
|
||||
});
|
||||
|
||||
if funcs.len() != n_functions as usize {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
|
||||
builder = builder.with_functions(funcs);
|
||||
}
|
||||
|
||||
Box::into_raw(Box::new(CompiledPlugin::new(builder).unwrap()))
|
||||
CompiledPlugin::new(builder)
|
||||
.map(|v| Box::into_raw(Box::new(v)))
|
||||
.unwrap_or_else(|e| {
|
||||
if !errmsg.is_null() {
|
||||
let e = std::ffi::CString::new(format!(
|
||||
"Unable to compile Extism plugin: {}",
|
||||
e.root_cause(),
|
||||
))
|
||||
.unwrap();
|
||||
*errmsg = e.into_raw();
|
||||
}
|
||||
std::ptr::null_mut()
|
||||
})
|
||||
}
|
||||
|
||||
/// Free `ExtismCompiledPlugin`
|
||||
@@ -369,41 +392,52 @@ pub unsafe extern "C" fn extism_plugin_new(
|
||||
errmsg: *mut *mut std::ffi::c_char,
|
||||
) -> *mut Plugin {
|
||||
let data = std::slice::from_raw_parts(wasm, wasm_size as usize);
|
||||
let mut funcs = vec![];
|
||||
|
||||
if !functions.is_null() {
|
||||
for i in 0..n_functions {
|
||||
unsafe {
|
||||
let f = *functions.add(i as usize);
|
||||
if f.is_null() {
|
||||
continue;
|
||||
let funcs = if functions.is_null() {
|
||||
vec![]
|
||||
} else {
|
||||
let funcs = (0..n_functions)
|
||||
.map(|i| unsafe { *functions.add(i as usize) })
|
||||
.map(|ptr| {
|
||||
if ptr.is_null() {
|
||||
return Err("Cannot pass null pointer");
|
||||
}
|
||||
if let Some(f) = (*f).0.take() {
|
||||
funcs.push(f);
|
||||
} else {
|
||||
let e = std::ffi::CString::new(
|
||||
"Function cannot be registered with multiple different Plugins",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let ExtismFunction(func) = &*ptr;
|
||||
let Some(func) = func.take() else {
|
||||
return Err("Function cannot be registered with multiple different Plugins");
|
||||
};
|
||||
|
||||
Ok(func)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.unwrap_or_else(|e| {
|
||||
if !errmsg.is_null() {
|
||||
let e = std::ffi::CString::new(e.to_string()).unwrap();
|
||||
*errmsg = e.into_raw();
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vec::new()
|
||||
});
|
||||
|
||||
let plugin = Plugin::new(data, funcs, with_wasi);
|
||||
match plugin {
|
||||
Err(e) => {
|
||||
if funcs.len() != n_functions as usize {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
|
||||
funcs
|
||||
};
|
||||
|
||||
Plugin::new(data, funcs, with_wasi)
|
||||
.map(|v| Box::into_raw(Box::new(v)))
|
||||
.unwrap_or_else(|e| {
|
||||
if !errmsg.is_null() {
|
||||
let e = std::ffi::CString::new(format!("Unable to create Extism plugin: {}", e))
|
||||
.unwrap();
|
||||
let e = std::ffi::CString::new(format!(
|
||||
"Unable to compile Extism plugin: {}",
|
||||
e.root_cause(),
|
||||
))
|
||||
.unwrap();
|
||||
*errmsg = e.into_raw();
|
||||
}
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
Ok(p) => Box::into_raw(Box::new(p)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new plugin from an `ExtismCompiledPlugin`
|
||||
@@ -416,8 +450,11 @@ pub unsafe extern "C" fn extism_plugin_new_from_compiled(
|
||||
match plugin {
|
||||
Err(e) => {
|
||||
if !errmsg.is_null() {
|
||||
let e = std::ffi::CString::new(format!("Unable to create Extism plugin: {}", e))
|
||||
.unwrap();
|
||||
let e = std::ffi::CString::new(format!(
|
||||
"Unable to create Extism plugin: {}",
|
||||
e.root_cause(),
|
||||
))
|
||||
.unwrap();
|
||||
*errmsg = e.into_raw();
|
||||
}
|
||||
std::ptr::null_mut()
|
||||
@@ -442,30 +479,38 @@ pub unsafe extern "C" fn extism_plugin_new_with_fuel_limit(
|
||||
wasm
|
||||
);
|
||||
let data = std::slice::from_raw_parts(wasm, wasm_size as usize);
|
||||
let mut funcs = vec![];
|
||||
let funcs = if functions.is_null() {
|
||||
vec![]
|
||||
} else {
|
||||
let funcs = (0..n_functions)
|
||||
.map(|i| unsafe { *functions.add(i as usize) })
|
||||
.map(|ptr| {
|
||||
if ptr.is_null() {
|
||||
return Err("Cannot pass null pointer");
|
||||
}
|
||||
|
||||
if !functions.is_null() {
|
||||
for i in 0..n_functions {
|
||||
unsafe {
|
||||
let f = *functions.add(i as usize);
|
||||
if f.is_null() {
|
||||
continue;
|
||||
let ExtismFunction(func) = &*ptr;
|
||||
let Some(func) = func.take() else {
|
||||
return Err("Function cannot be registered with multiple different Plugins");
|
||||
};
|
||||
|
||||
Ok(func)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.unwrap_or_else(|e| {
|
||||
if !errmsg.is_null() {
|
||||
let e = std::ffi::CString::new(e.to_string()).unwrap();
|
||||
*errmsg = e.into_raw();
|
||||
}
|
||||
if let Some(f) = (*f).0.take() {
|
||||
funcs.push(f);
|
||||
} else {
|
||||
if !errmsg.is_null() {
|
||||
let e = std::ffi::CString::new(
|
||||
"Function cannot be registered with multiple different Plugins",
|
||||
)
|
||||
.unwrap();
|
||||
*errmsg = e.into_raw();
|
||||
}
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
}
|
||||
Vec::new()
|
||||
});
|
||||
|
||||
if funcs.len() != n_functions as usize {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
}
|
||||
|
||||
funcs
|
||||
};
|
||||
|
||||
let compiled = match CompiledPlugin::new(
|
||||
PluginBuilder::new(data)
|
||||
@@ -476,8 +521,11 @@ pub unsafe extern "C" fn extism_plugin_new_with_fuel_limit(
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
if !errmsg.is_null() {
|
||||
let e = std::ffi::CString::new(format!("Unable to compile Extism plugin: {}", e))
|
||||
.unwrap();
|
||||
let e = std::ffi::CString::new(format!(
|
||||
"Unable to compile Extism plugin: {}",
|
||||
e.root_cause(),
|
||||
))
|
||||
.unwrap();
|
||||
*errmsg = e.into_raw();
|
||||
}
|
||||
return std::ptr::null_mut();
|
||||
@@ -489,8 +537,11 @@ pub unsafe extern "C" fn extism_plugin_new_with_fuel_limit(
|
||||
match plugin {
|
||||
Err(e) => {
|
||||
if !errmsg.is_null() {
|
||||
let e = std::ffi::CString::new(format!("Unable to create Extism plugin: {}", e))
|
||||
.unwrap();
|
||||
let e = std::ffi::CString::new(format!(
|
||||
"Unable to create Extism plugin: {}",
|
||||
e.root_cause(),
|
||||
))
|
||||
.unwrap();
|
||||
*errmsg = e.into_raw();
|
||||
}
|
||||
std::ptr::null_mut()
|
||||
@@ -820,7 +871,7 @@ fn set_log_file(log_file: impl Into<std::path::PathBuf>, filter: &str) -> Result
|
||||
let x = tracing_subscriber::EnvFilter::builder()
|
||||
.with_default_directive(tracing::Level::ERROR.into());
|
||||
if is_level {
|
||||
x.parse_lossy(format!("extism={}", filter))
|
||||
x.parse_lossy(format!("extism={filter}"))
|
||||
} else {
|
||||
x.parse_lossy(filter)
|
||||
}
|
||||
@@ -848,7 +899,7 @@ fn set_log_file(log_file: impl Into<std::path::PathBuf>, filter: &str) -> Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
static mut LOG_BUFFER: Option<LogBuffer> = None;
|
||||
static LOG_BUFFER: std::sync::Mutex<Option<LogBuffer>> = std::sync::Mutex::new(None);
|
||||
|
||||
/// Enable a custom log handler, this will buffer logs until `extism_log_drain` is called
|
||||
/// Log level should be one of: info, error, trace, debug, warn
|
||||
@@ -875,13 +926,13 @@ unsafe fn set_log_buffer(filter: &str) -> Result<(), Error> {
|
||||
let x = tracing_subscriber::EnvFilter::builder()
|
||||
.with_default_directive(tracing::Level::ERROR.into());
|
||||
if is_level {
|
||||
x.parse_lossy(format!("extism={}", filter))
|
||||
x.parse_lossy(format!("extism={filter}"))
|
||||
} else {
|
||||
x.parse_lossy(filter)
|
||||
}
|
||||
});
|
||||
LOG_BUFFER = Some(LogBuffer::default());
|
||||
let buf = LOG_BUFFER.clone().unwrap();
|
||||
*LOG_BUFFER.lock().unwrap() = Some(LogBuffer::default());
|
||||
let buf = LOG_BUFFER.lock().unwrap().clone().unwrap();
|
||||
cfg.with_ansi(false)
|
||||
.with_writer(move || buf.clone())
|
||||
.try_init()
|
||||
@@ -893,7 +944,7 @@ unsafe fn set_log_buffer(filter: &str) -> Result<(), Error> {
|
||||
/// Calls the provided callback function for each buffered log line.
|
||||
/// This is only needed when `extism_log_custom` is used.
|
||||
pub unsafe extern "C" fn extism_log_drain(handler: ExtismLogDrainFunctionType) {
|
||||
if let Some(buf) = LOG_BUFFER.as_mut() {
|
||||
if let Some(buf) = LOG_BUFFER.lock().unwrap().as_mut() {
|
||||
if let Ok(mut buf) = buf.buffer.lock() {
|
||||
for (line, len) in buf.drain(..) {
|
||||
handler(line.as_ptr(), len as u64);
|
||||
|
||||
@@ -16,7 +16,7 @@ fn test_issue_620() {
|
||||
// Call test method, this does not work
|
||||
let p = plugin.call::<(), String>("test", ()).unwrap();
|
||||
|
||||
println!("{}", p);
|
||||
println!("{p}");
|
||||
}
|
||||
|
||||
// https://github.com/extism/extism/issues/619
|
||||
@@ -53,5 +53,5 @@ fn test_issue_775() {
|
||||
Ok(code) => Err(code),
|
||||
}
|
||||
.unwrap();
|
||||
println!("{}", p);
|
||||
println!("{p}");
|
||||
}
|
||||
|
||||
@@ -225,6 +225,26 @@ fn test_kernel_allocations() {
|
||||
extism_free(&mut store, instance, q);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kernel_page_allocations() {
|
||||
let (mut store, mut instance) = init_kernel_test();
|
||||
let instance = &mut instance;
|
||||
|
||||
let a = extism_alloc(&mut store, instance, 65500 * 4);
|
||||
let a_size = extism_length(&mut store, instance, a);
|
||||
let b = extism_alloc(&mut store, instance, 65539);
|
||||
|
||||
extism_free(&mut store, instance, a);
|
||||
let c = extism_alloc(&mut store, instance, 65500 * 2);
|
||||
let c_size = extism_length(&mut store, instance, c);
|
||||
|
||||
let d = extism_alloc(&mut store, instance, 65500);
|
||||
|
||||
assert_eq!(a + (a_size - c_size), c);
|
||||
assert!(c < b);
|
||||
assert!(d < b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_kernel_error() {
|
||||
let (mut store, mut instance) = init_kernel_test();
|
||||
@@ -412,7 +432,7 @@ quickcheck! {
|
||||
quickcheck! {
|
||||
fn check_alloc_with_load_and_store(amounts: Vec<u16>) -> bool {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut rng = rand::rng();
|
||||
let (mut store, mut instance) = init_kernel_test();
|
||||
let instance = &mut instance;
|
||||
for a in amounts {
|
||||
@@ -425,7 +445,7 @@ quickcheck! {
|
||||
}
|
||||
|
||||
for _ in 0..16 {
|
||||
let i = rng.gen_range(ptr..ptr+a as u64);
|
||||
let i = rng.random_range(ptr..ptr+a as u64);
|
||||
extism_store_u8(&mut store, instance, i, i as u8);
|
||||
if extism_load_u8(&mut store, instance, i as u64) != i as u8 {
|
||||
return false
|
||||
@@ -436,3 +456,31 @@ quickcheck! {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
quickcheck! {
|
||||
fn check_block_reuse(allocs: Vec<u16>) -> bool {
|
||||
let (mut store, mut instance) = init_kernel_test();
|
||||
let instance = &mut instance;
|
||||
let init = extism_alloc(&mut store, instance, allocs.iter().map(|x| *x as u64).sum::<u64>() + allocs.len() as u64 * 64);
|
||||
let bounds = init + extism_length(&mut store, instance, init);
|
||||
extism_free(&mut store, instance, init);
|
||||
for a in allocs {
|
||||
let ptr = extism_alloc(&mut store, instance, a as u64);
|
||||
if ptr == 0 {
|
||||
continue
|
||||
}
|
||||
if extism_length(&mut store, instance, ptr) != a as u64 {
|
||||
return false
|
||||
}
|
||||
|
||||
extism_free(&mut store, instance , ptr);
|
||||
|
||||
if ptr > bounds {
|
||||
println!("ptr={ptr}, bounds={bounds}");
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod issues;
|
||||
mod kernel;
|
||||
mod pool;
|
||||
mod runtime;
|
||||
|
||||
48
runtime/src/tests/pool.rs
Normal file
48
runtime/src/tests/pool.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use crate::*;
|
||||
|
||||
fn run_thread(p: Pool, i: u64) -> std::thread::JoinHandle<()> {
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_millis(i));
|
||||
let s: String = p
|
||||
.get(std::time::Duration::from_secs(1))
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.call("count_vowels", "abc")
|
||||
.unwrap();
|
||||
println!("{s}");
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_threads() {
|
||||
for i in 1..=3 {
|
||||
let data = include_bytes!("../../../wasm/code.wasm");
|
||||
let plugin_builder =
|
||||
extism::PluginBuilder::new(extism::Manifest::new([extism::Wasm::data(data)]))
|
||||
.with_wasi(true);
|
||||
let pool: Pool = PoolBuilder::new()
|
||||
.with_max_instances(i)
|
||||
.build(move || plugin_builder.clone().build());
|
||||
|
||||
let threads = vec![
|
||||
run_thread(pool.clone(), 1000),
|
||||
run_thread(pool.clone(), 1000),
|
||||
run_thread(pool.clone(), 1000),
|
||||
run_thread(pool.clone(), 1000),
|
||||
run_thread(pool.clone(), 1000),
|
||||
run_thread(pool.clone(), 1000),
|
||||
run_thread(pool.clone(), 500),
|
||||
run_thread(pool.clone(), 500),
|
||||
run_thread(pool.clone(), 500),
|
||||
run_thread(pool.clone(), 500),
|
||||
run_thread(pool.clone(), 500),
|
||||
run_thread(pool.clone(), 0),
|
||||
];
|
||||
|
||||
for t in threads {
|
||||
t.join().unwrap();
|
||||
}
|
||||
|
||||
assert!(pool.count() <= i);
|
||||
}
|
||||
}
|
||||
@@ -133,10 +133,7 @@ fn it_works() {
|
||||
.unwrap();
|
||||
let native_avg: std::time::Duration = native_sum / native_num_tests as u32;
|
||||
|
||||
println!(
|
||||
"native function call (avg, N = {}): {:?}",
|
||||
native_num_tests, native_avg
|
||||
);
|
||||
println!("native function call (avg, N = {native_num_tests}): {native_avg:?}");
|
||||
|
||||
let num_tests = test_times.len();
|
||||
let sum: std::time::Duration = test_times
|
||||
@@ -145,7 +142,7 @@ fn it_works() {
|
||||
.unwrap();
|
||||
let avg: std::time::Duration = sum / num_tests as u32;
|
||||
|
||||
println!("wasm function call (avg, N = {}): {:?}", num_tests, avg);
|
||||
println!("wasm function call (avg, N = {num_tests}): {avg:?}");
|
||||
|
||||
// Check that log file was written to
|
||||
if log {
|
||||
@@ -212,7 +209,7 @@ fn test_cancel() {
|
||||
let _output: Result<&[u8], Error> = plugin.call("loop_forever", "abc123");
|
||||
let end = std::time::Instant::now();
|
||||
let time = end - start;
|
||||
println!("Cancelled plugin ran for {:?}", time);
|
||||
println!("Cancelled plugin ran for {time:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,6 +255,23 @@ fn test_fuel() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuel_consumption() {
|
||||
let manifest = Manifest::new([extism_manifest::Wasm::data(WASM_LOOP)]);
|
||||
let mut plugin = PluginBuilder::new(manifest)
|
||||
.with_wasi(true)
|
||||
.with_fuel_limit(10000)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let output: Result<&[u8], Error> = plugin.call("loop_forever", "abc123");
|
||||
assert!(output.is_err());
|
||||
|
||||
let fuel_consumed = plugin.fuel_consumed().unwrap();
|
||||
println!("Fuel consumed: {fuel_consumed}");
|
||||
assert!(fuel_consumed > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "http")]
|
||||
fn test_http_timeout() {
|
||||
@@ -423,7 +437,7 @@ fn test_memory_max() {
|
||||
assert!(output.is_err());
|
||||
|
||||
let err = output.unwrap_err().root_cause().to_string();
|
||||
println!("{:?}", err);
|
||||
println!("{err:?}");
|
||||
assert_eq!(err, "oom");
|
||||
|
||||
// Should pass with memory.max set to a large enough number
|
||||
@@ -447,7 +461,7 @@ fn hello_world_set_error(
|
||||
_user_data: UserData<()>,
|
||||
) -> Result<(), Error> {
|
||||
plugin.set_error("TEST")?;
|
||||
outputs[0] = inputs[0].clone();
|
||||
outputs[0] = inputs[0];
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -486,7 +500,7 @@ fn test_extism_error() {
|
||||
let mut plugin = Plugin::new(&manifest, [f], true).unwrap();
|
||||
let output: Result<String, Error> = plugin.call("count_vowels", "a".repeat(1024));
|
||||
assert!(output.is_err());
|
||||
println!("{:?}", output);
|
||||
println!("{output:?}");
|
||||
assert_eq!(output.unwrap_err().root_cause().to_string(), "TEST");
|
||||
}
|
||||
|
||||
@@ -544,7 +558,7 @@ fn hello_world_user_data(
|
||||
let mut data = data.lock().unwrap();
|
||||
let s = _plugin.memory_get_val(&inputs[0])?;
|
||||
data.write_all(s)?;
|
||||
outputs[0] = inputs[0].clone();
|
||||
outputs[0] = inputs[0];
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -806,7 +820,7 @@ fn test_http_response_headers() {
|
||||
.unwrap();
|
||||
let req = HttpRequest::new("https://extism.org");
|
||||
let Json(res): Json<HashMap<String, String>> = plugin.call("http_get", Json(req)).unwrap();
|
||||
println!("{:?}", res);
|
||||
println!("{res:?}");
|
||||
assert_eq!(res["content-type"], "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
@@ -821,6 +835,6 @@ fn test_http_response_headers_disabled() {
|
||||
.unwrap();
|
||||
let req = HttpRequest::new("https://extism.org");
|
||||
let Json(res): Json<HashMap<String, String>> = plugin.call("http_get", Json(req)).unwrap();
|
||||
println!("{:?}", res);
|
||||
println!("{res:?}");
|
||||
assert!(res.is_empty());
|
||||
}
|
||||
|
||||
@@ -22,18 +22,18 @@ pub(crate) struct Timer {
|
||||
|
||||
#[cfg(not(target_family = "windows"))]
|
||||
extern "C" fn cleanup_timer() {
|
||||
let mut timer = match unsafe { TIMER.lock() } {
|
||||
let mut timer = match TIMER.lock() {
|
||||
Ok(x) => x,
|
||||
Err(e) => e.into_inner(),
|
||||
};
|
||||
drop(timer.take());
|
||||
}
|
||||
|
||||
static mut TIMER: std::sync::Mutex<Option<Timer>> = std::sync::Mutex::new(None);
|
||||
static TIMER: std::sync::Mutex<Option<Timer>> = std::sync::Mutex::new(None);
|
||||
|
||||
impl Timer {
|
||||
pub(crate) fn tx() -> std::sync::mpsc::Sender<TimerAction> {
|
||||
let mut timer = match unsafe { TIMER.lock() } {
|
||||
let mut timer = match TIMER.lock() {
|
||||
Ok(x) => x,
|
||||
Err(e) => e.into_inner(),
|
||||
};
|
||||
@@ -92,25 +92,39 @@ impl Timer {
|
||||
loop {
|
||||
if plugins.is_empty() {
|
||||
if let Ok(x) = rx.recv() {
|
||||
handle!(x)
|
||||
handle!(x);
|
||||
}
|
||||
}
|
||||
|
||||
plugins = plugins
|
||||
.into_iter()
|
||||
.filter(|(_k, (engine, end))| {
|
||||
if let Some(end) = end {
|
||||
let now = std::time::Instant::now();
|
||||
if end <= &now {
|
||||
engine.increment_epoch();
|
||||
return false;
|
||||
let mut timeout: Option<std::time::Duration> = None;
|
||||
|
||||
plugins.retain(|_k, (engine, end)| {
|
||||
if let Some(end) = end {
|
||||
let now = std::time::Instant::now();
|
||||
if *end <= now {
|
||||
engine.increment_epoch();
|
||||
return false;
|
||||
} else {
|
||||
let time_left =
|
||||
(*end - now).saturating_sub(std::time::Duration::from_millis(1));
|
||||
if let Some(t) = &timeout {
|
||||
if time_left < *t {
|
||||
timeout = Some(time_left);
|
||||
}
|
||||
} else {
|
||||
timeout = Some(time_left);
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
for x in rx.try_iter() {
|
||||
true
|
||||
});
|
||||
|
||||
if let Some(timeout) = timeout {
|
||||
if let Ok(x) = rx.recv_timeout(timeout) {
|
||||
handle!(x)
|
||||
}
|
||||
} else if let Ok(x) = rx.recv() {
|
||||
handle!(x)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user