Skip to content

Commit 3245318

Browse files
committed
Add wrapper around dispatch_once_f
Part of #77.
1 parent 833405e commit 3245318

File tree

6 files changed

+347
-3
lines changed

6 files changed

+347
-3
lines changed

crates/dispatch2/CHANGELOG.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
4646
- `dispatch_write`.
4747
- Added `#[must_use]` in bindings where the source uses it.
4848
- Added `Queue::main` for fetching the main queue.
49-
- Moved `run_on_main` and `MainThreadBound` from `objc2-foundation` to this crate.
49+
- Moved `run_on_main` and `MainThreadBound` from `objc2-foundation` to this
50+
crate.
51+
- Added `Once`, a wrapper over `dispatch_once_f` which works similarly to
52+
`std::sync::Once`.
5053

5154
### Changed
5255
- Moved to the `objc2` project.

crates/dispatch2/TODO.md

-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
- CI test on Windows using https://github.com/apple/swift-corelibs-libdispatch
77
- Safe wrapper for ``dispatch_source_*`` + ``set_target_queue/activate/suspend/resume`` for it
88
- Safe wrapper for ``dispatch_data_*``
9-
- Safe wrapper for ``dispatch_once_f`` (is that relevant?)
109
- Safe wrapper for ``dispatch_get_context/dispatch_set_context`` (quite impossible without big overhead => wrap dispatch object destructor to release the boxed value)
1110
- All blocks related bindings and ``dispatch_block_*`` functions with compat with ``block2`` on Apple platforms.
1211
- Integrate conversion from SystemTime to dispatch_time_t via dispatch_walltime and safe APIs using that.

crates/dispatch2/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub mod group;
3333
#[cfg(feature = "objc2")]
3434
mod main_thread_bound;
3535
pub mod object;
36+
mod once;
3637
pub mod queue;
3738
pub mod semaphore;
3839
mod utils;
@@ -85,5 +86,6 @@ pub use self::group::*;
8586
#[cfg(feature = "objc2")]
8687
pub use self::main_thread_bound::{run_on_main, MainThreadBound};
8788
pub use self::object::*;
89+
pub use self::once::*;
8890
pub use self::queue::*;
8991
pub use self::semaphore::*;

crates/dispatch2/src/once.rs

