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 plus a
25//! real kernel timer (`timer_create(2)`) that notifies that thread directly:
26//!
27//! 1. `SIGALRM` is blocked in the calling thread (`sigprocmask`) before the
28//!    background thread is spawned, so the mask — and with it, the block —
29//!    is inherited by the new thread too. A blocked signal isn't discarded;
30//!    it becomes *pending* until something explicitly consumes it.
31//! 2. The background thread publishes its kernel thread ID (`gettid(2)`,
32//!    distinct from its `pthread_t`) and then loops on `sigwait(3)`, which
33//!    synchronously consumes one pending, blocked `SIGALRM` at a time and
34//!    invokes the user callback in response.
35//! 3. `timer_create` is configured with `SIGEV_THREAD_ID` notification,
36//!    targeting that kernel thread ID directly — so this timer's expirations
37//!    can only ever wake up this timer's own background thread, never any
38//!    other timer's or unrelated code's.
39//!
40//! This mirrors a common pattern for per-thread POSIX timers (create a
41//! dedicated waiter thread, mask + `sigwait` instead of an async-signal
42//! handler, `SIGEV_THREAD_ID` to target it precisely).
43//!
44//! # Caveats inherited from this design
45//!
46//! - Blocking `SIGALRM` in the calling thread is permanent for that thread:
47//!   this crate never unblocks it afterwards, so a thread that creates a
48//!   `Timer` can no longer receive `SIGALRM` itself.
49//! - A one-shot timer's background thread exits after its single callback
50//!   invocation. Calling `start()`/`reset()` again on an already-fired
51//!   one-shot timer re-arms the kernel timer, but nothing is left running to
52//!   consume its `SIGALRM` — create a new `Timer` instead of reusing one.
53//!
54//! # Examples
55//!
56//! ```
57//! use osal_rs::os::*;
58//! use std::sync::Arc;
59//! use std::sync::atomic::{AtomicBool, Ordering};
60//! use core::time::Duration;
61//!
62//! static FIRED: AtomicBool = AtomicBool::new(false);
63//!
64//! let timer = Timer::new_with_to_tick(
65//!     "heartbeat",
66//!     Duration::from_millis(10),
67//!     false, // one-shot
68//!     None,
69//!     |_timer, _param| {
70//!         FIRED.store(true, Ordering::SeqCst);
71//!         Ok(Arc::new(()))
72//!     }
73//! ).unwrap();
74//!
75//! timer.start(0);
76//! System::delay(50);
77//! assert!(FIRED.load(Ordering::SeqCst));
78//! ```
79
80use core::ffi::{c_int, c_long, c_void};
81use core::fmt::{Debug, Display};
82use core::ops::Deref;
83use core::ptr::null_mut;
84
85use std::sync::Mutex;
86use std::sync::atomic::{AtomicBool, AtomicI32, AtomicPtr, AtomicU32, Ordering};
87
88use alloc::boxed::Box;
89use alloc::string::{String, ToString};
90use alloc::sync::Arc;
91
92use crate::os::ThreadFn;
93use crate::posix::config::TICK_PERIOD_MS;
94use crate::posix::ffi::{
95    CLOCK_MONOTONIC, SIGALRM, SIGEV_THREAD_ID, SIG_BLOCK, gettid, itimerspec, pthread_kill, sched_yield, sigaddset, sigemptyset, sigevent, sigevent_un, sigprocmask, sigset_t, sigwait,
96    timer_create, timer_delete, timer_settime, timer_t, timespec,
97};
98use crate::posix::thread::Thread;
99use crate::posix::types::{StackType, TickType, TimerHandle, UBaseType};
100use crate::traits::{TimerFn, TimerFnPtr, TimerParam, ToTick};
101use crate::utils::{Error, OsalRsBool, Result};
102
103/// Name (glibc `pthread_setname_np`, `<= 15` chars) given to every timer's
104/// background thread. Fixed rather than derived from the timer's own name so
105/// it's always valid regardless of what the caller passed to `Timer::new`.
106const TIMER_THREAD_NAME: &str = "os_timer";
107
108/// Stack size requested for a timer's background thread. `Thread::spawn`
109/// enforces its own safe minimum regardless, so this only matters as a
110/// lower bound.
111const TIMER_THREAD_STACK: StackType = 1024;
112
113/// Priority given to a timer's background thread. Only meaningful with the
114/// `sched_fifo` feature enabled; otherwise the thread inherits the creating
115/// thread's scheduling policy/priority.
116const TIMER_THREAD_PRIORITY: UBaseType = 1;
117
118const NSECS_PER_SEC: u64 = 1_000_000_000;
119
120/// State shared, via `Arc`, between every clone of a given [`Timer`] and its
121/// background thread.
122///
123/// [`Timer`] itself is freely `Clone` (matching every other handle type in
124/// this crate), but the POSIX timer, its background thread, and its
125/// mutable period all belong to one underlying resource — this is that
126/// resource.
127struct TimerShared {
128    /// The real POSIX timer, once `ready` is set. Not read before then:
129    /// on modern glibc/Linux, `timer_t` is a small kernel-assigned integer
130    /// cast to a pointer, so the *first* timer a process ever creates gets
131    /// the all-zero handle — a bit pattern that would otherwise look
132    /// indistinguishable from "not created yet".
133    timerid: AtomicPtr<c_void>,
134    /// Set once `timerid` holds a real value from a successful
135    /// `timer_create`. Guards every read of `timerid` instead of checking
136    /// it for null (see `timerid`'s docs for why null isn't a safe sentinel
137    /// here), and lets `delete()` claim deletion exactly once.
138    ready: AtomicBool,
139    /// Kernel TID (`gettid()`) of the background thread, published once it
140    /// starts running; 0 until then.
141    thread_id: AtomicI32,
142    /// Current period, in microseconds.
143    us: AtomicU32,
144    oneshot: AtomicBool,
145    /// Set by `delete()` to tell the background thread's `sigwait` loop to
146    /// stop instead of invoking the callback again.
147    exit: AtomicBool,
148    /// The background thread, so `delete()` can wake and join it.
149    thread: Mutex<Option<Thread>>,
150}
151
152/// A software timer backed by a POSIX `timer_create`/`SIGALRM` timer and a
153/// dedicated background thread that waits for the signal and invokes the
154/// user callback. Freely [`Clone`]-able - every clone shares the same
155/// underlying timer. See [`Timer::new`] for a complete, testable example.
156#[derive(Clone)]
157pub struct Timer {
158    /// Raw handle to the underlying `timer_t`, exposed for diagnostics
159    /// (`Debug`/`Display`). `null` until [`Timer::new`] successfully calls
160    /// `timer_create`.
161    pub handle: TimerHandle,
162    name: String,
163    callback: Option<Arc<TimerFnPtr>>,
164    param: Option<TimerParam>,
165    shared: Option<Arc<TimerShared>>,
166}
167
168unsafe impl Send for Timer {}
169unsafe impl Sync for Timer {}
170
171/// Converts a tick count (this crate's ticks are milliseconds, see
172/// [`TICK_PERIOD_MS`]) to microseconds, saturating instead of overflowing
173/// `u32` (the width `timer_settime`'s nanosecond math is done in below).
174fn ticks_to_us(ticks: TickType) -> u32 {
175    (ticks as u64).saturating_mul(TICK_PERIOD_MS).saturating_mul(1000).min(u32::MAX as u64) as u32
176}
177
178/// Arms `shared`'s timer to fire `us` microseconds from now, or disarms it
179/// if `us == 0` (per `timer_settime(2)`, an all-zero `it_value` always
180/// disarms regardless of `it_interval`). No-op (returns `False`) if the
181/// timer hasn't been created yet.
182fn arm(shared: &TimerShared, us: u32) -> OsalRsBool {
183    if !shared.ready.load(Ordering::Acquire) {
184        return OsalRsBool::False;
185    }
186
187    let timerid = shared.timerid.load(Ordering::Acquire);
188
189    let nanoseconds = (us as u64) * 1000;
190    let it_value = timespec {
191        tv_sec: (nanoseconds / NSECS_PER_SEC) as c_long,
192        tv_nsec: (nanoseconds % NSECS_PER_SEC) as c_long,
193    };
194
195    let it_interval = if us == 0 || shared.oneshot.load(Ordering::Acquire) {
196        timespec::default()
197    } else {
198        it_value
199    };
200
201    let its = itimerspec { it_interval, it_value };
202
203    match unsafe { timer_settime(timerid, 0, &its, null_mut()) } {
204        0 => OsalRsBool::True,
205        _ => OsalRsBool::False,
206    }
207}
208
209/// Body of the background thread every [`Timer`] spawns: publishes its
210/// kernel TID, then loops accepting one blocked `SIGALRM` at a time via
211/// `sigwait` and invoking the user callback in response. See the module
212/// docs for the full rationale.
213fn run_timer_thread(shared: Arc<TimerShared>, callback: Option<Arc<TimerFnPtr>>, mut param: Option<TimerParam>, timer_self: Timer) -> Result<TimerParam> {
214    shared.thread_id.store(unsafe { gettid() }, Ordering::Release);
215
216    let mut sigset = sigset_t::default();
217    unsafe {
218        sigemptyset(&mut sigset);
219        sigaddset(&mut sigset, SIGALRM);
220    }
221
222    while !shared.exit.load(Ordering::Acquire) {
223        let mut sig: c_int = 0;
224
225        if unsafe { sigwait(&sigset, &mut sig) } != 0 || sig != SIGALRM {
226            continue;
227        }
228
229        // The signal that just woke us might be the real timer expiration,
230        // or the artificial one `delete()` sends to break out of `sigwait`.
231        if shared.exit.load(Ordering::Acquire) {
232            break;
233        }
234
235        if let Some(cb) = &callback {
236            if let Ok(new_param) = cb(Box::new(timer_self.clone()), param.clone()) {
237                param = Some(new_param);
238            }
239        }
240
241        if shared.oneshot.load(Ordering::Acquire) {
242            break;
243        }
244    }
245
246    let final_param: TimerParam = match param {
247        Some(p) => p,
248        None => Arc::new(()),
249    };
250
251    Ok(final_param)
252}
253
254impl Timer {
255    /// Same as [`Timer::new`], but accepts any [`ToTick`] period (e.g. a
256    /// [`core::time::Duration`]) instead of a raw tick count. See the
257    /// module-level docs above for a complete example.
258    #[inline]
259    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>
260    where
261        F: Fn(Box<dyn TimerFn>, Option<TimerParam>) -> Result<TimerParam> + Send + Sync + Clone + 'static,
262    {
263        Self::new(name, timer_period_in_ticks.to_ticks(), auto_reload, param, callback)
264    }
265
266    /// Same as [`TimerFn::start`], but accepts any [`ToTick`] value (e.g. a
267    /// [`core::time::Duration`]) instead of a raw tick count.
268    ///
269    /// # Examples
270    ///
271    /// ```
272    /// use osal_rs::os::*;
273    /// use std::sync::Arc;
274    /// use core::time::Duration;
275    ///
276    /// let timer = Timer::new_with_to_tick("t", Duration::from_millis(50), false, None, |_t, _p| Ok(Arc::new(()))).unwrap();
277    /// assert_eq!(timer.start_with_to_tick(Duration::from_millis(10)), osal_rs::utils::OsalRsBool::True);
278    /// ```
279    #[inline]
280    pub fn start_with_to_tick(&self, ticks_to_wait: impl ToTick) -> OsalRsBool {
281        self.start(ticks_to_wait.to_ticks())
282    }
283
284    /// Same as [`TimerFn::stop`], but accepts any [`ToTick`] value instead
285    /// of a raw tick count.
286    #[inline]
287    pub fn stop_with_to_tick(&self, ticks_to_wait: impl ToTick) -> OsalRsBool {
288        self.stop(ticks_to_wait.to_ticks())
289    }
290
291    /// Same as [`TimerFn::reset`], but accepts any [`ToTick`] value instead
292    /// of a raw tick count.
293    #[inline]
294    pub fn reset_with_to_tick(&self, ticks_to_wait: impl ToTick) -> OsalRsBool {
295        self.reset(ticks_to_wait.to_ticks())
296    }
297
298    /// Same as [`TimerFn::change_period`], but accepts any [`ToTick`] values
299    /// instead of raw tick counts.
300    #[inline]
301    pub fn change_period_with_to_tick(&self, new_period_in_ticks: impl ToTick, new_period_ticks: impl ToTick) -> OsalRsBool {
302        self.change_period(new_period_in_ticks.to_ticks(), new_period_ticks.to_ticks())
303    }
304
305    /// Same as [`TimerFn::delete`], but accepts any [`ToTick`] value instead
306    /// of a raw tick count.
307    #[inline]
308    pub fn delete_with_to_tick(&mut self, ticks_to_wait: impl ToTick) -> OsalRsBool {
309        self.delete(ticks_to_wait.to_ticks())
310    }
311
312    /// Creates a new timer named `name`, firing `callback` every
313    /// `timer_period_in_ticks` ticks if `auto_reload` (one-shot otherwise).
314    /// `param` is handed to the first callback invocation; each invocation
315    /// can return an updated value for the next one. The timer is created
316    /// stopped - call [`TimerFn::start`] to arm it.
317    ///
318    /// # Examples
319    ///
320    /// ```
321    /// use osal_rs::os::*;
322    /// use std::sync::Arc;
323    ///
324    /// let timer = Timer::new("t", 50, false, None, |_timer, _param| Ok(Arc::new(()))).unwrap();
325    /// assert!(!timer.is_null());
326    /// ```
327    pub fn new<F>(name: &str, timer_period_in_ticks: TickType, auto_reload: bool, param: Option<TimerParam>, callback: F) -> Result<Self>
328    where
329        F: Fn(Box<dyn TimerFn>, Option<TimerParam>) -> Result<TimerParam> + Send + Sync + Clone + 'static,
330    {
331        let shared = Arc::new(TimerShared {
332            timerid: AtomicPtr::new(null_mut()),
333            ready: AtomicBool::new(false),
334            thread_id: AtomicI32::new(0),
335            us: AtomicU32::new(ticks_to_us(timer_period_in_ticks)),
336            oneshot: AtomicBool::new(!auto_reload),
337            exit: AtomicBool::new(false),
338            thread: Mutex::new(None),
339        });
340
341        let mut timer = Self {
342            handle: null_mut(),
343            name: name.to_string(),
344            callback: Some(Arc::new(callback)),
345            param,
346            shared: Some(shared.clone()),
347        };
348
349        // Block SIGALRM here so the background thread we're about to spawn
350        // inherits it blocked too (see module docs).
351        let mut sigset = sigset_t::default();
352        unsafe {
353            sigemptyset(&mut sigset);
354            sigaddset(&mut sigset, SIGALRM);
355            sigprocmask(SIG_BLOCK, &sigset, null_mut());
356        }
357
358        let bg_shared = shared.clone();
359        let bg_callback = timer.callback.clone();
360        let bg_param = timer.param.clone();
361        let bg_self = timer.clone();
362
363        let mut bg_thread = Thread::new(TIMER_THREAD_NAME, TIMER_THREAD_STACK, TIMER_THREAD_PRIORITY);
364        let bg_thread = bg_thread.spawn_simple(move || run_timer_thread(bg_shared.clone(), bg_callback.clone(), bg_param.clone(), bg_self.clone()))?;
365
366        // Wait until the background thread has published its kernel TID,
367        // needed below to target it with SIGEV_THREAD_ID.
368        while shared.thread_id.load(Ordering::Acquire) == 0 {
369            unsafe {
370                sched_yield();
371            }
372        }
373
374        let sev = sigevent {
375            sigev_notify: SIGEV_THREAD_ID,
376            sigev_signo: SIGALRM,
377            sigev_un: sigevent_un {
378                tid: shared.thread_id.load(Ordering::Acquire),
379            },
380            ..Default::default()
381        };
382
383        let mut timerid: timer_t = null_mut();
384        let ret = unsafe { timer_create(CLOCK_MONOTONIC, &sev, &mut timerid) };
385
386        if ret != 0 {
387            shared.exit.store(true, Ordering::Release);
388            unsafe {
389                pthread_kill(*bg_thread, SIGALRM);
390            }
391            bg_thread.delete();
392            return Err(Error::ReturnWithCode(ret));
393        }
394
395        shared.timerid.store(timerid, Ordering::Release);
396        shared.ready.store(true, Ordering::Release);
397        *shared.thread.lock().unwrap() = Some(bg_thread);
398
399        timer.handle = timerid;
400        Ok(timer)
401    }
402}
403
404impl TimerFn for Timer {
405    /// Returns `true` if this timer has been [`TimerFn::delete`]d (or the
406    /// underlying kernel timer failed to create).
407    ///
408    /// # Examples
409    ///
410    /// ```
411    /// use osal_rs::os::*;
412    /// use std::sync::Arc;
413    ///
414    /// let mut timer = Timer::new("t", 50, false, None, |_t, _p| Ok(Arc::new(()))).unwrap();
415    /// assert!(!timer.is_null());
416    ///
417    /// timer.delete(0);
418    /// assert!(timer.is_null());
419    /// ```
420    fn is_null(&self) -> bool {
421        match &self.shared {
422            Some(shared) => !shared.ready.load(Ordering::Acquire),
423            None => true,
424        }
425    }
426
427    /// Arms the timer to fire after its configured period. See the
428    /// module-level docs above for a complete example.
429    fn start(&self, _ticks_to_wait: TickType) -> OsalRsBool {
430        let Some(shared) = &self.shared else {
431            return OsalRsBool::False;
432        };
433
434        arm(shared, shared.us.load(Ordering::Acquire))
435    }
436
437    /// Disarms the timer; a no-op if it isn't currently running.
438    ///
439    /// # Examples
440    ///
441    /// ```
442    /// use osal_rs::os::*;
443    /// use std::sync::Arc;
444    /// use std::sync::atomic::{AtomicBool, Ordering};
445    ///
446    /// static FIRED: AtomicBool = AtomicBool::new(false);
447    ///
448    /// let timer = Timer::new("t", 20, false, None, |_t, _p| {
449    ///     FIRED.store(true, Ordering::SeqCst);
450    ///     Ok(Arc::new(()))
451    /// }).unwrap();
452    ///
453    /// timer.start(0);
454    /// timer.stop(0); // cancels before it can fire
455    ///
456    /// System::delay(50);
457    /// assert!(!FIRED.load(Ordering::SeqCst));
458    /// ```
459    fn stop(&self, _ticks_to_wait: TickType) -> OsalRsBool {
460        let Some(shared) = &self.shared else {
461            return OsalRsBool::False;
462        };
463
464        arm(shared, 0)
465    }
466
467    /// Restarts the countdown from now, using the current period -
468    /// equivalent to calling [`TimerFn::start`] again, whether the timer was
469    /// previously running or stopped.
470    fn reset(&self, ticks_to_wait: TickType) -> OsalRsBool {
471        // A relative `timer_settime` call always restarts the countdown
472        // from now, whether the timer was previously running or stopped.
473        self.start(ticks_to_wait)
474    }
475
476    /// Updates the timer's period and immediately (re)arms it with the new
477    /// value.
478    ///
479    /// # Examples
480    ///
481    /// ```
482    /// use osal_rs::os::*;
483    /// use std::sync::Arc;
484    /// use std::sync::atomic::{AtomicBool, Ordering};
485    ///
486    /// static FIRED: AtomicBool = AtomicBool::new(false);
487    ///
488    /// let timer = Timer::new("t", 10_000, false, None, |_t, _p| {
489    ///     FIRED.store(true, Ordering::SeqCst);
490    ///     Ok(Arc::new(()))
491    /// }).unwrap();
492    ///
493    /// // Original 10s period would never fire in time; shrink it to 10ms.
494    /// timer.change_period(10, 0);
495    ///
496    /// System::delay(50);
497    /// assert!(FIRED.load(Ordering::SeqCst));
498    /// ```
499    fn change_period(&self, new_period_in_ticks: TickType, ticks_to_wait: TickType) -> OsalRsBool {
500        let Some(shared) = &self.shared else {
501            return OsalRsBool::False;
502        };
503
504        shared.us.store(ticks_to_us(new_period_in_ticks), Ordering::Release);
505        self.start(ticks_to_wait)
506    }
507
508    /// Destroys the underlying kernel timer and its background thread,
509    /// resetting this [`Timer`] to its "null" state. See
510    /// [`TimerFn::is_null`] for a complete example.
511    fn delete(&mut self, _ticks_to_wait: TickType) -> OsalRsBool {
512        let Some(shared) = self.shared.take() else {
513            return OsalRsBool::False;
514        };
515
516        // `swap` (not `load` + `store`) so concurrent `delete()` calls on
517        // clones of the same `Timer` can't both attempt to delete the
518        // underlying resources.
519        if shared.ready.swap(false, Ordering::AcqRel) {
520            let timerid = shared.timerid.load(Ordering::Acquire);
521            unsafe {
522                timer_delete(timerid);
523            }
524        }
525
526        shared.exit.store(true, Ordering::Release);
527
528        if let Ok(mut guard) = shared.thread.lock() {
529            if let Some(bg_thread) = guard.take() {
530                // Wake the background thread out of `sigwait` so it observes
531                // `exit` and returns; harmless if it's already on its way
532                // out because the timer just fired for the last time.
533                unsafe {
534                    pthread_kill(*bg_thread, SIGALRM);
535                }
536                bg_thread.delete();
537            }
538        }
539
540        self.handle = null_mut();
541        OsalRsBool::True
542    }
543}
544
545impl Drop for Timer {
546    fn drop(&mut self) {}
547}
548
549impl Deref for Timer {
550    type Target = TimerHandle;
551
552    fn deref(&self) -> &Self::Target {
553        &self.handle
554    }
555}
556
557impl Debug for Timer {
558    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
559        f.debug_struct("Timer")
560            .field("handle", &self.handle)
561            .field("name", &self.name)
562            .field("has_callback", &self.callback.is_some())
563            .field("has_param", &self.param.is_some())
564            .field("is_null", &self.is_null())
565            .finish()
566    }
567}
568
569impl Display for Timer {
570    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
571        write!(f, "Timer {{ name: {}, handle: {:?} }}", self.name, self.handle)
572    }
573}