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}