moduvex_runtime/platform/sys.rs
1//! Platform-specific I/O primitives.
2//!
3//! Provides a thin, cross-platform surface over OS I/O handles, interest flags,
4//! and event types. Unix uses `libc` raw file descriptors; Windows stubs use
5//! `windows-sys` HANDLE types.
6
7use std::io;
8
9// ── Unix ─────────────────────────────────────────────────────────────────────
10
11#[cfg(unix)]
12use std::os::unix::io::RawFd;
13
14/// Raw I/O handle type.
15/// - Unix: `i32` (raw file descriptor)
16/// - Windows: `isize` (HANDLE via windows-sys)
17#[cfg(unix)]
18pub type RawSource = RawFd;
19
20#[cfg(windows)]
21pub type RawSource = windows_sys::Win32::Foundation::HANDLE;
22
23// ── Interest flags ────────────────────────────────────────────────────────────
24
25/// Bitmask describing which I/O events a source is interested in.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct Interest(u8);
28
29impl Interest {
30 /// Register interest in read-readiness.
31 pub const READABLE: Interest = Interest(0b0000_0001);
32 /// Register interest in write-readiness.
33 pub const WRITABLE: Interest = Interest(0b0000_0010);
34
35 /// Returns `true` if the READABLE bit is set.
36 #[inline]
37 pub fn is_readable(self) -> bool {
38 self.0 & Self::READABLE.0 != 0
39 }
40
41 /// Returns `true` if the WRITABLE bit is set.
42 #[inline]
43 pub fn is_writable(self) -> bool {
44 self.0 & Self::WRITABLE.0 != 0
45 }
46
47 /// Returns the raw bitmask value.
48 #[inline]
49 pub(crate) fn bits(self) -> u8 {
50 self.0
51 }
52}
53
54impl std::ops::BitOr for Interest {
55 type Output = Interest;
56 #[inline]
57 fn bitor(self, rhs: Self) -> Self::Output {
58 Interest(self.0 | rhs.0)
59 }
60}
61
62impl std::ops::BitOrAssign for Interest {
63 #[inline]
64 fn bitor_assign(&mut self, rhs: Self) {
65 self.0 |= rhs.0;
66 }
67}
68
69// ── Event ─────────────────────────────────────────────────────────────────────
70
71/// A single I/O readiness event returned from a `poll` call.
72#[derive(Debug, Clone, Copy)]
73pub struct Event {
74 /// Caller-provided token identifying the I/O source.
75 pub token: usize,
76 /// True when the source is ready for reading.
77 pub readable: bool,
78 /// True when the source is ready for writing.
79 pub writable: bool,
80}
81
82impl Event {
83 #[inline]
84 pub(crate) fn new(token: usize, readable: bool, writable: bool) -> Self {
85 Self {
86 token,
87 readable,
88 writable,
89 }
90 }
91}
92
93/// Collection of events returned from a single `poll` call.
94/// Pre-allocated with a reasonable default capacity to avoid realloc on the
95/// hot path.
96pub type Events = Vec<Event>;
97
98/// Create a fresh `Events` buffer with the given capacity pre-allocated.
99#[inline]
100pub fn events_with_capacity(cap: usize) -> Events {
101 Vec::with_capacity(cap)
102}
103
104// ── Unix helpers ──────────────────────────────────────────────────────────────
105
106#[cfg(unix)]
107mod unix_impl {
108 use super::*;
109 use libc::{c_int, fcntl, F_GETFL, F_SETFL, O_NONBLOCK};
110
111 /// Set a file descriptor to non-blocking mode.
112 ///
113 /// # Errors
114 /// Returns `io::Error` if `fcntl` fails.
115 pub fn set_nonblocking(fd: RawSource) -> io::Result<()> {
116 // SAFETY: `fd` is a valid open file descriptor supplied by the caller.
117 // `fcntl(F_GETFL)` is read-only and always safe to call on a valid fd.
118 let flags = unsafe { fcntl(fd, F_GETFL) };
119 if flags == -1 {
120 return Err(io::Error::last_os_error());
121 }
122 // SAFETY: `fd` is valid, `flags` was obtained from `F_GETFL` above,
123 // and OR-ing with `O_NONBLOCK` is a documented, supported operation.
124 let rc = unsafe { fcntl(fd, F_SETFL, flags | O_NONBLOCK) };
125 if rc == -1 {
126 Err(io::Error::last_os_error())
127 } else {
128 Ok(())
129 }
130 }
131
132 /// Close a file descriptor.
133 ///
134 /// # Errors
135 /// Returns `io::Error` if `close` fails (e.g. EBADF, EIO).
136 pub fn close_fd(fd: RawSource) -> io::Result<()> {
137 // SAFETY: `fd` is a valid open file descriptor. After this call the fd
138 // is invalid and must not be used again — callers are responsible for
139 // ensuring this via RAII (Drop impls).
140 let rc = unsafe { libc::close(fd) };
141 if rc == -1 {
142 Err(io::Error::last_os_error())
143 } else {
144 Ok(())
145 }
146 }
147
148 /// Create an OS pipe and return `(read_fd, write_fd)`.
149 ///
150 /// Both ends are set to `O_NONBLOCK` before returning.
151 ///
152 /// # Errors
153 /// Returns `io::Error` if `pipe` or `set_nonblocking` fails.
154 pub fn create_pipe() -> io::Result<(RawSource, RawSource)> {
155 let mut fds: [c_int; 2] = [0; 2];
156 // SAFETY: `fds` is a stack-allocated array of the size required by
157 // `pipe(2)`. On success the kernel writes exactly two valid fds into it.
158 let rc = unsafe { libc::pipe(fds.as_mut_ptr()) };
159 if rc == -1 {
160 return Err(io::Error::last_os_error());
161 }
162 let (r, w) = (fds[0], fds[1]);
163 set_nonblocking(r)?;
164 set_nonblocking(w)?;
165 Ok((r, w))
166 }
167}
168
169#[cfg(unix)]
170pub use unix_impl::{close_fd, create_pipe, set_nonblocking};
171
172// ── Windows implementations ───────────────────────────────────────────────────
173
174#[cfg(windows)]
175mod windows_impl {
176 use super::*;
177
178 /// Set a Winsock socket to non-blocking mode using `ioctlsocket(FIONBIO)`.
179 ///
180 /// `handle` is treated as a `SOCKET` (which is `usize` on 64-bit Windows).
181 ///
182 /// # Safety
183 /// Caller must ensure `handle` is a valid open socket descriptor.
184 pub fn set_nonblocking(handle: RawSource) -> io::Result<()> {
185 // FIONBIO with value 1 enables non-blocking mode on a Winsock socket.
186 let mut nonblocking: u32 = 1;
187 // SAFETY: `handle` is a valid SOCKET cast to isize (RawSource = HANDLE = isize).
188 // `ioctlsocket` is safe to call with a valid socket and FIONBIO command.
189 let ret = unsafe {
190 windows_sys::Win32::Networking::WinSock::ioctlsocket(
191 handle as usize, // SOCKET is usize on 64-bit Windows
192 windows_sys::Win32::Networking::WinSock::FIONBIO as i32,
193 &mut nonblocking,
194 )
195 };
196 if ret != 0 {
197 Err(io::Error::last_os_error())
198 } else {
199 Ok(())
200 }
201 }
202
203 /// Close an OS handle via `CloseHandle`.
204 ///
205 /// # Safety
206 /// Caller must ensure `handle` is a valid, open HANDLE that has not been
207 /// closed already. After this call the handle is invalid.
208 pub fn close_fd(handle: RawSource) -> io::Result<()> {
209 // SAFETY: `handle` is a valid HANDLE. CloseHandle is the documented
210 // way to release kernel resources associated with any HANDLE type.
211 let ok = unsafe { windows_sys::Win32::Foundation::CloseHandle(handle) };
212 if ok == 0 {
213 Err(io::Error::last_os_error())
214 } else {
215 Ok(())
216 }
217 }
218
219 /// Create an anonymous pipe returning `(read_handle, write_handle)`.
220 ///
221 /// Uses `CreatePipe` with default security attributes (non-inheritable).
222 /// The returned handles are OS `HANDLE` values suitable for read/write.
223 ///
224 /// # Note
225 /// Anonymous pipes on Windows are not waitable via `WSAPoll` — they are
226 /// primarily used for the executor self-pipe wakeup mechanism where the
227 /// write side is signalled and the read side is drained. For reactor
228 /// readiness polling, prefer socket pairs or named pipes.
229 pub fn create_pipe() -> io::Result<(RawSource, RawSource)> {
230 let mut read_handle: RawSource = 0;
231 let mut write_handle: RawSource = 0;
232 // SAFETY: Both handle pointers are valid stack variables. `CreatePipe`
233 // writes valid HANDLE values into them on success (return value != 0).
234 // NULL security attributes uses the default descriptor; pipe size 0
235 // uses the system default buffer size.
236 let ok = unsafe {
237 windows_sys::Win32::System::Pipes::CreatePipe(
238 &mut read_handle,
239 &mut write_handle,
240 std::ptr::null(), // default security attributes (non-inheritable)
241 0, // default system buffer size
242 )
243 };
244 if ok == 0 {
245 Err(io::Error::last_os_error())
246 } else {
247 Ok((read_handle, write_handle))
248 }
249 }
250}
251
252#[cfg(windows)]
253pub use windows_impl::{close_fd, create_pipe, set_nonblocking};
254
255// ── Tests ─────────────────────────────────────────────────────────────────────
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn interest_readable_bit() {
263 assert!(Interest::READABLE.is_readable());
264 assert!(!Interest::READABLE.is_writable());
265 }
266
267 #[test]
268 fn interest_writable_bit() {
269 assert!(Interest::WRITABLE.is_writable());
270 assert!(!Interest::WRITABLE.is_readable());
271 }
272
273 #[test]
274 fn interest_bitor() {
275 let both = Interest::READABLE | Interest::WRITABLE;
276 assert!(both.is_readable());
277 assert!(both.is_writable());
278 }
279
280 #[test]
281 fn event_fields() {
282 let e = Event::new(42, true, false);
283 assert_eq!(e.token, 42);
284 assert!(e.readable);
285 assert!(!e.writable);
286 }
287
288 #[test]
289 fn events_capacity() {
290 let ev = events_with_capacity(64);
291 assert_eq!(ev.len(), 0);
292 assert!(ev.capacity() >= 64);
293 }
294
295 #[cfg(unix)]
296 #[test]
297 fn create_pipe_returns_valid_fds() {
298 let (r, w) = create_pipe().expect("pipe creation failed");
299 // Write one byte and read it back to prove the fds are connected.
300 let byte: u8 = 0xAB;
301 // SAFETY: `w` is a valid write-end fd; `&byte` is a valid 1-byte buffer.
302 let written = unsafe { libc::write(w, &byte as *const u8 as *const _, 1) };
303 assert_eq!(written, 1);
304 let mut buf: u8 = 0;
305 // SAFETY: `r` is the corresponding read-end fd; `&mut buf` is valid.
306 let read = unsafe { libc::read(r, &mut buf as *mut u8 as *mut _, 1) };
307 assert_eq!(read, 1);
308 assert_eq!(buf, 0xAB);
309 close_fd(r).unwrap();
310 close_fd(w).unwrap();
311 }
312}