From 4311703fb39df8f61089ed70118c22ad87174f76 Mon Sep 17 00:00:00 2001 From: zach Date: Thu, 16 May 2024 15:59:11 -0700 Subject: [PATCH 1/7] feat: add export_fn --- .cargo/{config => config.toml} | 0 derive/src/lib.rs | 104 ++++++++++++++++++++++++++++++++- examples/reflect.rs | 8 +++ src/lib.rs | 5 +- src/memory.rs | 13 +++++ 5 files changed, 127 insertions(+), 3 deletions(-) rename .cargo/{config => config.toml} (100%) create mode 100644 examples/reflect.rs diff --git a/.cargo/config b/.cargo/config.toml similarity index 100% rename from .cargo/config rename to .cargo/config.toml diff --git a/derive/src/lib.rs b/derive/src/lib.rs index f9c233c..1824dbf 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -1,7 +1,8 @@ +use proc_macro2::{Ident, Span}; use quote::quote; use syn::{parse_macro_input, ItemFn, ItemForeignMod}; -/// `plugin_fn` is used to define a function that will be exported by a plugin +/// `plugin_fn` is used to define an Extism callable function to export /// /// It should be added to a function you would like to export, the function should /// accept a parameter that implements `extism_pdk::FromBytes` and return a @@ -103,7 +104,106 @@ pub fn plugin_fn( } } -/// `host_fn` is used to define a host function that will be callable from within a plugin +/// `export_fn` is used to define a function that will be exported by a plugin, but is not directly +/// callable by an Extism runtime. These functions can be used for runtime linking and mocking host +/// functions for tests. If direct access to Wasm native parameters is needed, then a bare +/// `extern "C" fn` should be used instead. +/// +/// All arguments should implement `extism_pdk::ToBytes` and the return value should implement +/// `extism_pdk::FromBytes` +#[proc_macro_attribute] +pub fn export_fn( + _attr: proc_macro::TokenStream, + item: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + let mut function = parse_macro_input!(item as ItemFn); + + if !matches!(function.vis, syn::Visibility::Public(..)) { + panic!("extism_pdk::export_fn expects a public function"); + } + + let name = &function.sig.ident; + let constness = &function.sig.constness; + let unsafety = &function.sig.unsafety; + let generics = &function.sig.generics; + let inputs = &mut function.sig.inputs; + let output = &mut function.sig.output; + let block = &function.block; + + let (raw_inputs, raw_args): (Vec<_>, Vec<_>) = inputs + .iter() + .enumerate() + .map(|(i, _)| { + let arg = Ident::new(&format!("arg{i}"), Span::call_site()); + ( + quote! { #arg: extism_pdk::memory::Pointer }, + quote! { #arg.get()? }, + ) + }) + .unzip(); + + if name == "main" { + panic!( + "export_pdk::export_fn must not be applied to a `main` function. To fix, rename this to something other than `main`." + ) + } + + let (no_result, raw_output) = match output { + syn::ReturnType::Default => (true, quote! {}), + syn::ReturnType::Type(_, t) => { + if let syn::Type::Path(p) = t.as_ref() { + if let Some(t) = p.path.segments.last() { + if t.ident != "ExportResult" { + panic!("extism_pdk::export_fn expects a function that returns extism_pdk::ExportResult"); + } + } else { + panic!("extism_pdk::export_fn expects a function that returns extism_pdk::ExportResult"); + } + } + (false, quote! {-> u64 }) + } + }; + + if no_result { + quote! { + #[no_mangle] + pub #constness #unsafety extern "C" fn #name(#(#raw_inputs,)*) { + #constness #unsafety fn inner #generics(#inputs) -> extism_pdk::ExportResult<()> { + #block + } + + + let r = || inner(#(#raw_args,)*); + if let Err(rc) = r() { + panic!("{}", rc.to_string()); + } + } + } + .into() + } else { + quote! { + #[no_mangle] + pub #constness #unsafety extern "C" fn #name(#(#raw_inputs,)*) #raw_output { + #constness #unsafety fn inner #generics(#inputs) #output { + #block + } + + let r = || inner(#(#raw_args,)*); + match r().and_then(|x| extism_pdk::Memory::new(&x)) { + Ok(mem) => { + mem.offset() + }, + Err(rc) => { + panic!("{}", rc.to_string()); + } + } + } + } + .into() + } +} + +/// `host_fn` is used to import a host function from an `extern` block #[proc_macro_attribute] pub fn host_fn( attr: proc_macro::TokenStream, diff --git a/examples/reflect.rs b/examples/reflect.rs new file mode 100644 index 0000000..f9043b7 --- /dev/null +++ b/examples/reflect.rs @@ -0,0 +1,8 @@ +#![no_main] + +use extism_pdk::*; + +#[export_fn] +pub fn reflect(input: Vec) -> ExportResult> { + Ok(input) +} diff --git a/src/lib.rs b/src/lib.rs index 2fcb1e5..057d3b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,7 +22,7 @@ pub mod http; pub use anyhow::Error; pub use extism_convert::*; pub use extism_convert::{FromBytes, FromBytesOwned, ToBytes}; -pub use extism_pdk_derive::{host_fn, plugin_fn}; +pub use extism_pdk_derive::{export_fn, host_fn, plugin_fn}; pub use memory::Memory; pub use to_memory::ToMemory; @@ -37,6 +37,9 @@ pub use http::HttpResponse; /// The return type of a plugin function pub type FnResult = Result>; +/// The return type of an export function +pub type ExportResult = Result; + /// Logging levels #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LogLevel { diff --git a/src/memory.rs b/src/memory.rs index c58ae10..a83bffc 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -174,3 +174,16 @@ impl From for Memory { Memory::find(offset as u64).unwrap_or_else(Memory::null) } } + +#[repr(transparent)] +pub struct Pointer(u64); + +impl Pointer { + pub fn get(&self) -> Result { + let mem = Memory::find(self.0); + match mem { + Some(mem) => T::from_bytes_owned(&mem.to_vec()), + None => anyhow::bail!("Invalid pointer offset {}", self.0), + } + } +} From a8afd9569dbf590eef09b961b440cffe6674826c Mon Sep 17 00:00:00 2001 From: zach Date: Thu, 16 May 2024 16:57:53 -0700 Subject: [PATCH 2/7] feat: add Pointer type to help with export_fn --- derive/src/lib.rs | 14 ++++++++++---- examples/reflect.rs | 4 ++-- src/lib.rs | 2 +- src/memory.rs | 12 +++++++++--- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 1824dbf..0c009f8 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -1,6 +1,6 @@ use proc_macro2::{Ident, Span}; use quote::quote; -use syn::{parse_macro_input, ItemFn, ItemForeignMod}; +use syn::{parse_macro_input, FnArg, ItemFn, ItemForeignMod}; /// `plugin_fn` is used to define an Extism callable function to export /// @@ -133,10 +133,16 @@ pub fn export_fn( let (raw_inputs, raw_args): (Vec<_>, Vec<_>) = inputs .iter() .enumerate() - .map(|(i, _)| { + .map(|(i, x)| { + let t = match x { + FnArg::Receiver(_) => { + panic!("Receiver argument (self) cannot be used in extism_pdk::export_fn") + } + FnArg::Typed(t) => &t.ty, + }; let arg = Ident::new(&format!("arg{i}"), Span::call_site()); ( - quote! { #arg: extism_pdk::memory::Pointer }, + quote! { #arg: extism_pdk::memory::Pointer<#t> }, quote! { #arg.get()? }, ) }) @@ -159,7 +165,7 @@ pub fn export_fn( } else { panic!("extism_pdk::export_fn expects a function that returns extism_pdk::ExportResult"); } - } + }; (false, quote! {-> u64 }) } }; diff --git a/examples/reflect.rs b/examples/reflect.rs index f9043b7..8f30e2a 100644 --- a/examples/reflect.rs +++ b/examples/reflect.rs @@ -3,6 +3,6 @@ use extism_pdk::*; #[export_fn] -pub fn reflect(input: Vec) -> ExportResult> { - Ok(input) +pub fn reflect(input: String) -> ExportResult> { + Ok(input.to_lowercase().into_bytes()) } diff --git a/src/lib.rs b/src/lib.rs index 057d3b8..eb3f920 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,7 +23,7 @@ pub use anyhow::Error; pub use extism_convert::*; pub use extism_convert::{FromBytes, FromBytesOwned, ToBytes}; pub use extism_pdk_derive::{export_fn, host_fn, plugin_fn}; -pub use memory::Memory; +pub use memory::{Memory, Pointer}; pub use to_memory::ToMemory; #[cfg(feature = "http")] diff --git a/src/memory.rs b/src/memory.rs index a83bffc..c9a17df 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -176,10 +176,16 @@ impl From for Memory { } #[repr(transparent)] -pub struct Pointer(u64); +pub struct Pointer(u64, std::marker::PhantomData); -impl Pointer { - pub fn get(&self) -> Result { +impl Pointer { + pub unsafe fn new(x: u64) -> Self { + Pointer(x, Default::default()) + } +} + +impl Pointer { + pub fn get(&self) -> Result { let mem = Memory::find(self.0); match mem { Some(mem) => T::from_bytes_owned(&mem.to_vec()), From 3bfc1921208c0320d40e99359818a84ffdef1d97 Mon Sep 17 00:00:00 2001 From: zach Date: Thu, 16 May 2024 16:59:18 -0700 Subject: [PATCH 3/7] test: add nothing test --- examples/reflect.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/reflect.rs b/examples/reflect.rs index 8f30e2a..6880aa5 100644 --- a/examples/reflect.rs +++ b/examples/reflect.rs @@ -6,3 +6,8 @@ use extism_pdk::*; pub fn reflect(input: String) -> ExportResult> { Ok(input.to_lowercase().into_bytes()) } + +#[export_fn] +pub fn nothing() -> ExportResult<()> { + Ok(()) +} From 3d780156304d7ae22318af5de5599e1c93f0e00b Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 17 May 2024 12:08:18 -0700 Subject: [PATCH 4/7] cleanup: rename Pointer -> MemoryPointer --- derive/src/lib.rs | 2 +- examples/reflect.rs | 2 +- src/lib.rs | 2 +- src/memory.rs | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 0c009f8..b7cb647 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -142,7 +142,7 @@ pub fn export_fn( }; let arg = Ident::new(&format!("arg{i}"), Span::call_site()); ( - quote! { #arg: extism_pdk::memory::Pointer<#t> }, + quote! { #arg: extism_pdk::MemoryPointer<#t> }, quote! { #arg.get()? }, ) }) diff --git a/examples/reflect.rs b/examples/reflect.rs index 6880aa5..d2be471 100644 --- a/examples/reflect.rs +++ b/examples/reflect.rs @@ -3,7 +3,7 @@ use extism_pdk::*; #[export_fn] -pub fn reflect(input: String) -> ExportResult> { +pub fn host_reflect(input: String) -> ExportResult> { Ok(input.to_lowercase().into_bytes()) } diff --git a/src/lib.rs b/src/lib.rs index eb3f920..184106f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,7 +23,7 @@ pub use anyhow::Error; pub use extism_convert::*; pub use extism_convert::{FromBytes, FromBytesOwned, ToBytes}; pub use extism_pdk_derive::{export_fn, host_fn, plugin_fn}; -pub use memory::{Memory, Pointer}; +pub use memory::{Memory, MemoryPointer}; pub use to_memory::ToMemory; #[cfg(feature = "http")] diff --git a/src/memory.rs b/src/memory.rs index c9a17df..5e077a1 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -176,15 +176,15 @@ impl From for Memory { } #[repr(transparent)] -pub struct Pointer(u64, std::marker::PhantomData); +pub struct MemoryPointer(u64, std::marker::PhantomData); -impl Pointer { +impl MemoryPointer { pub unsafe fn new(x: u64) -> Self { - Pointer(x, Default::default()) + MemoryPointer(x, Default::default()) } } -impl Pointer { +impl MemoryPointer { pub fn get(&self) -> Result { let mem = Memory::find(self.0); match mem { From 355e45ab5b075dfeee92d0a3a6e2695c9082a906 Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 17 May 2024 14:41:29 -0700 Subject: [PATCH 5/7] cleanup: derives --- src/memory.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/memory.rs b/src/memory.rs index 5e077a1..eee3ffc 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -176,6 +176,7 @@ impl From for Memory { } #[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] pub struct MemoryPointer(u64, std::marker::PhantomData); impl MemoryPointer { From 70d18625f20661d17d2f94ba82fd048f51308557 Mon Sep 17 00:00:00 2001 From: zach Date: Mon, 20 May 2024 12:39:43 -0700 Subject: [PATCH 6/7] cleanup: rename export_fn -> shared_fn --- derive/src/lib.rs | 18 +++++++++--------- examples/reflect.rs | 8 ++++---- src/lib.rs | 6 +++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/derive/src/lib.rs b/derive/src/lib.rs index b7cb647..30f4f20 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -104,7 +104,7 @@ pub fn plugin_fn( } } -/// `export_fn` is used to define a function that will be exported by a plugin, but is not directly +/// `shared_fn` is used to define a function that will be exported by a plugin but is not directly /// callable by an Extism runtime. These functions can be used for runtime linking and mocking host /// functions for tests. If direct access to Wasm native parameters is needed, then a bare /// `extern "C" fn` should be used instead. @@ -112,14 +112,14 @@ pub fn plugin_fn( /// All arguments should implement `extism_pdk::ToBytes` and the return value should implement /// `extism_pdk::FromBytes` #[proc_macro_attribute] -pub fn export_fn( +pub fn shared_fn( _attr: proc_macro::TokenStream, item: proc_macro::TokenStream, ) -> proc_macro::TokenStream { let mut function = parse_macro_input!(item as ItemFn); if !matches!(function.vis, syn::Visibility::Public(..)) { - panic!("extism_pdk::export_fn expects a public function"); + panic!("extism_pdk::shared_fn expects a public function"); } let name = &function.sig.ident; @@ -136,7 +136,7 @@ pub fn export_fn( .map(|(i, x)| { let t = match x { FnArg::Receiver(_) => { - panic!("Receiver argument (self) cannot be used in extism_pdk::export_fn") + panic!("Receiver argument (self) cannot be used in extism_pdk::shared_fn") } FnArg::Typed(t) => &t.ty, }; @@ -150,7 +150,7 @@ pub fn export_fn( if name == "main" { panic!( - "export_pdk::export_fn must not be applied to a `main` function. To fix, rename this to something other than `main`." + "export_pdk::shared_fn must not be applied to a `main` function. To fix, rename this to something other than `main`." ) } @@ -159,11 +159,11 @@ pub fn export_fn( syn::ReturnType::Type(_, t) => { if let syn::Type::Path(p) = t.as_ref() { if let Some(t) = p.path.segments.last() { - if t.ident != "ExportResult" { - panic!("extism_pdk::export_fn expects a function that returns extism_pdk::ExportResult"); + if t.ident != "SharedFnResult" { + panic!("extism_pdk::shared_fn expects a function that returns extism_pdk::SharedFnResult"); } } else { - panic!("extism_pdk::export_fn expects a function that returns extism_pdk::ExportResult"); + panic!("extism_pdk::shared_fn expects a function that returns extism_pdk::SharedFnResult"); } }; (false, quote! {-> u64 }) @@ -174,7 +174,7 @@ pub fn export_fn( quote! { #[no_mangle] pub #constness #unsafety extern "C" fn #name(#(#raw_inputs,)*) { - #constness #unsafety fn inner #generics(#inputs) -> extism_pdk::ExportResult<()> { + #constness #unsafety fn inner #generics(#inputs) -> extism_pdk::SharedFnResult<()> { #block } diff --git a/examples/reflect.rs b/examples/reflect.rs index d2be471..a47df22 100644 --- a/examples/reflect.rs +++ b/examples/reflect.rs @@ -2,12 +2,12 @@ use extism_pdk::*; -#[export_fn] -pub fn host_reflect(input: String) -> ExportResult> { +#[shared_fn] +pub fn host_reflect(input: String) -> SharedFnResult> { Ok(input.to_lowercase().into_bytes()) } -#[export_fn] -pub fn nothing() -> ExportResult<()> { +#[shared_fn] +pub fn nothing() -> SharedFnResult<()> { Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 184106f..52937bb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,7 +22,7 @@ pub mod http; pub use anyhow::Error; pub use extism_convert::*; pub use extism_convert::{FromBytes, FromBytesOwned, ToBytes}; -pub use extism_pdk_derive::{export_fn, host_fn, plugin_fn}; +pub use extism_pdk_derive::{host_fn, plugin_fn, shared_fn}; pub use memory::{Memory, MemoryPointer}; pub use to_memory::ToMemory; @@ -37,8 +37,8 @@ pub use http::HttpResponse; /// The return type of a plugin function pub type FnResult = Result>; -/// The return type of an export function -pub type ExportResult = Result; +/// The return type of a `shared_fn` +pub type SharedFnResult = Result; /// Logging levels #[derive(Debug, Clone, Copy, PartialEq, Eq)] From 53b1a0ee0a87cc4b3d728841c58ffe8297f26577 Mon Sep 17 00:00:00 2001 From: zach Date: Tue, 21 May 2024 08:42:04 -0700 Subject: [PATCH 7/7] doc: add example to plugin_fn and shared_fn --- derive/src/lib.rs | 24 +++++++++++++++++++++++- src/extism.rs | 2 +- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 30f4f20..77f47b4 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -7,7 +7,19 @@ use syn::{parse_macro_input, FnArg, ItemFn, ItemForeignMod}; /// It should be added to a function you would like to export, the function should /// accept a parameter that implements `extism_pdk::FromBytes` and return a /// `extism_pdk::FnResult` that contains a value that implements -/// `extism_pdk::ToBytes`. +/// `extism_pdk::ToBytes`. This maps input and output parameters to Extism input +/// and output instead of using function arguments directly. +/// +/// ## Example +/// +/// ```rust +/// use extism_pdk::{FnResult, plugin_fn}; +/// #[plugin_fn] +/// pub fn greet(name: String) -> FnResult { +/// let s = format!("Hello, {name}"); +/// Ok(s) +/// } +/// ``` #[proc_macro_attribute] pub fn plugin_fn( _attr: proc_macro::TokenStream, @@ -111,6 +123,16 @@ pub fn plugin_fn( /// /// All arguments should implement `extism_pdk::ToBytes` and the return value should implement /// `extism_pdk::FromBytes` +/// ## Example +/// +/// ```rust +/// use extism_pdk::{FnResult, shared_fn}; +/// #[shared_fn] +/// pub fn greet2(greeting: String, name: String) -> FnResult { +/// let s = format!("{greeting}, {name}"); +/// Ok(name) +/// } +/// ``` #[proc_macro_attribute] pub fn shared_fn( _attr: proc_macro::TokenStream, diff --git a/src/extism.rs b/src/extism.rs index a473791..35b1116 100644 --- a/src/extism.rs +++ b/src/extism.rs @@ -25,7 +25,7 @@ extern "C" { } /// Loads a byte array from Extism's memory. Only use this if you -/// have already considered the plugin_fn macro as well as the [extism_load_input] function. +/// have already considered the plugin_fn macro as well as the `extism_load_input` function. /// /// # Arguments ///