Skip to main content

async_spawn_macros/
lib.rs

1use darling::{FromMeta, ast::NestedMeta};
2use proc_macro::TokenStream;
3use proc_macro_error::{abort, proc_macro_error};
4use proc_macro2::TokenStream as TokenStream2;
5use quote::quote;
6use syn::{
7    Attribute, Data, DeriveInput, Fields, FnArg, ItemFn, Meta, Variant, parse_macro_input,
8    parse_quote, parse2, spanned::Spanned,
9};
10
11#[derive(FromMeta)]
12struct CallbackArgs {
13    message_type: Option<syn::Path>,
14    abort_with: Option<syn::Expr>,
15}
16
17#[proc_macro_error]
18#[proc_macro_attribute]
19pub fn background_task(args: TokenStream, item: TokenStream) -> TokenStream {
20    let input = parse_macro_input!(item as ItemFn);
21    let attr_args = match NestedMeta::parse_meta_list(args.into()) {
22        Ok(args) => args,
23        Err(e) => abort!(e.span(), "Invalid attribute arguments: {}", e),
24    };
25    let args = match CallbackArgs::from_list(&attr_args) {
26        Ok(args) => args,
27        Err(e) => return TokenStream::from(e.write_errors()),
28    };
29    background_task_impl(input, args).into()
30}
31
32fn background_task_impl(mut function: ItemFn, args: CallbackArgs) -> TokenStream2 {
33    // Only add event sender parameter if event_enum is specified
34    if let Some(ref message_type) = args.message_type {
35        function
36            .sig
37            .inputs
38            .insert(0, event_sender_arg(message_type));
39    }
40
41    let notify_macro = if args.message_type.is_some() {
42        notify_macro()
43    } else {
44        eprintln_notify_macro()
45    };
46
47    let abort_macro = if let Some(ref abort_with) = args.abort_with {
48        abort_macro(abort_with)
49    } else {
50        eprintln_abort_macro()
51    };
52
53    let block = function.block.clone();
54    function.block = parse2(quote! {
55        {
56            #notify_macro
57            #abort_macro
58            #block
59        }
60    })
61    .unwrap_or_else(|e| {
62        abort!(
63            proc_macro2::Span::mixed_site(),
64            "Failed to parse function block: {}",
65            e
66        );
67    });
68    quote! { #function }
69}
70
71fn event_sender_arg(event_type: &syn::Path) -> FnArg {
72    let event_type_ident: syn::Type = syn::parse_quote!(#event_type);
73    parse_quote!(tx: tokio::sync::mpsc::Sender<#event_type_ident>)
74}
75
76fn notify_macro() -> TokenStream2 {
77    quote! {
78        macro_rules! notify {
79            ($variant: expr) => {
80                if let Err(_) = tx.send($variant).await {
81                    return;
82                }
83            }
84        }
85    }
86}
87
88fn eprintln_notify_macro() -> TokenStream2 {
89    quote! {
90        macro_rules! notify {
91            ($($arg:tt)*) => {
92                eprintln!($($arg)*);
93            }
94        }
95    }
96}
97
98fn abort_macro(abort_with: &syn::Expr) -> TokenStream2 {
99    quote! {
100        macro_rules! abort {
101            ($($arg:tt)*) => {
102                {
103                    let message = format!($($arg)*);
104                    let _ = tx.send(#abort_with(message.clone())).await.inspect_err(|_| eprintln!("{}", message));
105                    return;
106                }
107            }
108        }
109    }
110}
111
112fn eprintln_abort_macro() -> TokenStream2 {
113    quote! {
114        macro_rules! abort {
115            ($($arg:tt)*) => {
116                {
117                    eprintln!($($arg)*);
118                    return;
119                }
120            }
121        }
122    }
123}
124
125#[derive(FromMeta)]
126struct EnumArgs {
127    message_type: syn::Path,
128}
129
130#[derive(FromMeta)]
131struct VariantArgs {
132    callback: syn::Ident,
133}
134
135#[proc_macro_error]
136#[proc_macro_derive(TaskSpec, attributes(taskspec))]
137pub fn task_spec_derive(input: TokenStream) -> TokenStream {
138    let input = parse_macro_input!(input as DeriveInput);
139    task_spec_impl(input).into()
140}
141
142fn task_spec_impl(input: DeriveInput) -> TokenStream2 {
143    let enum_name = &input.ident;
144
145    // Parse the event_type from the derive macro arguments
146    let event_type = extract_message_type(&input.attrs);
147
148    let variants = match input.data {
149        Data::Enum(data_enum) => data_enum.variants,
150        _ => abort!(
151            input.ident.span(),
152            "TaskSpec can only be derived for enums, found {}",
153            match input.data {
154                Data::Struct(_) => "struct",
155                Data::Union(_) => "union",
156                _ => "unknown type",
157            }
158        ),
159    };
160
161    let spawn_arms = variants
162        .iter()
163        .map(|v| generate_spawn_arm(v, event_type.is_some()))
164        .collect::<Vec<_>>();
165
166    if let Some(event_type) = event_type {
167        let event_type_ident: syn::Type = syn::parse_quote!(#event_type);
168        quote! {
169            impl #enum_name {
170                /// Spawns a background task for this task specification.
171                ///
172                /// This method consumes `self` to move the contained data into the spawned task.
173                /// Returns a `JoinHandle` that can be used to await completion of the background task.
174                /// Returns `None` if no callback is specified for the variant.
175                pub fn spawn_task(self, tx: &tokio::sync::mpsc::Sender<#event_type_ident>) -> Option<tokio::task::JoinHandle<()>> {
176                    match self {
177                        #(#spawn_arms)*
178                    }
179                }
180            }
181        }
182    } else {
183        quote! {
184            impl #enum_name {
185                /// Spawns a background task for this task specification.
186                ///
187                /// This method consumes `self` to move the contained data into the spawned task.
188                /// Returns a `JoinHandle` that can be used to await completion of the background task.
189                /// Returns `None` if no callback is specified for the variant.
190                pub fn spawn_task(self) -> Option<tokio::task::JoinHandle<()>> {
191                    match self {
192                        #(#spawn_arms)*
193                    }
194                }
195            }
196        }
197    }
198}
199
200fn generate_spawn_arm(variant: &Variant, has_sender: bool) -> TokenStream2 {
201    let variant_name = &variant.ident;
202    let callback_name = extract_callback_name(&variant.attrs);
203
204    match &variant.fields {
205        Fields::Unit => {
206            if let Some(callback) = callback_name {
207                if has_sender {
208                    quote! {
209                        Self::#variant_name => {
210                            let tx_clone = tx.clone();
211                            Some(tokio::task::spawn(async move {
212                                #callback(tx_clone).await;
213                            }))
214                        }
215                    }
216                } else {
217                    quote! {
218                        Self::#variant_name => {
219                            Some(tokio::task::spawn(async move {
220                                #callback().await;
221                            }))
222                        }
223                    }
224                }
225            } else {
226                quote! {
227                    Self::#variant_name => None
228                }
229            }
230        }
231        Fields::Unnamed(fields) => {
232            let field_names: Vec<syn::Ident> = (0..fields.unnamed.len())
233                .map(|i| syn::Ident::new(&format!("field_{}", i), proc_macro2::Span::mixed_site()))
234                .collect();
235
236            let pattern = quote! { Self::#variant_name(#(#field_names),*) };
237
238            if let Some(callback) = callback_name {
239                if has_sender {
240                    let args = quote! { tx_clone, #(#field_names),* };
241                    quote! {
242                        #pattern => {
243                            let tx_clone = tx.clone();
244                            Some(tokio::task::spawn(async move {
245                                #callback(#args).await;
246                            }))
247                        }
248                    }
249                } else {
250                    let args = quote! { #(#field_names),* };
251                    quote! {
252                        #pattern => {
253                            Some(tokio::task::spawn(async move {
254                                #callback(#args).await;
255                            }))
256                        }
257                    }
258                }
259            } else {
260                quote! {
261                    #pattern => None
262                }
263            }
264        }
265        Fields::Named(fields) => {
266            let field_names: Vec<&syn::Ident> = fields
267                .named
268                .iter()
269                .filter_map(|f| f.ident.as_ref())
270                .collect();
271
272            if field_names.len() != fields.named.len() {
273                abort!(
274                    variant.ident.span(),
275                    "All named fields must have identifiers"
276                );
277            }
278
279            let pattern = quote! { Self::#variant_name { #(#field_names),* } };
280
281            if let Some(callback) = callback_name {
282                if has_sender {
283                    let args = quote! { tx_clone, #(#field_names),* };
284                    quote! {
285                        #pattern => {
286                            let tx_clone = tx.clone();
287                            Some(tokio::task::spawn(async move {
288                                #callback(#args).await;
289                            }))
290                        }
291                    }
292                } else {
293                    let args = quote! { #(#field_names),* };
294                    quote! {
295                        #pattern => {
296                            Some(tokio::task::spawn(async move {
297                                #callback(#args).await;
298                            }))
299                        }
300                    }
301                }
302            } else {
303                quote! {
304                    #pattern => None
305                }
306            }
307        }
308    }
309}
310
311fn extract_message_type(attrs: &[Attribute]) -> Option<syn::Path> {
312    for attr in attrs {
313        if attr.path().is_ident("taskspec") {
314            if let Meta::List(meta_list) = &attr.meta {
315                let nested = match NestedMeta::parse_meta_list(meta_list.tokens.clone()) {
316                    Ok(nested) => nested,
317                    Err(e) => abort!(attr.span(), "Invalid taskspec attribute syntax: {}", e),
318                };
319                let args = match EnumArgs::from_list(&nested) {
320                    Ok(args) => args,
321                    Err(e) => abort!(attr.span(), "Invalid taskspec attribute: {}", e),
322                };
323                return Some(args.message_type);
324            }
325        }
326    }
327    None
328}
329
330fn extract_callback_name(attrs: &[Attribute]) -> Option<syn::Ident> {
331    for attr in attrs {
332        if attr.path().is_ident("taskspec") {
333            if let Meta::List(meta_list) = &attr.meta {
334                let nested = match NestedMeta::parse_meta_list(meta_list.tokens.clone()) {
335                    Ok(nested) => nested,
336                    Err(e) => abort!(attr.span(), "Invalid taskspec attribute syntax: {}", e),
337                };
338                let args = match VariantArgs::from_list(&nested) {
339                    Ok(args) => args,
340                    Err(e) => abort!(attr.span(), "Invalid taskspec attribute: {}", e),
341                };
342                return Some(args.callback);
343            }
344        }
345    }
346    None
347}
348
349#[cfg(test)]
350mod tests {}