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}