Compare commits

..

2 Commits

Author SHA1 Message Date
Benjamin Eckel
3d08c1e2e8 center the image 2024-01-09 08:14:39 -06:00
Steve Manuel
65464e5e13 feat: update README 2024-01-09 02:03:51 -07:00
33 changed files with 117 additions and 1067 deletions

View File

@@ -39,7 +39,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
if: ${{ env.GIT_EXIT_CODE }} != 0 if: ${{ env.GIT_EXIT_CODE }} != 0
with: with:
author: "zshipko <zshipko@users.noreply.github.com>"
title: "update(kernel): extism-runtime.wasm in ${{ github.event.pull_request.head.ref }}" title: "update(kernel): extism-runtime.wasm in ${{ github.event.pull_request.head.ref }}"
body: "Automated PR to update `runtime/src/extism-runtime.wasm` in PR #${{ github.event.number }}" body: "Automated PR to update `runtime/src/extism-runtime.wasm` in PR #${{ github.event.number }}"
base: "${{ github.event.pull_request.head.ref }}" base: "${{ github.event.pull_request.head.ref }}"

View File

@@ -36,18 +36,6 @@ jobs:
override: true override: true
target: ${{ matrix.target }} 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 - name: Release Rust Convert Crate
env: env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_API_TOKEN }} CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_API_TOKEN }}

View File

@@ -1,6 +1,6 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = ["extism-maturin", "manifest", "runtime", "libextism", "convert", "convert-macros"] members = ["extism-maturin", "manifest", "runtime", "libextism", "convert"]
exclude = ["kernel"] exclude = ["kernel"]
[workspace.package] [workspace.package]
@@ -14,5 +14,4 @@ version = "0.0.0+replaced-by-ci"
[workspace.dependencies] [workspace.dependencies]
extism = { path = "./runtime", version = "0.0.0+replaced-by-ci" } extism = { path = "./runtime", version = "0.0.0+replaced-by-ci" }
extism-convert = { path = "./convert", 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" } 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) [![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 Org's stars](https://img.shields.io/github/stars/extism)
![Downloads](https://img.shields.io/crates/d/extism) ![GitHub all releases](https://img.shields.io/github/downloads/extism/extism/total)
![GitHub License](https://img.shields.io/github/license/extism/extism) ![GitHub License](https://img.shields.io/github/license/extism/extism)
![GitHub release (with filter)](https://img.shields.io/github/v/release/extism/extism) ![GitHub release (with filter)](https://img.shields.io/github/v/release/extism/extism)
@@ -76,9 +76,9 @@ get started:
| 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) | | 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) | | 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) | | 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 |
| 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/c.svg" width="50px"/> | https://github.com/extism/c-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 | | 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 |
| .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#!) | N/A |
# Support # Support

View File

@@ -1,26 +0,0 @@
[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"

View File

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

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

View File

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

@@ -1,44 +0,0 @@
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,14 +11,13 @@ description = "Traits to make Rust types usable with Extism"
[dependencies] [dependencies]
anyhow = "1.0.75" anyhow = "1.0.75"
base64 = "~0.22" base64 = "~0.21"
bytemuck = {version = "1.14.0", optional = true } bytemuck = {version = "1.14.0", optional = true }
prost = { version = "0.12.0", optional = true } prost = { version = "0.12.0", optional = true }
protobuf = { version = "3.2.0", optional = true } protobuf = { version = "3.2.0", optional = true }
rmp-serde = { version = "1.1.2", optional = true } rmp-serde = { version = "1.1.2", optional = true }
serde = "1.0.186" serde = "1.0.186"
serde_json = "1.0.105" serde_json = "1.0.105"
extism-convert-macros.workspace = true
[dev-dependencies] [dev-dependencies]
serde = { version = "1.0.186", features = ["derive"] } serde = { version = "1.0.186", features = ["derive"] }
@@ -27,5 +26,3 @@ serde = { version = "1.0.186", features = ["derive"] }
default = ["msgpack", "prost", "raw"] default = ["msgpack", "prost", "raw"]
msgpack = ["rmp-serde"] msgpack = ["rmp-serde"]
raw = ["bytemuck"] 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); /// 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`] /// 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`] /// and `FromBytesOwned` using `serde_json::from_vec`
#[macro_export] #[macro_export]
macro_rules! encoding { macro_rules! encoding {
($pub:vis $name:ident, $to_vec:expr, $from_slice:expr) => { ($pub:vis $name:ident, $to_vec:expr, $from_slice:expr) => {
@@ -183,5 +183,30 @@ impl<'a, T: bytemuck::Pod> FromBytes<'a> for Raw<'a, T> {
} }
} }
#[cfg(all(feature = "raw", target_endian = "big"))] #[cfg(all(test, feature = "raw", target_endian = "little"))]
compile_error!("The raw feature is only supported on little endian targets"); mod tests {
use crate::*;
#[test]
fn test_raw() {
#[derive(Debug, Clone, Copy, PartialEq)]
struct TestRaw {
a: i32,
b: f64,
c: bool,
}
unsafe impl bytemuck::Pod for TestRaw {}
unsafe impl bytemuck::Zeroable for TestRaw {}
let x = TestRaw {
a: 123,
b: 45678.91011,
c: true,
};
let raw = Raw(&x).to_bytes().unwrap();
let y = Raw::from_bytes(&raw).unwrap();
assert_eq!(&x, y.0);
let y: Result<Raw<[u8; std::mem::size_of::<TestRaw>()]>, Error> = Raw::from_bytes(&raw);
assert!(y.is_ok());
}
}

View File

@@ -1,68 +1,14 @@
use crate::*; use crate::*;
pub use extism_convert_macros::FromBytes;
/// `FromBytes` is used to define how a type should be decoded when working with /// `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. /// 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 { pub trait FromBytes<'a>: Sized {
/// Decode a value from a slice of bytes /// Decode a value from a slice of bytes
fn from_bytes(data: &'a [u8]) -> Result<Self, Error>; fn from_bytes(data: &'a [u8]) -> Result<Self, Error>;
} }
/// `FromBytesOwned` is similar to [`FromBytes`] but it doesn't borrow from the input slice. /// `FromBytesOwned` is similar to `FromBytes` but it doesn't borrow from the input slice.
/// [`FromBytes`] is automatically implemented for all types that implement `FromBytesOwned`. /// `FromBytes` is automatically implemented for all types that implement `FromBytesOwned`
///
/// `FromBytesOwned` can be derived through [`#[derive(FromBytes)]`](FromBytes).
pub trait FromBytesOwned: Sized { pub trait FromBytesOwned: Sized {
/// Decode a value from a slice of bytes, the resulting value should not borrow the input /// Decode a value from a slice of bytes, the resulting value should not borrow the input
/// data. /// data.
@@ -152,13 +98,3 @@ impl<'a, T: FromBytes<'a>> FromBytes<'a> for std::io::Cursor<T> {
Ok(std::io::Cursor::new(T::from_bytes(data)?)) Ok(std::io::Cursor::new(T::from_bytes(data)?))
} }
} }
impl<'a, T: FromBytes<'a>> FromBytes<'a> for Option<T> {
fn from_bytes(data: &'a [u8]) -> Result<Self, Error> {
if data.is_empty() {
return Ok(None);
}
T::from_bytes(data).map(Some)
}
}