+336
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
use core::ffi::c_void;
2+
use core::fmt;
3+
use core::ptr::NonNull;
4+
use std::cell::UnsafeCell;
5+
use std::panic::{RefUnwindSafe, UnwindSafe};
6+
use std::sync::atomic::AtomicIsize;
7+
use std::sync::atomic::Ordering;
8+
9+
use crate::ffi;
10+
11+
/// A low-level synchronization primitive for one-time global execution.
12+
///
13+
/// This is equivalent to [`std::sync::Once`], except that this uses the
14+
/// underlying system primitives from `libdispatch`, which:
15+
/// - Might result in less code-size overhead.
16+
/// - Aborts on panics in the initialization closure.
17+
///
18+
/// Generally, prefer [`std::sync::Once`] unless you have a specific need for
19+
/// this.
20+
///
21+
///
22+
/// # Example
23+
///
24+
/// Run a closure once for the duration of the entire program, without using
25+
/// [`std::sync::Once`].
26+
///
27+
/// ```
28+
/// use dispatch2::Once;
29+
///
30+
/// static INIT: Once = Once::new();
31+
///
32+
/// INIT.call_once(|| {
33+
/// // run initialization here
34+
/// });
35+
/// ```
36+
#[doc(alias = "dispatch_once_t")]
37+
pub struct Once {
38+
predicate: UnsafeCell<ffi::dispatch_once_t>,
39+
}
40+
41+
// This is intentionally `extern "C"`, since libdispatch will not propagate an
42+
// internal panic, but will simply abort.
43+
extern "C" fn invoke_closure<F>(context: *mut c_void)
44+
where
45+
F: FnOnce(),
46+
{
47+
let context: *mut Option<F> = context.cast();
48+
// SAFETY: Context was created below in `invoke_dispatch_once` from
49+
// `&mut Option<F>`.
50+
let closure: &mut Option<F> = unsafe { &mut *context };
51+
52+
// SAFETY: libdispatch is implemented correctly, and will only call this
53+
// once (and we set it to be available before calling dispatch_once).
54+
let closure = unsafe { closure.take().unwrap_unchecked() };
55+
56+
(closure)();
57+
}
58+
59+
#[cfg_attr(
60+
// DISPATCH_ONCE_INLINE_FASTPATH, see Once::call_once below.
61+
any(target_arch = "x86", target_arch = "x86_64", target_vendor = "apple"),
62+
cold,
63+
inline(never)
64+
)]
65+
fn invoke_dispatch_once<F>(predicate: NonNull<ffi::dispatch_once_t>, closure: F)
66+
where
67+
F: FnOnce(),
68+
{
69+
// Convert closure data to context parameter.
70+
let mut closure = Some(closure);
71+
let context: *mut Option<F> = &mut closure;
72+
let context: *mut c_void = context.cast();
73+
74+
// SAFETY: The function and context are valid, and the predicate pointer
75+
// is valid.
76+
//
77+
// NOTE: The documentation says:
78+
// > The predicate must point to a variable stored in global or static
79+
// > scope. The result of using a predicate with automatic or dynamic
80+
// > storage (including Objective-C instance variables) is undefined.
81+
//
82+
// In Rust though, we have stronger guarantees, and can guarantee that the
83+
// predicate is never moved while in use, because the `Once` itself is not
84+
// cloneable.
85+
//
86+
// Even if libdispatch may sometimes use the pointer as a condition
87+
// variable, or may internally store a self-referential pointer, it can
88+
// only do that while the Once is in use somewhere (i.e. it should not be
89+
// able to do that while the Once is being moved).
90+
//
91+
// Outside of being moved, the Once can only be in two states:
92+
// - Initialized.
93+
// - Done.
94+
//
95+
// And those two states are freely movable.
96+
unsafe { ffi::dispatch_once_f(predicate, context, invoke_closure::<F>) };
97+
98+
// Closure is dropped here, depending on if it was executed (and taken
99+
// from the `Option`) by `dispatch_once_f` or not.
100+
}
101+
102+
impl Once {
103+
/// Creates a new `Once`.
104+
#[inline]
105+
#[allow(clippy::new_without_default)] // `std::sync::Once` doesn't have it either
106+
pub const fn new() -> Self {
107+
Self {
108+
predicate: UnsafeCell::new(0),
109+
}
110+
}
111+
112+
/// Executes a closure once for the lifetime of the application.
113+
///
114+
/// If called simultaneously from multiple threads, this function waits
115+
/// synchronously until the work function has completed.
116+
///
117+
///
118+
/// # Aborts
119+
///
120+
/// The process will trap or abort if:
121+
/// - The given initialization closure unwinds.
122+
/// - The given closure recursively invokes `call_once` on the same
123+
/// `Once` instance.
124+
#[inline]
125+
#[doc(alias = "dispatch_once")]
126+
#[doc(alias = "dispatch_once_f")]
127+
pub fn call_once<F>(&self, work: F)
128+
where
129+
F: FnOnce(),
130+
{
131+
// Unwrap is fine, the pointer is valid so can never be NULL.
132+
let predicate = NonNull::new(self.predicate.get()).unwrap();
133+
134+
// DISPATCH_ONCE_INLINE_FASTPATH
135+
if cfg!(any(
136+
target_arch = "x86",
137+
target_arch = "x86_64",
138+
target_vendor = "apple"
139+
)) {
140+
// On certain platforms, the ABI of the predicate is stable enough
141+
// that we are allowed to read it to check if the condition is
142+
// done yet.
143+
//
144+
// The code in C is inside `_dispatch_once_f` in dispatch/once.h:
145+
//
146+
// if (DISPATCH_EXPECT(*predicate, ~0l) != ~0l) {
147+
// dispatch_once_f(predicate, context, function);
148+
// } else {
149+
// dispatch_compiler_barrier();
150+
// }
151+
// DISPATCH_COMPILER_CAN_ASSUME(*predicate == ~0l);
152+
153+
// NOTE: To uphold the rules set by the Rust AM, we use an atomic
154+
// comparison here to avoid a possible tear, even though the
155+
// equivalent C code just loads the predicate un-atomically.
156+
//
157+
// SAFETY: The predicate is a valid atomic pointer.
158+
// TODO: Use `AtomicIsize::from_ptr` once in MSRV.
159+
let atomic_predicate: &AtomicIsize = unsafe { predicate.cast().as_ref() };
160+
161+
// We use an acquire load, as that's what's done internally in
162+
// libdispatch, and matches what's done in Rust's std too:
163+
// <https://github.com/swiftlang/swift-corelibs-libdispatch/blob/swift-6.0.3-RELEASE/src/once.c#L57>
164+
// <https://github.com/rust-lang/rust/blob/1.83.0/library/std/src/sys/sync/once/queue.rs#L130>
165+
if atomic_predicate.load(Ordering::Acquire) != !0 {
166+
invoke_dispatch_once(predicate, work);
167+
}
168+
169+
// NOTE: Unlike in C, we cannot use `std::hint::assert_unchecked`,
170+
// since that would actually be lying from a language perspective;
171+
// the value seems to only settle on being !0 after some time
172+
// (something about the _COMM_PAGE_CPU_QUIESCENT_COUNTER?)
173+
//
174+
// TODO: Investigate this further!
175+
// std::hint::assert_unchecked(atomic_predicate.load(Ordering::Acquire) == !0);
176+
} else {
177+
invoke_dispatch_once(predicate, work);
178+
}
179+
}
180+
}
181+
182+
// SAFETY: Same as `std::sync::Once`
183+
unsafe impl Send for Once {}
184+
185+
// SAFETY: Same as `std::sync::Once`
186+
unsafe impl Sync for Once {}
187+
188+
impl UnwindSafe for Once {}
189+
impl RefUnwindSafe for Once {}
190+
191+
impl fmt::Debug for Once {
192+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193+
f.debug_struct("Once").finish_non_exhaustive()
194+
}
195+
}
196+
197+
#[cfg(test)]
198+
mod tests {
199+
use std::cell::Cell;
200+
use std::mem::ManuallyDrop;
201+
use std::thread;
202+
203+
use super::*;
204+
205+
#[test]
206+
fn test_static() {
207+
static ONCE: Once = Once::new();
208+
let mut num = 0;
209+
ONCE.call_once(|| num += 1);
210+
ONCE.call_once(|| num += 1);
211+
assert!(num == 1);
212+
}
213+
214+
#[test]
215+
fn test_in_loop() {
216+
let once = Once::new();
217+
218+
let mut call_count = 0;
219+
for _ in 0..10 {
220+
once.call_once(|| call_count += 1);
221+
}
222+
223+
assert_eq!(call_count, 1);
224+
}
225+
226+
#[test]
227+
fn test_move() {
228+
let once = Once::new();
229+
230+
let mut call_count = 0;
231+
for _ in 0..10 {
232+
once.call_once(|| call_count += 1);
233+
}
234+
235+
let once = once;
236+
for _ in 0..10 {
237+
once.call_once(|| call_count += 1);
238+
}
239+
240+
let once = Once {
241+
predicate: UnsafeCell::new(once.predicate.into_inner()),
242+
};
243+
for _ in 0..10 {
244+
once.call_once(|| call_count += 1);
245+
}
246+
247+
assert_eq!(call_count, 1);
248+
}
249+
250+
#[test]
251+
fn test_threaded() {
252+
let once = Once::new();
253+
254+
let num = AtomicIsize::new(0);
255+
256+
thread::scope(|scope| {
257+
scope.spawn(|| {
258+
once.call_once(|| {
259+
num.fetch_add(1, Ordering::Relaxed);
260+
});
261+
});
262+
scope.spawn(|| {
263+
once.call_once(|| {
264+
num.fetch_add(1, Ordering::Relaxed);
265+
});
266+
});
267+
scope.spawn(|| {
268+
once.call_once(|| {
269+
num.fetch_add(1, Ordering::Relaxed);
270+
});
271+
});
272+
});
273+
274+
assert!(num.load(Ordering::Relaxed) == 1);
275+
}
276+
277+
#[derive(Clone)]
278+
struct DropTest<'a>(&'a Cell<usize>);
279+
280+
impl Drop for DropTest<'_> {
281+
fn drop(&mut self) {
282+
self.0.set(self.0.get() + 1);
283+
}
284+
}
285+
286+
#[test]
287+
fn test_drop_in_closure() {
288+
let amount_of_drops = Cell::new(0);
289+
let once = Once::new();
290+
291+
let tester = DropTest(&amount_of_drops);
292+
once.call_once(move || {
293+
let _tester = tester;
294+
});
295+
assert_eq!(amount_of_drops.get(), 1);
296+
297+
let tester = DropTest(&amount_of_drops);
298+
once.call_once(move || {
299+
let _tester = tester;
300+
});
301+
assert_eq!(amount_of_drops.get(), 2);
302+
}
303+
304+
#[test]
305+
fn test_drop_in_closure_with_leak() {
306+
let amount_of_drops = Cell::new(0);
307+
let once = Once::new();
308+
309+
// Not dropped here, since we ManuallyDrop inside the closure (and the
310+
// closure is executed).
311+
let tester = DropTest(&amount_of_drops);
312+
once.call_once(move || {
313+
let _tester = ManuallyDrop::new(tester);
314+
});
315+
assert_eq!(amount_of_drops.get(), 0);
316+
317+
// Still dropped here, since the once is not executed
318+
let tester = DropTest(&amount_of_drops);
319+
once.call_once(move || {
320+
let _tester = ManuallyDrop::new(tester);
321+
});
322+
assert_eq!(amount_of_drops.get(), 1);
323+
}
324+
325+
#[test]
326+
#[ignore = "traps the process (as expected)"]
327+
fn test_recursive_invocation() {
328+
let once = Once::new();
329+
330+
once.call_once(|| {
331+
once.call_once(|| {
332+
println!("foo");
333+
});
334+
});
335+
}
336+
}

crates/dispatch2/translation-config.toml

+4
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,7 @@ typedef.dispatch_workloop_t.skipped = true
6868

6969
# Inline, defined manually for now
7070
fn.dispatch_get_main_queue.skipped = true
71+
72+
# Fast paths for dispatch_once, done manually
73+
fn._dispatch_once.skipped = true
74+
fn._dispatch_once_f.skipped = true

generated

0 commit comments

Comments
 (0)