Skip to main content

orbit_core/ring/
readiness.rs

1//! Native readiness bridge for an SHM ring.
2//!
3//! The shared signal is a generation in the ring header, waited through Linux
4//! futex, FreeBSD umtx, or macOS shared address waits. Each process owns a
5//! private readiness fd (`eventfd`, or a pipe on macOS) and a small
6//! blocking driver thread that converts generation changes into fd readiness.
7//! Async runtimes can therefore wait on their normal reactor without sharing
8//! one drainable eventfd across readers.
9
10#![cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
11
12use std::fmt;
13use std::io;
14use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd};
15use std::sync::Arc;
16use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
17use std::thread::JoinHandle;
18
19use super::shm::ShmRing;
20
21/// Process-local fd readiness bridge for one shared Orbit ring.
22///
23/// The established name is retained on macOS, where a nonblocking pipe backs
24/// the fd. Native macOS readiness requires 14.4 or later; older systems return
25/// `ErrorKind::Unsupported` from construction so callers can use polling.
26///
27/// Every subscribing process creates its own instance. Ring publishers bump a
28/// generation stored in SHM and wake all platform waiters; the local driver
29/// then marks this fd readable. Multiple publishes may coalesce into one wake,
30/// so a consumer must drain the fd and poll the ring through its own cursor.
31pub struct RingEventFd {
32    fd: OwnedFd,
33    ring: Arc<ShmRing>,
34    stop: Arc<AtomicBool>,
35    driver: Option<JoinHandle<()>>,
36}
37
38impl RingEventFd {
39    pub(crate) fn new(ring: Arc<ShmRing>) -> io::Result<Self> {
40        let (fd, driver_fd) = local_notification_pair()?;
41        let stop = Arc::new(AtomicBool::new(false));
42        let driver_stop = stop.clone();
43        let driver_ring = ring.clone();
44        let mut observed = ring.notification_generation().load(Ordering::Acquire);
45
46        // A pipe has two distinct ends; an eventfd is one descriptor cloned.
47        #[cfg(target_os = "macos")]
48        debug_assert_ne!(fd.as_raw_fd(), driver_fd.as_raw_fd());
49
50        let driver = std::thread::Builder::new()
51            .name(format!("orbit-ring-{}-eventfd", ring.kind()))
52            .spawn(move || {
53                while !driver_stop.load(Ordering::Acquire) {
54                    let current = driver_ring
55                        .notification_generation()
56                        .load(Ordering::Acquire);
57                    if current != observed {
58                        observed = current;
59                        if signal_event_fd(driver_fd.as_raw_fd()).is_err() {
60                            break;
61                        }
62                        continue;
63                    }
64                    if wait_for_generation(driver_ring.notification_generation(), observed).is_err()
65                    {
66                        break;
67                    }
68                }
69            })?;
70
71        Ok(Self {
72            fd,
73            ring,
74            stop,
75            driver: Some(driver),
76        })
77    }
78
79    pub(crate) fn notify(ring: &ShmRing) -> io::Result<()> {
80        ring.notification_generation()
81            .fetch_add(1, Ordering::Release);
82        wake_all_generation_waiters(ring.notification_generation())
83    }
84
85    /// Drain coalesced readiness tokens from the nonblocking local fd.
86    ///
87    /// Ring events themselves remain in SHM; the returned number is only the
88    /// local wake count and must not be interpreted as an event count.
89    pub fn drain(&self) -> io::Result<u64> {
90        let mut total = 0u64;
91        loop {
92            let mut value = 0u64;
93            let read = unsafe {
94                libc::read(
95                    self.fd.as_raw_fd(),
96                    (&mut value as *mut u64).cast(),
97                    std::mem::size_of::<u64>(),
98                )
99            };
100            if read == std::mem::size_of::<u64>() as isize {
101                #[cfg(target_os = "macos")]
102                debug_assert_eq!(value, 1, "local pipe carries unit readiness tokens only");
103                total = total.saturating_add(value);
104                continue;
105            }
106            if read == 0 {
107                return Err(io::Error::new(
108                    io::ErrorKind::UnexpectedEof,
109                    "Orbit eventfd closed while draining",
110                ));
111            }
112            if read < 0 {
113                let error = io::Error::last_os_error();
114                match error.raw_os_error() {
115                    Some(libc::EINTR) => continue,
116                    Some(libc::EAGAIN) => return Ok(total),
117                    _ => return Err(error),
118                }
119            }
120            return Err(io::Error::new(
121                io::ErrorKind::InvalidData,
122                "Orbit eventfd returned a partial counter",
123            ));
124        }
125    }
126}
127
128impl AsRawFd for RingEventFd {
129    fn as_raw_fd(&self) -> RawFd {
130        self.fd.as_raw_fd()
131    }
132}
133
134impl AsFd for RingEventFd {
135    fn as_fd(&self) -> BorrowedFd<'_> {
136        self.fd.as_fd()
137    }
138}
139
140impl fmt::Debug for RingEventFd {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        f.debug_struct("RingEventFd")
143            .field("fd", &self.fd.as_raw_fd())
144            .field("ring_kind", &self.ring.kind())
145            .finish_non_exhaustive()
146    }
147}
148
149impl Drop for RingEventFd {
150    fn drop(&mut self) {
151        self.stop.store(true, Ordering::Release);
152        // Change the generation before waking. If the driver passed its stop
153        // check but has not entered the platform wait yet, the atomic compare
154        // prevents it from parking after our wake and deadlocking join.
155        self.ring
156            .notification_generation()
157            .fetch_add(1, Ordering::Release);
158        let _ = wake_all_generation_waiters(self.ring.notification_generation());
159        if let Some(driver) = self.driver.take() {
160            let _ = driver.join();
161        }
162    }
163}
164
165#[cfg(target_os = "linux")]
166fn wait_for_generation(word: &AtomicU32, expected: u32) -> io::Result<()> {
167    let result = unsafe {
168        libc::syscall(
169            libc::SYS_futex,
170            word.as_ptr(),
171            libc::FUTEX_WAIT,
172            expected,
173            std::ptr::null::<libc::timespec>(),
174            std::ptr::null::<u32>(),
175            0,
176        )
177    };
178    if result == 0 {
179        return Ok(());
180    }
181
182    let error = io::Error::last_os_error();
183    match error.raw_os_error() {
184        // The generation changed before the kernel parked us, or the driver
185        // was interrupted. The outer loop re-checks both generation and stop.
186        Some(libc::EAGAIN) | Some(libc::EINTR) => Ok(()),
187        _ => Err(error),
188    }
189}
190
191#[cfg(target_os = "linux")]
192fn wake_all_generation_waiters(word: &AtomicU32) -> io::Result<()> {
193    let result = unsafe {
194        libc::syscall(
195            libc::SYS_futex,
196            word.as_ptr(),
197            libc::FUTEX_WAKE,
198            i32::MAX,
199            std::ptr::null::<libc::timespec>(),
200            std::ptr::null::<u32>(),
201            0,
202        )
203    };
204    if result >= 0 {
205        Ok(())
206    } else {
207        Err(io::Error::last_os_error())
208    }
209}
210
211#[cfg(target_os = "freebsd")]
212fn wait_for_generation(word: &AtomicU32, expected: u32) -> io::Result<()> {
213    let result = unsafe {
214        libc::_umtx_op(
215            word.as_ptr().cast(),
216            libc::UMTX_OP_WAIT_UINT,
217            expected as libc::c_ulong,
218            std::ptr::null_mut(),
219            std::ptr::null_mut(),
220        )
221    };
222    if result == 0 {
223        return Ok(());
224    }
225
226    let error = io::Error::last_os_error();
227    match error.raw_os_error() {
228        // The generation changed before the kernel parked us, or the driver
229        // was interrupted. The outer loop re-checks generation and stop.
230        Some(libc::EINTR) => Ok(()),
231        _ => Err(error),
232    }
233}
234
235#[cfg(target_os = "freebsd")]
236fn wake_all_generation_waiters(word: &AtomicU32) -> io::Result<()> {
237    let result = unsafe {
238        libc::_umtx_op(
239            word.as_ptr().cast(),
240            libc::UMTX_OP_WAKE,
241            i32::MAX as libc::c_ulong,
242            std::ptr::null_mut(),
243            std::ptr::null_mut(),
244        )
245    };
246    if result == 0 {
247        Ok(())
248    } else {
249        Err(io::Error::last_os_error())
250    }
251}
252
253fn signal_event_fd(fd: RawFd) -> io::Result<()> {
254    let value = 1u64;
255    loop {
256        let written = unsafe {
257            libc::write(
258                fd,
259                (&value as *const u64).cast(),
260                std::mem::size_of::<u64>(),
261            )
262        };
263        if written == std::mem::size_of::<u64>() as isize {
264            return Ok(());
265        }
266        if written < 0 {
267            let error = io::Error::last_os_error();
268            match error.raw_os_error() {
269                Some(libc::EINTR) => continue,
270                // A full eventfd or pipe is already readable, so the notification is
271                // represented even though this increment could not be added.
272                Some(libc::EAGAIN) => return Ok(()),
273                _ => return Err(error),
274            }
275        }
276        return Err(io::Error::new(
277            io::ErrorKind::WriteZero,
278            "Orbit eventfd accepted a partial counter",
279        ));
280    }
281}
282
283#[cfg(any(target_os = "linux", target_os = "freebsd"))]
284fn local_notification_pair() -> io::Result<(OwnedFd, OwnedFd)> {
285    let raw = unsafe { libc::eventfd(0, libc::EFD_CLOEXEC | libc::EFD_NONBLOCK) };
286    if raw < 0 {
287        return Err(io::Error::last_os_error());
288    }
289    let fd = unsafe { OwnedFd::from_raw_fd(raw) };
290    let driver = fd.try_clone()?;
291    Ok((fd, driver))
292}
293
294#[cfg(target_os = "macos")]
295fn local_notification_pair() -> io::Result<(OwnedFd, OwnedFd)> {
296    if macos::api().is_none() {
297        return Err(io::Error::new(
298            io::ErrorKind::Unsupported,
299            "Orbit native readiness requires macOS 14.4 or later",
300        ));
301    }
302    let mut raw = [-1; 2];
303    if unsafe { libc::pipe(raw.as_mut_ptr()) } < 0 {
304        return Err(io::Error::last_os_error());
305    }
306    // Take ownership of both ends before any fallible setup. Drop joins the
307    // driver before closing the read end, so writes cannot hit a closed pipe.
308    let read = unsafe { OwnedFd::from_raw_fd(raw[0]) };
309    let write = unsafe { OwnedFd::from_raw_fd(raw[1]) };
310    for fd in [&read, &write] {
311        if unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_SETFD, libc::FD_CLOEXEC) } < 0
312            || unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_SETFL, libc::O_NONBLOCK) } < 0
313        {
314            return Err(io::Error::last_os_error());
315        }
316    }
317    Ok((read, write))
318}
319
320#[cfg(target_os = "macos")]
321fn wait_for_generation(word: &AtomicU32, expected: u32) -> io::Result<()> {
322    let api = macos::api().ok_or_else(|| {
323        io::Error::new(
324            io::ErrorKind::Unsupported,
325            "macOS shared address waits unavailable",
326        )
327    })?;
328    debug_assert_eq!(
329        (word.as_ptr() as usize) % size_of::<u32>(),
330        0,
331        "shared wait word must be naturally aligned"
332    );
333    // The SHM word is AtomicU32, not u64. Wait and wake must agree on size
334    // and shared mode. Apple returns a nonnegative waiter count on success.
335    let result = unsafe {
336        (api.wait)(
337            word.as_ptr().cast(),
338            u64::from(expected),
339            size_of::<u32>(),
340            macos::SHARED,
341        )
342    };
343    if result >= 0 {
344        return Ok(());
345    }
346    let error = io::Error::last_os_error();
347    match error.raw_os_error() {
348        Some(libc::EINTR) => Ok(()),
349        _ => Err(error),
350    }
351}
352
353#[cfg(target_os = "macos")]
354fn wake_all_generation_waiters(word: &AtomicU32) -> io::Result<()> {
355    debug_assert_eq!(
356        (word.as_ptr() as usize) % size_of::<u32>(),
357        0,
358        "shared wake word must be naturally aligned"
359    );
360    // Older macOS has no native subscribers; publication still succeeds and
361    // polling readers see the committed frames.
362    let Some(api) = macos::api() else {
363        return Ok(());
364    };
365    loop {
366        let result =
367            unsafe { (api.wake_all)(word.as_ptr().cast(), size_of::<u32>(), macos::SHARED) };
368        if result >= 0 {
369            return Ok(());
370        }
371        let error = io::Error::last_os_error();
372        match error.raw_os_error() {
373            // No waiter is normal: publication can precede subscription or
374            // race the driver's compare-and-wait. The generation persists.
375            Some(libc::ENOENT) => return Ok(()),
376            Some(libc::EINTR) => continue,
377            _ => return Err(error),
378        }
379    }
380}
381
382#[cfg(target_os = "macos")]
383mod macos {
384    use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
385
386    // OS_SYNC_WAIT_ON_ADDRESS_SHARED and OS_SYNC_WAKE_BY_ADDRESS_SHARED
387    // have the same ABI value in <os/os_sync_wait_on_address.h>.
388    pub(super) const SHARED: u32 = 1;
389
390    type Wait = unsafe extern "C" fn(*mut libc::c_void, u64, usize, u32) -> libc::c_int;
391    type Wake = unsafe extern "C" fn(*mut libc::c_void, usize, u32) -> libc::c_int;
392
393    #[derive(Clone, Copy)]
394    pub(super) struct Api {
395        pub(super) wait: Wait,
396        pub(super) wake_all: Wake,
397    }
398
399    const UNRESOLVED: u8 = 0;
400    const UNAVAILABLE: u8 = 1;
401    const READY: u8 = 2;
402
403    static STATE: AtomicU8 = AtomicU8::new(UNRESOLVED);
404    static WAIT: AtomicUsize = AtomicUsize::new(0);
405    static WAKE: AtomicUsize = AtomicUsize::new(0);
406
407    /// Resolve the 14.4 entry points, once per process but never by waiting.
408    ///
409    /// Deliberately not a `OnceLock`. A publisher reaches this from
410    /// `wake_all_generation_waiters`, and a publisher may be a process that
411    /// was just forked while another thread of its parent was inside the
412    /// resolution -- the test harness runs tests on parallel threads, and a
413    /// supervisor forks workers with readiness threads alive. The child
414    /// inherits the "initializing" state and none of the thread that would
415    /// finish it, so a blocking once-cell parks forever. Resolution here is
416    /// idempotent: racing callers each `dlsym` the same two symbols and store
417    /// the same values, and nobody waits for anybody.
418    ///
419    /// Resolving lazily rather than linking avoids hard references to
420    /// 14.4-only symbols on older deployment targets. libSystem stays loaded
421    /// for the life of the process, so the pointers never dangle.
422    pub(super) fn api() -> Option<Api> {
423        match STATE.load(Ordering::Acquire) {
424            READY => Some(load()),
425            UNAVAILABLE => None,
426            _ => resolve(),
427        }
428    }
429
430    fn load() -> Api {
431        unsafe {
432            Api {
433                wait: std::mem::transmute::<usize, Wait>(WAIT.load(Ordering::Acquire)),
434                wake_all: std::mem::transmute::<usize, Wake>(WAKE.load(Ordering::Acquire)),
435            }
436        }
437    }
438
439    fn resolve() -> Option<Api> {
440        let wait = unsafe { libc::dlsym(libc::RTLD_DEFAULT, c"os_sync_wait_on_address".as_ptr()) };
441        let wake =
442            unsafe { libc::dlsym(libc::RTLD_DEFAULT, c"os_sync_wake_by_address_all".as_ptr()) };
443        if wait.is_null() || wake.is_null() {
444            STATE.store(UNAVAILABLE, Ordering::Release);
445            return None;
446        }
447        // Pointers first, then the state that publishes them.
448        WAIT.store(wait as usize, Ordering::Release);
449        WAKE.store(wake as usize, Ordering::Release);
450        STATE.store(READY, Ordering::Release);
451        Some(load())
452    }
453}
454
455#[cfg(all(test, target_os = "macos"))]
456mod tests {
457    use super::*;
458
459    #[test]
460    fn full_local_pipe_coalesces_and_rearms() {
461        let (read, write) = local_notification_pair().unwrap();
462        for fd in [&read, &write] {
463            assert_ne!(
464                unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETFL) } & libc::O_NONBLOCK,
465                0
466            );
467            assert_ne!(
468                unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETFD) } & libc::FD_CLOEXEC,
469                0
470            );
471        }
472        // Fill the pipe deliberately, then exercise the bridge's EAGAIN path.
473        let token = 1u64;
474        loop {
475            let n = unsafe { libc::write(write.as_raw_fd(), (&token as *const u64).cast(), 8) };
476            if n < 0 {
477                assert_eq!(
478                    io::Error::last_os_error().raw_os_error(),
479                    Some(libc::EAGAIN)
480                );
481                break;
482            }
483            assert_eq!(n, 8);
484        }
485        signal_event_fd(write.as_raw_fd()).unwrap();
486        let mut buffer = [0u64; 128];
487        loop {
488            let n = unsafe {
489                libc::read(
490                    read.as_raw_fd(),
491                    buffer.as_mut_ptr().cast(),
492                    size_of_val(&buffer),
493                )
494            };
495            if n < 0 {
496                assert_eq!(
497                    io::Error::last_os_error().raw_os_error(),
498                    Some(libc::EAGAIN)
499                );
500                break;
501            }
502            assert!(n > 0);
503        }
504        signal_event_fd(write.as_raw_fd()).unwrap();
505        let mut value = 0u64;
506        assert_eq!(
507            unsafe { libc::read(read.as_raw_fd(), (&mut value as *mut u64).cast(), 8) },
508            8
509        );
510        assert_eq!(value, 1);
511    }
512}