plugy_macros/
lib.rs

1//! # plugy-macro
2//!
3//! The `plugy-macro` crate provides a collection of macros that streamline the process of generating
4//! bindings and interfaces for plugy's dynamic plugin system. These macros enhance the ergonomics of
5//! working with plugins written in WebAssembly (Wasm) within your Rust applications.
6//!
7use convert_case::{Case, Casing};
8use proc_macro::TokenStream;
9use proc_macro2::{Ident, Span};
10use quote::{quote, ToTokens};
11use syn::{
12    parse_macro_input, DeriveInput, FnArg, ImplItem, ImplItemFn, ItemImpl, ItemTrait, MetaNameValue,
13};
14
15/// A procedural macro attribute for generating an asynchronous and callable version of a trait on the host side.
16///
17/// This procedural macro generates an asynchronous version of the provided trait by
18/// wrapping its methods with async equivalents. It also generates a struct that
19/// implements the asynchronous version of the trait and provides a way to call the
20/// wrapped methods asynchronously.
21///
22/// # Arguments
23///
24/// This macro takes no arguments directly. It operates on the trait provided in the
25/// input token stream.
26///
27/// # Examples
28///
29/// ```ignore
30/// #[plugy_macros::plugin]
31/// pub trait MyTrait {
32///     fn sync_method(&self, param: u32) -> u32;
33/// }
34/// ```
35#[proc_macro_attribute]
36pub fn plugin(_: TokenStream, input: TokenStream) -> TokenStream {
37    let original_trait = parse_macro_input!(input as ItemTrait);
38    let async_trait = generate_async_trait(&original_trait);
39
40    let output = quote! {
41        #original_trait
42        #async_trait
43    };
44
45    output.into()
46}
47
48fn generate_async_trait(trait_item: &ItemTrait) -> proc_macro2::TokenStream {
49    let trait_name = &trait_item.ident;
50    let trait_methods = &trait_item.items;
51
52    let mut generic_types = vec![];
53
54    let async_methods = trait_methods.iter().map(|item| match item {
55        syn::TraitItem::Fn(method) => {
56            let method_name = &method.sig.ident;
57            let method_inputs: Vec<_> = method
58                .sig
59                .inputs
60                .iter()
61                .map(|input| match &input {
62                    FnArg::Receiver(_) => input.to_token_stream(),
63                    FnArg::Typed(typed) => match *typed.ty.clone() {
64                        syn::Type::Path(path) => {
65                            if path.path.segments.iter().any(|seg| {
66                                seg.ident == "Self"
67                                    || seg.arguments.to_token_stream().to_string().contains("Self")
68                            }) {
69                                let arg_name = &typed.pat;
70                                quote! {
71                                    #arg_name: &Vec<u8>
72                                }
73                            } else {
74                                input.to_token_stream()
75                            }
76                        }
77                        _ => input.to_token_stream(),
78                    },
79                })
80                .collect();
81            let method_output = &method.sig.output;
82            let method_name_str = method_name.to_string();
83            let values: Vec<_> = method
84                .sig
85                .inputs
86                .iter()
87                .filter_map(|arg| match arg {
88                    syn::FnArg::Receiver(_) => None,
89                    syn::FnArg::Typed(t) => Some(t.pat.to_token_stream()),
90                })
91                .collect();
92            quote! {
93                pub async fn #method_name(#(#method_inputs), *) #method_output {
94                    let func = self.handle.get_func(#method_name_str).await.unwrap();
95                    func.call_unchecked(&(#(#values),*)).await
96                }
97            }
98        }
99        syn::TraitItem::Type(ty) => {
100            let ident_type = &ty.ident;
101
102            generic_types.push(quote! {
103                #ident_type = Vec<u8>
104            });
105            quote! {}
106        }
107
108        _ => {
109            quote! {}
110        }
111    });
112
113    let callable_trait_name = format!("{}Wrapper", trait_name);
114    let callable_trait_ident = syn::Ident::new(&callable_trait_name, trait_name.span());
115
116    quote! {
117        #[cfg(not(target_arch = "wasm32"))]
118        #[derive(Debug, Clone)]
119        pub struct #callable_trait_ident<P, D> {
120            pub handle: plugy::runtime::PluginHandle<plugy::runtime::Plugin<D>>,
121            inner: std::marker::PhantomData<P>
122        }
123        #[cfg(not(target_arch = "wasm32"))]
124        impl<P, D: Clone + Send> #callable_trait_ident<P, D> {
125            #(#async_methods)*
126        }
127        #[cfg(not(target_arch = "wasm32"))]
128        impl<P, D> plugy::runtime::IntoCallable<P, D> for Box<dyn #trait_name<#(#generic_types),*>> {
129            type Output = #callable_trait_ident<P, D>;
130            fn into_callable(handle: plugy::runtime::PluginHandle<plugy::runtime::Plugin<D>>) -> Self::Output {
131                #callable_trait_ident { handle, inner: std::marker::PhantomData }
132            }
133        }
134    }
135}
136
137fn impl_methods(imp: &ItemImpl) -> impl Iterator<Item = &ImplItemFn> {
138    imp.items
139        .iter()
140        .filter_map(|i| match i {
141            ImplItem::Fn(m) => Some(m),
142            _ => None,
143        })
144        .filter(|_m| imp.trait_.is_some())
145}
146
147/// A procedural macro for generating guest-side implementations of trait methods.
148///
149/// This macro takes an implementation block for a trait and generates corresponding
150/// guest-side functions for each method in the trait. The generated functions are
151/// meant to be used in an external C interface for interacting with the methods from
152/// a guest environment, such as WebAssembly.
153///
154/// The `plugin_impl` macro automates the process of generating unsafe external C
155/// functions that can be called from a guest environment to invoke methods on the
156/// trait implementation.
157///
158/// # Example
159///
160/// ```rust,ignore
161/// use plugy_macros::plugin_impl;
162///
163/// trait Plugin {
164///     fn greet(&self) -> String;
165/// }
166///
167/// struct MyGreetPlugin;
168///
169/// #[plugin_impl]
170/// impl Plugin for MyGreetPlugin {
171///     fn greet(&self) -> String {
172///         "Hello, from MyGreetPlugin!".to_string()
173///     }
174/// }
175/// ```
176///
177/// In this example, the `plugin_impl` macro will generate bindings
178/// the `greet` method from the `Plugin` trait. The generated function can then be
179/// used to call the `greet` method from a host environment.
180#[proc_macro_attribute]
181pub fn plugin_impl(_metadata: TokenStream, input: TokenStream) -> TokenStream {
182    let cur_impl: proc_macro2::TokenStream = input.clone().into();
183    let imp = parse_macro_input!(input as ItemImpl);
184    let ty = &imp.self_ty;
185    let methods: Vec<&ImplItemFn> = impl_methods(&imp).collect();
186    let derived: proc_macro2::TokenStream = methods
187        .iter()
188        .map(|m| {
189            let method_name = &m.sig.ident;
190            let args = &m.sig.inputs;
191            let types: Vec<_> = args
192                .iter()
193                .filter_map(|arg| match arg {
194                    syn::FnArg::Receiver(_) => None,
195                    syn::FnArg::Typed(t) => Some(t.ty.to_token_stream()),
196                })
197                .collect();
198            let values: Vec<_> = args
199                .iter()
200                .filter_map(|arg| match arg {
201                    syn::FnArg::Receiver(_) => None,
202                    syn::FnArg::Typed(t) => Some(t.pat.to_token_stream()),
203                })
204                .collect();
205            let expose_name = format!("_plugy_guest_{}", method_name);
206            let expose_name_ident = syn::Ident::new(&expose_name, Span::call_site());
207            quote! {
208                #[no_mangle]
209                pub unsafe extern "C" fn #expose_name_ident(value: u64) -> u64 {
210                    let (value, #(#values),*): (#ty, #(#types),*)  = plugy::core::guest::read_msg(value);
211                    plugy::core::guest::write_msg(&value.#method_name(#(#values),*))
212                }
213            }
214        })
215        .collect();
216
217    quote! {
218        #cur_impl
219        #derived
220    }
221    .into()
222}
223
224#[proc_macro_attribute]
225pub fn plugin_import(args: TokenStream, input: TokenStream) -> TokenStream {
226    let input = parse_macro_input!(input as DeriveInput);
227    let struct_name = &input.ident;
228    let parsed = syn::parse2::<MetaNameValue>(args.into()).unwrap();
229    assert_eq!(parsed.path.to_token_stream().to_string(), "file");
230    let file_path = parsed.value;
231
232    quote! {
233        #input
234
235        impl PluginLoader for #struct_name {
236            fn bytes(&self) -> std::pin::Pin<std::boxed::Box<dyn std::future::Future<Output = Result<Vec<u8>, anyhow::Error>>>> {
237                std::boxed::Box::pin(async {
238                    let res = std::fs::read(#file_path)?;
239                    Ok(res)
240                })
241            }
242            fn name(&self) -> &'static str {
243                std::any::type_name::<Self>()
244            }
245        }
246    }.into()
247}
248
249#[proc_macro_attribute]
250pub fn context(args: TokenStream, input: TokenStream) -> TokenStream {
251    // Parse the input as an ItemImpl
252    let input = parse_macro_input!(input as ItemImpl);
253
254    let data_ident = &args
255        .into_iter()
256        .nth(2)
257        .map(|d| Ident::new(&d.to_string(), d.span().into()))
258        .unwrap_or(Ident::new("_", Span::call_site()));
259
260    // Get the name of the struct being implemented
261    let struct_name = &input.self_ty.to_token_stream();
262
263    let mod_name = Ident::new(
264        &struct_name.to_string().to_case(Case::Snake),
265        Span::call_site(),
266    );
267
268    let mut externs = Vec::new();
269
270    let mut links = Vec::new();
271
272    // Iterate over the items in the impl block to find methods
273    let generated_methods = input
274        .items
275        .iter()
276        .filter_map(|item| {
277            if let syn::ImplItem::Fn(method) = item {
278                let generics = &method.sig.generics;
279                let method_name = &method.sig.ident;
280                let method_args: Vec<_> = method
281                    .sig
282                    .inputs
283                    .iter()
284                    .skip(1) // Skip &caller
285                    .map(|arg| {
286                        if let FnArg::Typed(pat_type) = arg {
287                            pat_type.to_token_stream()
288                        } else {
289                            panic!("Unsupported function argument type");
290                        }
291                    })
292                    .collect();
293                let method_pats: Vec<_> = method
294                    .sig
295                    .inputs
296                    .iter()
297                    .skip(1) // Skip &caller
298                    .map(|arg| {
299                        if let FnArg::Typed(pat_type) = arg {
300                            pat_type.pat.to_token_stream()
301                        } else {
302                            panic!("Unsupported function argument type");
303                        }
304                    })
305                    .collect();
306                let return_type = &method.sig.output;
307                let extern_method_name = Ident::new(
308                    &format!("_plugy_context_{}", method_name),
309                    Span::call_site(),
310                );
311
312                externs.push(quote::quote! {
313                    extern "C" {
314                        fn #extern_method_name(ptr: u64) -> u64;
315                    }
316                });
317
318                let extern_method_name_str = extern_method_name.to_string();
319
320                links.push(quote! {
321                    linker
322                        .func_wrap1_async(
323                            "env",
324                            #extern_method_name_str,
325                            move |mut caller: plugy::runtime::Caller<_>,
326                                ptr: u64|
327                                -> Box<dyn std::future::Future<Output = u64> + Send> {
328                                use plugy::core::bitwise::{from_bitwise, into_bitwise};
329                                Box::new(async move {
330                                    let store = caller.data().clone().unwrap();
331                                    let plugy::runtime::RuntimeCaller {
332                                        memory,
333                                        alloc_fn,
334                                        dealloc_fn,
335                                        plugin
336                                    } = store;
337
338                                    let (ptr, len) = from_bitwise(ptr);
339                                    let mut buffer = vec![0u8; len as _];
340                                    memory.read(&mut caller, ptr as _, &mut buffer).unwrap();
341                                    dealloc_fn
342                                        .call_async(&mut caller, into_bitwise(ptr, len))
343                                        .await
344                                        .unwrap();
345                                    let (#(#method_pats),*) = bincode::deserialize(&buffer).unwrap();
346                                    let buffer =
347                                        bincode::serialize(&#struct_name::#method_name(&mut caller, #(#method_pats),*).await)
348                                            .unwrap();
349                                    let ptr = alloc_fn
350                                        .call_async(&mut caller, buffer.len() as _)
351                                        .await
352                                        .unwrap();
353                                    memory.write(&mut caller, ptr as _, &buffer).unwrap();
354                                    into_bitwise(ptr, buffer.len() as _)
355                                })
356                            },
357                        )
358                        .unwrap();
359                });
360
361                Some(quote! {
362                    #[allow(unused_variables)]
363                    pub fn #method_name #generics (#(#method_args),*) #return_type {
364                        #[cfg(target_arch = "wasm32")]
365                        {
366                            let args = (#(#method_pats),*);
367                            let ptr = plugy::core::guest::write_msg(&args);
368                            unsafe { plugy::core::guest::read_msg(#extern_method_name(ptr)) }
369                        }
370                        #[cfg(not(target_arch = "wasm32"))]
371                        panic!("You are trying to call wasm methods outside of wasm32")
372                    }
373                })
374            } else {
375                None
376            }
377        })
378        .collect::<Vec<_>>();
379    // Generate the code for the context methods
380    let generated = quote::quote! {
381        #[cfg(not(target_arch = "wasm32"))]
382        #input
383        #[cfg(not(target_arch = "wasm32"))]
384        impl plugy::runtime::Context<#data_ident> for #struct_name {
385            fn link(&self, linker: &mut plugy::runtime::Linker<plugy::runtime::Plugin<#data_ident>>) {
386                #(#links)*
387            }
388        }
389
390        impl #struct_name {
391            pub fn get(&self) -> #mod_name::sync::#struct_name {
392                #mod_name::sync::#struct_name
393            }
394        }
395        pub mod #mod_name {
396            pub mod sync {
397                #[cfg(target_arch = "wasm32")]
398                #(#externs)*
399
400                pub struct #struct_name;
401                impl #struct_name {
402                    #(#generated_methods)*
403                }
404        }
405    }
406    };
407
408    // Return the generated code as a TokenStream
409    generated.into()
410}