Skip to main content

async_rt_macros/
lib.rs

1//! Attribute macros for `async-rt`.
2
3use proc_macro::TokenStream;
4use proc_macro_crate::{FoundCrate, crate_name};
5use proc_macro2::{Span, TokenStream as TokenStream2};
6use quote::{format_ident, quote};
7use syn::parse::Parser;
8use syn::punctuated::Punctuated;
9use syn::{Error, Expr, ExprLit, ItemFn, Lit, Meta, Token, parse2, spanned::Spanned};
10
11#[derive(Clone, Copy)]
12enum AttributeKind {
13    Main,
14    Test,
15}
16
17impl AttributeKind {
18    fn is_test(self) -> bool {
19        matches!(self, Self::Test)
20    }
21}
22
23#[derive(Clone, Copy, Default)]
24enum Executor {
25    #[default]
26    Global,
27    Tokio,
28    Smol,
29    Compio,
30    ThreadPool,
31    Lite,
32}
33
34impl Executor {
35    fn parse(value: &str, span: Span) -> syn::Result<Self> {
36        match value {
37            "global" => Ok(Self::Global),
38            "tokio" => Ok(Self::Tokio),
39            "smol" => Ok(Self::Smol),
40            "compio" => Ok(Self::Compio),
41            "threadpool" | "thread_pool" => Ok(Self::ThreadPool),
42            "lite" => Ok(Self::Lite),
43            _ => Err(Error::new(
44                span,
45                "unknown executor. expected `global`, `tokio`, `smol`, `compio`, `threadpool`, or `lite`",
46            )),
47        }
48    }
49
50    fn drive(
51        self,
52        kind: AttributeKind,
53        runtime_crate: &TokenStream2,
54        body: &syn::Block,
55    ) -> TokenStream2 {
56        let builtin_executor = match self {
57            Self::Tokio => quote!(#runtime_crate::global::BuiltinExecutor::Tokio),
58            Self::Smol => quote!(#runtime_crate::global::BuiltinExecutor::Smol),
59            Self::Compio => quote!(#runtime_crate::global::BuiltinExecutor::Compio),
60            Self::ThreadPool => quote!(#runtime_crate::global::BuiltinExecutor::ThreadPool),
61            Self::Lite => quote!(#runtime_crate::global::BuiltinExecutor::Lite),
62            Self::Global => unreachable!("the global executor must be resolved before expansion"),
63        };
64        let create_executor = match self {
65            Self::Tokio if kind.is_test() => quote! {
66                #runtime_crate::rt::tokio::TokioRuntimeExecutor::with_single_thread()
67                    .expect("async-rt failed to create the Tokio test runtime")
68            },
69            Self::Tokio => quote! {
70                #runtime_crate::rt::tokio::TokioRuntimeExecutor::with_multi_thread()
71                    .expect("async-rt failed to create the Tokio runtime")
72            },
73            Self::Smol => quote! {
74                #runtime_crate::rt::smol::SmolRuntimeExecutor::new()
75            },
76            Self::Compio => quote! {
77                #runtime_crate::rt::compio::CompioRuntimeExecutor::new()
78                    .expect("async-rt failed to create the Compio runtime")
79            },
80            Self::ThreadPool => quote! {
81                #runtime_crate::rt::threadpool::ThreadPoolExecutor
82            },
83            Self::Lite => quote! {
84                #runtime_crate::rt::lite::LiteExecutor
85            },
86            Self::Global => unreachable!("the global executor must be resolved before expansion"),
87        };
88
89        quote! {
90            let __async_rt_body = async move #body;
91            #[allow(
92                clippy::diverging_sub_expression,
93                clippy::expect_used,
94                clippy::needless_return,
95                clippy::unwrap_in_result
96            )]
97            {
98                let __async_rt_executor = #runtime_crate::global::ConfiguredExecutor::with_task_executor(
99                    #create_executor,
100                    #builtin_executor,
101                );
102                return #runtime_crate::ExecutorBlockOn::block_on(
103                    &__async_rt_executor,
104                    __async_rt_body,
105                );
106            }
107        }
108    }
109}
110
111#[derive(Default)]
112struct Arguments {
113    executor: Executor,
114    executor_set: bool,
115    driver: Option<Expr>,
116}
117
118impl Arguments {
119    fn parse(tokens: TokenStream2) -> syn::Result<Self> {
120        let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
121        let arguments = parser.parse2(tokens)?;
122        let mut result = Self::default();
123
124        for argument in arguments {
125            let Meta::NameValue(argument) = argument else {
126                return Err(Error::new(
127                    argument.span(),
128                    "expected `executor = \"...\"` or `driver = ...`",
129                ));
130            };
131
132            if argument.path.is_ident("driver") {
133                if result.driver.is_some() {
134                    return Err(Error::new(
135                        argument.path.span(),
136                        "duplicate `driver` option",
137                    ));
138                }
139                if result.executor_set {
140                    return Err(Error::new(
141                        argument.path.span(),
142                        "`driver` and `executor` cannot be used together",
143                    ));
144                }
145
146                result.driver = Some(argument.value);
147                continue;
148            }
149
150            if !argument.path.is_ident("executor") {
151                return Err(Error::new(
152                    argument.path.span(),
153                    "unknown option. expected `executor` or `driver`",
154                ));
155            }
156            if result.executor_set {
157                return Err(Error::new(
158                    argument.path.span(),
159                    "duplicate `executor` option",
160                ));
161            }
162            if result.driver.is_some() {
163                return Err(Error::new(
164                    argument.path.span(),
165                    "`driver` and `executor` cannot be used together",
166                ));
167            }
168
169            let (value, span) = match argument.value {
170                Expr::Lit(ExprLit {
171                    lit: Lit::Str(value),
172                    ..
173                }) => (value.value(), value.span()),
174                Expr::Path(value) if value.path.get_ident().is_some() => {
175                    let ident = value.path.get_ident().expect("checked above");
176                    (ident.to_string(), ident.span())
177                }
178                value => {
179                    return Err(Error::new(
180                        value.span(),
181                        "executor must be a string or identifier",
182                    ));
183                }
184            };
185
186            result.executor = Executor::parse(&value, span)?;
187            result.executor_set = true;
188        }
189
190        Ok(result)
191    }
192}
193
194macro_rules! entry_points {
195    ($main:ident, $test:ident, $executor:ident) => {
196        /// Runs an async function as a synchronous entry point.
197        ///
198        /// The `async-rt` crate selects the default executor when it re-exports
199        /// this macro. A built-in executor can be selected explicitly with, for
200        /// example, `#[async_rt::main(executor = "compio")]`.
201        ///
202        /// An explicit selection controls the runtime driving this function and the
203        /// executor used by `async_rt::task`.
204        ///
205        /// A custom driver can be supplied with `driver = expression`. It drives the
206        /// annotated future without changing the executor used by `async_rt::task`.
207        #[proc_macro_attribute]
208        pub fn $main(arguments: TokenStream, item: TokenStream) -> TokenStream {
209            expand(
210                arguments,
211                item,
212                AttributeKind::Main,
213                Some(Executor::$executor),
214            )
215        }
216
217        /// Runs an async function as a synchronous test.
218        ///
219        /// The `async-rt` crate selects the default executor when it re-exports
220        /// this macro. A built-in executor can be selected explicitly with, for
221        /// example, `#[async_rt::test(executor = "tokio")]`.
222        ///
223        /// An explicit selection controls the runtime driving this function and the
224        /// executor used by `async_rt::task`.
225        /// Tests using different executors in the same binary run one at a time.
226        ///
227        /// A custom driver can be supplied with `driver = expression`. It drives the
228        /// annotated future without changing the executor used by `async_rt::task`.
229        #[proc_macro_attribute]
230        pub fn $test(arguments: TokenStream, item: TokenStream) -> TokenStream {
231            expand(
232                arguments,
233                item,
234                AttributeKind::Test,
235                Some(Executor::$executor),
236            )
237        }
238    };
239}
240
241entry_points!(main_tokio, test_tokio, Tokio);
242entry_points!(main_smol, test_smol, Smol);
243entry_points!(main_compio, test_compio, Compio);
244entry_points!(main_threadpool, test_threadpool, ThreadPool);
245entry_points!(main_lite, test_lite, Lite);
246
247/// Runs an async function when no default runtime is enabled.
248#[proc_macro_attribute]
249pub fn main_fail(arguments: TokenStream, item: TokenStream) -> TokenStream {
250    expand(arguments, item, AttributeKind::Main, None)
251}
252
253/// Runs an async test when no default runtime is enabled.
254#[proc_macro_attribute]
255pub fn test_fail(arguments: TokenStream, item: TokenStream) -> TokenStream {
256    expand(arguments, item, AttributeKind::Test, None)
257}
258
259macro_rules! failure_entry_points {
260    ($main:ident, $test:ident, $message:literal) => {
261        /// Emits an error because no supported runtime is available.
262        #[proc_macro_attribute]
263        pub fn $main(_arguments: TokenStream, _item: TokenStream) -> TokenStream {
264            Error::new(Span::call_site(), $message)
265                .into_compile_error()
266                .into()
267        }
268
269        /// Emits an error because no supported test runtime is available.
270        #[proc_macro_attribute]
271        pub fn $test(_arguments: TokenStream, _item: TokenStream) -> TokenStream {
272            Error::new(Span::call_site(), $message)
273                .into_compile_error()
274                .into()
275        }
276    };
277}
278
279failure_entry_points!(
280    main_wasm_fail,
281    test_wasm_fail,
282    "async-rt's `main` and `test` macros do not yet support wasm32"
283);
284
285fn expand(
286    arguments: TokenStream,
287    item: TokenStream,
288    kind: AttributeKind,
289    default_executor: Option<Executor>,
290) -> TokenStream {
291    expand_inner(arguments.into(), item.into(), kind, default_executor)
292        .unwrap_or_else(Error::into_compile_error)
293        .into()
294}
295
296fn expand_inner(
297    arguments: TokenStream2,
298    item: TokenStream2,
299    kind: AttributeKind,
300    default_executor: Option<Executor>,
301) -> syn::Result<TokenStream2> {
302    let arguments = Arguments::parse(arguments)?;
303    let mut function: ItemFn = parse2(item)?;
304
305    if function.sig.asyncness.take().is_none() {
306        return Err(Error::new(
307            function.sig.fn_token.span,
308            "the `async` keyword is required",
309        ));
310    }
311
312    let runtime_crate = runtime_crate()?;
313    let test_attribute = match kind {
314        AttributeKind::Main => TokenStream2::new(),
315        AttributeKind::Test => quote!(#[::core::prelude::v1::test]),
316    };
317    let attributes = function.attrs;
318    let visibility = function.vis;
319    let signature = function.sig;
320    let body = function.block;
321    let drive = match arguments.driver {
322        Some(driver) => drive_custom(&runtime_crate, &body, driver),
323        None => {
324            let executor = match arguments.executor {
325                Executor::Global => default_executor.ok_or_else(|| {
326                    Error::new(
327                        Span::call_site(),
328                        "async-rt's `main` and `test` macros require a default runtime feature, an explicit executor, or a custom `driver`",
329                    )
330                })?,
331                executor => executor,
332            };
333            executor.drive(kind, &runtime_crate, &body)
334        }
335    };
336
337    Ok(quote! {
338        #(#attributes)*
339        #test_attribute
340        #visibility #signature {
341            #drive
342        }
343    })
344}
345
346fn drive_custom(runtime_crate: &TokenStream2, body: &syn::Block, driver: Expr) -> TokenStream2 {
347    quote! {
348        let __async_rt_body = async move #body;
349        let __async_rt_executor = #runtime_crate::global::ConfiguredExecutor::new(#driver);
350        return #runtime_crate::ExecutorBlockOn::block_on(
351            &__async_rt_executor,
352            __async_rt_body,
353        );
354    }
355}
356
357fn runtime_crate() -> syn::Result<TokenStream2> {
358    match crate_name("async-rt") {
359        Ok(FoundCrate::Itself) => Ok(quote!(::async_rt)),
360        Ok(FoundCrate::Name(name)) => {
361            let name = format_ident!("{}", name.replace('-', "_"));
362            Ok(quote!(::#name))
363        }
364        Err(error) => Err(Error::new(
365            Span::call_site(),
366            format!("could not find the `async-rt` crate: {error}"),
367        )),
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::{Arguments, Executor};
374    use quote::quote;
375    use syn::Expr;
376
377    #[test]
378    fn parses_string_and_identifier_executors() {
379        assert!(matches!(
380            Arguments::parse(quote!(executor = "compio"))
381                .unwrap()
382                .executor,
383            Executor::Compio
384        ));
385        assert!(matches!(
386            Arguments::parse(quote!(executor = tokio)).unwrap().executor,
387            Executor::Tokio
388        ));
389        assert!(matches!(
390            Arguments::parse(quote!(executor = "lite"))
391                .unwrap()
392                .executor,
393            Executor::Lite
394        ));
395    }
396
397    #[test]
398    fn rejects_unknown_options_and_executors() {
399        assert!(Arguments::parse(quote!(flavor = "current_thread")).is_err());
400        assert!(Arguments::parse(quote!(executor = "unknown")).is_err());
401    }
402
403    #[test]
404    fn parses_custom_driver_expression() {
405        let arguments = Arguments::parse(quote!(driver = custom::executor())).unwrap();
406        assert!(matches!(arguments.driver, Some(Expr::Call(_))));
407    }
408
409    #[test]
410    fn rejects_duplicate_or_conflicting_driver_options() {
411        assert!(Arguments::parse(quote!(driver = first(), driver = second())).is_err());
412        assert!(Arguments::parse(quote!(executor = "tokio", driver = custom())).is_err());
413        assert!(Arguments::parse(quote!(driver = custom(), executor = "tokio")).is_err());
414    }
415}