Skip to main content

ove/
thread.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//! RTOS thread management.
8//!
9//! The API splits the thread surface into three types so ownership,
10//! observation, and configuration each have a clear home:
11//!
12//!   - [`Thread`] is a **non-owning** handle.  Returned by
13//!     [`Thread::current`] and by [`JoinHandle::thread`].  Carries the
14//!     read-only and signal operations (`get_state`,
15//!     `get_stack_usage`, `set_priority`, `suspend`/`resume`,
16//!     `request_stop`, …).  Dropping a `Thread` value does nothing.
17//!   - [`JoinHandle`] is the **owning** type returned by
18//!     [`Builder::spawn`] (and its `spawn_cooperative`/`spawn_simple`/
19//!     `spawn_static` siblings).  On drop it calls
20//!     [`JoinHandle::request_stop`] and waits for the worker to finish.
21//!     Use [`JoinHandle::detach`] to opt out of the join-on-drop.
22//!   - [`Builder`] is the only way to spawn a new thread.  Cooperative
23//!     cancellation is opt-in via the closure signature
24//!     (`FnOnce(StopToken)` vs `fn()`).
25//!
26//! Spawning a cooperative worker:
27//! ```ignore
28//! let h = Thread::builder()
29//!     .name(c"worker")
30//!     .priority(Priority::Normal)
31//!     .stack_size(4096)
32//!     .spawn(|tok: StopToken| {
33//!         while !tok.is_stopped() { /* work */ }
34//!     })?;
35//! // h goes out of scope -> request_stop + join, no deadlock.
36//! ```
37//!
38//! Spawning a self-terminating one-shot (no token):
39//! ```ignore
40//! fn entry() { /* one-shot work */ }
41//! let _h = Thread::builder()
42//!     .name(c"oneshot")
43//!     .priority(Priority::Normal)
44//!     .stack_size(4096)
45//!     .spawn_simple(entry)?;
46//! ```
47
48#[cfg(zero_heap)]
49use core::cell::UnsafeCell;
50use core::fmt;
51use core::marker::PhantomData;
52#[cfg(zero_heap)]
53use core::mem::MaybeUninit;
54
55use crate::bindings;
56use crate::error::{Error, Result};
57
58// Round-tripping `fn()` / `fn(StopToken)` through `*mut c_void`
59// requires the two to be the same size.  Holds on every target
60// oveRTOS supports today — ARM Cortex-M (32-bit thumb), x86_64,
61// RISC-V (32/64), and WebAssembly.  If a future port lands on a
62// target where function pointers are wider than data pointers (some
63// segmented or capability architectures), this assertion fires at
64// compile time so the silent corruption mode becomes a build failure.
65const _: () = assert!(
66    core::mem::size_of::<fn()>() == core::mem::size_of::<*mut core::ffi::c_void>(),
67    "ove::thread: fn() and *mut c_void must be the same size for FFI round-trip"
68);
69const _: () = assert!(
70    core::mem::size_of::<fn(StopToken)>() == core::mem::size_of::<*mut core::ffi::c_void>(),
71    "ove::thread: fn(StopToken) and *mut c_void must be the same size for FFI round-trip"
72);
73
74// SAFETY (module-wide contract for the `unsafe { bindings::ove_*(...) }` FFI
75// calls below): any handle passed to the C API is non-null and refers to a
76// live RTOS object — wrapper constructors establish validity via
77// `Error::from_code`, and `Drop` (or an explicit `deinit`) is the only place
78// a handle is released. Pointer and slice arguments reference caller-owned
79// memory valid for the duration of the call; the C side copies whatever it
80// retains and does not alias them past return (verified against the
81// signatures in `include/ove/*.h`). Blocks that deviate — `transmute`, raw
82// pointer casts from user data, slice reconstruction via `from_raw_parts`,
83// or storing a callback across the FFI boundary — carry their own
84// `// SAFETY:` comment.
85
86/// Thread priority levels, matching `ove_prio_t`.
87///
88/// Variants are ordered from lowest (`Idle`) to highest (`Critical`) so that
89/// standard comparison operators (`<`, `>`) work intuitively.
90#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
91#[repr(u32)]
92pub enum Priority {
93    /// Lowest priority, typically the RTOS idle task level.
94    Idle = 0,
95    /// Low background priority.
96    Low = 1,
97    /// Below-normal priority.
98    BelowNormal = 2,
99    /// Default priority for most application threads.
100    Normal = 3,
101    /// Above-normal priority for time-sensitive work.
102    AboveNormal = 4,
103    /// High priority for latency-critical tasks.
104    High = 5,
105    /// Real-time priority; preempts most other threads.
106    Realtime = 6,
107    /// Highest priority; reserved for system-critical tasks.
108    Critical = 7,
109}
110
111/// Thread state, matching `ove_thread_state_t`.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum ThreadState {
114    /// The thread is currently executing on a CPU core.
115    Running,
116    /// The thread is ready to run but waiting for a CPU slot.
117    Ready,
118    /// The thread is waiting on a synchronization primitive or I/O.
119    Blocked,
120    /// The thread has been explicitly suspended via [`Thread::suspend`].
121    Suspended,
122    /// The thread has finished executing.
123    Terminated,
124    /// The state reported by the RTOS does not match any known variant.
125    Unknown,
126}
127
128/// Runtime statistics for a thread.
129#[derive(Debug, Clone, Copy)]
130pub struct ThreadStats {
131    /// Total CPU time consumed by this thread in microseconds.
132    pub runtime_us: u64,
133    /// CPU utilization as a percentage multiplied by 100 (e.g. 5025 = 50.25%).
134    pub cpu_percent_x100: u32,
135}
136
137// ---------------------------------------------------------------------------
138// StopToken — read-only handle to the per-thread cancellation flag.
139// ---------------------------------------------------------------------------
140
141/// Read-only handle to a thread's cooperative-cancellation flag.
142///
143/// Cheap to copy, `Send + Sync`.  Reads the per-thread atomic flag set
144/// by [`JoinHandle::request_stop`] (or implicitly by [`JoinHandle`]'s
145/// [`Drop`]).
146///
147/// Workers spawned via [`Builder::spawn`] receive a `StopToken` as
148/// their entry argument and poll [`StopToken::is_stopped`] to break
149/// cleanly out of their loop.  Tokens can be cloned and passed to
150/// helper functions that need to bail out without being handed the
151/// owning [`JoinHandle`].
152///
153/// ```ignore
154/// use ove::{Thread, StopToken, Priority};
155///
156/// let h = Thread::builder()
157///     .name(c"worker")
158///     .priority(Priority::Normal)
159///     .stack_size(4096)
160///     .spawn(|tok: StopToken| {
161///         while !tok.is_stopped() {
162///             // do work
163///         }
164///     })?;
165///
166/// // h goes out of scope -> Drop calls request_stop + join.
167/// ```
168#[derive(Debug, Clone, Copy)]
169pub struct StopToken {
170    handle: bindings::ove_thread_t,
171}
172
173// SAFETY: the handle is an opaque RTOS pointer.  Access through it
174// goes via __atomic_* primitives in the substrate; the handle itself
175// is shareable across threads.
176unsafe impl Send for StopToken {}
177unsafe impl Sync for StopToken {}
178
179impl StopToken {
180    /// Construct an empty token that never signals stop.  Useful as a
181    /// default for fields filled in later.
182    #[inline]
183    pub const fn empty() -> Self {
184        Self {
185            handle: core::ptr::null_mut(),
186        }
187    }
188
189    /// `true` if [`JoinHandle::request_stop`] (or the parent's [`Drop`])
190    /// has been called on the referenced thread.  Returns `false` for
191    /// an empty token.
192    #[inline]
193    pub fn is_stopped(&self) -> bool {
194        !self.handle.is_null() && unsafe { bindings::ove_thread_should_stop(self.handle) }
195    }
196
197    /// `true` if this token references a real thread (vs.
198    /// [`StopToken::empty`]).
199    #[inline]
200    pub fn stop_possible(&self) -> bool {
201        !self.handle.is_null()
202    }
203
204    /// Raw handle accessor for advanced use.
205    #[inline]
206    pub fn raw_handle(&self) -> bindings::ove_thread_t {
207        self.handle
208    }
209}
210
211// ---------------------------------------------------------------------------
212// Internal: trampoline closures + RAII guards for FnOnce capture boxes.
213// ---------------------------------------------------------------------------
214
215#[cfg(all(not(zero_heap), feature = "alloc"))]
216type ClosureBox = alloc::boxed::Box<dyn FnOnce(StopToken) + Send + 'static>;
217
218#[cfg(all(not(zero_heap), feature = "alloc"))]
219struct ThunkGuard {
220    raw: *mut ClosureBox,
221}
222
223#[cfg(all(not(zero_heap), feature = "alloc"))]
224impl ThunkGuard {
225    fn forget(mut self) {
226        self.raw = core::ptr::null_mut();
227    }
228}
229
230#[cfg(all(not(zero_heap), feature = "alloc"))]
231impl Drop for ThunkGuard {
232    fn drop(&mut self) {
233        if !self.raw.is_null() {
234            // SAFETY: forget() never ran, so the kernel did not adopt
235            // the boxed closure (spawn failed before forget).  We still
236            // own `raw`, free it now.
237            let _ = unsafe { alloc::boxed::Box::from_raw(self.raw) };
238        }
239    }
240}
241
242/// `Thread::current()` and worker trampolines both need to wait for the
243/// substrate to publish the per-thread "self" handle before constructing
244/// a [`StopToken`] — on FreeRTOS the parent task-tag publish happens
245/// AFTER `xTaskCreate` returns and equal-priority workers can outrace it.
246#[inline]
247unsafe fn wait_for_self() -> bindings::ove_thread_t {
248    let mut handle = unsafe { bindings::ove_thread_get_self() };
249    while handle.is_null() {
250        unsafe { bindings::ove_thread_yield() };
251        handle = unsafe { bindings::ove_thread_get_self() };
252    }
253    handle
254}
255
256unsafe extern "C" fn fn_simple_trampoline(arg: *mut core::ffi::c_void) {
257    // SAFETY: `arg` was set by the caller from a `fn()` pointer.  On
258    // supported targets `fn()` is pointer-sized and C-ABI-compatible.
259    let entry: fn() = unsafe { core::mem::transmute(arg) };
260    entry();
261}
262
263unsafe extern "C" fn fn_cooperative_trampoline(arg: *mut core::ffi::c_void) {
264    let entry: fn(StopToken) = unsafe { core::mem::transmute(arg) };
265    let handle = unsafe { wait_for_self() };
266    entry(StopToken { handle });
267}
268
269#[cfg(all(not(zero_heap), feature = "alloc"))]
270unsafe extern "C" fn box_cooperative_trampoline(arg: *mut core::ffi::c_void) {
271    // SAFETY: re-takes ownership of the box minted in Builder::spawn.
272    let boxed: alloc::boxed::Box<ClosureBox> =
273        unsafe { alloc::boxed::Box::from_raw(arg as *mut ClosureBox) };
274    let handle = unsafe { wait_for_self() };
275    (*boxed)(StopToken { handle });
276}
277
278// ---------------------------------------------------------------------------
279// Thread — non-owning handle (analogous to std::thread::Thread).
280// ---------------------------------------------------------------------------
281
282/// Non-owning handle to an RTOS thread.
283///
284/// Returned by [`Thread::current`] and [`JoinHandle::thread`].  Dropping
285/// a `Thread` does **not** destroy the underlying RTOS thread — use
286/// [`JoinHandle`] for that.  Carries the read-only and signal-only
287/// operations (`get_state`, `suspend`/`resume`, `request_stop`, …).
288#[derive(Clone, Copy)]
289pub struct Thread {
290    handle: bindings::ove_thread_t,
291}
292
293// SAFETY: RTOS thread handles are shareable across threads once created.
294// Access to the handle is synchronized by the RTOS itself.
295unsafe impl Send for Thread {}
296unsafe impl Sync for Thread {}
297
298impl Thread {
299    /// Sleep the current thread for `ms` milliseconds.
300    #[inline]
301    pub fn sleep_ms(ms: u32) {
302        unsafe { bindings::ove_thread_sleep_ms(ms) }
303    }
304
305    /// Yield the current thread's time slice.
306    #[inline]
307    pub fn yield_now() {
308        unsafe { bindings::ove_thread_yield() }
309    }
310
311    /// Get a non-owning handle to the currently running thread.
312    #[inline]
313    pub fn current() -> Self {
314        let handle = unsafe { bindings::ove_thread_get_self() };
315        Self { handle }
316    }
317
318    /// Begin spawning a new thread.  Returns a fluent [`Builder`].
319    ///
320    /// ```ignore
321    /// let h = Thread::builder()
322    ///     .name(c"worker")
323    ///     .priority(Priority::Normal)
324    ///     .stack_size(4096)
325    ///     .spawn(|tok| { while !tok.is_stopped() { /* work */ } })?;
326    /// ```
327    #[inline]
328    pub fn builder() -> Builder {
329        Builder::new()
330    }
331
332    /// Suspend this thread.
333    #[inline]
334    pub fn suspend(&self) {
335        unsafe { bindings::ove_thread_suspend(self.handle) }
336    }
337
338    /// Resume this thread.
339    #[inline]
340    pub fn resume(&self) {
341        unsafe { bindings::ove_thread_resume(self.handle) }
342    }
343
344    /// Set this thread's priority.
345    #[inline]
346    pub fn set_priority(&self, prio: Priority) {
347        unsafe { bindings::ove_thread_set_priority(self.handle, prio as bindings::ove_prio_t) }
348    }
349
350    /// Get current stack usage in bytes.
351    #[inline]
352    pub fn get_stack_usage(&self) -> usize {
353        unsafe { bindings::ove_thread_get_stack_usage(self.handle) }
354    }
355
356    /// Get the thread's current state.
357    pub fn get_state(&self) -> ThreadState {
358        let state = unsafe { bindings::ove_thread_get_state(self.handle) };
359        match state {
360            bindings::OVE_THREAD_STATE_RUNNING => ThreadState::Running,
361            bindings::OVE_THREAD_STATE_READY => ThreadState::Ready,
362            bindings::OVE_THREAD_STATE_BLOCKED => ThreadState::Blocked,
363            bindings::OVE_THREAD_STATE_SUSPENDED => ThreadState::Suspended,
364            bindings::OVE_THREAD_STATE_TERMINATED => ThreadState::Terminated,
365            _ => ThreadState::Unknown,
366        }
367    }
368
369    /// Get runtime statistics (CPU time and utilization) for this thread.
370    ///
371    /// # Errors
372    /// Returns an error if the RTOS does not support runtime statistics or the
373    /// thread handle is invalid.
374    pub fn get_runtime_stats(&self) -> Result<ThreadStats> {
375        let mut stats = bindings::ove_thread_stats {
376            runtime_us: 0,
377            cpu_percent_x100: 0,
378        };
379        let rc = unsafe { bindings::ove_thread_get_runtime_stats(self.handle, &mut stats) };
380        Error::from_code(rc)?;
381        Ok(ThreadStats {
382            runtime_us: stats.runtime_us,
383            cpu_percent_x100: stats.cpu_percent_x100,
384        })
385    }
386
387    /// Request the thread to stop cooperatively.
388    ///
389    /// Sets the per-thread atomic cancellation flag.  The worker must
390    /// poll [`StopToken::is_stopped`] (via the token it received at
391    /// spawn time) for this to have any effect — the substrate does
392    /// NOT force-terminate.  Safe from any context (ISR, other thread,
393    /// the thread itself).  Idempotent; the flag is sticky.
394    #[inline]
395    pub fn request_stop(&self) {
396        if !self.handle.is_null() {
397            unsafe { bindings::ove_thread_request_stop(self.handle) };
398        }
399    }
400
401    /// Get a [`StopToken`] referencing this thread's cancellation flag.
402    #[inline]
403    pub fn stop_token(&self) -> StopToken {
404        StopToken {
405            handle: self.handle,
406        }
407    }
408
409    /// `true` if [`Thread::request_stop`] has been called on this thread.
410    #[inline]
411    pub fn stop_requested(&self) -> bool {
412        !self.handle.is_null() && unsafe { bindings::ove_thread_should_stop(self.handle) }
413    }
414
415    /// Raw handle accessor for advanced use.
416    #[inline]
417    pub fn raw_handle(&self) -> bindings::ove_thread_t {
418        self.handle
419    }
420}
421
422impl fmt::Debug for Thread {
423    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
424        f.debug_struct("Thread")
425            .field("handle", &format_args!("{:p}", self.handle))
426            .finish()
427    }
428}
429
430// ---------------------------------------------------------------------------
431// Builder — fluent spawn configuration.
432// ---------------------------------------------------------------------------
433
434/// Fluent thread-spawn configuration.  Construct via [`Thread::builder`].
435///
436/// Default-constructable so chains like
437/// `Builder::new().name(c"foo").spawn(...)` also work.  Required-field
438/// validation happens at [`spawn`](Builder::spawn) time; missing name
439/// uses an empty default which most RTOSes accept.
440pub struct Builder {
441    name: &'static core::ffi::CStr,
442    priority: Priority,
443    stack_size: usize,
444}
445
446impl Default for Builder {
447    fn default() -> Self {
448        Self::new()
449    }
450}
451
452impl Builder {
453    /// Create a new builder with default settings: no name, normal
454    /// priority, 4 KB stack.
455    #[inline]
456    pub const fn new() -> Self {
457        Self {
458            // SAFETY: empty literal — `c""` requires Rust 1.77 which
459            // we already require; keep as const.
460            name: c"",
461            priority: Priority::Normal,
462            stack_size: 4096,
463        }
464    }
465
466    /// Set the thread name.  Use a `c"..."` literal (Rust 1.77+) to
467    /// guarantee null termination at compile time.
468    #[inline]
469    pub fn name(mut self, name: &'static core::ffi::CStr) -> Self {
470        self.name = name;
471        self
472    }
473
474    /// Set the thread priority.
475    #[inline]
476    pub fn priority(mut self, priority: Priority) -> Self {
477        self.priority = priority;
478        self
479    }
480
481    /// Set the stack size in bytes.
482    #[inline]
483    pub fn stack_size(mut self, stack_size: usize) -> Self {
484        self.stack_size = stack_size;
485        self
486    }
487
488    /// Spawn a thread with a `FnOnce(StopToken)` closure.
489    ///
490    /// Cooperative cancellation is built in: when the returned
491    /// [`JoinHandle`] goes out of scope, [`Drop`] calls
492    /// [`JoinHandle::request_stop`] before waiting for the worker so a
493    /// `while !tok.is_stopped()` loop exits cleanly.  Use
494    /// [`JoinHandle::detach`] to opt out of the join-on-drop.
495    ///
496    /// The closure is heap-allocated; requires the `alloc` feature and
497    /// a registered `#[global_allocator]`.
498    ///
499    /// # Errors
500    /// Returns [`Error::NoMemory`] on allocation failure or another
501    /// error if the RTOS rejects the descriptor.
502    #[cfg(all(not(zero_heap), feature = "alloc"))]
503    pub fn spawn<F>(self, f: F) -> Result<JoinHandle<()>>
504    where
505        F: FnOnce(StopToken) + Send + 'static,
506    {
507        let boxed: ClosureBox = alloc::boxed::Box::new(f);
508        let outer = alloc::boxed::Box::new(boxed);
509        let raw = alloc::boxed::Box::into_raw(outer);
510        let guard = ThunkGuard { raw };
511
512        let mut handle: bindings::ove_thread_t = core::ptr::null_mut();
513        let rc = unsafe {
514            bindings::ove_thread_create(
515                &mut handle,
516                self.name.as_ptr() as *const _,
517                Some(box_cooperative_trampoline),
518                raw as *mut core::ffi::c_void,
519                self.priority as bindings::ove_prio_t,
520                self.stack_size,
521            )
522        };
523        Error::from_code(rc)?;
524        guard.forget();
525        Ok(JoinHandle::new(handle))
526    }
527
528    /// Spawn a thread with a stateless `fn(StopToken)` entry.
529    ///
530    /// Heap-allocator-free variant of [`Builder::spawn`].  The entry
531    /// pointer is round-tripped through `*mut c_void` (guarded by a
532    /// compile-time size check).  No captures supported.
533    ///
534    /// # Errors
535    /// See [`Builder::spawn`].
536    #[cfg(not(zero_heap))]
537    pub fn spawn_cooperative(self, entry: fn(StopToken)) -> Result<JoinHandle<()>> {
538        let mut handle: bindings::ove_thread_t = core::ptr::null_mut();
539        let rc = unsafe {
540            bindings::ove_thread_create(
541                &mut handle,
542                self.name.as_ptr() as *const _,
543                Some(fn_cooperative_trampoline),
544                entry as *mut core::ffi::c_void,
545                self.priority as bindings::ove_prio_t,
546                self.stack_size,
547            )
548        };
549        Error::from_code(rc)?;
550        Ok(JoinHandle::new(handle))
551    }
552
553    /// Spawn a thread with a stateless `fn()` entry (no `StopToken`).
554    ///
555    /// Use when the worker is a self-terminating one-shot — it returns
556    /// from its entry function on its own.  [`Drop`] on the returned
557    /// [`JoinHandle`] still requests stop (no-op if the worker doesn't
558    /// observe), then waits for the entry function to return.
559    ///
560    /// # Errors
561    /// See [`Builder::spawn`].
562    #[cfg(not(zero_heap))]
563    pub fn spawn_simple(self, entry: fn()) -> Result<JoinHandle<()>> {
564        let mut handle: bindings::ove_thread_t = core::ptr::null_mut();
565        let rc = unsafe {
566            bindings::ove_thread_create(
567                &mut handle,
568                self.name.as_ptr() as *const _,
569                Some(fn_simple_trampoline),
570                entry as *mut core::ffi::c_void,
571                self.priority as bindings::ove_prio_t,
572                self.stack_size,
573            )
574        };
575        Error::from_code(rc)?;
576        Ok(JoinHandle::new(handle))
577    }
578
579    /// Spawn a cooperative-cancellation thread (`fn(StopToken)` entry)
580    /// into caller-provided static storage.  Zero-heap mode.
581    ///
582    /// `storage` carries both the kernel TCB and the stack, aligned per
583    /// the active backend's requirements.  Declare it as a `static` for
584    /// `'static`-lifetime workers or as a stack-local for scoped
585    /// workers — the borrow checker enforces "storage outlives handle"
586    /// either way.  Builder's `stack_size` field is ignored; the
587    /// type-level `N` is authoritative.
588    ///
589    /// ```ignore
590    /// static GFX: ThreadStorage<4096> = ThreadStorage::new();
591    /// let h = Thread::builder()
592    ///     .name(c"graphics")
593    ///     .spawn_static(&GFX, |tok| {
594    ///         while !tok.is_stopped() { /* work */ }
595    ///     })?;
596    /// ```
597    #[cfg(zero_heap)]
598    pub fn spawn_static<'storage, const N: usize>(
599        self,
600        storage: &'storage ThreadStorage<N>,
601        entry: fn(StopToken),
602    ) -> Result<JoinHandleBorrowed<'storage, ()>> {
603        let mut handle: bindings::ove_thread_t = core::ptr::null_mut();
604        // SAFETY: ThreadStorage owns aligned storage + stack via UnsafeCell.
605        // The kernel becomes the sole writer after ove_thread_init returns;
606        // the returned JoinHandleBorrowed carries `'storage`, so the borrow
607        // checker enforces that `storage` outlives the kernel's use of it.
608        let rc = unsafe {
609            bindings::ove_thread_init(
610                &mut handle,
611                UnsafeCell::raw_get(&storage.storage).cast(),
612                self.name.as_ptr() as *const _,
613                Some(fn_cooperative_trampoline),
614                entry as *mut core::ffi::c_void,
615                self.priority as bindings::ove_prio_t,
616                N,
617                UnsafeCell::raw_get(&storage.stack).cast(),
618            )
619        };
620        Error::from_code(rc)?;
621        Ok(JoinHandleBorrowed::new(handle))
622    }
623
624    /// Stateless `fn()` variant of [`Builder::spawn_static`] for legacy
625    /// entries that don't accept a `StopToken`.  Same safety / lifetime
626    /// rules as [`Builder::spawn_static`].
627    #[cfg(zero_heap)]
628    pub fn spawn_static_simple<'storage, const N: usize>(
629        self,
630        storage: &'storage ThreadStorage<N>,
631        entry: fn(),
632    ) -> Result<JoinHandleBorrowed<'storage, ()>> {
633        let mut handle: bindings::ove_thread_t = core::ptr::null_mut();
634        // SAFETY: see spawn_static above.
635        let rc = unsafe {
636            bindings::ove_thread_init(
637                &mut handle,
638                UnsafeCell::raw_get(&storage.storage).cast(),
639                self.name.as_ptr() as *const _,
640                Some(fn_simple_trampoline),
641                entry as *mut core::ffi::c_void,
642                self.priority as bindings::ove_prio_t,
643                N,
644                UnsafeCell::raw_get(&storage.stack).cast(),
645            )
646        };
647        Error::from_code(rc)?;
648        Ok(JoinHandleBorrowed::new(handle))
649    }
650}
651
652// ---------------------------------------------------------------------------
653// ThreadStorage — bundled TCB + aligned stack for zero-heap spawn.
654// ---------------------------------------------------------------------------
655
656/// Stack-and-TCB storage for a zero-heap thread.
657///
658/// Wraps the kernel thread control block and an aligned stack buffer
659/// into one const-constructible value.  Declare as a `static`, pass
660/// `&storage` to [`Builder::spawn_static`] / [`Builder::spawn_static_simple`].
661/// The substrate's `ove_thread_storage_t` and stack alignment are
662/// encapsulated — callers never touch them directly.
663///
664/// `STACK_SIZE` is in bytes.  The actual allocation may be larger to
665/// satisfy backend alignment (Zephyr MPU rounds up to the next power of
666/// two plus a 128-byte FPU guard region; other backends use 8-byte AAPCS
667/// alignment).
668///
669/// ```ignore
670/// static GFX_STORAGE: ThreadStorage<4096> = ThreadStorage::new();
671/// let h = Thread::builder()
672///     .name(c"graphics")
673///     .spawn_static_simple(&GFX_STORAGE, graphics_entry)?;
674/// ```
675#[cfg(zero_heap)]
676pub struct ThreadStorage<const STACK_SIZE: usize> {
677    storage: UnsafeCell<MaybeUninit<bindings::ove_thread_storage_t>>,
678    stack: UnsafeCell<AlignedStack<STACK_SIZE>>,
679}
680
681#[cfg(zero_heap)]
682impl<const STACK_SIZE: usize> ThreadStorage<STACK_SIZE> {
683    /// Construct an empty `ThreadStorage`.  `const fn` so it can
684    /// initialize `static` declarations.
685    #[inline]
686    pub const fn new() -> Self {
687        Self {
688            storage: UnsafeCell::new(MaybeUninit::zeroed()),
689            stack: UnsafeCell::new(AlignedStack::new()),
690        }
691    }
692}
693
694#[cfg(zero_heap)]
695impl<const STACK_SIZE: usize> Default for ThreadStorage<STACK_SIZE> {
696    fn default() -> Self {
697        Self::new()
698    }
699}
700
701// SAFETY: ThreadStorage's interior is touched only by the kernel after
702// spawn_static returns.  The kernel synchronizes its own access; the
703// Rust side never reads or writes the storage/stack again.  The
704// 'storage lifetime on JoinHandleBorrowed prevents the Rust side from
705// dropping the storage before the kernel is done with it.
706#[cfg(zero_heap)]
707unsafe impl<const STACK_SIZE: usize> Sync for ThreadStorage<STACK_SIZE> {}
708
709/// Backend-aligned stack buffer.  On Zephyr (MPU) the alignment is
710/// rounded to the next power of two of `(STACK_SIZE + 128)`; on other
711/// backends 8 bytes (ARM AAPCS) suffices.  Private — users go through
712/// [`ThreadStorage`].
713#[cfg(all(zero_heap, not(rtos_zephyr)))]
714#[repr(C, align(8))]
715struct AlignedStack<const N: usize>([u8; N]);
716
717#[cfg(all(zero_heap, rtos_zephyr))]
718#[repr(C, align(8192))]
719struct AlignedStack<const N: usize>([u8; N]);
720
721#[cfg(zero_heap)]
722impl<const N: usize> AlignedStack<N> {
723    const fn new() -> Self {
724        Self([0u8; N])
725    }
726}
727
728// ---------------------------------------------------------------------------
729// JoinHandle — owning, joinable handle.
730// ---------------------------------------------------------------------------
731
732// PhantomData payload for `JoinHandleBorrowed`:
733//   - `fn() -> T` keeps `T` covariant for the reserved-for-future-use
734//     worker-return-type slot;
735//   - `fn(&'storage ()) -> &'storage ()` makes the lifetime invariant
736//     so the borrow checker can neither widen nor narrow it across
737//     boundaries.
738type JoinHandleVariance<'storage, T> = (fn() -> T, fn(&'storage ()) -> &'storage ());
739
740/// Owning handle to a spawned RTOS thread.
741///
742/// Drop semantics are cooperative-cancel + join (not detach):
743///   - [`Drop`] calls [`request_stop`](Self::request_stop) then waits
744///     for the worker to finish (the substrate's join wait).
745///   - [`detach`](Self::detach) opts out of the join-on-drop — the
746///     thread keeps running and the handle is consumed without
747///     destroying the kernel object.
748///
749/// The `'storage` lifetime ties this handle to caller-provided storage
750/// in zero-heap mode.  Heap-mode spawns return [`JoinHandle<T>`] (the
751/// `'storage = 'static` alias) — kernel owns storage, no borrow.
752/// Zero-heap spawns return `JoinHandleBorrowed<'storage, T>` with the
753/// lifetime tied to the [`ThreadStorage`] reference passed to
754/// [`Builder::spawn_static`] / [`Builder::spawn_static_simple`], so the
755/// borrow checker prevents dropping the storage before the handle.
756///
757/// The `T` type parameter is reserved for future use (substrate doesn't
758/// surface a worker's return value today); ignore it and use
759/// `JoinHandle<()>`.
760#[must_use = "dropping a JoinHandle requests stop and blocks until the worker exits"]
761pub struct JoinHandleBorrowed<'storage, T = ()> {
762    handle: bindings::ove_thread_t,
763    detached: bool,
764    _phantom: PhantomData<JoinHandleVariance<'storage, T>>,
765}
766
767/// Heap-mode `JoinHandle`.  Alias of [`JoinHandleBorrowed`] with
768/// `'storage = 'static` (kernel owns the storage, no caller borrow).
769/// Matches the shape of `std::thread::JoinHandle` for source-level
770/// compatibility with std-style code.
771pub type JoinHandle<T = ()> = JoinHandleBorrowed<'static, T>;
772
773// SAFETY: same as Thread — the handle is RTOS-managed.
774unsafe impl<'storage, T> Send for JoinHandleBorrowed<'storage, T> {}
775unsafe impl<'storage, T> Sync for JoinHandleBorrowed<'storage, T> {}
776
777impl<'storage, T> JoinHandleBorrowed<'storage, T> {
778    fn new(handle: bindings::ove_thread_t) -> Self {
779        Self {
780            handle,
781            detached: false,
782            _phantom: PhantomData,
783        }
784    }
785
786    /// Get a non-owning [`Thread`] view of the running thread.  Use for
787    /// signalling (`suspend`, `set_priority`, …) without giving away
788    /// the owning handle.
789    #[inline]
790    pub fn thread(&self) -> Thread {
791        Thread {
792            handle: self.handle,
793        }
794    }
795
796    /// Request the thread to stop cooperatively.  See [`Thread::request_stop`].
797    #[inline]
798    pub fn request_stop(&self) {
799        if !self.handle.is_null() {
800            unsafe { bindings::ove_thread_request_stop(self.handle) };
801        }
802    }
803
804    /// Get a [`StopToken`] referencing this thread's cancellation flag.
805    #[inline]
806    pub fn stop_token(&self) -> StopToken {
807        StopToken {
808            handle: self.handle,
809        }
810    }
811
812    /// `true` if [`request_stop`](Self::request_stop) has been called.
813    #[inline]
814    pub fn stop_requested(&self) -> bool {
815        !self.handle.is_null() && unsafe { bindings::ove_thread_should_stop(self.handle) }
816    }
817
818    /// Wait for the worker to finish without requesting a stop first.
819    ///
820    /// Returns once the worker's entry function returns and the
821    /// substrate has joined.  For workers that loop forever without
822    /// observing [`StopToken`] this blocks indefinitely — call
823    /// [`request_stop`](Self::request_stop) beforehand if you need a
824    /// cooperative shutdown.
825    ///
826    /// `T` is always `()` today; the substrate doesn't surface worker
827    /// return values.  The method signature reserves the parameter for
828    /// a future expansion (matches `std::thread::JoinHandle::join`).
829    pub fn join(mut self) -> Result<T> {
830        if self.handle.is_null() {
831            return Err(Error::InvalidParam);
832        }
833        let handle = self.handle;
834        self.handle = core::ptr::null_mut();
835        self.detached = true; // prevent Drop from double-destroying
836        unsafe {
837            #[cfg(not(zero_heap))]
838            bindings::ove_thread_destroy(handle);
839            #[cfg(zero_heap)]
840            bindings::ove_thread_deinit(handle);
841        }
842        // SAFETY: T is required by the type system to be () for now —
843        // we transmute a unit to T because PhantomData<fn() -> T>
844        // doesn't constrain T at runtime.  Once the substrate surfaces
845        // a worker return value this becomes a real read.
846        Ok(unsafe { core::mem::MaybeUninit::<T>::zeroed().assume_init() })
847    }
848
849    /// Consume the handle without joining or requesting stop.  The
850    /// underlying kernel thread keeps running; its resources are leaked
851    /// from the binding's perspective (the RTOS may reap them when the
852    /// entry function eventually returns).
853    ///
854    /// Use this when the worker is fire-and-forget and you don't want
855    /// the join wait that [`Drop`] would otherwise do.  Prefer it over
856    /// `core::mem::forget(handle)` because the intent is visible at the
857    /// call site.
858    pub fn detach(mut self) {
859        self.detached = true;
860        // Drop runs immediately on `self`; sees detached=true and
861        // skips both request_stop and destroy.
862    }
863
864    /// Raw handle accessor for advanced use.
865    #[inline]
866    pub fn raw_handle(&self) -> bindings::ove_thread_t {
867        self.handle
868    }
869
870    // ── Convenience delegates to Thread (non-owning operations) ──
871    // These let callers do `h.suspend()` instead of `h.thread().suspend()`.
872
873    /// Suspend the running thread.  See [`Thread::suspend`].
874    #[inline]
875    pub fn suspend(&self) {
876        self.thread().suspend();
877    }
878
879    /// Resume the running thread.  See [`Thread::resume`].
880    #[inline]
881    pub fn resume(&self) {
882        self.thread().resume();
883    }
884
885    /// Change the running thread's priority.  See [`Thread::set_priority`].
886    #[inline]
887    pub fn set_priority(&self, prio: Priority) {
888        self.thread().set_priority(prio);
889    }
890
891    /// Read the running thread's state.  See [`Thread::get_state`].
892    pub fn get_state(&self) -> ThreadState {
893        self.thread().get_state()
894    }
895
896    /// Read the running thread's stack usage.  See [`Thread::get_stack_usage`].
897    #[inline]
898    pub fn get_stack_usage(&self) -> usize {
899        self.thread().get_stack_usage()
900    }
901
902    /// Read the running thread's runtime stats.  See [`Thread::get_runtime_stats`].
903    pub fn get_runtime_stats(&self) -> Result<ThreadStats> {
904        self.thread().get_runtime_stats()
905    }
906}
907
908impl<'storage, T> fmt::Debug for JoinHandleBorrowed<'storage, T> {
909    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
910        f.debug_struct("JoinHandle")
911            .field("handle", &format_args!("{:p}", self.handle))
912            .field("detached", &self.detached)
913            .finish()
914    }
915}
916
917impl<'storage, T> Drop for JoinHandleBorrowed<'storage, T> {
918    /// Signals the cooperative-cancellation flag via
919    /// [`Thread::request_stop`] then waits on the substrate's join.
920    ///
921    /// Workers built with [`Builder::spawn`] or
922    /// [`Builder::spawn_cooperative`] (poll `StopToken::is_stopped`)
923    /// exit cleanly without deadlocking.  Workers built with
924    /// [`Builder::spawn_simple`] still block the join if their entry
925    /// function doesn't return on its own — the stop flag is set but
926    /// nothing observes it.
927    ///
928    /// Suppressed entirely by [`JoinHandle::detach`].
929    fn drop(&mut self) {
930        if !self.detached && !self.handle.is_null() {
931            unsafe { bindings::ove_thread_request_stop(self.handle) };
932            #[cfg(not(zero_heap))]
933            unsafe {
934                bindings::ove_thread_destroy(self.handle)
935            };
936            #[cfg(zero_heap)]
937            unsafe {
938                bindings::ove_thread_deinit(self.handle)
939            };
940        }
941    }
942}
943
944// ---------------------------------------------------------------------------
945// System heap statistics
946// ---------------------------------------------------------------------------
947
948/// System heap statistics.
949#[derive(Debug, Clone, Copy)]
950pub struct MemStats {
951    /// Total heap size in bytes.
952    pub total: usize,
953    /// Current free heap in bytes.
954    pub free: usize,
955    /// Current used heap in bytes.
956    pub used: usize,
957    /// Peak used heap in bytes since boot.
958    pub peak_used: usize,
959}
960
961/// Query system heap statistics.
962///
963/// # Errors
964/// Returns an error if the RTOS does not provide heap statistics.
965pub fn get_mem_stats() -> Result<MemStats> {
966    let mut stats = bindings::ove_mem_stats {
967        total: 0,
968        free: 0,
969        used: 0,
970        peak_used: 0,
971    };
972    let rc = unsafe { bindings::ove_sys_get_mem_stats(&mut stats) };
973    Error::from_code(rc)?;
974    Ok(MemStats {
975        total: stats.total,
976        free: stats.free,
977        used: stats.used,
978        peak_used: stats.peak_used,
979    })
980}
981
982// ---------------------------------------------------------------------------
983// Thread enumeration
984// ---------------------------------------------------------------------------
985
986/// Snapshot of a single thread's info.
987#[derive(Debug, Clone, Copy)]
988pub struct ThreadInfo {
989    /// Thread name (static string from RTOS).
990    pub name: &'static [u8],
991    /// Execution state.
992    pub state: bindings::ove_thread_state_t,
993    /// Priority level.
994    pub priority: i32,
995    /// Stack high-water mark in bytes.
996    pub stack_used: usize,
997}
998
999/// List all threads in the system.
1000///
1001/// Fills the provided buffer with thread info snapshots and returns the
1002/// slice of entries actually written.
1003///
1004/// # Errors
1005/// Returns an error if the RTOS does not support thread enumeration.
1006pub fn thread_list(buf: &mut [ThreadInfo]) -> Result<&[ThreadInfo]> {
1007    const MAX_THREADS: usize = 32;
1008    let count = buf.len().min(MAX_THREADS);
1009    let mut raw: [bindings::ove_thread_info; MAX_THREADS] = unsafe { core::mem::zeroed() };
1010    let mut actual: usize = 0;
1011    let rc = unsafe { bindings::ove_thread_list(raw.as_mut_ptr(), count, &mut actual) };
1012    Error::from_code(rc)?;
1013
1014    let actual = actual.min(count);
1015    for i in 0..actual {
1016        let name = if raw[i].name.is_null() {
1017            &[]
1018        } else {
1019            // SAFETY: non-null checked above; `name` is a static NUL-terminated
1020            // string owned by the RTOS thread object (outlives this snapshot).
1021            // The scan stops at the NUL and the slice covers the bytes before it.
1022            unsafe {
1023                let p = raw[i].name as *const u8;
1024                let mut len = 0;
1025                while *p.add(len) != 0 {
1026                    len += 1;
1027                }
1028                core::slice::from_raw_parts(p, len)
1029            }
1030        };
1031        buf[i] = ThreadInfo {
1032            name,
1033            state: raw[i].state,
1034            priority: raw[i].priority,
1035            stack_used: raw[i].stack_used,
1036        };
1037    }
1038    Ok(&buf[..actual])
1039}