Skip to main content

ove_macros/
lib.rs

1// Copyright (C) 2026 Kamil Lulko <kamil.lulko@gmail.com>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4//
5// This file is part of oveRTOS.
6
7//! Proc-macros for the oveRTOS Rust binding.
8//!
9//! Provides `#[ove::main]`, which marks an `fn` as the application
10//! entry point.  Two shapes are accepted:
11//!
12//!   - **Sync**: `fn app_main()` — expands to a no-arg `extern "C" fn
13//!     ove_main()` trampoline that calls the user function directly.
14//!     This is the historical shape and works without the `async`
15//!     feature.
16//!   - **Async**: `async fn app_main(spawner: Spawner)` — expands to a
17//!     trampoline that takes the global `Executor`, builds an
18//!     `embassy_executor::task`, and runs the executor forever.
19//!     Requires the `async` Cargo feature on the `ove` crate (and
20//!     `CONFIG_OVE_ASYNC=y` on the C substrate, enforced by a
21//!     `compile_error!` in `ove::lib.rs`).
22
23use proc_macro::TokenStream;
24use quote::{format_ident, quote};
25use syn::parse::{Parse, ParseStream};
26use syn::{ItemFn, LitInt, LitStr, Token, parse_macro_input};
27
28/// Attribute macro: mark a function as the oveRTOS application entry
29/// point.
30///
31/// Generates an `#[unsafe(no_mangle)] pub extern "C" fn ove_main()`
32/// trampoline that calls the annotated function.
33///
34/// # Forms
35///
36/// Sync:
37/// ```ignore
38/// #[ove::main]
39/// fn app_main() {
40///     ove::log::try_init();
41///     log::info!("hello");
42///     ove::run();
43/// }
44/// ```
45///
46/// Async (requires `ove` feature `async`):
47/// ```ignore
48/// use embassy_executor::Spawner;
49///
50/// #[ove::main]
51/// async fn app_main(spawner: Spawner) {
52///     spawner.must_spawn(my_task());
53///     // Executor::run() never returns; ove::run() is implicit.
54/// }
55/// ```
56#[proc_macro_attribute]
57pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream {
58    let input = parse_macro_input!(item as ItemFn);
59
60    if !matches!(input.sig.output, syn::ReturnType::Default) {
61        return syn::Error::new_spanned(
62            &input.sig.output,
63            "#[ove::main] entry function must return `()` (drop the `-> ()`)",
64        )
65        .to_compile_error()
66        .into();
67    }
68
69    if input.sig.asyncness.is_some() {
70        return expand_async(input);
71    }
72    if !input.sig.inputs.is_empty() {
73        return syn::Error::new_spanned(
74            &input.sig.inputs,
75            "#[ove::main] sync entry function must take no arguments \
76             (use an `async fn(spawner: Spawner)` for async entry)",
77        )
78        .to_compile_error()
79        .into();
80    }
81    expand_sync(input)
82}
83
84fn expand_sync(input: ItemFn) -> TokenStream {
85    let user_fn_name = &input.sig.ident;
86    let expanded = quote! {
87        #input
88
89        #[unsafe(no_mangle)]
90        pub extern "C" fn ove_main() {
91            #user_fn_name();
92        }
93    };
94    expanded.into()
95}
96
97fn expand_async(input: ItemFn) -> TokenStream {
98    if input.sig.inputs.len() != 1 {
99        return syn::Error::new_spanned(
100            &input.sig.inputs,
101            "#[ove::main] async entry function must take exactly one \
102             argument of type `embassy_executor::Spawner`",
103        )
104        .to_compile_error()
105        .into();
106    }
107
108    let user_fn_name = &input.sig.ident;
109    // Rename the user fn to a task-decorated wrapper so it can be
110    // spawned on the executor. The trampoline takes the executor (via
111    // a function-local mutable slot upgraded to 'static lifetime) and
112    // runs it forever.
113    let renamed_ident = syn::Ident::new(
114        &format!("__ove_async_entry_{user_fn_name}"),
115        user_fn_name.span(),
116    );
117
118    let mut user_sig = input.sig.clone();
119    user_sig.ident = renamed_ident.clone();
120    let user_block = &input.block;
121    let user_attrs = &input.attrs;
122    let user_vis = &input.vis;
123
124    let expanded = quote! {
125        #(#user_attrs)*
126        #[::embassy_executor::task]
127        #user_vis #user_sig #user_block
128
129        // The async executor must run after vTaskStartScheduler (FreeRTOS)
130        // / pthread_create (POSIX) / k_thread_create (Zephyr) so the
131        // backend's timer service task, ISR plumbing, and waitqueues are
132        // alive — otherwise our ove_timer callback never fires and the
133        // executor stalls forever.  Spawn the executor on an ove::Thread,
134        // then hand control to ove::run() which starts the backend
135        // scheduler.  Once the scheduler is up, the executor thread is
136        // dispatched and Executor::run takes over, blocking on
137        // ove_event_wait between polls — yielding cleanly to the RTOS
138        // scheduler when other tasks share the CPU.
139        fn __ove_async_exec_thread() {
140            let exec: &'static mut ::ove::async_runtime::Executor =
141                ::ove::async_runtime::Executor::take()
142                    .expect("ove::async_runtime::Executor::take called twice");
143            exec.run(|spawner| {
144                spawner
145                    .spawn(#renamed_ident(spawner))
146                    .expect("failed to spawn application entry task");
147            });
148        }
149
150        #[unsafe(no_mangle)]
151        pub extern "C" fn ove_main() {
152            // In heap mode, Thread::spawn_simple mallocs the kernel
153            // TCB + stack.  In zero-heap mode, the caller supplies a
154            // static ThreadStorage<STACK_SIZE> slot to spawn_static_simple.
155            #[cfg(not(zero_heap))]
156            let _handle = ::ove::Thread::builder()
157                .name(c"async-exec")
158                .stack_size(8192)
159                .spawn_simple(__ove_async_exec_thread)
160                .expect("failed to create async-exec thread");
161            #[cfg(zero_heap)]
162            let _handle = {
163                static EXEC_THREAD_STORAGE: ::ove::ThreadStorage<8192> =
164                    ::ove::ThreadStorage::new();
165                ::ove::Thread::builder()
166                    .name(c"async-exec")
167                    .spawn_static_simple(&EXEC_THREAD_STORAGE, __ove_async_exec_thread)
168                    .expect("failed to create async-exec thread")
169            };
170            // Intentionally leak the join handle so its Drop doesn't
171            // request_stop on the executor thread when ove_main "returns"
172            // (it won't — ove::run never returns, but be explicit).
173            ::core::mem::forget(_handle);
174            ::ove::run();
175        }
176    };
177    expanded.into()
178}
179
180// ── #[ove::thread] (G2) ─────────────────────────────────────────────
181
182/// Args parsed from `#[ove::thread(stack_size = 4096, name = "gfx")]`.
183struct ThreadArgs {
184    stack_size: Option<usize>,
185    name: Option<String>,
186}
187
188impl Parse for ThreadArgs {
189    fn parse(input: ParseStream) -> syn::Result<Self> {
190        let mut stack_size = None;
191        let mut name = None;
192        while !input.is_empty() {
193            let ident: syn::Ident = input.parse()?;
194            input.parse::<Token![=]>()?;
195            match ident.to_string().as_str() {
196                "stack_size" => {
197                    let lit: LitInt = input.parse()?;
198                    stack_size = Some(lit.base10_parse::<usize>()?);
199                }
200                "name" => {
201                    let lit: LitStr = input.parse()?;
202                    name = Some(lit.value());
203                }
204                other => {
205                    return Err(syn::Error::new_spanned(
206                        ident,
207                        format!(
208                            "unknown #[ove::thread] argument `{other}` (expected stack_size or name)"
209                        ),
210                    ));
211                }
212            }
213            if !input.is_empty() {
214                input.parse::<Token![,]>()?;
215            }
216        }
217        Ok(Self { stack_size, name })
218    }
219}
220
221/// Attribute macro: declare a function as a thread entry-point with a
222/// statically-allocated stack. Calling the generated wrapper spawns
223/// the thread once; subsequent calls return `Err(Error::Inval)`
224/// (the underlying [`ove::ThreadStorage`] is single-use).
225///
226/// ```ignore
227/// #[ove::thread(stack_size = 4096, name = "blinker")]
228/// fn blink() {
229///     loop {
230///         ove::Thread::sleep_ms(500);
231///         ove::printk!("tick\n");
232///     }
233/// }
234///
235/// fn ove_main() {
236///     blink().expect("spawn blinker");
237///     ove::run();
238/// }
239/// ```
240///
241/// Generates:
242/// - A `pub fn <name>() -> ove::Result<()>` that, on first call,
243///   creates the thread with a `static ThreadStorage<STACK_SIZE>`.
244/// - The user's original function body is moved into a hidden
245///   `__ove_thread_<name>_body` so the public `<name>` is the spawn
246///   helper.
247///
248/// Defaults: `stack_size = 4096`, `name = "ove-thread"` (override via
249/// the attribute args). The pool size is fixed at 1 — at most one
250/// live instance of the thread per program. Calling the spawn helper
251/// from inside the thread body itself is allowed but does nothing
252/// (returns `Err`); use [`ove::Thread::builder`] for dynamic spawns.
253///
254/// Works in both heap and zero-heap modes: the storage is `static` so
255/// no allocation happens at spawn time.
256#[proc_macro_attribute]
257pub fn thread(attr: TokenStream, item: TokenStream) -> TokenStream {
258    let args = parse_macro_input!(attr as ThreadArgs);
259    let input = parse_macro_input!(item as ItemFn);
260
261    if input.sig.asyncness.is_some() {
262        return syn::Error::new_spanned(
263            &input.sig.fn_token,
264            "#[ove::thread] cannot annotate `async fn` — use #[embassy_executor::task] instead",
265        )
266        .to_compile_error()
267        .into();
268    }
269    if !input.sig.inputs.is_empty() {
270        return syn::Error::new_spanned(
271            &input.sig.inputs,
272            "#[ove::thread] entry function must take no arguments",
273        )
274        .to_compile_error()
275        .into();
276    }
277    if !matches!(input.sig.output, syn::ReturnType::Default) {
278        return syn::Error::new_spanned(
279            &input.sig.output,
280            "#[ove::thread] entry function must return `()` (drop the `-> ()`)",
281        )
282        .to_compile_error()
283        .into();
284    }
285
286    let stack_size = args.stack_size.unwrap_or(4096);
287    let user_fn = &input.sig.ident;
288    let body_ident = format_ident!("__ove_thread_{}_body", user_fn);
289    let name_str = args
290        .name
291        .unwrap_or_else(|| format!("ove-{user_fn}"))
292        .replace('\0', "");
293    let name_cstr = syn::LitByteStr::new(format!("{name_str}\0").as_bytes(), user_fn.span());
294
295    let user_vis = &input.vis;
296    let user_attrs = &input.attrs;
297    let user_block = &input.block;
298
299    let expanded = quote! {
300        // The original function body, hidden as the actual thread entry.
301        #[doc(hidden)]
302        #(#user_attrs)*
303        fn #body_ident() #user_block
304
305        // The spawn helper takes the original ident so users call e.g.
306        // `blink()` to start their thread.
307        #user_vis fn #user_fn() -> ::ove::Result<()> {
308            static STORAGE: ::ove::ThreadStorage<#stack_size> = ::ove::ThreadStorage::new();
309            static STARTED: ::core::sync::atomic::AtomicBool =
310                ::core::sync::atomic::AtomicBool::new(false);
311
312            if STARTED.swap(true, ::core::sync::atomic::Ordering::AcqRel) {
313                return Err(::ove::Error::Inval);
314            }
315            // SAFETY: the LitByteStr above is null-terminated.
316            let name = unsafe {
317                ::core::ffi::CStr::from_bytes_with_nul_unchecked(#name_cstr)
318            };
319            let handle = ::ove::Thread::builder()
320                .name(name)
321                .spawn_static_simple(&STORAGE, #body_ident)?;
322            // Detach: the thread runs until the body returns; Drop on
323            // the JoinHandle would request_stop which is wrong for a
324            // user-managed thread.
325            ::core::mem::forget(handle);
326            Ok(())
327        }
328    };
329    expanded.into()
330}