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}