ove/async_runtime/executor.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//! oveRTOS-native embassy executor.
8//!
9//! Wraps `embassy_executor::raw::Executor` with a run loop that blocks
10//! on `ove_event_wait` between polls — yields cleanly to the
11//! FreeRTOS / Zephyr / NuttX scheduler on embedded targets instead of
12//! spinning on WFE or parking a host thread.
13//!
14//! Replaces the upstream `__pender` symbol with our own that signals
15//! an `ove_event` using either `ove_event_signal` or
16//! `ove_event_signal_from_isr` based on `ove_is_in_isr()`. To use this
17//! executor, depend on `embassy-executor` without selecting any
18//! `arch-*` / `executor-thread` feature — that keeps the upstream
19//! `__pender` from being linked.
20//!
21//! ## Single-executor design
22//!
23//! [`Executor::take`] returns a `&'static mut Self` and can be called
24//! exactly once per process. Multi-executor priority levels are Phase 3
25//! work (per-thread-id pender dispatch).
26
27#[cfg(zero_heap)]
28use ::core::cell::UnsafeCell;
29#[cfg(zero_heap)]
30use ::core::mem::MaybeUninit;
31use ::core::sync::atomic::{AtomicBool, Ordering};
32
33use embassy_executor::{Spawner, raw};
34
35use crate::bindings;
36use crate::error::{Error, Result};
37use crate::init_cell::InitCell;
38use crate::sync::Event;
39
40/// One-time-init store for the global pender event. The executor's
41/// run loop blocks on `evt.wait()` between polls; the `__pender`
42/// trampoline (called by Embassy at every waker.wake) signals it.
43static PENDER_EVENT: InitCell<Event> = InitCell::new();
44
45/// Executor singleton guard.
46static EXECUTOR_TAKEN: AtomicBool = AtomicBool::new(false);
47
48/// oveRTOS-native embassy executor.
49pub struct Executor {
50 inner: raw::Executor,
51}
52
53// SAFETY: the executor is moved across thread boundaries once at
54// `take()` time (the macro spawns it on a dedicated ove::Thread) and
55// thereafter mutated only on that thread. The `&'static mut` returned
56// by `take()` is the only handle to it.
57unsafe impl Send for Executor {}
58unsafe impl Sync for Executor {}
59
60impl Executor {
61 /// Construct the global executor and return an exclusive reference.
62 /// May only be called once per process; subsequent calls return
63 /// [`Error::Inval`].
64 ///
65 /// In heap mode the executor lives in a leaked [`Box`]; in
66 /// zero-heap mode it lives in a function-scope `static mut`
67 /// [`MaybeUninit`] slot. Either way the returned reference has
68 /// `'static` lifetime.
69 pub fn take() -> Result<&'static mut Self> {
70 if EXECUTOR_TAKEN.swap(true, Ordering::AcqRel) {
71 return Err(Error::Inval);
72 }
73
74 #[cfg(not(zero_heap))]
75 {
76 extern crate alloc;
77 let boxed = alloc::boxed::Box::new(Self {
78 // The pender context is unused — our __pender uses the
79 // global PENDER_EVENT instead.
80 inner: raw::Executor::new(::core::ptr::null_mut()),
81 });
82 Ok(alloc::boxed::Box::leak(boxed))
83 }
84 #[cfg(zero_heap)]
85 {
86 // Function-scope static — initialised exactly once because
87 // EXECUTOR_TAKEN swaps to true above. Wrapped in an
88 // UnsafeCell so the static itself is Sync.
89 static mut EXEC_SLOT: MaybeUninit<UnsafeCell<Executor>> = MaybeUninit::uninit();
90 // SAFETY: EXEC_SLOT is touched exactly once across the
91 // process; subsequent take() calls are gated by
92 // EXECUTOR_TAKEN above.
93 unsafe {
94 let slot = &raw mut EXEC_SLOT;
95 (*slot).write(UnsafeCell::new(Self {
96 inner: raw::Executor::new(::core::ptr::null_mut()),
97 }));
98 let cell: &UnsafeCell<Executor> = (*slot).assume_init_ref();
99 Ok(&mut *cell.get())
100 }
101 }
102 }
103
104 /// Run the executor forever. Initialises the pender event, calls
105 /// `init(spawner)` once to spawn the initial task set, then loops:
106 /// `poll()` to advance ready tasks, then `ove_event_wait` until
107 /// the next waker fires.
108 pub fn run(&'static mut self, init: impl FnOnce(Spawner)) -> ! {
109 // Lazy-init the pender event on first run. Idempotent: if a
110 // previous Executor::take + run cycle happened we keep the
111 // existing event. In practice take() prevents that — kept
112 // defensive for future multi-executor support.
113 if PENDER_EVENT.try_get().is_none() {
114 let evt = make_pender_event().expect("create pender event");
115 PENDER_EVENT.init(evt);
116 }
117
118 let spawner: Spawner = self.inner.spawner();
119 init(spawner);
120
121 loop {
122 // SAFETY: we hold &'static mut self, so no concurrent
123 // poll() exists; embassy's wake path is interior-mutability
124 // safe.
125 unsafe {
126 self.inner.poll();
127 }
128 // Block on ove_event_wait — yields the CPU to the RTOS
129 // scheduler. SysTick or any other interrupt that signals
130 // a waker will fire __pender, which signals this event.
131 let _ = PENDER_EVENT.get().wait();
132 }
133 }
134}
135
136#[cfg(not(zero_heap))]
137fn make_pender_event() -> Result<Event> {
138 Event::new()
139}
140
141#[cfg(zero_heap)]
142fn make_pender_event() -> Result<Event> {
143 // Function-scope static storage for the binary event.
144 static mut STORAGE: bindings::ove_event_storage_t = unsafe { ::core::mem::zeroed() };
145 // SAFETY: STORAGE is only touched here, and take()'s singleton
146 // guarantees this runs once.
147 unsafe { Event::from_static(::core::ptr::addr_of_mut!(STORAGE)) }
148}
149
150/// Embassy looks up this exact symbol via `extern "Rust"` linkage when
151/// the user crate pulls in `embassy-executor` (without an `arch-*`
152/// feature). Each `waker.wake()` lands here.
153///
154/// Dispatches between `ove_event_signal` (thread context) and
155/// `ove_event_signal_from_isr` (ISR context) based on `ove_is_in_isr()`.
156/// `_context` is the value passed to `raw::Executor::new` — we always
157/// use `null_mut()` and route via the global `PENDER_EVENT`.
158#[unsafe(no_mangle)]
159extern "Rust" fn __pender(_context: *mut ()) {
160 // PENDER_EVENT may not be initialised yet if a stray waker fires
161 // before run() — e.g. during task setup. Tolerate that.
162 let Some(evt) = PENDER_EVENT.try_get() else {
163 return;
164 };
165 if unsafe { bindings::ove_is_in_isr() } {
166 evt.signal_from_isr();
167 } else {
168 evt.signal();
169 }
170}