sdl2/
timer.rs

1use crate::sys;
2use libc::c_void;
3use std::marker::PhantomData;
4use std::panic::catch_unwind;
5use std::process::abort;
6
7use crate::TimerSubsystem;
8
9impl TimerSubsystem {
10    /// Constructs a new timer using the boxed closure `callback`.
11    ///
12    /// The timer is started immediately, it will be cancelled either:
13    ///
14    /// * when the timer is dropped
15    /// * or when the callback returns a non-positive continuation interval
16    ///
17    /// The callback is run in a thread that is created and managed internally
18    /// by SDL2 from C. If the callback panics, the process will be [aborted][abort].
19    #[must_use = "if unused the Timer will be dropped immediately"]
20    #[doc(alias = "SDL_AddTimer")]
21    pub fn add_timer(&self, delay: u32, callback: TimerCallback) -> Timer<'_> {
22        unsafe {
23            let mut callback = Box::new(callback);
24            let timer_id = sys::SDL_AddTimer(
25                delay,
26                Some(c_timer_callback),
27                &mut *callback as *mut TimerCallback as *mut c_void,
28            );
29
30            Timer {
31                callback: Some(callback),
32                raw: timer_id,
33                _marker: PhantomData,
34            }
35        }
36    }
37
38    /// Gets the number of milliseconds elapsed since the timer subsystem was initialized.
39    ///
40    /// It's recommended that you use another library for timekeeping, such as `time`.
41    ///
42    /// This function is not recommended in upstream SDL2 as of 2.0.18 and internally
43    /// calls the 64-bit variant and masks the result.
44    #[doc(alias = "SDL_GetTicks")]
45    pub fn ticks(&self) -> u32 {
46        // This is thread-safe as long as the ticks subsystem is inited, and
47        // tying this to `TimerSubsystem` ensures the timer subsystem can
48        // safely make calls into the ticks subsystem without invoking a
49        // thread-unsafe `SDL_TicksInit()`.
50        //
51        // This binding is offered for completeness but is debatably a relic.
52        unsafe { sys::SDL_GetTicks() }
53    }
54
55    /// Gets the number of milliseconds elapsed since the timer subsystem was initialized.
56    ///
57    /// It's recommended that you use another library for timekeeping, such as `time`.
58    #[doc(alias = "SDL_GetTicks64")]
59    pub fn ticks64(&self) -> u64 {
60        // This is thread-safe as long as the ticks subsystem is inited, and
61        // tying this to `TimerSubsystem` ensures the timer subsystem can
62        // safely make calls into the ticks subsystem without invoking a
63        // thread-unsafe `SDL_TicksInit()`.
64        //
65        // This binding is offered for completeness but is debatably a relic.
66        unsafe { sys::SDL_GetTicks64() }
67    }
68
69    /// Sleeps the current thread for the specified amount of milliseconds.
70    ///
71    /// It's recommended that you use `std::thread::sleep()` instead.
72    #[doc(alias = "SDL_Delay")]
73    pub fn delay(&self, ms: u32) {
74        // This is thread-safe as long as the ticks subsystem is inited, and
75        // tying this to `TimerSubsystem` ensures the timer subsystem can
76        // safely make calls into the ticks subsystem without invoking a
77        // thread-unsafe `SDL_TicksInit()`.
78        //
79        // This binding is offered for completeness but is debatably a relic.
80        unsafe { sys::SDL_Delay(ms) }
81    }
82
83    #[doc(alias = "SDL_GetPerformanceCounter")]
84    pub fn performance_counter(&self) -> u64 {
85        unsafe { sys::SDL_GetPerformanceCounter() }
86    }
87
88    #[doc(alias = "SDL_GetPerformanceFrequency")]
89    pub fn performance_frequency(&self) -> u64 {
90        unsafe { sys::SDL_GetPerformanceFrequency() }
91    }
92}
93
94pub type TimerCallback = Box<dyn FnMut() -> u32 + 'static + Send>;
95
96pub struct Timer<'a> {
97    callback: Option<Box<TimerCallback>>,
98    raw: sys::SDL_TimerID,
99    _marker: PhantomData<&'a ()>,
100}
101
102impl<'a> Timer<'a> {
103    /// Returns the closure as a trait-object and cancels the timer
104    /// by consuming it...
105    pub fn into_inner(mut self) -> TimerCallback {
106        *self.callback.take().unwrap()
107    }
108}
109
110impl<'a> Drop for Timer<'a> {
111    #[inline]
112    #[doc(alias = "SDL_RemoveTimer")]
113    fn drop(&mut self) {
114        // SDL_RemoveTimer returns SDL_FALSE if the timer wasn't found (impossible),
115        // or the timer has been cancelled via the callback (possible).
116        // The timer being cancelled isn't an issue, so we ignore the result.
117        unsafe { sys::SDL_RemoveTimer(self.raw) };
118    }
119}
120
121unsafe extern "C" fn c_timer_callback(_interval: u32, param: *mut c_void) -> u32 {
122    match catch_unwind(|| {
123        let f = param.cast::<TimerCallback>();
124        unsafe { (*f)() }
125    }) {
126        Ok(ret) => ret,
127        Err(_) => abort(),
128    }
129}
130
131#[cfg(not(target_os = "macos"))]
132#[cfg(test)]
133mod test {
134    use std::sync::{Arc, Mutex};
135    use std::time::Duration;
136
137    #[test]
138    fn test_timer() {
139        test_timer_runs_multiple_times();
140        test_timer_runs_at_least_once();
141        test_timer_can_be_recreated();
142    }
143
144    fn test_timer_runs_multiple_times() {
145        let sdl_context = crate::sdl::init().unwrap();
146        let timer_subsystem = sdl_context.timer().unwrap();
147
148        let local_num = Arc::new(Mutex::new(0));
149        let timer_num = local_num.clone();
150
151        let _timer = timer_subsystem.add_timer(
152            20,
153            Box::new(move || {
154                // increment up to 10 times (0 -> 9)
155                // tick again in 100ms after each increment
156                //
157                let mut num = timer_num.lock().unwrap();
158                if *num < 9 {
159                    *num += 1;
160                    20
161                } else {
162                    0
163                }
164            }),
165        );
166
167        // tick the timer at least 10 times w/ 200ms of "buffer"
168        ::std::thread::sleep(Duration::from_millis(250));
169        let num = local_num.lock().unwrap(); // read the number back
170        assert_eq!(*num, 9); // it should have incremented at least 10 times...
171    }
172
173    fn test_timer_runs_at_least_once() {
174        let sdl_context = crate::sdl::init().unwrap();
175        let timer_subsystem = sdl_context.timer().unwrap();
176
177        let local_flag = Arc::new(Mutex::new(false));
178        let timer_flag = local_flag.clone();
179
180        let _timer = timer_subsystem.add_timer(
181            20,
182            Box::new(move || {
183                let mut flag = timer_flag.lock().unwrap();
184                *flag = true;
185                0
186            }),
187        );
188
189        ::std::thread::sleep(Duration::from_millis(50));
190        let flag = local_flag.lock().unwrap();
191        assert!(*flag);
192    }
193
194    fn test_timer_can_be_recreated() {
195        let sdl_context = crate::sdl::init().unwrap();
196        let timer_subsystem = sdl_context.timer().unwrap();
197
198        let local_num = Arc::new(Mutex::new(0));
199        let timer_num = local_num.clone();
200
201        // run the timer once and reclaim its closure
202        let timer_1 = timer_subsystem.add_timer(
203            20,
204            Box::new(move || {
205                let mut num = timer_num.lock().unwrap();
206                *num += 1; // increment the number
207                0 // do not run timer again
208            }),
209        );
210
211        // reclaim closure after timer runs
212        ::std::thread::sleep(Duration::from_millis(50));
213        let closure = timer_1.into_inner();
214
215        // create a second timer and increment again
216        let _timer_2 = timer_subsystem.add_timer(20, closure);
217        ::std::thread::sleep(Duration::from_millis(50));
218
219        // check that timer was incremented twice
220        let num = local_num.lock().unwrap();
221        assert_eq!(*num, 2);
222    }
223}