async_spawn_macros/
lib.rs1use 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 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 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 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 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 {}