Compare commits

...

20 Commits

Author SHA1 Message Date
zach
d704a5068e refactor!: make UserData more generic 2024-03-18 10:24:47 -07:00
zach
054a29e91d v1.2.0 2024-03-12 08:52:04 -07:00
zach
d32d4a3dd7 fix(pdk): return error when no response is available (#694) 2024-03-11 10:32:31 -07:00
Steve Manuel
5f62554aa1 chore: update badge to reflect rust installs (#693) 2024-03-08 11:29:07 -07:00
zach
d47af24552 feat: add ability to configure size of the Extism var store (#692)
- Adds `memory.max_var_bytes` to the manifest to limit the number of
bytes allowed to be stored in Extism vars - if `max_var_bytes` is set to
0 then vars are disabled.
- Adds some builder functions to `MemoryOptions` struct
- Sets the default var store size to 1mb
- Includes a test to make sure `var_set` returns an error when the limit
is reached
2024-03-07 09:55:02 -08:00
dependabot[bot]
8a29e5b1d4 chore(deps): Update base64 requirement from ~0.21 to ~0.22 (#690)
Updates the requirements on
[base64](https://github.com/marshallpierce/rust-base64) to permit the
latest version.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/marshallpierce/rust-base64/blob/master/RELEASE-NOTES.md">base64's
changelog</a>.</em></p>
<blockquote>
<h1>0.22.0</h1>
<ul>
<li><code>DecodeSliceError::OutputSliceTooSmall</code> is now
conservative rather than precise. That is, the error will only occur if
the decoded output <em>cannot</em> fit, meaning that
<code>Engine::decode_slice</code> can now be used with exactly-sized
output slices. As part of this, <code>Engine::internal_decode</code> now
returns <code>DecodeSliceError</code> instead of
<code>DecodeError</code>, but that is not expected to affect any
external callers.</li>
<li><code>DecodeError::InvalidLength</code> now refers specifically to
the <em>number of valid symbols</em> being invalid (i.e. <code>len % 4
== 1</code>), rather than just the number of input bytes. This avoids
confusing scenarios when based on interpretation you could make a case
for either <code>InvalidLength</code> or <code>InvalidByte</code> being
appropriate.</li>
<li>Decoding is somewhat faster (5-10%)</li>
</ul>
<h1>0.21.7</h1>
<ul>
<li>Support getting an alphabet's contents as a str via
<code>Alphabet::as_str()</code></li>
</ul>
<h1>0.21.6</h1>
<ul>
<li>Improved introductory documentation and example</li>
</ul>
<h1>0.21.5</h1>
<ul>
<li>Add <code>Debug</code> and <code>Clone</code> impls for the general
purpose Engine</li>
</ul>
<h1>0.21.4</h1>
<ul>
<li>Make <code>encoded_len</code> <code>const</code>, allowing the
creation of arrays sized to encode compile-time-known data lengths</li>
</ul>
<h1>0.21.3</h1>
<ul>
<li>Implement <code>source</code> instead of <code>cause</code> on Error
types</li>
<li>Roll back MSRV to 1.48.0 so Debian can continue to live in a time
warp</li>
<li>Slightly faster chunked encoding for short inputs</li>
<li>Decrease binary size</li>
</ul>
<h1>0.21.2</h1>
<ul>
<li>Rollback MSRV to 1.57.0 -- only dev dependencies need 1.60, not the
main code</li>
</ul>
<h1>0.21.1</h1>
<ul>
<li>Remove the possibility of panicking during decoded length
calculations</li>
<li><code>DecoderReader</code> no longer sometimes erroneously ignores
padding <a
href="https://redirect.github.com/marshallpierce/rust-base64/issues/226">#226</a></li>
</ul>
<h2>Breaking changes</h2>
<ul>
<li><code>Engine.internal_decode</code> return type changed</li>
<li>Update MSRV to 1.60.0</li>
</ul>
<h1>0.21.0</h1>
<h2>Migration</h2>
<h3>Functions</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="5d70ba7576"><code>5d70ba7</code></a>
Merge pull request <a
href="https://redirect.github.com/marshallpierce/rust-base64/issues/269">#269</a>
from marshallpierce/mp/decode-precisely</li>
<li><a
href="efb6c006c7"><code>efb6c00</code></a>
Release notes</li>
<li><a
href="2b91084a31"><code>2b91084</code></a>
Add some tests to boost coverage</li>
<li><a
href="9e9c7abe65"><code>9e9c7ab</code></a>
Engine::internal_decode now returns DecodeSliceError</li>
<li><a
href="a8a60f43c5"><code>a8a60f4</code></a>
Decode main loop improvements</li>
<li><a
href="a25be0667c"><code>a25be06</code></a>
Simplify leftover output writes</li>
<li><a
href="9979cc33bb"><code>9979cc3</code></a>
Keep morsels as separate bytes</li>
<li><a
href="37670c5ec2"><code>37670c5</code></a>
Bump dev toolchain version (<a
href="https://redirect.github.com/marshallpierce/rust-base64/issues/268">#268</a>)</li>
<li><a
href="9652c78773"><code>9652c78</code></a>
v0.21.7</li>
<li><a
href="08deccf703"><code>08deccf</code></a>
provide as_str() method to return the alphabet characters (<a
href="https://redirect.github.com/marshallpierce/rust-base64/issues/264">#264</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/marshallpierce/rust-base64/compare/v0.21.0...v0.22.0">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-04 09:04:37 -08:00
zach
4e0cd3b1cf doc: remove old default for timeout_ms (#688) 2024-02-26 16:29:35 -08:00
zach
f4013c5ac0 fix: circular dependencies 2024-02-22 14:04:01 -08:00
zach
ddc339334e fix: remove readme field 2024-02-22 13:07:06 -08:00
zach
ff5b714f95 v1.1.0 2024-02-22 11:01:19 -08:00
zach
ed1439ec2d fix: linker issue that depends on the ordering of the linked functions (#685)
It looks like when a module is added to the linker, all of its imports
must already be present. This PR updates the linking process to take
that into consideration and adds a test with a reproduction of the issue
@chrisdickinson shared with me.
2024-02-22 10:56:21 -08:00
zach
62f0a231b0 ci: add release workflow for convert-macros crate (#683) 2024-02-14 09:18:28 -08:00
zach
fc22412ff0 feat: allow max HTTP response size to be configured in the manifest (#674)
- Adds `memory.max_http_response_bytes` field to specify the maximum
size of an HTTP request response
2024-02-07 14:31:23 -08:00
Roland Fredenhagen
1a083f612a feat(convert): add derive macros for To and FromBytes (#667)
closes #661.

- [x] docs
- [x] tests
- [x] depend on `extism-convert/extism-pdk-path` feature in
https://github.com/extism/rust-pdk
https://github.com/extism/rust-pdk/pull/47
2024-02-05 15:59:55 -08:00
zach
efa69d3668 chore: support for wasmtime 17.0.0 (#665) 2024-01-25 16:11:17 -08:00
zach
fa1beb9155 v1.0.3 2024-01-22 10:21:30 -08:00
Onigbinde Oluwamuyiwa Elijah
fbae853505 fix: make function Plugin::function_extists check the type of the functions. (#664)
This PR addresses #654
It checks the parameters and results of the function as described in the
mentioned issue.
2024-01-22 08:40:09 -08:00
zach
8c8e4a6ffb fix(kernel): fix potential overflow in bounds check when lots of memory has been allocated (#663)
- Fixes potential overflow in bounds checking function
- Found by running the `check_large_allocations` quickcheck test in a
loop
2024-01-19 16:14:36 -08:00
zach
1f1e2699cb v1.0.2 2024-01-19 13:10:13 -08:00
zach
d5dc9b41ab feat: Use quickcheck to test allocations, fix one bug that was uncovered. (#662)
- Adds quickcheck tests for alloc/free/load/store from the kernel, I
wasn't able to include these in the new wasm-bindgen tests because
`getrandom` isn't available on wasm32-unknown-unknown
- Fixes a bug in the calculation of how much memory is needed to
allocate the next block in the kernel. This bug is triggered when
allocating at the end of a page, the size of the MemoryBlock value
wasn't being taken in consideration when determining whether or not to
call memory.grow
2024-01-19 13:04:21 -08:00
27 changed files with 828 additions and 72 deletions

View File

@@ -36,6 +36,18 @@ jobs:
override: true
target: ${{ matrix.target }}
- name: Release Rust convert-macros Crate
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_API_TOKEN }}
run: |
version=$(cargo metadata --format-version=1 | jq -r '.packages[] | select(.name == "extism") | .version')
if ! &>/dev/null curl -sLIf https://crates.io/api/v1/crates/extism-convert-macros/${version}/download; then
cargo publish --manifest-path convert-macros/Cargo.toml --allow-dirty
else
echo "already published ${version}"
fi
- name: Release Rust Convert Crate
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_API_TOKEN }}

View File

@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = ["extism-maturin", "manifest", "runtime", "libextism", "convert"]
members = ["extism-maturin", "manifest", "runtime", "libextism", "convert", "convert-macros"]
exclude = ["kernel"]
[workspace.package]
@@ -14,4 +14,5 @@ version = "0.0.0+replaced-by-ci"
[workspace.dependencies]
extism = { path = "./runtime", version = "0.0.0+replaced-by-ci" }
extism-convert = { path = "./convert", version = "0.0.0+replaced-by-ci" }
extism-convert-macros = { path = "./convert-macros", version = "0.0.0+replaced-by-ci" }
extism-manifest = { path = "./manifest", version = "0.0.0+replaced-by-ci" }

View File

@@ -8,7 +8,7 @@
[![Discord](https://img.shields.io/discord/1011124058408112148?color=%23404eed&label=Community%20Chat&logo=Discord&logoColor=%23404eed)](https://extism.org/discord)
![GitHub Org's stars](https://img.shields.io/github/stars/extism)
![GitHub all releases](https://img.shields.io/github/downloads/extism/extism/total)
![Downloads](https://img.shields.io/crates/d/extism)
![GitHub License](https://img.shields.io/github/license/extism/extism)
![GitHub release (with filter)](https://img.shields.io/github/v/release/extism/extism)

26
convert-macros/Cargo.toml Normal file
View File

@@ -0,0 +1,26 @@
[package]
name = "extism-convert-macros"
edition.workspace = true
authors.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
version.workspace = true
description = "Macros to remove boilerplate with Extism"
[lib]
proc-macro = true
[features]
extism-path = []
extism-pdk-path = []
[dependencies]
manyhow.version = "0.11.0"
proc-macro-crate = "3.1.0"
proc-macro2 = "1.0.78"
quote = "1.0.35"
syn = { version = "2.0.48", features = ["derive"] }
[dev-dependencies]
trybuild = "1.0.89"

108
convert-macros/src/lib.rs Normal file
View File

@@ -0,0 +1,108 @@
use std::iter;
use manyhow::{ensure, error_message, manyhow, Result};
use proc_macro_crate::{crate_name, FoundCrate};
use quote::{format_ident, quote, ToTokens};
use syn::{parse_quote, Attribute, DeriveInput, Path};
/// Tries to resolve the path to `extism_convert` dynamically, falling back to feature flags when unsuccessful.
fn convert_path() -> Path {
match (
crate_name("extism"),
crate_name("extism-convert"),
crate_name("extism-pdk"),
) {
(Ok(FoundCrate::Name(name)), ..) => {
let ident = format_ident!("{name}");
parse_quote!(::#ident::convert)
}
(_, Ok(FoundCrate::Name(name)), ..) | (.., Ok(FoundCrate::Name(name))) => {
let ident = format_ident!("{name}");
parse_quote!(::#ident)
}
(Ok(FoundCrate::Itself), ..) => parse_quote!(::extism::convert),
(_, Ok(FoundCrate::Itself), ..) => parse_quote!(::extism_convert),
(.., Ok(FoundCrate::Itself)) => parse_quote!(::extism_pdk),
_ if cfg!(feature = "extism-path") => parse_quote!(::extism::convert),
_ if cfg!(feature = "extism-pdk-path") => parse_quote!(::extism_pdk),
_ => parse_quote!(::extism_convert),
}
}
fn extract_encoding(attrs: &[Attribute]) -> Result<Path> {
let encodings: Vec<_> = attrs
.iter()
.filter(|attr| attr.path().is_ident("encoding"))
.collect();
ensure!(!encodings.is_empty(), "encoding needs to be specified"; try = "`#[encoding(ToJson)]`");
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)]`"),
)?)
}
#[manyhow]
#[proc_macro_derive(ToBytes, attributes(encoding))]
pub fn to_bytes(
DeriveInput {
attrs,
ident,
generics,
..
}: DeriveInput,
) -> Result {
let encoding = extract_encoding(&attrs)?;
let convert = convert_path();
let (_, type_generics, _) = generics.split_for_impl();
let mut generics = generics.clone();
generics.make_where_clause().predicates.push(
parse_quote!(for<'__to_bytes_b> #encoding<&'__to_bytes_b Self>: #convert::ToBytes<'__to_bytes_b>)
);
generics.params = iter::once(parse_quote!('__to_bytes_a))
.chain(generics.params)
.collect();
let (impl_generics, _, where_clause) = generics.split_for_impl();
Ok(quote! {
impl #impl_generics #convert::ToBytes<'__to_bytes_a> for #ident #type_generics #where_clause
{
type Bytes = ::std::vec::Vec<u8>;
fn to_bytes(&self) -> Result<Self::Bytes, #convert::Error> {
#convert::ToBytes::to_bytes(&#encoding(self)).map(|__bytes| __bytes.as_ref().to_vec())
}
}
})
}
#[manyhow]
#[proc_macro_derive(FromBytes, attributes(encoding))]
pub fn from_bytes(
DeriveInput {
attrs,
ident,
mut generics,
..
}: DeriveInput,
) -> Result {
let encoding = extract_encoding(&attrs)?;
let convert = convert_path();
generics
.make_where_clause()
.predicates
.push(parse_quote!(#encoding<Self>: #convert::FromBytesOwned));
let (impl_generics, type_generics, where_clause) = generics.split_for_impl();
Ok(quote! {
impl #impl_generics #convert::FromBytesOwned for #ident #type_generics #where_clause
{
fn from_bytes_owned(__data: &[u8]) -> Result<Self, #convert::Error> {
<#encoding<Self> as #convert::FromBytesOwned>::from_bytes_owned(__data).map(|__encoding| __encoding.0)
}
}
})
}

View File

@@ -0,0 +1,5 @@
#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/*.rs");
}

View File

@@ -0,0 +1,23 @@
use extism_convert_macros::ToBytes;
#[derive(ToBytes)]
struct MissingEncoding;
#[derive(ToBytes)]
#[encoding]
struct EmptyAttr;
#[derive(ToBytes)]
#[encoding = "string"]
struct EqNoParen;
#[derive(ToBytes)]
#[encoding(something, else)]
struct NotAPath;
#[derive(ToBytes)]
#[encoding(Multiple)]
#[encoding(Encodings)]
struct MultipleEncodings;
fn main() {}

View File

@@ -0,0 +1,44 @@
error: encoding needs to be specified
= try: `#[encoding(ToJson)]`
--> tests/ui/invalid-encoding.rs:3:10
|
3 | #[derive(ToBytes)]
| ^^^^^^^
|
= note: this error originates in the derive macro `ToBytes` (in Nightly builds, run with -Z macro-backtrace for more info)
error: expected attribute arguments in parentheses: #[encoding(...)]
= note: expects a path
= try: `#[encoding(ToJson)]`
--> tests/ui/invalid-encoding.rs:7:3
|
7 | #[encoding]
| ^^^^^^^^
error: expected parentheses: #[encoding(...)]
= note: expects a path
= try: `#[encoding(ToJson)]`
--> tests/ui/invalid-encoding.rs:11:12
|
11 | #[encoding = "string"]
| ^
error: unexpected token
= note: expects a path
= try: `#[encoding(ToJson)]`
--> tests/ui/invalid-encoding.rs:15:21
|
15 | #[encoding(something, else)]
| ^
error: only one encoding can be specified
= try: remove `#[encoding(Encodings)]`
--> tests/ui/invalid-encoding.rs:20:1
|
20 | #[encoding(Encodings)]
| ^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -11,13 +11,14 @@ description = "Traits to make Rust types usable with Extism"
[dependencies]
anyhow = "1.0.75"
base64 = "~0.21"
base64 = "~0.22"
bytemuck = {version = "1.14.0", optional = true }
prost = { version = "0.12.0", optional = true }
protobuf = { version = "3.2.0", optional = true }
rmp-serde = { version = "1.1.2", optional = true }
serde = "1.0.186"
serde_json = "1.0.105"
extism-convert-macros.workspace = true
[dev-dependencies]
serde = { version = "1.0.186", features = ["derive"] }
@@ -26,3 +27,5 @@ serde = { version = "1.0.186", features = ["derive"] }
default = ["msgpack", "prost", "raw"]
msgpack = ["rmp-serde"]
raw = ["bytemuck"]
extism-path = ["extism-convert-macros/extism-path"]
extism-pdk-path = ["extism-convert-macros/extism-pdk-path"]

View File

@@ -11,8 +11,8 @@ use base64::Engine;
/// extism_convert::encoding!(MyJson, serde_json::to_vec, serde_json::from_slice);
/// ```
///
/// This will create a struct `struct MyJson<T>(pub T)` and implement `ToBytes` using `serde_json::to_vec`
/// and `FromBytesOwned` using `serde_json::from_vec`
/// This will create a struct `struct MyJson<T>(pub T)` and implement [`ToBytes`] using [`serde_json::to_vec`]
/// and [`FromBytesOwned`] using [`serde_json::from_slice`]
#[macro_export]
macro_rules! encoding {
($pub:vis $name:ident, $to_vec:expr, $from_slice:expr) => {

View File

@@ -1,14 +1,68 @@
use crate::*;
pub use extism_convert_macros::FromBytes;
/// `FromBytes` is used to define how a type should be decoded when working with
/// Extism memory. It is used for plugin output and host function input.
///
/// `FromBytes` can be derived by delegating encoding to generic type implementing
/// `FromBytes`, e.g., [`Json`], [`Msgpack`].
///
/// ```
/// use extism_convert::{Json, FromBytes};
/// use serde::Deserialize;
///
/// #[derive(FromBytes, Deserialize, PartialEq, Debug)]
/// #[encoding(Json)]
/// struct Struct {
/// hello: String,
/// }
///
/// assert_eq!(Struct::from_bytes(br#"{"hello":"hi"}"#)?, Struct { hello: "hi".into() });
/// # Ok::<(), extism_convert::Error>(())
/// ```
///
/// Custom encodings can also be used, through new-types with a single generic
/// argument, i.e., `Type<T>(T)`, that implement `FromBytesOwned` for the struct.
///
/// ```
/// use std::str::{self, FromStr};
/// use std::convert::Infallible;
/// use extism_convert::{Error, FromBytes, FromBytesOwned};
///
/// // Custom serialization using `FromStr`
/// struct StringEnc<T>(T);
/// impl<T: FromStr> FromBytesOwned for StringEnc<T> where Error: From<<T as FromStr>::Err> {
/// fn from_bytes_owned(data: &[u8]) -> Result<Self, Error> {
/// Ok(Self(str::from_utf8(data)?.parse()?))
/// }
/// }
///
/// #[derive(FromBytes, PartialEq, Debug)]
/// #[encoding(StringEnc)]
/// struct Struct {
/// hello: String,
/// }
///
/// impl FromStr for Struct {
/// type Err = Infallible;
/// fn from_str(s: &str) -> Result<Self, Infallible> {
/// Ok(Self { hello: s.to_owned() })
/// }
/// }
///
/// assert_eq!(Struct::from_bytes(b"hi")?, Struct { hello: "hi".into() });
/// # Ok::<(), extism_convert::Error>(())
/// ```
pub trait FromBytes<'a>: Sized {
/// Decode a value from a slice of bytes
fn from_bytes(data: &'a [u8]) -> Result<Self, Error>;
}
/// `FromBytesOwned` is similar to `FromBytes` but it doesn't borrow from the input slice.
/// `FromBytes` is automatically implemented for all types that implement `FromBytesOwned`
/// `FromBytesOwned` is similar to [`FromBytes`] but it doesn't borrow from the input slice.
/// [`FromBytes`] is automatically implemented for all types that implement `FromBytesOwned`.
///
/// `FromBytesOwned` can be derived through [`#[derive(FromBytes)]`](FromBytes).
pub trait FromBytesOwned: Sized {
/// Decode a value from a slice of bytes, the resulting value should not borrow the input
/// data.

View File

@@ -5,6 +5,9 @@
//! similar to [axum extractors](https://docs.rs/axum/latest/axum/extract/index.html#intro) - they are
//! implemented as a tuple struct with a single field that is meant to be extracted using pattern matching.
// Makes proc-macros able to resolve `::extism_convert` correctly
extern crate self as extism_convert;
pub use anyhow::Error;
mod encoding;

View File

@@ -1,7 +1,58 @@
use crate::*;
pub use extism_convert_macros::ToBytes;
/// `ToBytes` is used to define how a type should be encoded when working with
/// Extism memory. It is used for plugin input and host function output.
///
/// `ToBytes` can be derived by delegating encoding to generic type implementing
/// `ToBytes`, e.g., [`Json`], [`Msgpack`].
///
/// ```
/// use extism_convert::{Json, ToBytes};
/// use serde::Serialize;
///
/// #[derive(ToBytes, Serialize)]
/// #[encoding(Json)]
/// struct Struct {
/// hello: String,
/// }
///
/// assert_eq!(Struct { hello: "hi".into() }.to_bytes()?, br#"{"hello":"hi"}"#);
/// # Ok::<(), extism_convert::Error>(())
/// ```
///
/// But custom types can also be used, as long as they are new-types with a single
/// generic argument, i.e., `Type<T>(T)`, that implement `ToBytes` for the struct.
///
/// ```
/// use extism_convert::{Error, ToBytes};
///
/// // Custom serialization using `ToString`
/// struct StringEnc<T>(T);
/// impl<T: ToString> ToBytes<'_> for StringEnc<&T> {
/// type Bytes = String;
///
/// fn to_bytes(&self) -> Result<String, Error> {
/// Ok(self.0.to_string())
/// }
/// }
///
/// #[derive(ToBytes)]
/// #[encoding(StringEnc)]
/// struct Struct {
/// hello: String,
/// }
///
/// impl ToString for Struct {
/// fn to_string(&self) -> String {
/// self.hello.clone()
/// }
/// }
///
/// assert_eq!(Struct { hello: "hi".into() }.to_bytes()?, b"hi");
/// # Ok::<(), Error>(())
/// ```
pub trait ToBytes<'a> {
/// A configurable byte slice representation, allows any type that implements `AsRef<[u8]>`
type Bytes: AsRef<[u8]>;
@@ -111,3 +162,15 @@ impl<'a, T: ToBytes<'a>> ToBytes<'a> for Option<T> {
}
}
}
#[test]
fn test() {
use extism_convert::{Json, ToBytes};
use serde::Serialize;
#[derive(ToBytes, Serialize)]
#[encoding(Json)]
struct Struct {
hello: String,
}
}

View File

@@ -198,8 +198,8 @@ impl MemoryRoot {
fn pointer_in_bounds_fast(p: Pointer) -> bool {
// Similar to `pointer_in_bounds` but less accurate on the upper bound. This uses the total memory size,
// instead of checking `MemoryRoot::length`
let end = core::arch::wasm32::memory_size(0) << 16;
p >= core::mem::size_of::<Self>() as Pointer && p <= end as Pointer
let end = (core::arch::wasm32::memory_size(0) as u64) << 16;
p >= core::mem::size_of::<Self>() as Pointer && p <= end as u64
}
// Find a block that is free to use, this can be a new block or an existing freed block. The `self_position` argument
@@ -270,12 +270,13 @@ impl MemoryRoot {
// Get the number of bytes available
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;
// When the allocation is larger than the number of bytes available
// we will need to try to grow the memory
if length >= mem_left {
if length_with_block >= mem_left {
// Calculate the number of pages needed to cover the remaining bytes
let npages = num_pages(length - mem_left);
let npages = num_pages(length_with_block - mem_left);
let x = core::arch::wasm32::memory_grow(0, npages);
if x == usize::MAX {
return None;
@@ -618,4 +619,23 @@ mod test {
assert_eq!(length(3788), 4);
}
}
#[wasm_bindgen_test]
fn test_oom() {
let size = 1024 * 1024 * 5;
let mut last = 0;
for _ in 0..1024 {
unsafe {
let ptr = alloc(size);
last = ptr;
if ptr == 0 {
break;
}
assert_eq!(length(ptr), size);
}
}
assert_eq!(last, 0);
}
}

View File

@@ -10,7 +10,7 @@ version.workspace = true
[dependencies]
serde = { version = "1", features = ["derive"] }
base64 = "~0.21"
base64 = "~0.22"
schemars = { version = "0.8", optional = true }
serde_json = "1"

View File

@@ -37,7 +37,9 @@
"memory": {
"description": "Memory options",
"default": {
"max_pages": null
"max_http_response_bytes": null,
"max_pages": null,
"max_var_bytes": null
},
"allOf": [
{
@@ -46,7 +48,7 @@
]
},
"timeout_ms": {
"description": "The plugin timeout, by default this is set to 30s",
"description": "The plugin timeout in milliseconds",
"default": null,
"type": [
"integer",
@@ -70,6 +72,16 @@
"description": "Configure memory settings",
"type": "object",
"properties": {
"max_http_response_bytes": {
"description": "The maximum number of bytes allowed in an HTTP response",
"default": null,
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0.0
},
"max_pages": {
"description": "The max number of WebAssembly pages that should be allocated",
"type": [
@@ -78,6 +90,16 @@
],
"format": "uint32",
"minimum": 0.0
},
"max_var_bytes": {
"description": "The maximum number of bytes allowed to be used by plugin vars. Setting this to 0 will disable Extism vars. The default value is 1mb.",
"default": 1048576,
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false

View File

@@ -12,6 +12,44 @@ pub struct MemoryOptions {
/// The max number of WebAssembly pages that should be allocated
#[serde(alias = "max")]
pub max_pages: Option<u32>,
/// The maximum number of bytes allowed in an HTTP response
#[serde(default)]
pub max_http_response_bytes: Option<u64>,
/// The maximum number of bytes allowed to be used by plugin vars. Setting this to 0
/// will disable Extism vars. The default value is 1mb.
#[serde(default = "default_var_bytes")]
pub max_var_bytes: Option<u64>,
}
impl MemoryOptions {
/// Create an empty `MemoryOptions` value
pub fn new() -> Self {
Default::default()
}
/// Set max pages
pub fn with_max_pages(mut self, pages: u32) -> Self {
self.max_pages = Some(pages);
self
}
/// Set max HTTP response size
pub fn with_max_http_response_bytes(mut self, bytes: u64) -> Self {
self.max_http_response_bytes = Some(bytes);
self
}
/// Set max size of Extism vars
pub fn with_max_var_bytes(mut self, bytes: u64) -> Self {
self.max_var_bytes = Some(bytes);
self
}
}
fn default_var_bytes() -> Option<u64> {
Some(1024 * 1024)
}
/// Generic HTTP request structure
@@ -225,6 +263,7 @@ pub struct Manifest {
/// WebAssembly modules, the `main` module should be named `main` or listed last
#[serde(default)]
pub wasm: Vec<Wasm>,
/// Memory options
#[serde(default)]
pub memory: MemoryOptions,
@@ -244,7 +283,7 @@ pub struct Manifest {
#[serde(default)]
pub allowed_paths: Option<BTreeMap<PathBuf, PathBuf>>,
/// The plugin timeout, by default this is set to 30s
/// The plugin timeout in milliseconds
#[serde(default)]
pub timeout_ms: Option<u64>,
}

View File

@@ -9,8 +9,8 @@ repository.workspace = true
version.workspace = true
[dependencies]
wasmtime = ">= 14.0.0, < 17.0.0"
wasmtime-wasi = ">= 14.0.0, < 17.0.0"
wasmtime = ">= 14.0.0, < 18.0.0"
wasmtime-wasi = ">= 14.0.0, < 18.0.0"
anyhow = "1"
serde = {version = "1", features = ["derive"]}
serde_json = "1"
@@ -22,7 +22,7 @@ url = "2"
glob = "0.3"
ureq = {version = "2.5", optional=true}
extism-manifest = { workspace = true }
extism-convert = { workspace = true }
extism-convert = { workspace = true, features = ["extism-path"] }
uuid = { version = "1", features = ["v4"] }
libc = "0.2"
@@ -37,6 +37,8 @@ cbindgen = { version = "0.26", default-features = false }
[dev-dependencies]
criterion = "0.5.1"
quickcheck = "1"
rand = "0.8.5"
[[bench]]
name = "bench"

View File

@@ -1,7 +1,7 @@
use extism::*;
// pretend this is redis or something :)
type KVStore = std::collections::BTreeMap<String, Vec<u8>>;
type KVStore = std::sync::Arc<std::sync::Mutex<std::collections::BTreeMap<String, Vec<u8>>>>;
// When a first argument separated with a semicolon is provided to `host_fn` it is used as the
// variable name and type for the `UserData` parameter

Binary file not shown.

View File

@@ -81,18 +81,18 @@ pub(crate) enum UserDataHandle {
/// using `UserData::get`. The `C` data is stored as a pointer and cleanup function and isn't usable from Rust. The cleanup function
/// will be called when the inner `CPtr` is dropped.
#[derive(Debug)]
pub enum UserData<T: Sized> {
pub enum UserData<T: Sync + Clone + Sized> {
C(Arc<CPtr>),
Rust(Arc<std::sync::Mutex<T>>),
Rust(T),
}
impl<T: Default> Default for UserData<T> {
impl<T: Default + Sync + Clone> Default for UserData<T> {
fn default() -> Self {
UserData::new(T::default())
}
}
impl<T> Clone for UserData<T> {
impl<T: Sync + Clone> Clone for UserData<T> {
fn clone(&self) -> Self {
match self {
UserData::C(ptr) => UserData::C(ptr.clone()),
@@ -101,7 +101,7 @@ impl<T> Clone for UserData<T> {
}
}
impl<T> UserData<T> {
impl<T: Sync + Clone> UserData<T> {
/// Create a new `UserData` from an existing pointer and free function, this is used
/// by the C API to wrap C pointers into user data
pub(crate) fn new_pointer(
@@ -126,12 +126,11 @@ impl<T> UserData<T> {
///
/// This will wrap the provided value in a reference-counted mutex
pub fn new(x: T) -> Self {
let data = Arc::new(std::sync::Mutex::new(x));
UserData::Rust(data)
UserData::Rust(x)
}
/// Get a copy of the inner value
pub fn get(&self) -> Result<Arc<std::sync::Mutex<T>>, Error> {
pub fn get(&self) -> Result<T, Error> {
match self {
UserData::C { .. } => anyhow::bail!("C UserData should not be used from Rust"),
UserData::Rust(data) => Ok(data.clone()),
@@ -150,8 +149,8 @@ impl Drop for CPtr {
}
}
unsafe impl<T> Send for UserData<T> {}
unsafe impl<T> Sync for UserData<T> {}
unsafe impl<T: Sync + Clone> Send for UserData<T> {}
unsafe impl<T: Sync + Clone> Sync for UserData<T> {}
unsafe impl Send for CPtr {}
unsafe impl Sync for CPtr {}
@@ -180,7 +179,7 @@ pub struct Function {
impl Function {
/// Create a new host function
pub fn new<T: 'static, F>(
pub fn new<T: 'static + Sync + Clone, F>(
name: impl Into<String>,
args: impl IntoIterator<Item = ValType>,
returns: impl IntoIterator<Item = ValType>,
@@ -211,7 +210,9 @@ impl Function {
namespace: None,
_user_data: match &user_data {
UserData::C(ptr) => UserDataHandle::C(ptr.clone()),
UserData::Rust(x) => UserDataHandle::Rust(x.clone()),
UserData::Rust(x) => {
UserDataHandle::Rust(std::sync::Arc::new(std::sync::Mutex::new(x.clone())))
}
},
}
}

View File

@@ -1,8 +1,12 @@
// Makes proc-macros able to resolve `::extism` correctly
extern crate self as extism;
pub(crate) use extism_convert::*;
pub(crate) use std::collections::BTreeMap;
use std::str::FromStr;
pub(crate) use wasmtime::*;
#[doc(inline)]
pub use extism_convert as convert;
pub use anyhow::Error;

View File

@@ -97,19 +97,13 @@ pub(crate) fn var_set(
) -> Result<(), Error> {
let data: &mut CurrentPlugin = caller.data_mut();
let mut size = 0;
for v in data.vars.values() {
size += v.len();
if data.manifest.memory.max_var_bytes.is_some_and(|x| x == 0) {
anyhow::bail!("Vars are disabled by this host")
}
let voffset = args!(input, 1, i64) as u64;
// If the store is larger than 100MB then stop adding things
if size > 1024 * 1024 * 100 && voffset != 0 {
return Err(Error::msg("Variable store is full"));
}
let key_offs = args!(input, 0, i64) as u64;
let key = {
let handle = match data.memory_handle(key_offs) {
Some(h) => h,
@@ -132,6 +126,22 @@ pub(crate) fn var_set(
None => anyhow::bail!("invalid handle offset for var value: {voffset}"),
};
let mut size = std::mem::size_of::<String>()
+ std::mem::size_of::<Vec<u8>>()
+ key.len()
+ handle.length as usize;
for (k, v) in data.vars.iter() {
size += k.len();
size += v.len();
size += std::mem::size_of::<String>() + std::mem::size_of::<Vec<u8>>();
}
// If the store is larger than the configured size, or 1mb by default, then stop adding things
if size > data.manifest.memory.max_var_bytes.unwrap_or(1024 * 1024) as usize && voffset != 0 {
return Err(Error::msg("Variable store is full"));
}
let value = data.memory_bytes(handle)?.to_vec();
// Insert the value from memory into the `vars` map
@@ -226,20 +236,29 @@ pub(crate) fn http_request(
Some(res.into_reader())
}
Err(e) => {
let msg = e.to_string();
if let Some(res) = e.into_response() {
data.http_status = res.status();
Some(res.into_reader())
} else {
None
return Err(Error::msg(msg));
}
}
};
if let Some(reader) = reader {
let mut buf = Vec::new();
reader
.take(1024 * 1024 * 50) // TODO: make this limit configurable
.read_to_end(&mut buf)?;
let max = if let Some(max) = &data.manifest.memory.max_http_response_bytes {
reader.take(*max + 1).read_to_end(&mut buf)?;
*max
} else {
reader.take(1024 * 1024 * 50 + 1).read_to_end(&mut buf)?;
1024 * 1024 * 50
};
if buf.len() > max as usize {
anyhow::bail!("HTTP response exceeds the configured maximum number of bytes: {max}")
}
let mem = data.memory_new(&buf)?;
output[0] = Val::I64(mem.offset() as i64);

View File

@@ -1,4 +1,7 @@
use std::{collections::BTreeMap, path::PathBuf};
use std::{
collections::{BTreeMap, BTreeSet},
path::PathBuf,
};
use crate::*;
@@ -177,6 +180,39 @@ impl<'a> From<&'a Vec<u8>> for WasmInput<'a> {
}
}
fn add_module<T: 'static>(
store: &mut Store<T>,
linker: &mut Linker<T>,
linked: &mut BTreeSet<String>,
modules: &BTreeMap<String, Module>,
name: String,
module: &Module,
) -> Result<(), Error> {
if linked.contains(&name) {
return Ok(());
}
for import in module.imports() {
if !linked.contains(import.module()) {
if let Some(m) = modules.get(import.module()) {
add_module(
store,
linker,
linked,
modules,
import.module().to_string(),
m,
)?;
}
}
}
linker.module(store, name.as_str(), module)?;
linked.insert(name);
Ok(())
}
impl Plugin {
/// Create a new plugin from a Manifest or WebAssembly module, and host functions. The `with_wasi`
/// parameter determines whether or not the module should be executed with WASI enabled.
@@ -240,22 +276,6 @@ impl Plugin {
store.set_epoch_deadline(1);
let mut linker = Linker::new(&engine);
linker.allow_shadowing(true);
// If wasi is enabled then add it to the linker
if with_wasi {
wasmtime_wasi::add_to_linker(&mut linker, |x: &mut CurrentPlugin| {
&mut x.wasi.as_mut().unwrap().ctx
})?;
}
let main = &modules[MAIN_KEY];
for (name, module) in modules.iter() {
if name != MAIN_KEY {
linker.module(&mut store, name, module)?;
}
}
let mut imports: Vec<_> = imports.into_iter().collect();
// Define PDK functions
macro_rules! add_funcs {
@@ -281,14 +301,37 @@ impl Plugin {
log_error(I64);
);
let mut linked = BTreeSet::new();
linker.module(&mut store, EXTISM_ENV_MODULE, &modules[EXTISM_ENV_MODULE])?;
linked.insert(EXTISM_ENV_MODULE.to_string());
// If wasi is enabled then add it to the linker
if with_wasi {
wasmtime_wasi::add_to_linker(&mut linker, |x: &mut CurrentPlugin| {
&mut x.wasi.as_mut().unwrap().ctx
})?;
}
for f in &mut imports {
let name = f.name().to_string();
let name = f.name();
let ns = f.namespace().unwrap_or(EXTISM_USER_MODULE);
unsafe {
linker.func_new(ns, &name, f.ty().clone(), &*(f.f.as_ref() as *const _))?;
linker.func_new(ns, name, f.ty().clone(), &*(f.f.as_ref() as *const _))?;
}
}
for (name, module) in modules.iter() {
add_module(
&mut store,
&mut linker,
&mut linked,
&modules,
name.clone(),
module,
)?;
}
let main = &modules[MAIN_KEY];
let instance_pre = linker.instantiate_pre(main)?;
let timer_tx = Timer::tx();
let mut plugin = Plugin {
@@ -403,7 +446,18 @@ impl Plugin {
pub fn function_exists(&mut self, function: impl AsRef<str>) -> bool {
self.modules[MAIN_KEY]
.get_export(function.as_ref())
.map(|x| x.func().is_some())
.map(|x| {
if let Some(f) = x.func() {
let (params, mut results) = (f.params(), f.results());
match (params.len(), results.len()) {
(0, 1) => results.next() == Some(wasmtime::ValType::I32),
(0, 0) => true,
_ => false,
}
} else {
false
}
})
.unwrap_or(false)
}

View File

@@ -60,7 +60,7 @@ impl<'a> PluginBuilder<'a> {
}
/// Add a single host function
pub fn with_function<T: 'static, F>(
pub fn with_function<T: Sync + Clone + 'static, F>(
mut self,
name: impl Into<String>,
args: impl IntoIterator<Item = ValType>,
@@ -80,7 +80,7 @@ impl<'a> PluginBuilder<'a> {
}
/// Add a single host function in a specific namespace
pub fn with_function_in_namespace<T: 'static, F>(
pub fn with_function_in_namespace<T: Sync + Clone + 'static, F>(
mut self,
namespace: impl Into<String>,
name: impl Into<String>,

View File

@@ -1,4 +1,5 @@
use crate::*;
use quickcheck::*;
const KERNEL: &[u8] = include_bytes!("../extism-runtime.wasm");
@@ -319,3 +320,158 @@ fn test_load_input() {
// Out of bounds should return 0
assert_eq!(extism_input_load_u64(&mut store, instance, 123457), 0);
}
#[test]
fn test_failed_quickcheck1() {
let (mut store, mut instance) = init_kernel_test();
let allocs = [
20622, 23162, 58594, 32421, 25928, 44611, 26318, 24455, 5798, 60202, 42126, 64928, 57832,
50888, 63256, 37562, 46334, 47985, 60836, 28132, 65535, 37800, 33150, 48768, 38457, 57249,
5734, 58587, 26294, 26653, 24519, 1,
];
extism_reset(&mut store, &mut instance);
for a in allocs {
println!("Alloc: {a}");
let n = extism_alloc(&mut store, &mut instance, a);
if n == 0 {
continue;
}
assert_eq!(a, extism_length(&mut store, &mut instance, n));
}
}
#[test]
fn test_failed_quickcheck2() {
let (mut store, mut instance) = init_kernel_test();
let allocs = [352054710, 1248853976, 2678441931, 14567928];
extism_reset(&mut store, &mut instance);
for a in allocs {
println!("Alloc: {a}");
let n = extism_alloc(&mut store, &mut instance, a);
if n == 0 {
continue;
}
assert_eq!(a, extism_length(&mut store, &mut instance, n));
}
}
quickcheck! {
fn check_alloc(amounts: Vec<u16>) -> bool {
let (mut store, mut instance) = init_kernel_test();
let instance = &mut instance;
for a in amounts {
let ptr = extism_alloc(&mut store, instance, a as u64);
if ptr == 0 || ptr == u64::MAX {
continue
}
if extism_length(&mut store, instance, ptr) != a as u64 {
return false
}
}
true
}
}
quickcheck! {
fn check_large_alloc(amounts: Vec<u32>) -> bool {
let (mut store, mut instance) = init_kernel_test();
let instance = &mut instance;
for a in amounts {
let ptr = extism_alloc(&mut store, instance, a as u64);
if ptr == 0 {
continue
}
let len = extism_length_unsafe(&mut store, instance, ptr);
if len != a as u64 {
return false
}
}
true
}
}
quickcheck! {
fn check_alloc_with_frees(amounts: Vec<u16>) -> bool {
let (mut store, mut instance) = init_kernel_test();
let instance = &mut instance;
let mut prev = 0;
for a in amounts {
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
}
if a % 2 == 0 {
extism_free(&mut store, instance, ptr);
} else if a % 3 == 0 {
extism_free(&mut store, instance, prev);
}
prev = ptr;
}
true
}
}
quickcheck! {
fn check_large_alloc_with_frees(amounts: Vec<u32>) -> bool {
let (mut store, mut instance) = init_kernel_test();
let instance = &mut instance;
let mut prev = 0;
for a in amounts {
let ptr = extism_alloc(&mut store, instance, a as u64);
if ptr == 0 || ptr == u64::MAX {
continue
}
if extism_length(&mut store, instance, ptr) != a as u64 {
return false
}
if a % 2 == 0 {
extism_free(&mut store, instance, ptr);
} else if a % 3 == 0 {
extism_free(&mut store, instance, prev);
}
prev = ptr;
}
true
}
}
quickcheck! {
fn check_alloc_with_load_and_store(amounts: Vec<u16>) -> bool {
use rand::Rng;
let mut rng = rand::thread_rng();
let (mut store, mut instance) = init_kernel_test();
let instance = &mut instance;
for a in amounts {
let ptr = extism_alloc(&mut store, instance, a as u64);
if ptr == 0 || ptr == u64::MAX {
continue
}
if extism_length(&mut store, instance, ptr) != a as u64 {
return false
}
for _ in 0..16 {
let i = rng.gen_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
}
}
}
true
}
}

View File

@@ -1,3 +1,5 @@
use extism_manifest::MemoryOptions;
use crate::*;
use std::{io::Write, time::Instant};
@@ -39,11 +41,12 @@ pub struct Count {
#[test]
fn it_works() {
tracing_subscriber::fmt()
let log = tracing_subscriber::fmt()
.with_ansi(false)
.with_env_filter("extism=debug")
.with_writer(std::fs::File::create("test.log").unwrap())
.init();
.try_init()
.is_ok();
let wasm_start = Instant::now();
@@ -143,8 +146,10 @@ fn it_works() {
println!("wasm function call (avg, N = {}): {:?}", num_tests, avg);
// Check that log file was written to
let meta = std::fs::metadata("test.log").unwrap();
assert!(meta.len() > 0);
if log {
let meta = std::fs::metadata("test.log").unwrap();
assert!(meta.len() > 0);
}
}
#[test]
@@ -448,7 +453,7 @@ fn hello_world_user_data(
_plugin: &mut CurrentPlugin,
inputs: &[Val],
outputs: &mut [Val],
user_data: UserData<std::fs::File>,
user_data: UserData<std::sync::Arc<std::sync::Mutex<std::fs::File>>>,
) -> Result<(), Error> {
let data = user_data.get()?;
let mut data = data.lock().unwrap();
@@ -465,7 +470,8 @@ fn test_userdata() {
if path.exists() {
std::fs::remove_file(&path).unwrap();
}
let file = std::fs::File::create(&path).unwrap();
let file =
std::sync::Arc::new(std::sync::Mutex::new(std::fs::File::create(&path).unwrap()));
let f = Function::new(
"hello_world",
[PTR],
@@ -590,3 +596,94 @@ fn test_manifest_ptr_len() {
let count: serde_json::Value = serde_json::from_slice(output).unwrap();
assert_eq!(count.get("count").unwrap().as_i64().unwrap(), 1);
}
#[test]
fn test_no_vars() {
let data = br#"
(module
(import "extism:host/env" "var_set" (func $var_set (param i64 i64)))
(import "extism:host/env" "input_offset" (func $input_offset (result i64)))
(func (export "test") (result i32)
(call $input_offset)
(call $input_offset)
(call $var_set)
(i32.const 0)
)
)
"#;
let manifest = Manifest::new([Wasm::data(data)])
.with_memory_options(MemoryOptions::new().with_max_var_bytes(1));
let mut plugin = Plugin::new(manifest, [], true).unwrap();
let output: Result<(), Error> = plugin.call("test", b"A".repeat(1024));
assert!(output.is_err());
let output: Result<(), Error> = plugin.call("test", vec![]);
assert!(output.is_ok());
}
#[test]
fn test_linking() {
let manifest = Manifest::new([
Wasm::Data {
data: br#"
(module
(import "wasi_snapshot_preview1" "random_get" (func $random (param i32 i32) (result i32)))
(import "extism:host/env" "alloc" (func $alloc (param i64) (result i64)))
(import "extism:host/user" "hello" (func $hello))
(global $counter (mut i32) (i32.const 0))
(func $start (export "_start")
(global.set $counter (i32.add (global.get $counter) (i32.const 1)))
)
(func (export "read_counter") (result i32)
(global.get $counter)
)
(start $start)
)
"#.to_vec(),
meta: WasmMetadata {
name: Some("commander".to_string()),
hash: None,
},
},
Wasm::Data {
data: br#"
(module
(import "commander" "_start" (func $commander_start))
(import "commander" "read_counter" (func $commander_read_counter (result i32)))
(import "extism:host/env" "store_u64" (func $store_u64 (param i64 i64)))
(import "extism:host/env" "alloc" (func $alloc (param i64) (result i64)))
(import "extism:host/user" "hello" (func $hello))
(import "extism:host/env" "output_set" (func $output_set (param i64 i64)))
(func (export "run") (result i32)
(local $output i64)
(local.set $output (call $alloc (i64.const 8)))
(call $commander_start)
(call $commander_start)
(call $commander_start)
(call $commander_start)
(call $hello)
(call $store_u64 (local.get $output) (i64.extend_i32_u (call $commander_read_counter)))
(call $output_set (local.get $output) (i64.const 8))
i32.const 0
)
)
"#.to_vec(),
meta: WasmMetadata {
name: Some("main".to_string()),
hash: None,
},
},
]);
let mut plugin = PluginBuilder::new(manifest)
.with_wasi(true)
.with_function("hello", [], [], UserData::new(()), |_, _, _, _| {
eprintln!("hello!");
Ok(())
})
.build()
.unwrap();
for _ in 0..5 {
assert_eq!(plugin.call::<&str, i64>("run", "Hello, world!").unwrap(), 1);
}
}