View File

@@ -5,9 +5,6 @@
//! similar to [axum extractors](https://docs.rs/axum/latest/axum/extract/index.html#intro) - they are //! 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. //! 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; pub use anyhow::Error;
mod encoding; mod encoding;

View File

@@ -20,7 +20,6 @@ fn roundtrip_json() {
} }
#[test] #[test]
#[cfg(feature = "msgpack")]
fn roundtrip_msgpack() { fn roundtrip_msgpack() {
let x = Testing { let x = Testing {
a: "foobar".to_string(), a: "foobar".to_string(),
@@ -38,53 +37,3 @@ fn roundtrip_base64() {
let Base64(s): Base64<String> = FromBytes::from_bytes(bytes.as_bytes()).unwrap(); let Base64(s): Base64<String> = FromBytes::from_bytes(bytes.as_bytes()).unwrap();
assert_eq!(s, "this is a test"); assert_eq!(s, "this is a test");
} }
#[test]
fn rountrip_option() {
// `None` case
let e0: Option<Json<Testing>> = FromBytes::from_bytes(&[]).unwrap();
let b = e0.to_bytes().unwrap();
let e1: Option<Json<Testing>> = FromBytes::from_bytes(&b).unwrap();
assert!(e0.is_none());
assert_eq!(e0.is_none(), e1.is_none());
// `Some` case
let x = Testing {
a: "foobar".to_string(),
b: 123,
c: 456.7,
};
let bytes = Json(&x).to_bytes().unwrap();
let y: Option<Json<Testing>> = FromBytes::from_bytes(&bytes).unwrap();
let b = ToBytes::to_bytes(&y).unwrap();
let z: Option<Json<Testing>> = FromBytes::from_bytes(&b).unwrap();
assert_eq!(y.unwrap().0, z.unwrap().0);
}
#[cfg(all(feature = "raw", target_endian = "little"))]
mod tests {
use crate::*;
#[test]
fn test_raw() {
#[derive(Debug, Clone, Copy, PartialEq)]
struct TestRaw {
a: i32,
b: f64,
c: bool,
}
unsafe impl bytemuck::Pod for TestRaw {}
unsafe impl bytemuck::Zeroable for TestRaw {}
let x = TestRaw {
a: 123,
b: 45678.91011,
c: true,
};
let raw = Raw(&x).to_bytes().unwrap();
let y = Raw::from_bytes(&raw).unwrap();
assert_eq!(&x, y.0);
let y: Result<Raw<[u8; std::mem::size_of::<TestRaw>()]>, Error> = Raw::from_bytes(&raw);
assert!(y.is_ok());
}
}

View File

@@ -1,58 +1,7 @@
use crate::*; use crate::*;
pub use extism_convert_macros::ToBytes;
/// `ToBytes` is used to define how a type should be encoded when working with /// `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. /// 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> { pub trait ToBytes<'a> {
/// A configurable byte slice representation, allows any type that implements `AsRef<[u8]>` /// A configurable byte slice representation, allows any type that implements `AsRef<[u8]>`
type Bytes: AsRef<[u8]>; type Bytes: AsRef<[u8]>;
@@ -151,26 +100,3 @@ impl<'a, T: ToBytes<'a>> ToBytes<'a> for &'a T {
<T as ToBytes>::to_bytes(self) <T as ToBytes>::to_bytes(self)
} }
} }
impl<'a, T: ToBytes<'a>> ToBytes<'a> for Option<T> {
type Bytes = Vec<u8>;
fn to_bytes(&self) -> Result<Self::Bytes, Error> {
match self {
Some(x) => x.to_bytes().map(|x| x.as_ref().to_vec()),
None => Ok(vec![]),
}
}
}
#[test]
fn test() {
use extism_convert::{Json, ToBytes};
use serde::Serialize;
#[derive(ToBytes, Serialize)]
#[encoding(Json)]
struct Struct {
hello: String,
}
}

