Skip to main content

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}