Skip to content

Commit 81c4254

Browse files
committed
Add CFRunLoopTimer example
1 parent d8ae943 commit 81c4254

File tree

4 files changed

+152
-0
lines changed

4 files changed

+152
-0
lines changed

framework-crates/objc2-core-foundation/Cargo.modified.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,10 @@ CFBase = []
66

77
[dev-dependencies]
88
static_assertions = "1.1.0"
9+
10+
[[example]]
11+
name = "timer"
12+
required-features = [
13+
"CFDate",
14+
"CFRunLoop",
15+
]

framework-crates/objc2-core-foundation/Cargo.toml

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
//! Creating a `CFRunLoopTimer` and attaching it with a runloop.
2+
3+
use std::sync::Arc;
4+
use std::{cell::Cell, ffi::c_void};
5+
6+
use objc2_core_foundation::{
7+
kCFAllocatorDefault, kCFRunLoopCommonModes, CFAbsoluteTime, CFAbsoluteTimeGetCurrent, CFIndex,
8+
CFRetained, CFRunLoop, CFRunLoopTimer, CFRunLoopTimerContext, CFTimeInterval,
9+
};
10+
11+
fn main() {
12+
let rl = CFRunLoop::main().unwrap();
13+
14+
let iterations = Cell::new(0);
15+
16+
let callback = {
17+
let rl = rl.clone();
18+
move |timer: &CFRunLoopTimer| {
19+
println!("Timer called: {}", iterations.get());
20+
21+
if 10 <= iterations.get() {
22+
// Remove the timer.
23+
rl.remove_timer(Some(timer), unsafe { kCFRunLoopCommonModes });
24+
25+
// Stop the run loop explicitly
26+
// (the main run-loop won't stop otherwise).
27+
println!("Stopping run loop");
28+
rl.stop();
29+
}
30+
31+
iterations.set(iterations.get() + 1);
32+
}
33+
};
34+
35+
// SAFETY: The timer is added to a run loop on the same thread.
36+
let timer = unsafe { create_timer_unchecked(0.0, 0.1, 0, callback) };
37+
rl.add_timer(Some(&timer), unsafe { kCFRunLoopCommonModes });
38+
39+
// Add a single-shot timer from another thread.
40+
let fire_date = CFAbsoluteTimeGetCurrent() + 0.5;
41+
let timer = create_timer(fire_date, 0.0, 0, |_| {
42+
println!("Fired one-shot timer after 0.5 seconds");
43+
// Still runs on the main thread.
44+
assert_eq!(CFRunLoop::current().unwrap(), CFRunLoop::main().unwrap());
45+
});
46+
rl.add_timer(Some(&timer), unsafe { kCFRunLoopCommonModes });
47+
48+
println!("Starting run loop");
49+
CFRunLoop::run();
50+
}
51+
52+
/// Create a new `CFRunLoopTimer`.
53+
fn create_timer<F: Fn(&CFRunLoopTimer) + Send + Sync + 'static>(
54+
fire_date: CFAbsoluteTime,
55+
interval: CFTimeInterval,
56+
order: CFIndex,
57+
callback: F,
58+
) -> CFRetained<CFRunLoopTimer> {
59+
// SAFETY: The callback is `Send + Sync`.
60+
unsafe { create_timer_unchecked(fire_date, interval, order, callback) }
61+
}
62+
63+
/// Create a new `CFRunLoopTimer`, without enforcing the callback to be
64+
/// thread-safe.
65+
///
66+
/// # Safety
67+
///
68+
/// The callback must be either `Send` + `Sync`, or the timer must only be
69+
/// added to a run loop that runs on the current thread.
70+
unsafe fn create_timer_unchecked<F: Fn(&CFRunLoopTimer) + 'static>(
71+
fire_date: CFAbsoluteTime,
72+
interval: CFTimeInterval,
73+
order: CFIndex,
74+
callback: F,
75+
) -> CFRetained<CFRunLoopTimer> {
76+
// We use an `Arc` here to make sure that the reference-counting of the
77+
// signal container is atomic (`Retained`/`CFRetained` would be valid
78+
// alternatives too).
79+
let callback = Arc::new(callback);
80+
81+
unsafe extern "C-unwind" fn retain<F>(info: *const c_void) -> *const c_void {
82+
// SAFETY: The pointer was passed to `CFRunLoopTimerContext.info` below.
83+
unsafe { Arc::increment_strong_count(info.cast::<F>()) };
84+
info
85+
}
86+
unsafe extern "C-unwind" fn release<F>(info: *const c_void) {
87+
// SAFETY: The pointer was passed to `CFRunLoopTimerContext.info` below.
88+
unsafe { Arc::decrement_strong_count(info.cast::<F>()) };
89+
}
90+
91+
unsafe extern "C-unwind" fn callout<F: Fn(&CFRunLoopTimer)>(
92+
timer: *mut CFRunLoopTimer,
93+
info: *mut c_void,
94+
) {
95+
// SAFETY: The timer is valid for at least the duration of the callback.
96+
let timer = unsafe { &*timer };
97+
98+
// SAFETY: The pointer was passed to `CFRunLoopTimerContext.info` below.
99+
let callback = unsafe { &*info.cast::<F>() };
100+
101+
// Call the provided closure.
102+
callback(timer);
103+
}
104+
105+
// This is marked `mut` to match the signature of `CFRunLoopTimerCreate`,
106+
// but the information is copied, and not actually mutated.
107+
let mut context = CFRunLoopTimerContext {
108+
version: 0,
109+
// This pointer is retained by CF on creation.
110+
info: Arc::as_ptr(&callback) as *mut c_void,
111+
retain: Some(retain::<F>),
112+
release: Some(release::<F>),
113+
copyDescription: None,
114+
};
115+
116+
// SAFETY: The retain/release callbacks are thread-safe, and caller
117+
// upholds that the main callback is used in a thread-safe manner.
118+
//
119+
// `F: 'static`, so extending the lifetime of the closure is fine.
120+
unsafe {
121+
CFRunLoopTimer::new(
122+
kCFAllocatorDefault,
123+
fire_date,
124+
interval,
125+
0, // Documentation says to pass 0 for future compat.
126+
order,
127+
Some(callout::<F>),
128+
&mut context,
129+
)
130+
}
131+
.unwrap()
132+
}

framework-crates/objc2-core-foundation/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
//!
55
//! [apple-doc]: https://developer.apple.com/documentation/corefoundation/
66
//! [framework-crates]: https://docs.rs/objc2/latest/objc2/topics/about_generated/index.html
7+
//!
8+
//! ## Examples
9+
//!
10+
//! ```ignore
11+
#![doc = include_str!("../examples/timer.rs")]
12+
//! ```
713
#![no_std]
814
#![cfg_attr(feature = "unstable-coerce-pointee", feature(derive_coerce_pointee))]
915
#![cfg_attr(docsrs, feature(doc_auto_cfg))]

0 commit comments

Comments
 (0)