iridis_file_ext_derive/
lib.rs

1//! This module contains the macros `FileExtPlugin` and `file_ext_plugin(runtime)`.
2//! It's used to generate the necessary boilerplate code for creating a file extension plugin.
3
4extern crate proc_macro;
5
6use proc_macro::TokenStream;
7use quote::quote;
8use syn::{
9    DeriveInput, ImplItem, ItemImpl, ReturnType, Token,
10    parse::{Parse, ParseStream},
11    parse_macro_input,
12    punctuated::Punctuated,
13};
14
15/// Apply this macro to a struct to generate the `C` symbols
16/// and the according `tokio::runtime::Runtime`.
17#[proc_macro_derive(FileExtPlugin)]
18pub fn derive_file_ext_plugin(input: TokenStream) -> TokenStream {
19    let input = parse_macro_input!(input as DeriveInput);
20    let name = input.ident;
21
22    let expanded = quote! {
23        #[cfg(feature = "cdylib")]
24        #[doc(hidden)]
25        #[unsafe(no_mangle)]
26        pub static IRIDIS_FILE_EXT_PLUGIN: DynamicallyLinkedFileExtPluginInstance =
27            || <#name>::new();
28
29        static DEFAULT_TOKIO_RUNTIME: std::sync::LazyLock<iridis_file_ext::prelude::thirdparty::tokio::runtime::Runtime> =
30            std::sync::LazyLock::new(|| {
31                iridis_file_ext::prelude::thirdparty::tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime")
32            });
33
34        fn default_runtime<T: Send + 'static>(
35            task: impl Future<Output = T> + Send + 'static,
36        ) -> iridis_file_ext::prelude::thirdparty::tokio::task::JoinHandle<T> {
37            match iridis_file_ext::prelude::thirdparty::tokio::runtime::Handle::try_current() {
38                Ok(handle) => handle.spawn(task),
39                Err(_) => DEFAULT_TOKIO_RUNTIME.spawn(task),
40            }
41        }
42    };
43
44    TokenStream::from(expanded)
45}
46
47struct MacroArgs {
48    runtime: String,
49}
50
51impl Parse for MacroArgs {
52    fn parse(input: ParseStream) -> syn::Result<Self> {
53        let mut runtime = String::new();
54
55        let vars = Punctuated::<syn::Meta, Token![,]>::parse_terminated(input)?;
56
57        for var in vars {
58            if let syn::Meta::NameValue(name_value) = var {
59                let name = name_value.path.get_ident().unwrap().to_string();
60
61                if name == "runtime" {
62                    if let syn::Expr::Lit(lit) = &name_value.value {
63                        if let syn::Lit::Str(lit_str) = &lit.lit {
64                            runtime = lit_str.value();
65                        }
66                    }
67                }
68            }
69        }
70
71        Ok(MacroArgs { runtime })
72    }
73}
74
75/// Use this macro to mark an `impl` block on a file extension plugin. This will alter
76/// the `new` and `load` methods to return a `tokio::task::JoinHandle` with the provided
77/// runtime. The parameter must be a function that takes an `async` closure and returns
78/// a `JoinHandle`.
79///
80/// ```rust
81/// static DEFAULT_TOKIO_RUNTIME: std::sync::LazyLock<tokio::runtime::Runtime> =
82///     std::sync::LazyLock::new(|| {
83///         tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime")
84///     });
85///
86/// fn default_runtime<T: Send + 'static>(
87///     task: impl Future<Output = T> + Send + 'static,
88/// ) -> tokio::task::JoinHandle<T> {
89///     match tokio::runtime::Handle::try_current() {
90///         Ok(handle) => handle.spawn(task),
91///         Err(_) => DEFAULT_TOKIO_RUNTIME.spawn(task),
92///     }
93/// }
94/// ```
95#[proc_macro_attribute]
96pub fn file_ext_plugin(attr: TokenStream, item: TokenStream) -> TokenStream {
97    let mut impl_block = parse_macro_input!(item as ItemImpl);
98
99    let args = parse_macro_input!(attr as MacroArgs);
100    let runtime_tokens = args.runtime.parse::<proc_macro2::TokenStream>().unwrap();
101
102    for item in &mut impl_block.items {
103        if let ImplItem::Fn(method) = item {
104            let was_async = method.sig.asyncness.is_some();
105            method.sig.asyncness = None;
106
107            let old_block = method.block.clone();
108
109            if was_async {
110                let old_return_type = match &method.sig.output {
111                    ReturnType::Default => quote! { () },
112                    ReturnType::Type(_, ty) => {
113                        if method.sig.ident == "new" {
114                            quote! { iridis_file_ext::prelude::thirdparty::eyre::Result<Box<dyn iridis_file_ext::prelude::FileExtPlugin>> }
115                        } else {
116                            quote! { #ty }
117                        }
118                    }
119                };
120
121                method.sig.output = syn::parse_quote! {
122                    -> iridis_file_ext::prelude::thirdparty::tokio::task::JoinHandle<#old_return_type>
123                };
124
125                if method.sig.ident == "new" {
126                    method.block = syn::parse_quote! {
127                        {
128                            #runtime_tokens(async move {
129                                #old_block.map(|node| Box::new(node) as Box<dyn iridis_file_ext::prelude::FileExtPlugin>)
130                            })
131                        }
132                    };
133                } else if method.sig.ident == "load" {
134                    method.block = syn::parse_quote! {
135                        {
136                            #runtime_tokens(async move {
137                                #old_block
138                            })
139                        }
140                    };
141                }
142            }
143        }
144    }
145
146    quote! {
147        #impl_block
148    }
149    .into()
150}