View File

@@ -5,9 +5,6 @@ edition = "2021"
[dependencies] [dependencies]
[dev-dependencies]
wasm-bindgen-test = "0.3.39"
[features] [features]
default = ["bounds-checking"] default = ["bounds-checking"]
bounds-checking = [] bounds-checking = []

View File

@@ -3,7 +3,7 @@
pub use extism_runtime_kernel::*; pub use extism_runtime_kernel::*;
#[cfg(all(target_arch = "wasm32", not(test)))] #[cfg(target_arch = "wasm32")]
#[panic_handler] #[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! { fn panic(_info: &core::panic::PanicInfo) -> ! {
core::arch::wasm32::unreachable() core::arch::wasm32::unreachable()

View File

@@ -198,8 +198,8 @@ impl MemoryRoot {
fn pointer_in_bounds_fast(p: Pointer) -> bool { 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, // Similar to `pointer_in_bounds` but less accurate on the upper bound. This uses the total memory size,
// instead of checking `MemoryRoot::length` // instead of checking `MemoryRoot::length`
let end = (core::arch::wasm32::memory_size(0) as u64) << 16; let end = core::arch::wasm32::memory_size(0) << 16;
p >= core::mem::size_of::<Self>() as Pointer && p <= end as u64 p >= core::mem::size_of::<Self>() as Pointer && p <= end as Pointer
} }
// Find a block that is free to use, this can be a new block or an existing freed block. The `self_position` argument // Find a block that is free to use, this can be a new block or an existing freed block. The `self_position` argument
@@ -228,7 +228,7 @@ impl MemoryRoot {
if status == MemoryStatus::Free as u8 && b.size >= length as usize { if status == MemoryStatus::Free as u8 && b.size >= length as usize {
// Split block if there is too much excess // Split block if there is too much excess
if b.size - length as usize >= 128 { if b.size - length as usize >= 128 {
b.size -= length as usize + core::mem::size_of::<MemoryBlock>(); b.size -= length as usize;
b.used = 0; b.used = 0;
let block1 = b.data.as_mut_ptr().add(b.size) as *mut MemoryBlock; let block1 = b.data.as_mut_ptr().add(b.size) as *mut MemoryBlock;
@@ -270,13 +270,12 @@ impl MemoryRoot {
// Get the number of bytes available // Get the number of bytes available
let mem_left = self_length - self_position - core::mem::size_of::<MemoryRoot>() as u64; 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 // When the allocation is larger than the number of bytes available
// we will need to try to grow the memory // we will need to try to grow the memory
if length_with_block >= mem_left { if length >= mem_left {
// Calculate the number of pages needed to cover the remaining bytes // Calculate the number of pages needed to cover the remaining bytes
let npages = num_pages(length_with_block - mem_left); let npages = num_pages(length - mem_left);
let x = core::arch::wasm32::memory_grow(0, npages); let x = core::arch::wasm32::memory_grow(0, npages);
if x == usize::MAX { if x == usize::MAX {
return None; return None;
@@ -575,67 +574,3 @@ pub unsafe fn error_get() -> Handle {
pub unsafe fn memory_bytes() -> u64 { pub unsafe fn memory_bytes() -> u64 {
MemoryRoot::new().length.load(Ordering::Acquire) MemoryRoot::new().length.load(Ordering::Acquire)
} }
#[cfg(test)]
mod test {
use crate::*;
use wasm_bindgen_test::*;
// See https://github.com/extism/extism/pull/659
#[wasm_bindgen_test]
fn test_659() {
unsafe {
// Warning: These offsets will need to change if we adjust the kernel memory layout at all
reset();
assert_eq!(alloc(1065), 77);
assert_eq!(alloc(288), 1154);
assert_eq!(alloc(128), 1454);
assert_eq!(length(1154), 288);
assert_eq!(length(1454), 128);
free(1454);
assert_eq!(alloc(213), 1594);
length_unsafe(1594);
assert_eq!(alloc(511), 1819);
assert_eq!(alloc(4), 1454);
assert_eq!(length(1454), 4);
assert_eq!(length(1819), 511);
assert_eq!(alloc(13), 2342);
assert_eq!(length(2342), 13);
assert_eq!(alloc(336), 2367);
assert_eq!(alloc(1077), 2715);
assert_eq!(length(2367), 336);
assert_eq!(length(2715), 1077);
free(2715);
assert_eq!(alloc(1094), 3804);
length_unsafe(3804);
// Allocate 4 bytes, expect to receive address 3788
assert_eq!(alloc(4), 3788);
assert_eq!(alloc(4), 3772);
assert_eq!(length(3772), 4);
// Address 3788 has not been freed yet, so expect it to have 4 bytes allocated
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

@@ -1,5 +0,0 @@
# install wasm-bindgen-cli to get wasm-bindgen-runner if it is not installed yet
which wasm-bindgen-test-runner 1>/dev/null || cargo install -f wasm-bindgen-cli
# run tests with the wasm-bindgen-runner
CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner cargo test --release --target=wasm32-unknown-unknown

View File

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

View File

@@ -1,11 +1,9 @@
{ {
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "Manifest", "title": "Manifest",
"description": "The `Manifest` type is used to configure the runtime and specify how to load modules.",
"type": "object", "type": "object",
"properties": { "properties": {
"allowed_hosts": { "allowed_hosts": {
"description": "Specifies which hosts may be accessed via HTTP, if this is empty then no hosts may be accessed. Wildcards may be used.",
"default": null, "default": null,
"type": [ "type": [
"array", "array",
@@ -16,7 +14,6 @@
} }
}, },
"allowed_paths": { "allowed_paths": {
"description": "Specifies which paths should be made available on disk when using WASI. This is a mapping from the path on disk to the path it should be available inside the plugin. For example, `\".\": \"/tmp\"` would mount the current directory as `/tmp` inside the module",
"default": null, "default": null,
"type": [ "type": [
"object", "object",
@@ -27,7 +24,6 @@
} }
}, },
"config": { "config": {
"description": "Config values are made accessible using the PDK `extism_config_get` function",
"default": {}, "default": {},
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
@@ -35,11 +31,8 @@
} }
}, },
"memory": { "memory": {
"description": "Memory options",
"default": { "default": {
"max_http_response_bytes": null, "max_pages": null
"max_pages": null,
"max_var_bytes": null
}, },
"allOf": [ "allOf": [
{ {
@@ -48,8 +41,6 @@
] ]
}, },
"timeout_ms": { "timeout_ms": {
"description": "The plugin timeout in milliseconds",
"default": null,
"type": [ "type": [
"integer", "integer",
"null" "null"
@@ -58,7 +49,6 @@
"minimum": 0.0 "minimum": 0.0
}, },
"wasm": { "wasm": {
"description": "WebAssembly modules, the `main` module should be named `main` or listed last",
"default": [], "default": [],
"type": "array", "type": "array",
"items": { "items": {
@@ -66,63 +56,35 @@
} }
} }
}, },
"additionalProperties": false,
"definitions": { "definitions": {
"MemoryOptions": { "MemoryOptions": {
"description": "Configure memory settings",
"type": "object", "type": "object",
"properties": { "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": { "max_pages": {
"description": "The max number of WebAssembly pages that should be allocated",
"type": [ "type": [
"integer", "integer",
"null" "null"
], ],
"format": "uint32", "format": "uint32",
"minimum": 0.0 "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
}, },
"Wasm": { "Wasm": {
"description": "The `Wasm` type specifies how to access a WebAssembly module",
"anyOf": [ "anyOf": [
{ {
"description": "From disk",
"type": "object", "type": "object",
"required": [ "required": [
"path" "path"
], ],
"properties": { "properties": {
"hash": { "hash": {
"description": "Module hash, if the data loaded from disk or via HTTP doesn't match an error will be raised",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"name": { "name": {
"description": "Module name, this is used by Extism to determine which is the `main` module",
"type": [ "type": [
"string", "string",
"null" "null"
@@ -131,72 +93,45 @@
"path": { "path": {
"type": "string" "type": "string"
} }
}, }
"additionalProperties": false
}, },
{ {
"description": "From memory",
"type": "object", "type": "object",
"required": [ "required": [
"data" "data"
], ],
"properties": { "properties": {
"data": { "data": {
"type": [ "type": "string",
"string", "format": "string"
"object"
],
"required": [
"len",
"ptr"
],
"properties": {
"len": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"ptr": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
}, },
"hash": { "hash": {
"description": "Module hash, if the data loaded from disk or via HTTP doesn't match an error will be raised",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"name": { "name": {
"description": "Module name, this is used by Extism to determine which is the `main` module",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
} }
}, }
"additionalProperties": false
}, },
{ {
"description": "Via HTTP",
"type": "object", "type": "object",
"required": [ "required": [
"url" "url"
], ],
"properties": { "properties": {
"hash": { "hash": {
"description": "Module hash, if the data loaded from disk or via HTTP doesn't match an error will be raised",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"headers": { "headers": {
"description": "Request headers",
"default": {}, "default": {},
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
@@ -204,25 +139,21 @@
} }
}, },
"method": { "method": {
"description": "Request method",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"name": { "name": {
"description": "Module name, this is used by Extism to determine which is the `main` module",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"url": { "url": {
"description": "The request URL",
"type": "string" "type": "string"
} }
}, }
"additionalProperties": false
} }
] ]
} }

View File

@@ -12,44 +12,6 @@ pub struct MemoryOptions {
/// The max number of WebAssembly pages that should be allocated /// The max number of WebAssembly pages that should be allocated
#[serde(alias = "max")] #[serde(alias = "max")]
pub max_pages: Option<u32>, 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 /// Generic HTTP request structure
@@ -149,8 +111,8 @@ pub enum Wasm {
/// From memory /// From memory
Data { Data {
#[serde(with = "wasmdata")] #[serde(with = "base64")]
#[cfg_attr(feature = "json_schema", schemars(schema_with = "wasmdata_schema"))] #[cfg_attr(feature = "json_schema", schemars(schema_with = "base64_schema"))]
data: Vec<u8>, data: Vec<u8>,
#[serde(flatten)] #[serde(flatten)]
meta: WasmMetadata, meta: WasmMetadata,
@@ -233,25 +195,11 @@ impl Wasm {
} }
} }
#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
struct DataPtrLength {
ptr: u64,
len: u64,
}
#[cfg(feature = "json_schema")] #[cfg(feature = "json_schema")]
fn wasmdata_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { fn base64_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
use schemars::{schema::SchemaObject, JsonSchema}; use schemars::{schema::SchemaObject, JsonSchema};
let mut schema: SchemaObject = <String>::json_schema(gen).into(); let mut schema: SchemaObject = <String>::json_schema(gen).into();
let objschema: SchemaObject = <DataPtrLength>::json_schema(gen).into(); schema.format = Some("string".to_owned());
let types = schemars::schema::SingleOrVec::<schemars::schema::InstanceType>::Vec(vec![
schemars::schema::InstanceType::String,
schemars::schema::InstanceType::Object,
]);
schema.instance_type = Some(types);
schema.object = objschema.object;
schema.into() schema.into()
} }
@@ -263,7 +211,6 @@ pub struct Manifest {
/// WebAssembly modules, the `main` module should be named `main` or listed last /// WebAssembly modules, the `main` module should be named `main` or listed last
#[serde(default)] #[serde(default)]
pub wasm: Vec<Wasm>, pub wasm: Vec<Wasm>,
/// Memory options /// Memory options
#[serde(default)] #[serde(default)]
pub memory: MemoryOptions, pub memory: MemoryOptions,
@@ -283,7 +230,7 @@ pub struct Manifest {
#[serde(default)] #[serde(default)]
pub allowed_paths: Option<BTreeMap<PathBuf, PathBuf>>, pub allowed_paths: Option<BTreeMap<PathBuf, PathBuf>>,
/// The plugin timeout in milliseconds /// The plugin timeout, by default this is set to 30s
#[serde(default)] #[serde(default)]
pub timeout_ms: Option<u64>, pub timeout_ms: Option<u64>,
} }
@@ -388,12 +335,10 @@ impl Manifest {
} }
} }
mod wasmdata { mod base64 {
use crate::DataPtrLength;
use base64::{engine::general_purpose, Engine as _}; use base64::{engine::general_purpose, Engine as _};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde::{Deserializer, Serializer}; use serde::{Deserializer, Serializer};
use std::slice;
pub fn serialize<S: Serializer>(v: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> { pub fn serialize<S: Serializer>(v: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {
let base64 = general_purpose::STANDARD.encode(v.as_slice()); let base64 = general_purpose::STANDARD.encode(v.as_slice());
@@ -401,22 +346,10 @@ mod wasmdata {
} }
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> { pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
#[derive(Deserialize)] let base64 = String::deserialize(d)?;
#[serde(untagged)] general_purpose::STANDARD
enum WasmDataTypes { .decode(base64.as_bytes())
String(String), .map_err(serde::de::Error::custom)
DataPtrLength(DataPtrLength),
}
Ok(match WasmDataTypes::deserialize(d)? {
WasmDataTypes::String(string) => general_purpose::STANDARD
.decode(string.as_bytes())
.map_err(serde::de::Error::custom)?,
WasmDataTypes::DataPtrLength(ptrlen) => {
let slice =
unsafe { slice::from_raw_parts(ptrlen.ptr as *const u8, ptrlen.len as usize) };
slice.to_vec()
}
})
} }
} }

View File

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

View File

@@ -12,7 +12,7 @@ To use the `extism` crate, you can add it to your Cargo file:
```toml ```toml
[dependencies] [dependencies]
extism = "1.0.0" extism = "^1.0.0-rc3"
``` ```
## Environment variables ## Environment variables

View File

@@ -1,7 +1,7 @@
use extism::*; use extism::*;
// pretend this is redis or something :) // pretend this is redis or something :)
type KVStore = std::sync::Arc<std::sync::Mutex<std::collections::BTreeMap<String, Vec<u8>>>>; type KVStore = std::collections::BTreeMap<String, Vec<u8>>;
// When a first argument separated with a semicolon is provided to `host_fn` it is used as the // 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 // 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 /// 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. /// will be called when the inner `CPtr` is dropped.
#[derive(Debug)] #[derive(Debug)]
pub enum UserData<T: Sync + Clone + Sized> { pub enum UserData<T: Sized> {
C(Arc<CPtr>), C(Arc<CPtr>),
Rust(T), Rust(Arc<std::sync::Mutex<T>>),
} }
impl<T: Default + Sync + Clone> Default for UserData<T> { impl<T: Default> Default for UserData<T> {
fn default() -> Self { fn default() -> Self {
UserData::new(T::default()) UserData::new(T::default())
} }
} }
impl<T: Sync + Clone> Clone for UserData<T> { impl<T> Clone for UserData<T> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
match self { match self {
UserData::C(ptr) => UserData::C(ptr.clone()), UserData::C(ptr) => UserData::C(ptr.clone()),
@@ -101,7 +101,7 @@ impl<T: Sync + Clone> Clone for UserData<T> {
} }
} }
impl<T: Sync + Clone> UserData<T> { impl<T> UserData<T> {
/// Create a new `UserData` from an existing pointer and free function, this is used /// 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 /// by the C API to wrap C pointers into user data
pub(crate) fn new_pointer( pub(crate) fn new_pointer(
@@ -126,11 +126,12 @@ impl<T: Sync + Clone> UserData<T> {
/// ///
/// This will wrap the provided value in a reference-counted mutex /// This will wrap the provided value in a reference-counted mutex
pub fn new(x: T) -> Self { pub fn new(x: T) -> Self {
UserData::Rust(x) let data = Arc::new(std::sync::Mutex::new(x));
UserData::Rust(data)
} }
/// Get a copy of the inner value /// Get a copy of the inner value
pub fn get(&self) -> Result<T, Error> { pub fn get(&self) -> Result<Arc<std::sync::Mutex<T>>, Error> {
match self { match self {
UserData::C { .. } => anyhow::bail!("C UserData should not be used from Rust"), UserData::C { .. } => anyhow::bail!("C UserData should not be used from Rust"),
UserData::Rust(data) => Ok(data.clone()), UserData::Rust(data) => Ok(data.clone()),
@@ -149,8 +150,8 @@ impl Drop for CPtr {
} }
} }
unsafe impl<T: Sync + Clone> Send for UserData<T> {} unsafe impl<T> Send for UserData<T> {}
unsafe impl<T: Sync + Clone> Sync for UserData<T> {} unsafe impl<T> Sync for UserData<T> {}
unsafe impl Send for CPtr {} unsafe impl Send for CPtr {}
unsafe impl Sync for CPtr {} unsafe impl Sync for CPtr {}
@@ -179,7 +180,7 @@ pub struct Function {
impl Function { impl Function {
/// Create a new host function /// Create a new host function
pub fn new<T: 'static + Sync + Clone, F>( pub fn new<T: 'static, F>(
name: impl Into<String>, name: impl Into<String>,
args: impl IntoIterator<Item = ValType>, args: impl IntoIterator<Item = ValType>,
returns: impl IntoIterator<Item = ValType>, returns: impl IntoIterator<Item = ValType>,
@@ -210,9 +211,7 @@ impl Function {
namespace: None, namespace: None,
_user_data: match &user_data { _user_data: match &user_data {
UserData::C(ptr) => UserDataHandle::C(ptr.clone()), UserData::C(ptr) => UserDataHandle::C(ptr.clone()),
UserData::Rust(x) => { UserData::Rust(x) => UserDataHandle::Rust(x.clone()),
UserDataHandle::Rust(std::sync::Arc::new(std::sync::Mutex::new(x.clone())))
}
}, },
} }
} }

View File

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

View File

@@ -97,13 +97,19 @@ pub(crate) fn var_set(
) -> Result<(), Error> { ) -> Result<(), Error> {
let data: &mut CurrentPlugin = caller.data_mut(); let data: &mut CurrentPlugin = caller.data_mut();
if data.manifest.memory.max_var_bytes.is_some_and(|x| x == 0) { let mut size = 0;
anyhow::bail!("Vars are disabled by this host") for v in data.vars.values() {
size += v.len();
} }
let voffset = args!(input, 1, i64) as u64; let voffset = args!(input, 1, i64) as u64;
let key_offs = args!(input, 0, 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 key = {
let handle = match data.memory_handle(key_offs) { let handle = match data.memory_handle(key_offs) {
Some(h) => h, Some(h) => h,
@@ -126,22 +132,6 @@ pub(crate) fn var_set(
None => anyhow::bail!("invalid handle offset for var value: {voffset}"), 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(); let value = data.memory_bytes(handle)?.to_vec();
// Insert the value from memory into the `vars` map // Insert the value from memory into the `vars` map
@@ -236,29 +226,20 @@ pub(crate) fn http_request(
Some(res.into_reader()) Some(res.into_reader())
} }
Err(e) => { Err(e) => {
let msg = e.to_string();
if let Some(res) = e.into_response() { if let Some(res) = e.into_response() {
data.http_status = res.status(); data.http_status = res.status();
Some(res.into_reader()) Some(res.into_reader())
} else { } else {
return Err(Error::msg(msg)); None
} }
} }
}; };
if let Some(reader) = reader { if let Some(reader) = reader {
let mut buf = Vec::new(); let mut buf = Vec::new();
let max = if let Some(max) = &data.manifest.memory.max_http_response_bytes { reader
reader.take(*max + 1).read_to_end(&mut buf)?; .take(1024 * 1024 * 50) // TODO: make this limit configurable
*max .read_to_end(&mut buf)?;
} 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)?; let mem = data.memory_new(&buf)?;
output[0] = Val::I64(mem.offset() as i64); output[0] = Val::I64(mem.offset() as i64);

View File

@@ -1,7 +1,4 @@
use std::{ use std::{collections::BTreeMap, path::PathBuf};
collections::{BTreeMap, BTreeSet},
path::PathBuf,
};
use crate::*; use crate::*;
@@ -180,39 +177,6 @@ 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 { impl Plugin {
/// Create a new plugin from a Manifest or WebAssembly module, and host functions. The `with_wasi` /// 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. /// parameter determines whether or not the module should be executed with WASI enabled.
@@ -276,6 +240,22 @@ impl Plugin {
store.set_epoch_deadline(1); store.set_epoch_deadline(1);
let mut linker = Linker::new(&engine); 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(); let mut imports: Vec<_> = imports.into_iter().collect();
// Define PDK functions // Define PDK functions
macro_rules! add_funcs { macro_rules! add_funcs {
@@ -301,37 +281,14 @@ impl Plugin {
log_error(I64); 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 { for f in &mut imports {
let name = f.name(); let name = f.name().to_string();
let ns = f.namespace().unwrap_or(EXTISM_USER_MODULE); let ns = f.namespace().unwrap_or(EXTISM_USER_MODULE);
unsafe { 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 instance_pre = linker.instantiate_pre(main)?;
let timer_tx = Timer::tx(); let timer_tx = Timer::tx();
let mut plugin = Plugin { let mut plugin = Plugin {
@@ -446,18 +403,7 @@ impl Plugin {
pub fn function_exists(&mut self, function: impl AsRef<str>) -> bool { pub fn function_exists(&mut self, function: impl AsRef<str>) -> bool {
self.modules[MAIN_KEY] self.modules[MAIN_KEY]
.get_export(function.as_ref()) .get_export(function.as_ref())
.map(|x| { .map(|x| x.func().is_some())
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) .unwrap_or(false)
} }

View File

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

View File

@@ -1,5 +1,4 @@
use crate::*; use crate::*;
use quickcheck::*;
const KERNEL: &[u8] = include_bytes!("../extism-runtime.wasm"); const KERNEL: &[u8] = include_bytes!("../extism-runtime.wasm");
@@ -320,158 +319,3 @@ fn test_load_input() {
// Out of bounds should return 0 // Out of bounds should return 0
assert_eq!(extism_input_load_u64(&mut store, instance, 123457), 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,5 +1,3 @@
use extism_manifest::MemoryOptions;
use crate::*; use crate::*;
use std::{io::Write, time::Instant}; use std::{io::Write, time::Instant};
@@ -41,12 +39,11 @@ pub struct Count {
#[test] #[test]
fn it_works() { fn it_works() {
let log = tracing_subscriber::fmt() tracing_subscriber::fmt()
.with_ansi(false) .with_ansi(false)
.with_env_filter("extism=debug") .with_env_filter("extism=debug")
.with_writer(std::fs::File::create("test.log").unwrap()) .with_writer(std::fs::File::create("test.log").unwrap())
.try_init() .init();
.is_ok();
let wasm_start = Instant::now(); let wasm_start = Instant::now();
@@ -146,10 +143,8 @@ fn it_works() {
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 // Check that log file was written to
if log { let meta = std::fs::metadata("test.log").unwrap();
let meta = std::fs::metadata("test.log").unwrap(); assert!(meta.len() > 0);
assert!(meta.len() > 0);
}
} }
#[test] #[test]
@@ -453,7 +448,7 @@ fn hello_world_user_data(
_plugin: &mut CurrentPlugin, _plugin: &mut CurrentPlugin,
inputs: &[Val], inputs: &[Val],
outputs: &mut [Val], outputs: &mut [Val],
user_data: UserData<std::sync::Arc<std::sync::Mutex<std::fs::File>>>, user_data: UserData<std::fs::File>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let data = user_data.get()?; let data = user_data.get()?;
let mut data = data.lock().unwrap(); let mut data = data.lock().unwrap();
@@ -470,8 +465,7 @@ fn test_userdata() {
if path.exists() { if path.exists() {
std::fs::remove_file(&path).unwrap(); std::fs::remove_file(&path).unwrap();
} }
let file = let file = std::fs::File::create(&path).unwrap();
std::sync::Arc::new(std::sync::Mutex::new(std::fs::File::create(&path).unwrap()));
let f = Function::new( let f = Function::new(
"hello_world", "hello_world",
[PTR], [PTR],
@@ -578,112 +572,3 @@ fn test_disable_cache() {
assert!(t < t1); assert!(t < t1);
} }
#[test]
fn test_manifest_ptr_len() {
let manifest = serde_json::json!({
"wasm" : [
{
"data" : {
"ptr" : WASM_NO_FUNCTIONS.as_ptr() as u64,
"len" : WASM_NO_FUNCTIONS.len()
}
}
]
});
let mut plugin = Plugin::new(manifest.to_string().as_bytes(), [], true).unwrap();
let output = plugin.call("count_vowels", "abc123").unwrap();
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);
}
}