1use 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] #[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#[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#[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#[proc_macro_attribute]
217pub fn test(_attr: TokenStream, item: TokenStream) -> TokenStream {
218 let input = parse_macro_input!(item as ItemFn);
219
220 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#[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}