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}