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::sync::{Arc, Weak};
90
91use crate::os::ThreadFn;
92use crate::posix::config::TICK_PERIOD_MS;
93use crate::posix::ffi::{
94    CLOCK_MONOTONIC, SIGALRM, SIGEV_THREAD_ID, SIG_BLOCK, gettid, itimerspec, pthread_kill, sched_yield, sigaddset, sigemptyset, sigevent, sigevent_un, sigprocmask, sigset_t, sigwait,
95    timer_create, timer_delete, timer_settime, timer_t, timespec,
96};
97use crate::posix::thread::Thread;
98use crate::posix::types::{StackType, TickType, TimerHandle, UBaseType};
99use crate::traits::{MAX_TASK_NAME_LEN, TimerFn, TimerFnPtr, TimerParam, ToTick};
100use crate::utils::{Bytes, Error, OsalRsBool, Result};
101
102/// Name (glibc `pthread_setname_np`, `<= 15` chars) given to every timer's
103/// background thread. Fixed rather than derived from the timer's own name so
104/// it's always valid regardless of what the caller passed to `Timer::new`.
105const TIMER_THREAD_NAME: &str = "os_timer";
106
107/// Stack size requested for a timer's background thread. `Thread::spawn`
108/// enforces its own safe minimum regardless, so this only matters as a
109/// lower bound.
110const TIMER_THREAD_STACK: StackType = 1024;
111
112/// Priority given to a timer's background thread. Only meaningful with the
113/// `sched_fifo` feature enabled; otherwise the thread inherits the creating
114/// thread's scheduling policy/priority.
115const TIMER_THREAD_PRIORITY: UBaseType = 1;
116
117const NSECS_PER_SEC: u64 = 1_000_000_000;
118
119/// State shared, via `Arc`, between every clone of a given [`Timer`] and its
120/// background thread.
121///
122/// [`Timer`] itself is freely `Clone` (matching every other handle type in
123/// this crate), but the POSIX timer, its background thread, and its
124/// mutable period all belong to one underlying resource — this is that
125/// resource.
126struct TimerShared {
127    /// The real POSIX timer, once `ready` is set. Not read before then:
128    /// on modern glibc/Linux, `timer_t` is a small kernel-assigned integer
129    /// cast to a pointer, so the *first* timer a process ever creates gets
130    /// the all-zero handle — a bit pattern that would otherwise look
131    /// indistinguishable from "not created yet".
132    timerid: AtomicPtr<c_void>,
133    /// Set once `timerid` holds a real value from a successful
134    /// `timer_create`. Guards every read of `timerid` instead of checking
135    /// it for null (see `timerid`'s docs for why null isn't a safe sentinel
136    /// here), and lets `delete()` claim deletion exactly once.
137    ready: AtomicBool,
138    /// Kernel TID (`gettid()`) of the background thread, published once it
139    /// starts running; 0 until then.
140    thread_id: AtomicI32,
141    /// Current period, in microseconds.
142    us: AtomicU32,
143    oneshot: AtomicBool,
144    /// Set by teardown to tell the background thread's `sigwait` loop to
145    /// stop instead of invoking the callback again.
146    exit: AtomicBool,
147    /// The background thread, so teardown can wake and join it.
148    thread: Mutex<Option<Thread>>,
149}
150
151impl TimerShared {
152    /// Destroys the kernel timer and reaps the background thread, at most
153    /// once however many handles ask for it. Shared by [`TimerFn::delete`]
154    /// and by `Drop`.
155    fn destroy(&self) {
156        // `swap` (not `load` + `store`) so concurrent teardowns from clones
157        // of the same `Timer` can't both attempt to delete the underlying
158        // resources.
159        if self.ready.swap(false, Ordering::AcqRel) {
160            let timerid = self.timerid.load(Ordering::Acquire);
161            unsafe {
162                timer_delete(timerid);
163            }
164        }
165
166        self.exit.store(true, Ordering::Release);
167
168        let Ok(mut guard) = self.thread.lock() else {
169            return;
170        };
171
172        let Some(bg_thread) = guard.take() else {
173            return;
174        };
175
176        // Wake the background thread out of `sigwait` so it observes `exit`
177        // and returns; harmless if it's already on its way out because the
178        // timer just fired for the last time.
179        unsafe {
180            pthread_kill(*bg_thread, SIGALRM);
181        }
182
183        if unsafe { gettid() } == self.thread_id.load(Ordering::Acquire) {
184            // Teardown is running *on* the background thread, which happens
185            // when a callback drops the last `Timer` handle. Joining would
186            // be joining ourselves, so detach instead and let the thread
187            // release itself once it falls out of the loop.
188            bg_thread.detach();
189            return;
190        }
191
192        bg_thread.delete();
193    }
194}
195
196/// Destroys the underlying timer once the last [`Timer`] handle referring to
197/// it is gone - the RAII half of [`TimerFn::delete`], and the reason `Timer`
198/// itself has no `Drop` of its own (a per-handle `Drop` would tear the timer
199/// down as soon as *any* clone was dropped).
200///
201/// For this to be reachable at all, the background thread must hold only a
202/// [`Weak`] reference and must not hold even that across `sigwait` - see
203/// [`run_timer_thread`].
204impl Drop for TimerShared {
205    fn drop(&mut self) {
206        self.destroy();
207    }
208}
209
210/// A software timer backed by a POSIX `timer_create`/`SIGALRM` timer and a
211/// dedicated background thread that waits for the signal and invokes the
212/// user callback. Freely [`Clone`]-able - every clone shares the same
213/// underlying timer. See [`Timer::new`] for a complete, testable example.
214#[derive(Clone)]
215pub struct Timer {
216    /// Raw handle to the underlying `timer_t`, exposed for diagnostics
217    /// (`Debug`/`Display`). `null` until [`Timer::new`] successfully calls
218    /// `timer_create`.
219    pub handle: TimerHandle,
220    /// In the same fixed-size buffer every other named object in this crate
221    /// uses. `Bytes` is `Copy`, so handing a named handle to the callback on
222    /// every firing costs nothing.
223    name: Bytes<MAX_TASK_NAME_LEN>,
224    callback: Option<Arc<TimerFnPtr>>,
225    param: Option<TimerParam>,
226    shared: Option<Arc<TimerShared>>,
227}
228
229unsafe impl Send for Timer {}
230unsafe impl Sync for Timer {}
231
232/// Converts a tick count (this crate's ticks are milliseconds, see
233/// [`TICK_PERIOD_MS`]) to microseconds, saturating instead of overflowing
234/// `u32` (the width `timer_settime`'s nanosecond math is done in below).
235fn ticks_to_us(ticks: TickType) -> u32 {
236    (ticks as u64).saturating_mul(TICK_PERIOD_MS).saturating_mul(1000).min(u32::MAX as u64) as u32
237}
238
239/// Arms `shared`'s timer to fire `us` microseconds from now, or disarms it
240/// if `us == 0` (per `timer_settime(2)`, an all-zero `it_value` always
241/// disarms regardless of `it_interval`). No-op (returns `False`) if the
242/// timer hasn't been created yet.
243fn arm(shared: &TimerShared, us: u32) -> OsalRsBool {
244    if !shared.ready.load(Ordering::Acquire) {
245        return OsalRsBool::False;
246    }
247
248    let timerid = shared.timerid.load(Ordering::Acquire);
249
250    let nanoseconds = (us as u64) * 1000;
251    let it_value = timespec {
252        tv_sec: (nanoseconds / NSECS_PER_SEC) as c_long,
253        tv_nsec: (nanoseconds % NSECS_PER_SEC) as c_long,
254    };
255
256    let it_interval = if us == 0 || shared.oneshot.load(Ordering::Acquire) {
257        timespec::default()
258    } else {
259        it_value
260    };
261
262    let its = itimerspec { it_interval, it_value };
263
264    match unsafe { timer_settime(timerid, 0, &its, null_mut()) } {
265        0 => OsalRsBool::True,
266        _ => OsalRsBool::False,
267    }
268}
269
270/// Body of the background thread every [`Timer`] spawns: publishes its
271/// kernel TID, then loops accepting one blocked `SIGALRM` at a time via
272/// `sigwait` and invoking the user callback in response. See the module
273/// docs for the full rationale.
274///
275/// Holds a [`Weak`] rather than an [`Arc`], and does not hold even that
276/// across `sigwait`: a strong reference parked here for the lifetime of the
277/// thread would keep [`TimerShared`] alive for as long as the thread runs,
278/// and the thread only stops when [`TimerShared`] is dropped - a cycle in
279/// which neither ever goes away.
280fn run_timer_thread(weak: Weak<TimerShared>, name: Bytes<MAX_TASK_NAME_LEN>, callback: Option<Arc<TimerFnPtr>>, mut param: Option<TimerParam>) -> Result<TimerParam> {
281    if let Some(shared) = weak.upgrade() {
282        shared.thread_id.store(unsafe { gettid() }, Ordering::Release);
283    }
284
285    let mut sigset = sigset_t::default();
286    unsafe {
287        sigemptyset(&mut sigset);
288        sigaddset(&mut sigset, SIGALRM);
289    }
290
291    loop {
292        let mut sig: c_int = 0;
293
294        if unsafe { sigwait(&sigset, &mut sig) } != 0 || sig != SIGALRM {
295            continue;
296        }
297
298        // The signal that just woke us might be the real timer expiration,
299        // or the artificial one teardown sends to break out of `sigwait`.
300        let Some(shared) = weak.upgrade() else {
301            break;
302        };
303
304        if shared.exit.load(Ordering::Acquire) {
305            break;
306        }
307
308        if let Some(cb) = &callback {
309            // The callback is handed a fresh handle onto the same shared
310            // state - never anything that owns the timer, since
311            // `TimerFnPtr` takes its `Box<dyn TimerFn>` by value and drops
312            // it on return.
313            let timer_self = Timer {
314                handle: shared.timerid.load(Ordering::Acquire),
315                name,
316                callback: callback.clone(),
317                param: param.clone(),
318                shared: Some(shared.clone()),
319            };
320
321            if let Ok(new_param) = cb(Box::new(timer_self), param.clone()) {
322                param = Some(new_param);
323            }
324        }
325
326        if shared.oneshot.load(Ordering::Acquire) {
327            break;
328        }
329    }
330
331    let final_param: TimerParam = match param {
332        Some(p) => p,
333        None => Arc::new(()),
334    };
335
336    Ok(final_param)
337}
338
339impl Timer {
340    /// Same as [`Timer::new`], but accepts any [`ToTick`] period (e.g. a
341    /// [`core::time::Duration`]) instead of a raw tick count. See the
342    /// module-level docs above for a complete example.
343    #[inline]
344    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>
345    where
346        F: Fn(Box<dyn TimerFn>, Option<TimerParam>) -> Result<TimerParam> + Send + Sync + Clone + 'static,
347    {
348        Self::new(name, timer_period_in_ticks.to_ticks(), auto_reload, param, callback)
349    }
350
351    /// Same as [`TimerFn::start`], but accepts any [`ToTick`] value (e.g. a
352    /// [`core::time::Duration`]) instead of a raw tick count.
353    ///
354    /// # Examples
355    ///
356    /// ```
357    /// use osal_rs::os::*;
358    /// use std::sync::Arc;
359    /// use core::time::Duration;
360    ///
361    /// let timer = Timer::new_with_to_tick("t", Duration::from_millis(50), false, None, |_t, _p| Ok(Arc::new(()))).unwrap();
362    /// assert_eq!(timer.start_with_to_tick(Duration::from_millis(10)), osal_rs::utils::OsalRsBool::True);
363    /// ```
364    #[inline]
365    pub fn start_with_to_tick(&self, ticks_to_wait: impl ToTick) -> OsalRsBool {
366        self.start(ticks_to_wait.to_ticks())
367    }
368
369    /// Same as [`TimerFn::stop`], but accepts any [`ToTick`] value instead
370    /// of a raw tick count.
371    #[inline]
372    pub fn stop_with_to_tick(&self, ticks_to_wait: impl ToTick) -> OsalRsBool {
373        self.stop(ticks_to_wait.to_ticks())
374    }
375
376    /// Same as [`TimerFn::reset`], but accepts any [`ToTick`] value instead
377    /// of a raw tick count.
378    #[inline]
379    pub fn reset_with_to_tick(&self, ticks_to_wait: impl ToTick) -> OsalRsBool {
380        self.reset(ticks_to_wait.to_ticks())
381    }
382
383    /// Same as [`TimerFn::change_period`], but accepts any [`ToTick`] values
384    /// instead of raw tick counts.
385    #[inline]
386    pub fn change_period_with_to_tick(&self, new_period_in_ticks: impl ToTick, new_period_ticks: impl ToTick) -> OsalRsBool {
387        self.change_period(new_period_in_ticks.to_ticks(), new_period_ticks.to_ticks())
388    }
389
390    /// Same as [`TimerFn::delete`], but accepts any [`ToTick`] value instead
391    /// of a raw tick count.
392    #[inline]
393    pub fn delete_with_to_tick(&mut self, ticks_to_wait: impl ToTick) -> OsalRsBool {
394        self.delete(ticks_to_wait.to_ticks())
395    }
396
397    /// Creates a new timer named `name`, firing `callback` every
398    /// `timer_period_in_ticks` ticks if `auto_reload` (one-shot otherwise).
399    /// `param` is handed to the first callback invocation; each invocation
400    /// can return an updated value for the next one. The timer is created
401    /// stopped - call [`TimerFn::start`] to arm it.
402    ///
403    /// # Examples
404    ///
405    /// ```
406    /// use osal_rs::os::*;
407    /// use std::sync::Arc;
408    ///
409    /// let timer = Timer::new("t", 50, false, None, |_timer, _param| Ok(Arc::new(()))).unwrap();
410    /// assert!(!timer.is_null());
411    /// ```
412    pub fn new<F>(name: &str, timer_period_in_ticks: TickType, auto_reload: bool, param: Option<TimerParam>, callback: F) -> Result<Self>
413    where
414        F: Fn(Box<dyn TimerFn>, Option<TimerParam>) -> Result<TimerParam> + Send + Sync + Clone + 'static,
415    {
416        let shared = Arc::new(TimerShared {
417            timerid: AtomicPtr::new(null_mut()),
418            ready: AtomicBool::new(false),
419            thread_id: AtomicI32::new(0),
420            us: AtomicU32::new(ticks_to_us(timer_period_in_ticks)),
421            oneshot: AtomicBool::new(!auto_reload),
422            exit: AtomicBool::new(false),
423            thread: Mutex::new(None),
424        });
425
426        let name = Bytes::<MAX_TASK_NAME_LEN>::from_str(name);
427
428        let mut timer = Self {
429            handle: null_mut(),
430            name,
431            callback: Some(Arc::new(callback)),
432            param,
433            shared: Some(shared.clone()),
434        };
435
436        // Block SIGALRM here so the background thread we're about to spawn
437        // inherits it blocked too (see module docs).
438        let mut sigset = sigset_t::default();
439        unsafe {
440            sigemptyset(&mut sigset);
441            sigaddset(&mut sigset, SIGALRM);
442            sigprocmask(SIG_BLOCK, &sigset, null_mut());
443        }
444
445        let bg_shared = Arc::downgrade(&shared);
446        let bg_name = name;
447        let bg_callback = timer.callback.clone();
448        let bg_param = timer.param.clone();
449
450        let mut bg_thread = Thread::new(TIMER_THREAD_NAME, TIMER_THREAD_STACK, TIMER_THREAD_PRIORITY);
451        let bg_thread = bg_thread.spawn_simple(move || run_timer_thread(bg_shared.clone(), bg_name, bg_callback.clone(), bg_param.clone()))?;
452
453        // Handed over before anything below can fail, so that an early
454        // return reaps the thread through `TimerShared::drop` rather than
455        // stranding it in `sigwait`.
456        *shared.thread.lock().unwrap() = Some(bg_thread);
457
458        // Wait until the background thread has published its kernel TID,
459        // needed below to target it with SIGEV_THREAD_ID.
460        while shared.thread_id.load(Ordering::Acquire) == 0 {
461            unsafe {
462                sched_yield();
463            }
464        }
465
466        let sev = sigevent {
467            sigev_notify: SIGEV_THREAD_ID,
468            sigev_signo: SIGALRM,
469            sigev_un: sigevent_un {
470                tid: shared.thread_id.load(Ordering::Acquire),
471            },
472            ..Default::default()
473        };
474
475        let mut timerid: timer_t = null_mut();
476        let ret = unsafe { timer_create(CLOCK_MONOTONIC, &sev, &mut timerid) };
477
478        if ret != 0 {
479            // Dropping `timer` and `shared` on the way out runs
480            // `TimerShared::drop`, which wakes and reaps the thread spawned
481            // above. `ready` is still false, so it won't try to delete a
482            // kernel timer that was never created.
483            return Err(Error::ReturnWithCode(ret));
484        }
485
486        shared.timerid.store(timerid, Ordering::Release);
487        shared.ready.store(true, Ordering::Release);
488
489        timer.handle = timerid;
490        Ok(timer)
491    }
492}
493
494impl TimerFn for Timer {
495    /// Returns `true` if this timer has been [`TimerFn::delete`]d (or the
496    /// underlying kernel timer failed to create).
497    ///
498    /// # Examples
499    ///
500    /// ```
501    /// use osal_rs::os::*;
502    /// use std::sync::Arc;
503    ///
504    /// let mut timer = Timer::new("t", 50, false, None, |_t, _p| Ok(Arc::new(()))).unwrap();
505    /// assert!(!timer.is_null());
506    ///
507    /// timer.delete(0);
508    /// assert!(timer.is_null());
509    /// ```
510    fn is_null(&self) -> bool {
511        match &self.shared {
512            Some(shared) => !shared.ready.load(Ordering::Acquire),
513            None => true,
514        }
515    }
516
517    /// Arms the timer to fire after its configured period. See the
518    /// module-level docs above for a complete example.
519    fn start(&self, _ticks_to_wait: TickType) -> OsalRsBool {
520        let Some(shared) = &self.shared else {
521            return OsalRsBool::False;
522        };
523
524        arm(shared, shared.us.load(Ordering::Acquire))
525    }
526
527    /// Disarms the timer; a no-op if it isn't currently running.
528    ///
529    /// # Examples
530    ///
531    /// ```
532    /// use osal_rs::os::*;
533    /// use std::sync::Arc;
534    /// use std::sync::atomic::{AtomicBool, Ordering};
535    ///
536    /// static FIRED: AtomicBool = AtomicBool::new(false);
537    ///
538    /// let timer = Timer::new("t", 20, false, None, |_t, _p| {
539    ///     FIRED.store(true, Ordering::SeqCst);
540    ///     Ok(Arc::new(()))
541    /// }).unwrap();
542    ///
543    /// timer.start(0);
544    /// timer.stop(0); // cancels before it can fire
545    ///
546    /// System::delay(50);
547    /// assert!(!FIRED.load(Ordering::SeqCst));
548    /// ```
549    fn stop(&self, _ticks_to_wait: TickType) -> OsalRsBool {
550        let Some(shared) = &self.shared else {
551            return OsalRsBool::False;
552        };
553
554        arm(shared, 0)
555    }
556
557    /// Restarts the countdown from now, using the current period -
558    /// equivalent to calling [`TimerFn::start`] again, whether the timer was
559    /// previously running or stopped.
560    fn reset(&self, ticks_to_wait: TickType) -> OsalRsBool {
561        // A relative `timer_settime` call always restarts the countdown
562        // from now, whether the timer was previously running or stopped.
563        self.start(ticks_to_wait)
564    }
565
566    /// Updates the timer's period and immediately (re)arms it with the new
567    /// value.
568    ///
569    /// # Examples
570    ///
571    /// ```
572    /// use osal_rs::os::*;
573    /// use std::sync::Arc;
574    /// use std::sync::atomic::{AtomicBool, Ordering};
575    ///
576    /// static FIRED: AtomicBool = AtomicBool::new(false);
577    ///
578    /// let timer = Timer::new("t", 10_000, false, None, |_t, _p| {
579    ///     FIRED.store(true, Ordering::SeqCst);
580    ///     Ok(Arc::new(()))
581    /// }).unwrap();
582    ///
583    /// // Original 10s period would never fire in time; shrink it to 10ms.
584    /// timer.change_period(10, 0);
585    ///
586    /// System::delay(50);
587    /// assert!(FIRED.load(Ordering::SeqCst));
588    /// ```
589    fn change_period(&self, new_period_in_ticks: TickType, ticks_to_wait: TickType) -> OsalRsBool {
590        let Some(shared) = &self.shared else {
591            return OsalRsBool::False;
592        };
593
594        shared.us.store(ticks_to_us(new_period_in_ticks), Ordering::Release);
595        self.start(ticks_to_wait)
596    }
597
598    /// Destroys the underlying kernel timer and its background thread,
599    /// resetting this [`Timer`] to its "null" state. See
600    /// [`TimerFn::is_null`] for a complete example.
601    fn delete(&mut self, _ticks_to_wait: TickType) -> OsalRsBool {
602        // Giving up the shared state is what makes this handle null; the
603        // timer itself is destroyed by whichever handle gets there first,
604        // and every clone sees it through the `ready` flag.
605        let Some(shared) = self.shared.take() else {
606            return OsalRsBool::False;
607        };
608
609        shared.destroy();
610
611        self.handle = null_mut();
612        OsalRsBool::True
613    }
614}
615
616impl Deref for Timer {
617    type Target = TimerHandle;
618
619    fn deref(&self) -> &Self::Target {
620        &self.handle
621    }
622}
623
624impl Debug for Timer {
625    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
626        f.debug_struct("Timer")
627            .field("handle", &self.handle)
628            .field("name", &self.name)
629            .field("has_callback", &self.callback.is_some())
630            .field("has_param", &self.param.is_some())
631            .field("is_null", &self.is_null())
632            .finish()
633    }
634}
635
636impl Display for Timer {
637    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
638        write!(f, "Timer {{ name: {}, handle: {:?} }}", self.name, self.handle)
639    }
640}