vexide_macro/
lib.rs

1//! This crate provides procedural macros for [vexide](https://vexide.dev) crates.
2
3use parse::{Attrs, MacroOpts};
4use proc_macro::TokenStream;
5use quote::quote;
6use syn::{ItemFn, Signature, parse_macro_input};
7
8mod parse;
9
10const NO_SYNC_ERR: &str = "The vexide entrypoint must be marked `async`.";
11const NO_UNSAFE_ERR: &str = "The vexide entrypoint must be not marked `unsafe`.";
12const WRONG_ARGS_ERR: &str = "The vexide entrypoint must take a single parameter of type `vexide_devices::peripherals::Peripherals`";
13
14fn verify_function_sig(sig: &Signature) -> Result<(), syn::Error> {
15    let mut error = None;
16
17    if sig.asyncness.is_none() {
18        let message = syn::Error::new_spanned(sig, NO_SYNC_ERR);
19        error.replace(message);
20    }
21    if sig.unsafety.is_some() {
22        let message = syn::Error::new_spanned(sig, NO_UNSAFE_ERR);
23        match error {
24            Some(ref mut e) => e.combine(message),
25            None => {
26                error.replace(message);
27            }
28        }
29    }
30    if sig.inputs.len() != 1 {
31        let message = syn::Error::new_spanned(sig, WRONG_ARGS_ERR);
32        match error {
33            Some(ref mut e) => e.combine(message),
34            None => {
35                error.replace(message);
36            }
37        }
38    }
39
40    match error {
41        Some(e) => Err(e),
42        None => Ok(()),
43    }
44}
45
46fn make_code_sig(opts: MacroOpts) -> proc_macro2::TokenStream {
47    let sig = if let Some(code_sig) = opts.code_sig {
48        quote! { #code_sig }
49    } else {
50        quote! {  ::vexide::program::CodeSignature::new(
51            ::vexide::program::ProgramType::User,
52            ::vexide::program::ProgramOwner::Partner,
53            ::vexide::program::ProgramOptions::empty(),
54        ) }
55    };
56
57    quote! {
58        #[cfg_attr(target_os = "vexos", unsafe(link_section = ".code_signature"))]
59        #[used] // This is needed to prevent the linker from removing this object in release builds
60        #[unsafe(no_mangle)]
61        static __VEXIDE_CODE_SIGNATURE: ::vexide::program::CodeSignature = #sig;
62    }
63}
64
65fn make_entrypoint(inner: &ItemFn, opts: MacroOpts) -> proc_macro2::TokenStream {
66    match verify_function_sig(&inner.sig) {
67        Ok(()) => {}
68        Err(e) => return e.to_compile_error(),
69    }
70    let inner_ident = inner.sig.ident.clone();
71    let ret_type = match &inner.sig.output {
72        syn::ReturnType::Default => quote! { () },
73        syn::ReturnType::Type(_, ty) => quote! { #ty },
74    };
75
76    let banner_theme = if let Some(theme) = opts.banner_theme {
77        quote! { #theme }
78    } else {
79        quote! { ::vexide::startup::banner::themes::THEME_DEFAULT }
80    };
81
82    let banner_print = if opts.banner_enabled {
83        quote! {
84            ::vexide::startup::banner::print(#banner_theme);
85        }
86    } else {
87        quote! {}
88    };
89
90    quote! {
91        fn main() -> #ret_type {
92            unsafe {
93                ::vexide::startup::startup();
94            }
95
96            #banner_print
97            #inner
98
99            ::vexide::runtime::block_on(
100                #inner_ident(::vexide::peripherals::Peripherals::take().unwrap())
101            )
102        }
103    }
104}
105
106/// vexide's entrypoint macro
107///
108/// Marks a function as the entrypoint for a vexide program. When the program is started, the `main`
109/// function will be called with a single argument of type `Peripherals` which allows access to
110/// device peripherals like motors, sensors, and the display.
111///
112/// The `main` function must be marked `async` and must not be marked `unsafe`. It may return any
113/// type that implements `Termination`, which includes `()`, `!`, and `Result`.
114///
115/// # Parameters
116///
117/// The `main` attribute can be provided with parameters that alter the behavior of the program.
118///
119/// - `banner`: Allows for disabling or using a custom banner theme. When `enabled = false` the
120///   banner will be disabled. `theme` can be set to a custom `BannerTheme` struct.
121/// - `code_sig`: Allows using a custom `CodeSignature` struct to configure program behavior.
122///
123/// # Examples
124///
125/// The most basic usage of the `main` attribute is to mark an async function as the entrypoint for
126/// a vexide program. The function must take a single argument of type `Peripherals`.
127///
128/// ```
129/// use std::fmt::Write;
130///
131/// use vexide::prelude::*;
132///
133/// #[vexide::main]
134/// async fn main(mut peripherals: Peripherals) {
135///     write!(peripherals.display, "Hello, vexide!").unwrap();
136/// }
137/// ```
138///
139/// The `main` attribute can also be provided with parameters to customize the behavior of the
140/// program.
141///
142/// This includes disabling the banner or using a custom banner theme:
143///
144/// ```
145/// use vexide::prelude::*;
146///
147/// #[vexide::main(banner(enabled = false))]
148/// async fn main(_p: Peripherals) {
149///     println!("This is the only serial output from this program!")
150/// }
151/// ```
152///
153/// ```
154/// use vexide::{prelude::*, startup::banner::themes::THEME_SYNTHWAVE};
155///
156/// #[vexide::main(banner(theme = THEME_SYNTHWAVE))]
157/// async fn main(_p: Peripherals) {
158///     println!("This program has a synthwave themed banner!")
159/// }
160/// ```
161///
162/// A custom code signature may be used to further configure the behavior of the program.
163///
164/// ```
165/// use vexide::{
166///     prelude::*,
167///     program::{CodeSignature, ProgramOptions, ProgramOwner, ProgramType},
168/// };
169///
170/// static CODE_SIG: CodeSignature = CodeSignature::new(
171///     ProgramType::User,
172///     ProgramOwner::Partner,
173///     ProgramOptions::empty(),
174/// );
175///
176/// #[vexide::main(code_sig = CODE_SIG)]
177/// async fn main(_p: Peripherals) {
178///     println!("Hello world!")
179/// }
180/// ```
181#[proc_macro_attribute]
182pub fn main(attrs: TokenStream, item: TokenStream) -> TokenStream {
183    let item = parse_macro_input!(item as ItemFn);
184    let opts = MacroOpts::from(parse_macro_input!(attrs as Attrs));
185
186    let entrypoint = make_entrypoint(&item, opts.clone());
187    let code_signature = make_code_sig(opts);
188
189    quote! {
190        const _: () = {
191            #code_signature
192        };
193
194        #entrypoint
195    }
196    .into()
197}
198
199/// Prints a failure message indicating that the required features for the [`main`] macro are not
200/// enabled.
201#[proc_macro_attribute]
202#[doc(hidden)]
203pub fn main_fail(_args: TokenStream, _item: TokenStream) -> TokenStream {
204    syn::Error::new(
205        proc_macro2::Span::call_site(),
206        "The #[vexide::main] macro requires the `core`, `async`, `startup`, and `devices` features to be enabled.",
207    )
208    .to_compile_error()
209    .into()
210}
211
212/// Wraps a Rust unit test in vexide's async runtime.
213///
214/// This macro should be accompanied with an SDK provider capable of running vexide programs on a
215/// host system for unit tests, such as `vex-sdk-mock`.
216#[proc_macro_attribute]
217pub fn test(_attr: TokenStream, item: TokenStream) -> TokenStream {
218    let input = parse_macro_input!(item as ItemFn);
219
220    // Ensure it's async
221    if input.sig.asyncness.is_none() {
222        return syn::Error::new_spanned(input.sig.fn_token, "#[vexide::test] requires an async fn")
223            .to_compile_error()
224            .into();
225    }
226
227    let vis = &input.vis;
228    let ident = &input.sig.ident;
229    let inputs = &input.sig.inputs;
230    let block = &input.block;
231
232    quote! {
233        #[::core::prelude::v1::test]
234        #vis fn #ident() {
235            async fn #ident(#inputs) #block
236
237            ::vexide::runtime::block_on(
238                #ident(unsafe { ::vexide::peripherals::Peripherals::steal() })
239            )
240        }
241    }
242    .into()
243}
244
245/// Prints a failure message indicating that the required features for the [`test`] macro are not
246/// enabled.
247#[proc_macro_attribute]
248#[doc(hidden)]
249pub fn test_fail(_args: TokenStream, _item: TokenStream) -> TokenStream {
250    syn::Error::new(
251        proc_macro2::Span::call_site(),
252        "The #[vexide::test] macro requires the `core`, `async`, `startup`, and `devices` features to be enabled.",
253    )
254    .to_compile_error()
255    .into()
256}
257
258#[cfg(test)]
259mod test {
260    use quote::quote;
261    use syn::{Ident, ItemFn};
262
263    use super::{make_code_sig, make_entrypoint};
264    use crate::{MacroOpts, NO_SYNC_ERR, NO_UNSAFE_ERR, WRONG_ARGS_ERR};
265
266    #[test]
267    fn wraps_main_fn() {
268        let source = quote! {
269            async fn main(_peripherals: Peripherals) {
270                println!("Hello, world!");
271            }
272        };
273
274        let input = syn::parse2::<ItemFn>(source.clone()).unwrap();
275        let output = make_entrypoint(&input, MacroOpts::default());
276
277        assert_eq!(
278            output.to_string(),
279            quote! {
280                fn main() -> () {
281                    unsafe {
282                        ::vexide::startup::startup();
283                    }
284
285                    ::vexide::startup::banner::print(::vexide::startup::banner::themes::THEME_DEFAULT);
286                    #source
287
288                    ::vexide::runtime::block_on(
289                        main(::vexide::peripherals::Peripherals::take().unwrap())
290                    )
291                }
292            }
293            .to_string()
294        );
295    }
296
297    #[test]
298    fn toggles_banner_using_parsed_opts() {
299        let source = quote! {
300            async fn main(_peripherals: Peripherals) {
301                println!("Hello, world!");
302            }
303        };
304        let input = syn::parse2::<ItemFn>(source.clone()).unwrap();
305        let entrypoint = make_entrypoint(
306            &input,
307            MacroOpts {
308                banner_enabled: false,
309                banner_theme: None,
310                code_sig: None,
311            },
312        );
313        assert!(!entrypoint.to_string().contains("banner"));
314
315        let entrypoint = make_entrypoint(
316            &input,
317            MacroOpts {
318                banner_enabled: true,
319                banner_theme: None,
320                code_sig: None,
321            },
322        );
323        assert!(entrypoint.to_string().contains("banner"));
324    }
325
326    #[test]
327    fn uses_custom_code_sig_from_parsed_opts() {
328        let code_sig = make_code_sig(MacroOpts {
329            banner_enabled: false,
330            banner_theme: None,
331            code_sig: Some(Ident::new(
332                "__custom_code_sig_ident__",
333                proc_macro2::Span::call_site(),
334            )),
335        });
336
337        assert!(code_sig.to_string().contains(
338            "static __VEXIDE_CODE_SIGNATURE : :: vexide :: program :: CodeSignature = __custom_code_sig_ident__ ;"
339        ));
340    }
341
342    #[test]
343    fn requires_async() {
344        let source = quote! {
345            fn main(_peripherals: Peripherals) {
346                println!("Hello, world!");
347            }
348        };
349
350        let input = syn::parse2::<ItemFn>(source.clone()).unwrap();
351        let output = make_entrypoint(&input, MacroOpts::default());
352
353        assert!(output.to_string().contains(NO_SYNC_ERR));
354    }
355
356    #[test]
357    fn requires_safe() {
358        let source = quote! {
359            async unsafe fn main(_peripherals: Peripherals) {
360                println!("Hello, world!");
361            }
362        };
363
364        let input = syn::parse2::<ItemFn>(source.clone()).unwrap();
365        let output = make_entrypoint(&input, MacroOpts::default());
366
367        assert!(output.to_string().contains(NO_UNSAFE_ERR));
368    }
369
370    #[test]
371    fn disallows_0_args() {
372        let source = quote! {
373            async fn main() {
374                println!("Hello, world!");
375            }
376        };
377
378        let input = syn::parse2::<ItemFn>(source.clone()).unwrap();
379        let output = make_entrypoint(&input, MacroOpts::default());
380
381        assert!(output.to_string().contains(WRONG_ARGS_ERR));
382    }
383
384    #[test]
385    fn disallows_2_args() {
386        let source = quote! {
387            async fn main(_peripherals: Peripherals, _other: Peripherals) {
388                println!("Hello, world!");
389            }
390        };
391
392        let input = syn::parse2::<ItemFn>(source.clone()).unwrap();
393        let output = make_entrypoint(&input, MacroOpts::default());
394
395        assert!(output.to_string().contains(WRONG_ARGS_ERR));
396    }
397}