mirror of
https://github.com/extism/extism.git
synced 2026-01-12 07:18:02 -05:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d704a5068e | ||
|
|
054a29e91d | ||
|
|
d32d4a3dd7 | ||
|
|
5f62554aa1 | ||
|
|
d47af24552 | ||
|
|
8a29e5b1d4 | ||
|
|
4e0cd3b1cf | ||
|
|
f4013c5ac0 | ||
|
|
ddc339334e | ||
|
|
ff5b714f95 | ||
|
|
ed1439ec2d | ||
|
|
62f0a231b0 | ||
|
|
fc22412ff0 | ||
|
|
1a083f612a | ||
|
|
efa69d3668 | ||
|
|
fa1beb9155 | ||
|
|
fbae853505 | ||
|
|
8c8e4a6ffb |
12
.github/workflows/release-rust.yaml
vendored
12
.github/workflows/release-rust.yaml
vendored
@@ -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 }}
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
[](https://extism.org/discord)
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
26
convert-macros/Cargo.toml
Normal file
26
convert-macros/Cargo.toml
Normal 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
108
convert-macros/src/lib.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
5
convert-macros/tests/ui.rs
Normal file
5
convert-macros/tests/ui.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#[test]
|
||||
fn ui() {
|
||||
let t = trybuild::TestCases::new();
|
||||
t.compile_fail("tests/ui/*.rs");
|
||||
}
|
||||
23
convert-macros/tests/ui/invalid-encoding.rs
Normal file
23
convert-macros/tests/ui/invalid-encoding.rs
Normal 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() {}
|
||||
44
convert-macros/tests/ui/invalid-encoding.stderr
Normal file
44
convert-macros/tests/ui/invalid-encoding.stderr
Normal 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)]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
@@ -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"]
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -619,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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.
@@ -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())))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -341,6 +341,22 @@ fn test_failed_quickcheck1() {
|
||||
}
|
||||
}
|
||||
|
||||
#[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();
|
||||
@@ -365,10 +381,11 @@ quickcheck! {
|
||||
let instance = &mut instance;
|
||||
for a in amounts {
|
||||
let ptr = extism_alloc(&mut store, instance, a as u64);
|
||||
if ptr == 0 || ptr == u64::MAX {
|
||||
if ptr == 0 {
|
||||
continue
|
||||
}
|
||||
if extism_length(&mut store, instance, ptr) != a as u64 {
|
||||
let len = extism_length_unsafe(&mut store, instance, ptr);
|
||||
if len != a as u64 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use extism_manifest::MemoryOptions;
|
||||
|
||||
use crate::*;
|
||||
use std::{io::Write, time::Instant};
|
||||
|
||||
@@ -451,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();
|
||||
@@ -468,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],
|
||||
@@ -593,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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user