sdl2/
timer.rs

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