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 stubs ─────────────────────────────────────────────────────────────
173
174#[cfg(windows)]
175mod windows_impl {
176    use super::*;
177
178    /// Set a handle to non-blocking mode (stub — requires WSA or IOCP).
179    pub fn set_nonblocking(_handle: RawSource) -> io::Result<()> {
180        // TODO: implement via ioctlsocket / SetNamedPipeHandleState
181        Err(io::Error::new(
182            io::ErrorKind::Unsupported,
183            "not yet implemented on Windows",
184        ))
185    }
186
187    /// Close an OS handle.
188    pub fn close_fd(handle: RawSource) -> io::Result<()> {
189        // SAFETY: `handle` is a valid HANDLE. CloseHandle is the documented
190        // way to release kernel resources associated with any HANDLE type.
191        let ok = unsafe { windows_sys::Win32::Foundation::CloseHandle(handle) };
192        if ok == 0 {
193            Err(io::Error::last_os_error())
194        } else {
195            Ok(())
196        }
197    }
198
199    /// Create a pipe pair returning (read_handle, write_handle).
200    pub fn create_pipe() -> io::Result<(RawSource, RawSource)> {
201        // TODO: implement via CreatePipe / anonymous pipe
202        Err(io::Error::new(
203            io::ErrorKind::Unsupported,
204            "not yet implemented on Windows",
205        ))
206    }
207}
208
209#[cfg(windows)]
210pub use windows_impl::{close_fd, create_pipe, set_nonblocking};
211
212// ── Tests ─────────────────────────────────────────────────────────────────────
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn interest_readable_bit() {
220        assert!(Interest::READABLE.is_readable());
221        assert!(!Interest::READABLE.is_writable());
222    }
223
224    #[test]
225    fn interest_writable_bit() {
226        assert!(Interest::WRITABLE.is_writable());
227        assert!(!Interest::WRITABLE.is_readable());
228    }
229
230    #[test]
231    fn interest_bitor() {
232        let both = Interest::READABLE | Interest::WRITABLE;
233        assert!(both.is_readable());
234        assert!(both.is_writable());
235    }
236
237    #[test]
238    fn event_fields() {
239        let e = Event::new(42, true, false);
240        assert_eq!(e.token, 42);
241        assert!(e.readable);
242        assert!(!e.writable);
243    }
244
245    #[test]
246    fn events_capacity() {
247        let ev = events_with_capacity(64);
248        assert_eq!(ev.len(), 0);
249        assert!(ev.capacity() >= 64);
250    }
251
252    #[cfg(unix)]
253    #[test]
254    fn create_pipe_returns_valid_fds() {
255        let (r, w) = create_pipe().expect("pipe creation failed");
256        // Write one byte and read it back to prove the fds are connected.
257        let byte: u8 = 0xAB;
258        // SAFETY: `w` is a valid write-end fd; `&byte` is a valid 1-byte buffer.
259        let written = unsafe { libc::write(w, &byte as *const u8 as *const _, 1) };
260        assert_eq!(written, 1);
261        let mut buf: u8 = 0;
262        // SAFETY: `r` is the corresponding read-end fd; `&mut buf` is valid.
263        let read = unsafe { libc::read(r, &mut buf as *mut u8 as *mut _, 1) };
264        assert_eq!(read, 1);
265        assert_eq!(buf, 0xAB);
266        close_fd(r).unwrap();
267        close_fd(w).unwrap();
268    }
269}