Skip to main content

rivet_macros/
lib.rs

1//! Procedural macros for Rivet RTOS.
2//!
3//! Provides `#[rivet::task]` for declaring static async tasks that run as
4//! real compiler-generated `Future` state machines, stored in a
5//! [`rivet::task::TaskCell`] — zero heap allocation, and no nightly
6//! features required.
7
8use proc_macro::TokenStream;
9use quote::{format_ident, quote};
10use syn::{parse_macro_input, ItemFn, LitInt};
11
12/// Declare the application entry point. Expands to the `#[no_mangle]
13/// extern "C" fn rivet_main() -> !` that `rivet-rt`'s boot code (`_start`
14/// on RISC-V, `Reset` on Cortex-M) calls after bss/data init, with
15/// [`rivet::init`](https://docs.rs/rivet) inserted automatically before
16/// the function body runs.
17///
18/// ```ignore
19/// #[rivet::main]
20/// fn main() -> ! {
21///     rivet::println!("hello");
22///     rivet::spawn_ptask!(stack = 512, priority = 1, entry = my_task, arg = ());
23///     rivet::run()
24/// }
25/// ```
26///
27/// Takes no arguments; the annotated function must take no parameters
28/// (link the board/arch you want via `use rivet_bsp_...  as _;` instead —
29/// see `docs/porting.md`).
30#[proc_macro_attribute]
31pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream {
32    if !attr.is_empty() {
33        return syn::Error::new_spanned(
34            proc_macro2::TokenStream::from(attr),
35            "#[rivet::main] takes no arguments",
36        )
37        .to_compile_error()
38        .into();
39    }
40    let input = parse_macro_input!(item as ItemFn);
41    if !input.sig.inputs.is_empty() {
42        return syn::Error::new_spanned(
43            &input.sig.inputs,
44            "#[rivet::main] functions take no parameters",
45        )
46        .to_compile_error()
47        .into();
48    }
49    let fn_attrs = &input.attrs;
50    let fn_block = &input.block;
51
52    let expanded = quote! {
53        #[no_mangle]
54        #(#fn_attrs)*
55        extern "C" fn rivet_main() -> ! {
56            ::rivet::init();
57            #fn_block
58        }
59    };
60
61    TokenStream::from(expanded)
62}
63
64/// Declare a static async task.
65///
66/// The annotated function must be `async fn name() { ... }` — no
67/// parameters (shared state goes through `static`s, matching the usual
68/// embedded pattern for peripherals/queues). The body may freely use
69/// `.await` — `Sleep`, `Semaphore::acquire()`, `Channel::send()/recv()`
70/// all work as real futures polled by the executor.
71///
72/// # Attributes
73/// - `priority` (required): Task priority (0 = lowest, 31 = highest).
74/// - `stack` (optional): bytes reserved for the future's state machine
75///   (default 512). Increase this if you get a
76///   "task future exceeds reserved stack size" panic at boot.
77///
78/// # Example
79/// ```ignore
80/// #[rivet::task(priority = 1, stack = 256)]
81/// async fn blinky() {
82///     loop {
83///         rivet::time::Sleep::<500_000>::new().await; // 500ms
84///         toggle_led();
85///     }
86/// }
87/// ```
88///
89/// # How it works
90///
91/// `F` (the compiler-generated `Future` type of an `async fn`) is
92/// unnameable on stable Rust, so it can't appear in a `static`'s type.
93/// Instead the macro declares `static CELL: TaskCell<STACK_SIZE>`
94/// (`STACK_SIZE` is just a `usize`, always nameable) and generates a
95/// thin non-generic wrapper function that calls the crate's generic
96/// `TaskCell::poll::<F>`, letting the compiler monomorphize the actual
97/// future read/write per task without ever needing to write `F` down.
98#[proc_macro_attribute]
99pub fn task(attr: TokenStream, item: TokenStream) -> TokenStream {
100    let input = parse_macro_input!(item as ItemFn);
101    let fn_name = &input.sig.ident;
102    let fn_visibility = &input.vis;
103    let fn_sig = &input.sig;
104    let fn_attrs = &input.attrs;
105    let task_body = &input.block;
106
107    if input.sig.asyncness.is_none() {
108        return syn::Error::new_spanned(&input.sig, "#[rivet::task] requires an `async fn`")
109            .to_compile_error()
110            .into();
111    }
112    if !input.sig.inputs.is_empty() {
113        return syn::Error::new_spanned(
114            &input.sig.inputs,
115            "#[rivet::task] functions currently take no parameters; \
116             use a `static` for shared state (peripherals, queues, etc.)",
117        )
118        .to_compile_error()
119        .into();
120    }
121
122    let mut priority: u8 = 0;
123    let mut stack_size: usize = 512;
124    let mut saw_priority = false;
125
126    let parser = syn::meta::parser(|meta| {
127        if meta.path.is_ident("priority") {
128            let value = meta.value()?;
129            let lit: LitInt = value.parse()?;
130            priority = lit.base10_parse::<u8>()?;
131            saw_priority = true;
132        } else if meta.path.is_ident("stack") {
133            let value = meta.value()?;
134            let lit: LitInt = value.parse()?;
135            stack_size = lit.base10_parse::<usize>()?;
136        } else {
137            return Err(
138                meta.error("unsupported #[rivet::task] attribute; expected `priority` or `stack`")
139            );
140        }
141        Ok(())
142    });
143    // `parse_macro_input!` with a parser that returns `()` both parses the
144    // attribute and converts parse errors into compile errors.
145    parse_macro_input!(attr with parser);
146
147    if !saw_priority {
148        return syn::Error::new_spanned(
149            fn_name,
150            "#[rivet::task] requires `priority = N`, e.g. #[rivet::task(priority = 1)]",
151        )
152        .to_compile_error()
153        .into();
154    }
155
156    let poll_fn_name = format_ident!("__rivet_poll_{}", fn_name);
157    let completed_fn_name = format_ident!("__rivet_completed_{}", fn_name);
158    let cell_name = format_ident!("__RIVET_CELL_{}", fn_name);
159    let reg_name = format_ident!("__RIVET_REG_{}", fn_name);
160
161    let expanded = quote! {
162        // The user's async fn, unchanged — the compiler generates its
163        // Future state machine as normal.
164        #(#fn_attrs)*
165        #fn_visibility #fn_sig #task_body
166
167        // Zero-alloc storage for the future, sized (not typed) generically.
168        #[allow(non_upper_case_globals)]
169        static #cell_name: ::rivet::task::TaskCell<#stack_size> = ::rivet::task::TaskCell::new();
170
171        // Thin non-generic wrapper: calls the generic, monomorphized
172        // TaskCell::poll::<F> where F is inferred from `#fn_name`.
173        #[allow(non_snake_case)]
174        unsafe fn #poll_fn_name(
175            _user_data: *mut (),
176            waker: &::core::task::Waker,
177        ) -> ::core::task::Poll<()> {
178            #cell_name.poll(#fn_name, waker)
179        }
180
181        // Type-erased completed probe for this concrete cell.
182        #[allow(non_snake_case)]
183        unsafe fn #completed_fn_name(_user_data: *mut ()) -> bool {
184            #cell_name.is_completed()
185        }
186
187        // Registration entry discovered by the executor at boot.
188        #[link_section = ".rivet_tasks"]
189        #[used]
190        #[allow(non_upper_case_globals)]
191        static #reg_name: ::rivet::task::TaskReg = ::rivet::task::TaskReg {
192            priority: #priority,
193            index_in_priority: 0,
194            _reserved: [0; 2],
195            poll_fn: #poll_fn_name as unsafe fn(*mut (), &::core::task::Waker) -> ::core::task::Poll<()>,
196            completed_fn: #completed_fn_name as unsafe fn(*mut ()) -> bool,
197            user_data: ::core::ptr::null_mut(),
198        };
199    };
200
201    TokenStream::from(expanded)
202}