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}