Skip to main content

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}