Skip to main content

cntrlr_macros/
lib.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright 2020 Branan Riley <me@branan.info>
3
4//! Support macros for Cntrlr
5
6#![deny(missing_docs)]
7
8use proc_macro::TokenStream;
9use quote::quote;
10use syn::{
11    parse::{Error as ParseError, Parse, ParseStream, Result},
12    parse_macro_input,
13    punctuated::Punctuated,
14    spanned::Spanned,
15    token::Comma,
16    FnArg, Ident, ItemFn, ItemUse, Pat, ReturnType, Type,
17};
18
19struct IdentList {
20    boards: Punctuated<Ident, Comma>,
21}
22
23impl Parse for IdentList {
24    fn parse(input: ParseStream) -> Result<Self> {
25        Ok(IdentList {
26            boards: input.parse_terminated(Ident::parse)?,
27        })
28    }
29}
30
31/// Add a function to the prelude
32///
33/// This macro generates the appropriate attributes for a function to
34/// be added to the Cntrlr prelude.
35#[proc_macro_attribute]
36pub fn prelude_fn(args: TokenStream, input: TokenStream) -> TokenStream {
37    let input_use = parse_macro_input!(input as ItemUse);
38    let boards = parse_macro_input!(args as IdentList);
39
40    let cfgs = boards
41        .boards
42        .iter()
43        .map(|board| {
44            let board_name = format!("{}", board);
45            quote!(board = #board_name)
46        })
47        .collect::<Vec<_>>();
48
49    quote!(
50        #[cfg(any(#(#cfgs),*, doc))]
51        #[cfg_attr(feature = "doc-cfg", doc(cfg(any(#(#cfgs),*))))]
52        #input_use
53    )
54    .into()
55}
56
57/// Add a board function to a module
58///
59/// This macro generates an implementation of the marked function,
60/// which defers to the appropriate board implementation based on
61/// compile-time configuration.
62#[proc_macro_attribute]
63pub fn board_fn(args: TokenStream, input: TokenStream) -> TokenStream {
64    let input_fn = parse_macro_input!(input as ItemFn);
65    let attrs = &input_fn.attrs;
66    let vis = &input_fn.vis;
67    let sig = &input_fn.sig;
68    let fn_name = &sig.ident;
69    let boards = parse_macro_input!(args as IdentList);
70    let module = &boards.boards[0];
71
72    let cfgs = boards
73        .boards
74        .iter()
75        .skip(1)
76        .map(|board| {
77            let board_name = format!("{}", board);
78            quote!(board = #board_name)
79        })
80        .collect::<Vec<_>>();
81
82    // This isn't super robust, but good enough for what we need to do.
83    let args = sig
84        .inputs
85        .iter()
86        .filter_map(|input| match input {
87            FnArg::Receiver(_) => None,
88            FnArg::Typed(pat) => match *pat.pat {
89                Pat::Ident(ref ident) => Some(ident.clone()),
90                _ => None,
91            },
92        })
93        .collect::<Vec<_>>();
94
95    let impls = boards
96        .boards
97        .iter()
98        .skip(1)
99        .map(|board| {
100            let board_name = format!("{}", board);
101            quote!(
102            #[cfg(board = #board_name)]
103            {
104                crate::hw::board::#board::#module::#fn_name(#(#args),*)
105            }
106            )
107        })
108        .collect::<Vec<_>>();
109
110    quote!(
111        #[cfg(any(#(#cfgs),*, doc))]
112        #[cfg_attr(feature = "doc-cfg", doc(cfg(any(#(#cfgs),*))))]
113        #(#attrs)*
114        #vis #sig {
115            #(#impls)*
116        }
117    )
118    .into()
119}
120
121/// The main task of a Cntrlr application
122///
123/// This macro defines a startup routine named which creates an
124/// executor and adds the marked function as a task. If any enabled
125/// Cntrlr features require background tasks (such as USB), those
126/// tasks will also be added to the executor.
127#[proc_macro_attribute]
128pub fn entry(_args: TokenStream, input: TokenStream) -> TokenStream {
129    let input_fn = parse_macro_input!(input as ItemFn);
130    let sig = &input_fn.sig;
131    let fn_name = &sig.ident;
132
133    let main_is_valid = sig.asyncness.is_some()
134        && sig.generics.params.is_empty()
135        && sig.generics.where_clause.is_none()
136        && sig.inputs.is_empty()
137        && match sig.output {
138            ReturnType::Type(_, ref typ) => matches!(**typ, Type::Never(_)),
139            ReturnType::Default => false,
140        };
141    if !main_is_valid {
142        return ParseError::new(
143            input_fn.sig.span(),
144            format!(
145                "Cntrlr entry function must be of the form `async fn {}() -> !`",
146                fn_name
147            ),
148        )
149        .to_compile_error()
150        .into();
151    }
152
153    quote!(
154        #[export_name = "__cntrlr_main"]
155        // This is flagged as unsafe just in case the input_fn is
156        // unsafe, so that we can call it.
157        pub unsafe extern "C" fn #fn_name() -> ! {
158            #input_fn
159
160            let mut executor =  ::cntrlr::task::Executor::new();
161            executor.add_task(#fn_name());
162            // TODO: Add tasks for device drivers as needed.
163            executor.run()
164        }
165    )
166    .into()
167}
168
169/// Override the default task initialization
170///
171/// This allows you control of Cntrlr application startup, including
172/// whether or not to use an async executor and which tasks are added
173/// to it.
174#[proc_macro_attribute]
175pub fn raw_entry(_args: TokenStream, input: TokenStream) -> TokenStream {
176    let input_fn = parse_macro_input!(input as ItemFn);
177    let sig = &input_fn.sig;
178    let fn_name = &sig.ident;
179
180    let main_is_valid = sig.asyncness.is_none()
181        && match sig.abi {
182            None => false,
183            Some(ref abi) => match abi.name {
184                None => true,
185                Some(ref abi) => abi.value() == "C",
186            },
187        }
188        && sig.generics.params.is_empty()
189        && sig.generics.where_clause.is_none()
190        && sig.inputs.is_empty()
191        && match sig.output {
192            ReturnType::Type(_, ref typ) => matches!(**typ, Type::Never(_)),
193            ReturnType::Default => false,
194        };
195    if !main_is_valid {
196        return ParseError::new(
197            input_fn.sig.span(),
198            format!(
199                "Cntrlr entry function must be of the form `extern \"C\" fn {}() -> !`",
200                fn_name
201            ),
202        )
203        .to_compile_error()
204        .into();
205    }
206
207    quote!(
208        #[export_name = "__cntrlr_main"]
209        #input_fn
210    )
211    .into()
212}
213
214/// Override the default reset vector
215///
216/// When you implement the reset vector, you are responsible for all
217/// chip and runtime initialization, including such things as loading
218/// the data segment and clearing bss. You probably don't want to do
219/// this. See [`macro@raw_entry`] if you want to take over after minimal
220/// board init has been completed.
221#[proc_macro_attribute]
222pub fn reset(_args: TokenStream, input: TokenStream) -> TokenStream {
223    let input_fn = parse_macro_input!(input as ItemFn);
224    let sig = &input_fn.sig;
225    let fn_name = &sig.ident;
226
227    let reset_is_valid = sig.asyncness.is_none()
228        && match sig.abi {
229            None => false,
230            Some(ref abi) => match abi.name {
231                None => true,
232                Some(ref abi) => abi.value() == "C",
233            },
234        }
235        && sig.generics.params.is_empty()
236        && sig.generics.where_clause.is_none()
237        && sig.inputs.is_empty()
238        && match sig.output {
239            ReturnType::Type(_, ref typ) => matches!(**typ, Type::Never(_)),
240            ReturnType::Default => false,
241        };
242    if !reset_is_valid {
243        return ParseError::new(
244            input_fn.sig.span(),
245            format!(
246                "Cntrlr reset function must be of the form `extern \"C\" fn {}() -> !`",
247                fn_name
248            ),
249        )
250        .to_compile_error()
251        .into();
252    }
253
254    quote!(
255        #[link_section = ".__CNTRLR_START"]
256        #[export_name = "__cntrlr_reset"]
257        #input_fn
258    )
259    .into()
260}