Skip to main content

microsandbox_utils/
wake_pipe.rs

1//! Cross-platform wake notification built on `pipe()`.
2//!
3//! Works on both Linux and macOS (unlike `eventfd` which is Linux-only).
4//! The write end signals, the read end is pollable via `epoll`/`kqueue`/`poll`.
5
6use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
7
8//--------------------------------------------------------------------------------------------------
9// Types
10//--------------------------------------------------------------------------------------------------
11
12/// Cross-platform wake notification built on `pipe()`.
13///
14/// The write end signals, the read end is pollable via `epoll`/`kqueue`/`poll`.
15pub struct WakePipe {
16    read_fd: OwnedFd,
17    write_fd: OwnedFd,
18}
19
20//--------------------------------------------------------------------------------------------------
21// Methods
22//--------------------------------------------------------------------------------------------------
23
24impl WakePipe {
25    /// Create a new wake pipe.
26    ///
27    /// Both ends are set to non-blocking and close-on-exec.
28    pub fn new() -> Self {
29        let mut fds = [0i32; 2];
30
31        // SAFETY: pipe() is a standard POSIX call. We check the return value
32        // and immediately wrap the raw fds in OwnedFd for RAII cleanup.
33        let ret = unsafe { libc::pipe(fds.as_mut_ptr()) };
34        assert!(
35            ret == 0,
36            "pipe() failed: {}",
37            std::io::Error::last_os_error()
38        );
39
40        // Set non-blocking and close-on-exec on both ends.
41        // SAFETY: fds are valid open file descriptors from the pipe() call above.
42        unsafe {
43            set_nonblock_cloexec(fds[0]);
44            set_nonblock_cloexec(fds[1]);
45        }
46
47        Self {
48            // SAFETY: fds are valid and not owned by anything else yet.
49            read_fd: unsafe { OwnedFd::from_raw_fd(fds[0]) },
50            write_fd: unsafe { OwnedFd::from_raw_fd(fds[1]) },
51        }
52    }
53
54    /// Signal the reader. Safe to call from any thread, multiple times.
55    ///
56    /// Writes a single byte. If the pipe buffer is full the write is silently
57    /// dropped — the reader will still wake because there are unread bytes.
58    pub fn wake(&self) {
59        // SAFETY: write_fd is a valid, non-blocking file descriptor.
60        // Writing 1 byte to a pipe is atomic on all POSIX systems.
61        unsafe {
62            libc::write(self.write_fd.as_raw_fd(), [1u8].as_ptr().cast(), 1);
63        }
64    }
65
66    /// Drain all pending wake signals. Call after processing to reset the
67    /// pipe for the next edge-triggered notification.
68    pub fn drain(&self) {
69        let mut buf = [0u8; 512];
70        loop {
71            // SAFETY: read_fd is a valid, non-blocking file descriptor.
72            let n =
73                unsafe { libc::read(self.read_fd.as_raw_fd(), buf.as_mut_ptr().cast(), buf.len()) };
74            if n <= 0 {
75                break;
76            }
77        }
78    }
79
80    /// File descriptor for `epoll`/`kqueue`/`poll(2)` registration.
81    ///
82    /// Becomes readable when [`wake()`](Self::wake) has been called.
83    pub fn as_raw_fd(&self) -> RawFd {
84        self.read_fd.as_raw_fd()
85    }
86}
87
88//--------------------------------------------------------------------------------------------------
89// Trait Implementations
90//--------------------------------------------------------------------------------------------------
91
92impl Default for WakePipe {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98//--------------------------------------------------------------------------------------------------
99// Functions
100//--------------------------------------------------------------------------------------------------
101
102/// Set `O_NONBLOCK` and `FD_CLOEXEC` on a file descriptor.
103///
104/// # Safety
105///
106/// `fd` must be a valid, open file descriptor.
107unsafe fn set_nonblock_cloexec(fd: RawFd) {
108    unsafe {
109        // Set non-blocking.
110        let flags = libc::fcntl(fd, libc::F_GETFL);
111        assert!(
112            flags >= 0,
113            "fcntl(F_GETFL) failed: {}",
114            std::io::Error::last_os_error()
115        );
116        let ret = libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
117        assert!(
118            ret >= 0,
119            "fcntl(F_SETFL) failed: {}",
120            std::io::Error::last_os_error()
121        );
122
123        // Set close-on-exec.
124        let flags = libc::fcntl(fd, libc::F_GETFD);
125        assert!(
126            flags >= 0,
127            "fcntl(F_GETFD) failed: {}",
128            std::io::Error::last_os_error()
129        );
130        let ret = libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC);
131        assert!(
132            ret >= 0,
133            "fcntl(F_SETFD) failed: {}",
134            std::io::Error::last_os_error()
135        );
136    }
137}
138
139//--------------------------------------------------------------------------------------------------
140// Tests
141//--------------------------------------------------------------------------------------------------
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn wake_and_drain() {
149        let pipe = WakePipe::new();
150        // Initially no data — drain is a no-op.
151        pipe.drain();
152
153        // Wake then drain.
154        pipe.wake();
155        pipe.wake();
156        pipe.drain();
157
158        // After drain, another wake should work.
159        pipe.wake();
160        pipe.drain();
161    }
162
163    #[test]
164    fn fd_is_valid() {
165        let pipe = WakePipe::new();
166        let fd = pipe.as_raw_fd();
167        assert!(fd >= 0);
168    }
169
170    #[test]
171    fn nonblocking_read() {
172        let pipe = WakePipe::new();
173        // Reading from an empty non-blocking pipe should not block.
174        pipe.drain();
175    }
176}