Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add shared_fn macro #55

Merged
merged 7 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
112 changes: 109 additions & 3 deletions derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
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 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
Expand Down Expand Up @@ -103,7 +104,112 @@ 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe ass an example of this here

///
/// All arguments should implement `extism_pdk::ToBytes` and the return value should implement
/// `extism_pdk::FromBytes`
#[proc_macro_attribute]
pub fn export_fn(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little unsure of name. The docs make it clear. But it's hard to tell which to use just from the name. I can't think of a better name though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, agreed that although the docs make it clear, the name is a little ambiguous.

What about something like #[linkable_fn]? That is, a function which can be linked by Extism. I had also thought about #[shared_fn], since like a shared library, you don't execute it via a main entrypoint, but through some other execution path some code finds a function and calls it?

I think my main concern is that an Extism user, with even rudimentary knowledge of Wasm is going to see #[export_fn] and mistake it for #[plugin_fn], since the name correctly suggests the function will be exported.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like shared_fn - I will switch it

_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, 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::MemoryPointer<#t> },
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,
Expand Down
13 changes: 13 additions & 0 deletions examples/reflect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#![no_main]

use extism_pdk::*;

#[export_fn]
pub fn host_reflect(input: String) -> ExportResult<Vec<u8>> {
Ok(input.to_lowercase().into_bytes())
}

#[export_fn]
pub fn nothing() -> ExportResult<()> {
Ok(())
}
7 changes: 5 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ 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 memory::Memory;
pub use extism_pdk_derive::{export_fn, host_fn, plugin_fn};
pub use memory::{Memory, MemoryPointer};
pub use to_memory::ToMemory;

#[cfg(feature = "http")]
Expand All @@ -37,6 +37,9 @@ pub use http::HttpResponse;
/// The return type of a plugin function
pub type FnResult<T> = Result<T, WithReturnCode<Error>>;

/// The return type of an export function
pub type ExportResult<T> = Result<T, Error>;

/// Logging levels
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Expand Down
20 changes: 20 additions & 0 deletions src/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,23 @@ impl From<i64> for Memory {
Memory::find(offset as u64).unwrap_or_else(Memory::null)
}
}

#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct MemoryPointer<T>(u64, std::marker::PhantomData<T>);

impl<T> MemoryPointer<T> {
pub unsafe fn new(x: u64) -> Self {
MemoryPointer(x, Default::default())
}
}

impl<T: FromBytesOwned> MemoryPointer<T> {
pub fn get(&self) -> Result<T, Error> {
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),
}
}
}
Loading