osal_rs/posix/timer.rs
1/***************************************************************************
2 *
3 * osal-rs
4 * Copyright (C) 2026 Antonio Salsi <passy.linux@zresa.it>
5 *
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
10 *
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, see <https://www.gnu.org/licenses/>.
18 *
19 ***************************************************************************/
20
21//! Software timer support for POSIX.
22//!
23//! pthreads has no notion of a shared "timer daemon task" the way FreeRTOS
24//! does, so each [`Timer`] gets its own dedicated background thread, which
25//! sleeps on a condition variable until the timer's next deadline:
26//!
27//! 1. The deadline lives in the timer's shared state (mutex + condvar +
28//! next expiry on the monotonic clock). `start`/`reset`/`change_period`
29//! set it, `stop` clears it, and every change wakes the thread so it
30//! re-evaluates how long to sleep.
31//! 2. The background thread waits with
32//! [`cond_timedwait_monotonic`](crate::posix::ffi), so the countdown is
33//! unaffected by wall-clock changes on both Linux and macOS. When the
34//! deadline passes it re-arms (auto-reload) or disarms (one-shot) the
35//! timer, releases the lock and invokes the user callback.
36//! 3. Auto-reload deadlines advance from the previous deadline rather than
37//! from "now", so the period does not drift by the callback's run time.
38//! If the callback overruns a whole period the missed expirations are
39//! coalesced into one, as with a kernel `timer_settime` interval.
40//!
41//! The same implementation runs on every supported platform: it relies only
42//! on pthread mutexes/condvars, unlike a `timer_create(2)`/`SIGEV_THREAD_ID`
43//! design, which macOS does not provide. No signal is involved, so creating
44//! a `Timer` leaves the calling thread's signal mask untouched, and a
45//! one-shot timer can be started again after it fired.
46//!
47//! # Examples
48//!
49//! ```
50//! use osal_rs::os::*;
51//! use std::sync::Arc;
52//! use std::sync::atomic::{AtomicBool, Ordering};
53//! use core::time::Duration;
54//!
55//! static FIRED: AtomicBool = AtomicBool::new(false);
56//!
57//! let timer = Timer::new_with_to_tick(
58//! "heartbeat",
59//! Duration::from_millis(10),
60//! false, // one-shot
61//! None,
62//! |_timer, _param| {
63//! FIRED.store(true, Ordering::SeqCst);
64//! Ok(Arc::new(()))
65//! }
66//! ).unwrap();
67//!
68//! timer.start(0);
69//! System::delay(50);
70//! assert!(FIRED.load(Ordering::SeqCst));
71//! ```
72
73use core::ffi::c_long;
74use core::fmt::{Debug, Display};
75use core::ops::Deref;
76use core::ptr::null_mut;
77use core::time::Duration;
78
79use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
80
81use alloc::boxed::Box;
82use alloc::sync::Arc;
83
84use crate::os::ThreadFn;
85use crate::posix::config::TICK_PERIOD_MS;
86use crate::posix::ffi::{CLOCK_MONOTONIC, clock_gettime, monotonic_deadline, pthread_self, timespec};
87use crate::posix::mutex::Mutex;
88use crate::posix::thread::{RawCondvar, Thread};
89use crate::posix::types::{StackType, TickType, TimerHandle, UBaseType};
90use crate::traits::{MAX_TASK_NAME_LEN, MutexFn, TimerFn, TimerFnPtr, TimerParam, ToTick};
91use crate::utils::{Bytes, OsalRsBool, Result};
92
93/// Name (`<= 15` chars, the Linux limit) given to every timer's background
94/// thread. Fixed rather than derived from the timer's own name so it's
95/// always valid regardless of what the caller passed to `Timer::new`.
96const TIMER_THREAD_NAME: &str = "os_timer";
97
98/// Stack size requested for a timer's background thread. `Thread::spawn`
99/// enforces its own safe minimum regardless, so this only matters as a
100/// lower bound.
101const TIMER_THREAD_STACK: StackType = 1024;
102
103const NSECS_PER_SEC: c_long = 1_000_000_000;
104
105/// Priority given to a timer's background thread: the lowest valid
106/// `SCHED_FIFO` priority with the `real_time` feature (1 on Linux, 15 on
107/// macOS), otherwise a placeholder, since the thread then inherits the
108/// creating thread's scheduling policy/priority.
109fn timer_thread_priority() -> UBaseType {
110 #[cfg(feature = "real_time")]
111 {
112 use crate::posix::ffi::{SCHED_FIFO, sched_get_priority_min};
113
114 (unsafe { sched_get_priority_min(SCHED_FIFO) }).max(0) as UBaseType
115 }
116
117 #[cfg(not(feature = "real_time"))]
118 {
119 1
120 }
121}
122
123/// Mutable timer state, guarded by [`TimerShared::state`].
124#[derive(Default)]
125struct TimerState {
126 /// Next expiry on the monotonic clock; `None` while the timer is stopped.
127 deadline: Option<timespec>,
128 /// Set by teardown to make the background thread return.
129 exit: bool,
130}
131
132/// State shared, via `Arc`, between every clone of a given [`Timer`], the
133/// handles given to its callback, and its background thread.
134///
135/// [`Timer`] itself is freely `Clone` (matching every other handle type in
136/// this crate), but the background thread, its deadline, and the mutable
137/// period all belong to one underlying resource — this is that resource.
138/// Holding it does not keep the timer running; [`TimerOwner`] does.
139struct TimerShared {
140 /// Deadline and exit flag; every change is announced on `cv`.
141 state: Mutex<TimerState>,
142 cv: RawCondvar,
143 /// `true` from successful creation until teardown. Lets `delete()`
144 /// claim teardown exactly once and makes every clone see it.
145 ready: AtomicBool,
146 /// Current period, in microseconds.
147 us: AtomicU32,
148 oneshot: AtomicBool,
149 /// The background thread, so teardown can wake and join it.
150 thread: Mutex<Option<Thread>>,
151}
152
153impl TimerShared {
154 /// Stops the background thread and reaps it, at most once however many
155 /// handles ask for it. Shared by [`TimerFn::delete`] and by
156 /// [`TimerOwner`]'s `Drop`.
157 ///
158 /// Joins the background thread, so once this returns no callback is
159 /// running or will run - except when called *from* the background
160 /// thread (a callback deleting its timer or dropping its last handle),
161 /// which detaches instead and lets the thread exit after the callback.
162 fn destroy(&self) {
163 self.ready.store(false, Ordering::Release);
164
165 if let Ok(mut state) = self.state.lock() {
166 state.exit = true;
167 state.deadline = None;
168 }
169 self.cv.notify_all();
170
171 let Ok(mut guard) = self.thread.lock() else {
172 return;
173 };
174
175 let Some(bg_thread) = guard.take() else {
176 return;
177 };
178
179 if *bg_thread == unsafe { pthread_self() } {
180 // Joining would be joining ourselves, so detach instead and let
181 // the thread release itself once it sees `exit`.
182 bg_thread.detach();
183 return;
184 }
185
186 bg_thread.delete();
187 }
188}
189
190/// Ownership token held by every [`Timer`] handle the user creates or
191/// clones, but never by the handle given to the callback (nor by the
192/// background thread). Dropping the last one destroys the timer - the RAII
193/// half of [`TimerFn::delete`], and the reason `Timer` itself has no `Drop`
194/// of its own (a per-handle `Drop` would tear the timer down as soon as
195/// *any* clone was dropped).
196///
197/// Keeping it apart from [`TimerShared`] is what makes the drop
198/// deterministic: if a callback were running with an owning reference, the
199/// user's last drop would not be the last one, and the timer would only
200/// stop after that callback - on the background thread, after the user's
201/// drop had already returned.
202struct TimerOwner(Arc<TimerShared>);
203
204impl Drop for TimerOwner {
205 fn drop(&mut self) {
206 self.0.destroy();
207 }
208}
209
210/// A software timer backed by a dedicated background thread that sleeps
211/// until the timer's next deadline and invokes the user callback. Freely
212/// [`Clone`]-able - every clone shares the same underlying timer. See
213/// [`Timer::new`] for a complete, testable example.
214#[derive(Clone)]
215pub struct Timer {
216 /// Opaque identifier of the underlying timer, exposed for diagnostics
217 /// (`Debug`/`Display`). `null` once [`TimerFn::delete`]d.
218 pub handle: TimerHandle,
219 /// In the same fixed-size buffer every other named object in this crate
220 /// uses. `Bytes` is `Copy`, so handing a named handle to the callback on
221 /// every firing costs nothing.
222 name: Bytes<MAX_TASK_NAME_LEN>,
223 callback: Option<Arc<TimerFnPtr>>,
224 param: Option<TimerParam>,
225 /// `Some` for handles the user owns; `None` for the borrowed handle
226 /// passed to the callback (see [`TimerOwner`]).
227 owner: Option<Arc<TimerOwner>>,
228 shared: Option<Arc<TimerShared>>,
229}
230
231unsafe impl Send for Timer {}
232unsafe impl Sync for Timer {}
233
234/// Converts a tick count (this crate's ticks are milliseconds, see
235/// [`TICK_PERIOD_MS`]) to microseconds, saturating instead of overflowing
236/// `u32`.
237fn ticks_to_us(ticks: TickType) -> u32 {
238 (ticks as u64).saturating_mul(TICK_PERIOD_MS).saturating_mul(1000).min(u32::MAX as u64) as u32
239}
240
241/// Current time on the monotonic clock.
242fn monotonic_now() -> timespec {
243 let mut now = timespec::default();
244 unsafe {
245 clock_gettime(CLOCK_MONOTONIC, &mut now);
246 }
247 now
248}
249
250/// `true` once `deadline` is no later than `now`.
251fn is_due(deadline: ×pec, now: ×pec) -> bool {
252 (deadline.tv_sec, deadline.tv_nsec) <= (now.tv_sec, now.tv_nsec)
253}
254
255/// `time` advanced by `us` microseconds.
256fn add_us(time: ×pec, us: u32) -> timespec {
257 let nanoseconds = (us as c_long % 1_000_000) * 1000 + time.tv_nsec;
258
259 timespec {
260 tv_sec: time.tv_sec + us as c_long / 1_000_000 + nanoseconds / NSECS_PER_SEC,
261 tv_nsec: nanoseconds % NSECS_PER_SEC,
262 }
263}
264
265/// Arms `shared`'s timer to fire `us` microseconds from now, or disarms it
266/// if `us == 0`, and wakes the background thread so it picks up the change.
267/// No-op (returns `False`) once the timer has been deleted.
268fn arm(shared: &TimerShared, us: u32) -> OsalRsBool {
269 if !shared.ready.load(Ordering::Acquire) {
270 return OsalRsBool::False;
271 }
272
273 let Ok(mut state) = shared.state.lock() else {
274 return OsalRsBool::False;
275 };
276
277 state.deadline = if us == 0 {
278 None
279 } else {
280 Some(monotonic_deadline(Duration::from_micros(us as u64)))
281 };
282
283 drop(state);
284 shared.cv.notify_all();
285
286 OsalRsBool::True
287}
288
289/// Body of the background thread every [`Timer`] spawns: sleeps until the
290/// deadline in `shared` passes, re-arms or disarms it, then invokes the user
291/// callback with the lock released. See the module docs for the full
292/// rationale.
293///
294/// Holding `shared` does not keep the timer alive (see [`TimerOwner`]): the
295/// thread runs until teardown sets `exit`.
296fn run_timer_thread(shared: Arc<TimerShared>, name: Bytes<MAX_TASK_NAME_LEN>, callback: Option<Arc<TimerFnPtr>>, mut param: Option<TimerParam>) -> Result<TimerParam> {
297 loop {
298 let Ok(mut state) = shared.state.lock() else {
299 break;
300 };
301
302 // Sleep until the deadline passes. Any change to the deadline (or
303 // teardown) wakes the condvar, and waits may also return spuriously,
304 // so the state is re-examined after every wake-up.
305 loop {
306 if state.exit {
307 break;
308 }
309
310 match state.deadline {
311 None => shared.cv.wait(&state),
312 Some(deadline) if is_due(&deadline, &monotonic_now()) => break,
313 Some(deadline) => {
314 shared.cv.wait_until(&state, deadline);
315 }
316 }
317 }
318
319 if state.exit {
320 break;
321 }
322
323 // Re-arm before running the callback, so a callback that calls
324 // `stop`/`start`/`change_period` overrides this.
325 state.deadline = if shared.oneshot.load(Ordering::Acquire) {
326 None
327 } else {
328 let us = shared.us.load(Ordering::Acquire);
329 let now = monotonic_now();
330 let next = state.deadline.map(|deadline| add_us(&deadline, us)).unwrap_or(now);
331
332 // Overran a whole period: coalesce the missed expirations.
333 Some(if is_due(&next, &now) { add_us(&now, us) } else { next })
334 };
335
336 drop(state);
337
338 if let Some(cb) = &callback {
339 // The callback is handed a borrowed handle onto the same shared
340 // state: it does not own the timer (`owner: None`), since
341 // `TimerFnPtr` takes its `Box<dyn TimerFn>` by value and drops
342 // it on return.
343 let timer_self = Timer {
344 handle: Arc::as_ptr(&shared) as TimerHandle,
345 name,
346 callback: callback.clone(),
347 param: param.clone(),
348 owner: None,
349 shared: Some(shared.clone()),
350 };
351
352 if let Ok(new_param) = cb(Box::new(timer_self), param.clone()) {
353 param = Some(new_param);
354 }
355 }
356 }
357
358 let final_param: TimerParam = match param {
359 Some(p) => p,
360 None => Arc::new(()),
361 };
362
363 Ok(final_param)
364}
365
366impl Timer {
367 /// Same as [`Timer::new`], but accepts any [`ToTick`] period (e.g. a
368 /// [`core::time::Duration`]) instead of a raw tick count. See the
369 /// module-level docs above for a complete example.
370 #[inline]
371 pub fn new_with_to_tick<F>(name: &str, timer_period_in_ticks: impl ToTick, auto_reload: bool, param: Option<TimerParam>, callback: F) -> Result<Self>
372 where
373 F: Fn(Box<dyn TimerFn>, Option<TimerParam>) -> Result<TimerParam> + Send + Sync + Clone + 'static,
374 {
375 Self::new(name, timer_period_in_ticks.to_ticks(), auto_reload, param, callback)
376 }
377
378 /// Same as [`TimerFn::start`], but accepts any [`ToTick`] value (e.g. a
379 /// [`core::time::Duration`]) instead of a raw tick count.
380 ///
381 /// # Examples
382 ///
383 /// ```
384 /// use osal_rs::os::*;
385 /// use std::sync::Arc;
386 /// use core::time::Duration;
387 ///
388 /// let timer = Timer::new_with_to_tick("t", Duration::from_millis(50), false, None, |_t, _p| Ok(Arc::new(()))).unwrap();
389 /// assert_eq!(timer.start_with_to_tick(Duration::from_millis(10)), osal_rs::utils::OsalRsBool::True);
390 /// ```
391 #[inline]
392 pub fn start_with_to_tick(&self, ticks_to_wait: impl ToTick) -> OsalRsBool {
393 self.start(ticks_to_wait.to_ticks())
394 }
395
396 /// Same as [`TimerFn::stop`], but accepts any [`ToTick`] value instead
397 /// of a raw tick count.
398 #[inline]
399 pub fn stop_with_to_tick(&self, ticks_to_wait: impl ToTick) -> OsalRsBool {
400 self.stop(ticks_to_wait.to_ticks())
401 }
402
403 /// Same as [`TimerFn::reset`], but accepts any [`ToTick`] value instead
404 /// of a raw tick count.
405 #[inline]
406 pub fn reset_with_to_tick(&self, ticks_to_wait: impl ToTick) -> OsalRsBool {
407 self.reset(ticks_to_wait.to_ticks())
408 }
409
410 /// Same as [`TimerFn::change_period`], but accepts any [`ToTick`] values
411 /// instead of raw tick counts.
412 #[inline]
413 pub fn change_period_with_to_tick(&self, new_period_in_ticks: impl ToTick, new_period_ticks: impl ToTick) -> OsalRsBool {
414 self.change_period(new_period_in_ticks.to_ticks(), new_period_ticks.to_ticks())
415 }
416
417 /// Same as [`TimerFn::delete`], but accepts any [`ToTick`] value instead
418 /// of a raw tick count.
419 #[inline]
420 pub fn delete_with_to_tick(&mut self, ticks_to_wait: impl ToTick) -> OsalRsBool {
421 self.delete(ticks_to_wait.to_ticks())
422 }
423
424 /// Creates a new timer named `name`, firing `callback` every
425 /// `timer_period_in_ticks` ticks if `auto_reload` (one-shot otherwise).
426 /// `param` is handed to the first callback invocation; each invocation
427 /// can return an updated value for the next one. The timer is created
428 /// stopped - call [`TimerFn::start`] to arm it.
429 ///
430 /// # Examples
431 ///
432 /// ```
433 /// use osal_rs::os::*;
434 /// use std::sync::Arc;
435 ///
436 /// let timer = Timer::new("t", 50, false, None, |_timer, _param| Ok(Arc::new(()))).unwrap();
437 /// assert!(!timer.is_null());
438 /// ```
439 pub fn new<F>(name: &str, timer_period_in_ticks: TickType, auto_reload: bool, param: Option<TimerParam>, callback: F) -> Result<Self>
440 where
441 F: Fn(Box<dyn TimerFn>, Option<TimerParam>) -> Result<TimerParam> + Send + Sync + Clone + 'static,
442 {
443 let shared = Arc::new(TimerShared {
444 state: Mutex::new(TimerState::default()),
445 cv: RawCondvar::new(),
446 ready: AtomicBool::new(false),
447 us: AtomicU32::new(ticks_to_us(timer_period_in_ticks)),
448 oneshot: AtomicBool::new(!auto_reload),
449 thread: Mutex::new(None),
450 });
451
452 let name = Bytes::<MAX_TASK_NAME_LEN>::from_str(name);
453
454 let mut timer = Self {
455 handle: Arc::as_ptr(&shared) as TimerHandle,
456 name,
457 callback: Some(Arc::new(callback)),
458 param,
459 owner: Some(Arc::new(TimerOwner(shared.clone()))),
460 shared: Some(shared.clone()),
461 };
462
463 let bg_shared = shared.clone();
464 let bg_name = name;
465 let bg_callback = timer.callback.clone();
466 let bg_param = timer.param.clone();
467
468 let mut bg_thread = Thread::new(TIMER_THREAD_NAME, TIMER_THREAD_STACK, timer_thread_priority());
469 let bg_thread = match bg_thread.spawn_simple(move || run_timer_thread(bg_shared.clone(), bg_name, bg_callback.clone(), bg_param.clone())) {
470 Ok(thread) => thread,
471 Err(err) => {
472 timer.handle = null_mut();
473 return Err(err);
474 }
475 };
476
477 *shared.thread.lock().unwrap() = Some(bg_thread);
478 shared.ready.store(true, Ordering::Release);
479
480 Ok(timer)
481 }
482}
483
484impl TimerFn for Timer {
485 /// Returns `true` if this timer has been [`TimerFn::delete`]d (by this
486 /// handle or by any clone of it).
487 ///
488 /// # Examples
489 ///
490 /// ```
491 /// use osal_rs::os::*;
492 /// use std::sync::Arc;
493 ///
494 /// let mut timer = Timer::new("t", 50, false, None, |_t, _p| Ok(Arc::new(()))).unwrap();
495 /// assert!(!timer.is_null());
496 ///
497 /// timer.delete(0);
498 /// assert!(timer.is_null());
499 /// ```
500 fn is_null(&self) -> bool {
501 match &self.shared {
502 Some(shared) => !shared.ready.load(Ordering::Acquire),
503 None => true,
504 }
505 }
506
507 /// Arms the timer to fire after its configured period. See the
508 /// module-level docs above for a complete example.
509 fn start(&self, _ticks_to_wait: TickType) -> OsalRsBool {
510 let Some(shared) = &self.shared else {
511 return OsalRsBool::False;
512 };
513
514 arm(shared, shared.us.load(Ordering::Acquire))
515 }
516
517 /// Disarms the timer; a no-op if it isn't currently running.
518 ///
519 /// # Examples
520 ///
521 /// ```
522 /// use osal_rs::os::*;
523 /// use std::sync::Arc;
524 /// use std::sync::atomic::{AtomicBool, Ordering};
525 ///
526 /// static FIRED: AtomicBool = AtomicBool::new(false);
527 ///
528 /// let timer = Timer::new("t", 20, false, None, |_t, _p| {
529 /// FIRED.store(true, Ordering::SeqCst);
530 /// Ok(Arc::new(()))
531 /// }).unwrap();
532 ///
533 /// timer.start(0);
534 /// timer.stop(0); // cancels before it can fire
535 ///
536 /// System::delay(50);
537 /// assert!(!FIRED.load(Ordering::SeqCst));
538 /// ```
539 fn stop(&self, _ticks_to_wait: TickType) -> OsalRsBool {
540 let Some(shared) = &self.shared else {
541 return OsalRsBool::False;
542 };
543
544 arm(shared, 0)
545 }
546
547 /// Restarts the countdown from now, using the current period -
548 /// equivalent to calling [`TimerFn::start`] again, whether the timer was
549 /// previously running or stopped.
550 fn reset(&self, ticks_to_wait: TickType) -> OsalRsBool {
551 // Arming always restarts the countdown from now, whether the timer
552 // was previously running or stopped.
553 self.start(ticks_to_wait)
554 }
555
556 /// Updates the timer's period and immediately (re)arms it with the new
557 /// value.
558 ///
559 /// # Examples
560 ///
561 /// ```
562 /// use osal_rs::os::*;
563 /// use std::sync::Arc;
564 /// use std::sync::atomic::{AtomicBool, Ordering};
565 ///
566 /// static FIRED: AtomicBool = AtomicBool::new(false);
567 ///
568 /// let timer = Timer::new("t", 10_000, false, None, |_t, _p| {
569 /// FIRED.store(true, Ordering::SeqCst);
570 /// Ok(Arc::new(()))
571 /// }).unwrap();
572 ///
573 /// // Original 10s period would never fire in time; shrink it to 10ms.
574 /// timer.change_period(10, 0);
575 ///
576 /// System::delay(50);
577 /// assert!(FIRED.load(Ordering::SeqCst));
578 /// ```
579 fn change_period(&self, new_period_in_ticks: TickType, ticks_to_wait: TickType) -> OsalRsBool {
580 let Some(shared) = &self.shared else {
581 return OsalRsBool::False;
582 };
583
584 shared.us.store(ticks_to_us(new_period_in_ticks), Ordering::Release);
585 self.start(ticks_to_wait)
586 }
587
588 /// Stops the timer and reaps its background thread,
589 /// resetting this [`Timer`] to its "null" state. See
590 /// [`TimerFn::is_null`] for a complete example.
591 fn delete(&mut self, _ticks_to_wait: TickType) -> OsalRsBool {
592 // Giving up the shared state is what makes this handle null; the
593 // timer itself is destroyed by whichever handle gets there first,
594 // and every clone sees it through the `ready` flag.
595 let Some(shared) = self.shared.take() else {
596 return OsalRsBool::False;
597 };
598
599 shared.destroy();
600 self.owner = None;
601
602 self.handle = null_mut();
603 OsalRsBool::True
604 }
605}
606
607impl Deref for Timer {
608 type Target = TimerHandle;
609
610 fn deref(&self) -> &Self::Target {
611 &self.handle
612 }
613}
614
615impl Debug for Timer {
616 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
617 f.debug_struct("Timer")
618 .field("handle", &self.handle)
619 .field("name", &self.name)
620 .field("has_callback", &self.callback.is_some())
621 .field("has_param", &self.param.is_some())
622 .field("is_null", &self.is_null())
623 .finish()
624 }
625}
626
627impl Display for Timer {
628 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
629 write!(f, "Timer {{ name: {}, handle: {:?} }}", self.name, self.handle)
630 }
631}