Skip to main content

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: &timespec, now: &timespec) -> 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: &timespec, 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}