Skip to main content

orbit_core/
sync.rs

1//! Waiting on a shared 32-bit word, the primitive under every "wake me when
2//! this changes" in Orbit: ring readiness, cell changes.
3//!
4//! [`wait_word`] parks the caller until the word no longer holds `expected`
5//! (or spuriously; callers loop). [`wake_word`] wakes every waiter on it. The
6//! word may live in shared memory: Linux futex, FreeBSD umtx and macOS
7//! `os_sync_wait_on_address` all key waiters by the physical location, so a
8//! wake in one process reaches a waiter in another with nothing carried
9//! between them. No descriptor, no channel: the memory is the signal.
10//!
11//! macOS needs 14.4 for the shared form. Below that there is no wait to
12//! be had, and [`supported`] says so: a crate that parks on words refuses
13//! to open rather than pretending, because a sleep loop wearing the shape
14//! of a wait is worse than a clear no.
15
16#![cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
17
18use std::io;
19#[cfg(target_os = "macos")]
20use std::mem::size_of;
21use std::sync::atomic::AtomicU32;
22use std::time::Duration;
23
24/// Whether this build can park on a shared word at all.
25///
26/// Linux and FreeBSD always can. macOS can from 14.4; below it, and on
27/// any other target, nothing here works and callers should refuse at
28/// their own front door rather than degrade quietly.
29pub fn supported() -> bool {
30    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
31    {
32        true
33    }
34    #[cfg(target_os = "macos")]
35    {
36        macos::api().is_some()
37    }
38}
39
40#[cfg(target_os = "linux")]
41pub fn wait_word(word: &AtomicU32, expected: u32) -> io::Result<()> {
42    let result = unsafe {
43        libc::syscall(
44            libc::SYS_futex,
45            word.as_ptr(),
46            libc::FUTEX_WAIT,
47            expected,
48            std::ptr::null::<libc::timespec>(),
49            std::ptr::null::<u32>(),
50            0,
51        )
52    };
53    if result == 0 {
54        return Ok(());
55    }
56
57    let error = io::Error::last_os_error();
58    match error.raw_os_error() {
59        // The generation changed before the kernel parked us, or the driver
60        // was interrupted. The outer loop re-checks both generation and stop.
61        Some(libc::EAGAIN) | Some(libc::EINTR) => Ok(()),
62        _ => Err(error),
63    }
64}
65
66/// Park until the word no longer holds `expected`, or `timeout` passes.
67///
68/// `Ok(true)` means something may have changed — a wake, a value that
69/// moved before the kernel parked us, or a signal — and the caller
70/// re-checks as it does after [`wait_word`]. `Ok(false)` means the
71/// timeout passed and nothing else. The timeout is relative and measured
72/// on a monotonic clock, so a caller holding a deadline recomputes what
73/// is left on each turn of its loop.
74#[cfg(target_os = "linux")]
75pub fn wait_word_timeout(word: &AtomicU32, expected: u32, timeout: Duration) -> io::Result<bool> {
76    // FUTEX_WAIT reads this as relative, on CLOCK_MONOTONIC.
77    let left = libc::timespec {
78        tv_sec: timeout.as_secs().min(i64::MAX as u64) as libc::time_t,
79        tv_nsec: timeout.subsec_nanos() as libc::c_long,
80    };
81    let result = unsafe {
82        libc::syscall(
83            libc::SYS_futex,
84            word.as_ptr(),
85            libc::FUTEX_WAIT,
86            expected,
87            &left as *const libc::timespec,
88            std::ptr::null::<u32>(),
89            0,
90        )
91    };
92    if result == 0 {
93        return Ok(true);
94    }
95
96    let error = io::Error::last_os_error();
97    match error.raw_os_error() {
98        Some(libc::EAGAIN) | Some(libc::EINTR) => Ok(true),
99        Some(libc::ETIMEDOUT) => Ok(false),
100        _ => Err(error),
101    }
102}
103
104#[cfg(target_os = "linux")]
105pub fn wake_word(word: &AtomicU32) -> io::Result<()> {
106    let result = unsafe {
107        libc::syscall(
108            libc::SYS_futex,
109            word.as_ptr(),
110            libc::FUTEX_WAKE,
111            i32::MAX,
112            std::ptr::null::<libc::timespec>(),
113            std::ptr::null::<u32>(),
114            0,
115        )
116    };
117    if result >= 0 {
118        Ok(())
119    } else {
120        Err(io::Error::last_os_error())
121    }
122}
123
124#[cfg(target_os = "freebsd")]
125pub fn wait_word(word: &AtomicU32, expected: u32) -> io::Result<()> {
126    let result = unsafe {
127        libc::_umtx_op(
128            word.as_ptr().cast(),
129            libc::UMTX_OP_WAIT_UINT,
130            expected as libc::c_ulong,
131            std::ptr::null_mut(),
132            std::ptr::null_mut(),
133        )
134    };
135    if result == 0 {
136        return Ok(());
137    }
138
139    let error = io::Error::last_os_error();
140    match error.raw_os_error() {
141        // The generation changed before the kernel parked us, or the driver
142        // was interrupted. The outer loop re-checks generation and stop.
143        Some(libc::EINTR) => Ok(()),
144        _ => Err(error),
145    }
146}
147
148/// Park until the word no longer holds `expected`, or `timeout` passes.
149///
150/// `Ok(true)` means something may have changed — a wake, a value that
151/// moved before the kernel parked us, or a signal — and the caller
152/// re-checks as it does after [`wait_word`]. `Ok(false)` means the
153/// timeout passed and nothing else. The timeout is relative and measured
154/// on a monotonic clock, so a caller holding a deadline recomputes what
155/// is left on each turn of its loop.
156#[cfg(target_os = "freebsd")]
157pub fn wait_word_timeout(word: &AtomicU32, expected: u32, timeout: Duration) -> io::Result<bool> {
158    // For the UMTX_OP_WAIT family the fourth argument is the size of the
159    // timeout structure and the fifth points at it; a bare `timespec` is
160    // read as relative.
161    let left = libc::timespec {
162        tv_sec: timeout.as_secs().min(i64::MAX as u64) as libc::time_t,
163        tv_nsec: timeout.subsec_nanos() as libc::c_long,
164    };
165    let result = unsafe {
166        libc::_umtx_op(
167            word.as_ptr().cast(),
168            libc::UMTX_OP_WAIT_UINT,
169            expected as libc::c_ulong,
170            size_of::<libc::timespec>() as *mut libc::c_void,
171            &left as *const libc::timespec as *mut libc::c_void,
172        )
173    };
174    if result == 0 {
175        return Ok(true);
176    }
177
178    let error = io::Error::last_os_error();
179    match error.raw_os_error() {
180        Some(libc::EINTR) => Ok(true),
181        Some(libc::ETIMEDOUT) => Ok(false),
182        _ => Err(error),
183    }
184}
185
186#[cfg(target_os = "freebsd")]
187pub fn wake_word(word: &AtomicU32) -> io::Result<()> {
188    let result = unsafe {
189        libc::_umtx_op(
190            word.as_ptr().cast(),
191            libc::UMTX_OP_WAKE,
192            i32::MAX as libc::c_ulong,
193            std::ptr::null_mut(),
194            std::ptr::null_mut(),
195        )
196    };
197    if result == 0 {
198        Ok(())
199    } else {
200        Err(io::Error::last_os_error())
201    }
202}
203
204#[cfg(target_os = "macos")]
205pub fn wait_word(word: &AtomicU32, expected: u32) -> io::Result<()> {
206    let api = macos::api().ok_or_else(|| {
207        io::Error::new(
208            io::ErrorKind::Unsupported,
209            "macOS shared address waits unavailable",
210        )
211    })?;
212    debug_assert_eq!(
213        (word.as_ptr() as usize) % size_of::<u32>(),
214        0,
215        "shared wait word must be naturally aligned"
216    );
217    // The SHM word is AtomicU32, not u64. Wait and wake must agree on size
218    // and shared mode. Apple returns a nonnegative waiter count on success.
219    let result = unsafe {
220        (api.wait)(
221            word.as_ptr().cast(),
222            u64::from(expected),
223            size_of::<u32>(),
224            macos::SHARED,
225        )
226    };
227    if result >= 0 {
228        return Ok(());
229    }
230    let error = io::Error::last_os_error();
231    match error.raw_os_error() {
232        Some(libc::EINTR) => Ok(()),
233        _ => Err(error),
234    }
235}
236
237/// Park until the word no longer holds `expected`, or `timeout` passes.
238///
239/// `Ok(true)` means something may have changed — a wake, a value that
240/// moved before the kernel parked us, or a signal — and the caller
241/// re-checks as it does after [`wait_word`]. `Ok(false)` means the
242/// timeout passed and nothing else. The timeout is relative and measured
243/// on a monotonic clock, so a caller holding a deadline recomputes what
244/// is left on each turn of its loop.
245#[cfg(target_os = "macos")]
246pub fn wait_word_timeout(word: &AtomicU32, expected: u32, timeout: Duration) -> io::Result<bool> {
247    let api = macos::api().ok_or_else(|| {
248        io::Error::new(
249            io::ErrorKind::Unsupported,
250            "macOS shared address waits unavailable",
251        )
252    })?;
253    let nanos = timeout.as_nanos().min(u128::from(u64::MAX)) as u64;
254    // SAFETY: the same word, size and shared flag as the untimed wait.
255    let result = unsafe {
256        (api.wait_timeout)(
257            word.as_ptr().cast(),
258            u64::from(expected),
259            size_of::<u32>(),
260            macos::SHARED,
261            macos::MACH_ABSOLUTE_TIME,
262            nanos,
263        )
264    };
265    if result >= 0 {
266        return Ok(true);
267    }
268    let error = io::Error::last_os_error();
269    match error.raw_os_error() {
270        Some(libc::EINTR) => Ok(true),
271        Some(libc::ETIMEDOUT) => Ok(false),
272        _ => Err(error),
273    }
274}
275
276#[cfg(target_os = "macos")]
277pub fn wake_word(word: &AtomicU32) -> io::Result<()> {
278    debug_assert_eq!(
279        (word.as_ptr() as usize) % size_of::<u32>(),
280        0,
281        "shared wake word must be naturally aligned"
282    );
283    // Older macOS has no native subscribers; publication still succeeds and
284    // polling readers see the committed frames.
285    let Some(api) = macos::api() else {
286        return Ok(());
287    };
288    loop {
289        let result =
290            unsafe { (api.wake_all)(word.as_ptr().cast(), size_of::<u32>(), macos::SHARED) };
291        if result >= 0 {
292            return Ok(());
293        }
294        let error = io::Error::last_os_error();
295        match error.raw_os_error() {
296            // No waiter is normal: publication can precede subscription or
297            // race the driver's compare-and-wait. The generation persists.
298            Some(libc::ENOENT) => return Ok(()),
299            Some(libc::EINTR) => continue,
300            _ => return Err(error),
301        }
302    }
303}
304
305#[cfg(target_os = "macos")]
306pub(crate) mod macos {
307    use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
308
309    // OS_SYNC_WAIT_ON_ADDRESS_SHARED and OS_SYNC_WAKE_BY_ADDRESS_SHARED
310    // have the same ABI value in <os/os_sync_wait_on_address.h>.
311    pub(super) const SHARED: u32 = 1;
312
313    /// `os_clockid_t` in <os/clock.h>: the only clock the timed wait
314    /// takes, and the one a relative timeout is measured on.
315    pub(super) const MACH_ABSOLUTE_TIME: u32 = 32;
316
317    type Wait = unsafe extern "C" fn(*mut libc::c_void, u64, usize, u32) -> libc::c_int;
318    type WaitTimeout =
319        unsafe extern "C" fn(*mut libc::c_void, u64, usize, u32, u32, u64) -> libc::c_int;
320    type Wake = unsafe extern "C" fn(*mut libc::c_void, usize, u32) -> libc::c_int;
321
322    #[derive(Clone, Copy)]
323    pub(crate) struct Api {
324        pub(super) wait: Wait,
325        pub(super) wait_timeout: WaitTimeout,
326        pub(super) wake_all: Wake,
327    }
328
329    const UNRESOLVED: u8 = 0;
330    const UNAVAILABLE: u8 = 1;
331    const READY: u8 = 2;
332
333    static STATE: AtomicU8 = AtomicU8::new(UNRESOLVED);
334    static WAIT: AtomicUsize = AtomicUsize::new(0);
335    static WAIT_TIMEOUT: AtomicUsize = AtomicUsize::new(0);
336    static WAKE: AtomicUsize = AtomicUsize::new(0);
337
338    /// Resolve the 14.4 entry points, once per process but never by waiting.
339    ///
340    /// Deliberately not a `OnceLock`. A publisher reaches this from
341    /// `wake_all_generation_waiters`, and a publisher may be a process that
342    /// was just forked while another thread of its parent was inside the
343    /// resolution -- the test harness runs tests on parallel threads, and a
344    /// supervisor forks workers with readiness threads alive. The child
345    /// inherits the "initializing" state and none of the thread that would
346    /// finish it, so a blocking once-cell parks forever. Resolution here is
347    /// idempotent: racing callers each `dlsym` the same two symbols and store
348    /// the same values, and nobody waits for anybody.
349    ///
350    /// Resolving lazily rather than linking avoids hard references to
351    /// 14.4-only symbols on older deployment targets. libSystem stays loaded
352    /// for the life of the process, so the pointers never dangle.
353    pub(crate) fn api() -> Option<Api> {
354        match STATE.load(Ordering::Acquire) {
355            READY => Some(load()),
356            UNAVAILABLE => None,
357            _ => resolve(),
358        }
359    }
360
361    fn load() -> Api {
362        unsafe {
363            Api {
364                wait: std::mem::transmute::<usize, Wait>(WAIT.load(Ordering::Acquire)),
365                wait_timeout: std::mem::transmute::<usize, WaitTimeout>(
366                    WAIT_TIMEOUT.load(Ordering::Acquire),
367                ),
368                wake_all: std::mem::transmute::<usize, Wake>(WAKE.load(Ordering::Acquire)),
369            }
370        }
371    }
372
373    fn resolve() -> Option<Api> {
374        let wait = unsafe { libc::dlsym(libc::RTLD_DEFAULT, c"os_sync_wait_on_address".as_ptr()) };
375        // Shipped in the same release as the other two; all or nothing.
376        let wait_timeout = unsafe {
377            libc::dlsym(
378                libc::RTLD_DEFAULT,
379                c"os_sync_wait_on_address_with_timeout".as_ptr(),
380            )
381        };
382        let wake =
383            unsafe { libc::dlsym(libc::RTLD_DEFAULT, c"os_sync_wake_by_address_all".as_ptr()) };
384        if wait.is_null() || wait_timeout.is_null() || wake.is_null() {
385            STATE.store(UNAVAILABLE, Ordering::Release);
386            return None;
387        }
388        // Pointers first, then the state that publishes them.
389        WAIT.store(wait as usize, Ordering::Release);
390        WAIT_TIMEOUT.store(wait_timeout as usize, Ordering::Release);
391        WAKE.store(wake as usize, Ordering::Release);
392        STATE.store(READY, Ordering::Release);
393        Some(load())
394    }
395}
396
397#[cfg(test)]
398mod timeout_tests {
399    use std::sync::Arc;
400    use std::sync::atomic::Ordering;
401    use std::time::Instant;
402
403    use super::*;
404
405    /// Also pins the unit the platform reads the timeout in: a wrong one
406    /// shows up here as a wait that is orders of magnitude off, not as a
407    /// wrong answer.
408    #[test]
409    fn a_timeout_is_a_timeout() {
410        if !supported() {
411            return;
412        }
413        let word = AtomicU32::new(7);
414        let started = Instant::now();
415        assert!(!wait_word_timeout(&word, 7, Duration::from_millis(200)).expect("wait"));
416        let waited = started.elapsed();
417        assert!(waited >= Duration::from_millis(150), "returned after {waited:?}");
418        assert!(waited < Duration::from_secs(2), "returned after {waited:?}");
419    }
420
421    #[test]
422    fn a_wake_beats_the_timeout() {
423        if !supported() {
424            return;
425        }
426        let word = Arc::new(AtomicU32::new(0));
427        let waker = Arc::clone(&word);
428        std::thread::spawn(move || {
429            std::thread::sleep(Duration::from_millis(50));
430            waker.store(1, Ordering::SeqCst);
431            let _ = wake_word(&waker);
432        });
433        let started = Instant::now();
434        assert!(wait_word_timeout(&word, 0, Duration::from_secs(10)).expect("wait"));
435        assert!(started.elapsed() < Duration::from_secs(5));
436    }
437
438    #[test]
439    fn a_word_that_already_moved_does_not_park_at_all() {
440        if !supported() {
441            return;
442        }
443        let word = AtomicU32::new(3);
444        let started = Instant::now();
445        assert!(wait_word_timeout(&word, 9, Duration::from_secs(30)).expect("wait"));
446        assert!(started.elapsed() < Duration::from_secs(1));
447    }
448}