Skip to main content

coreshift_core/reactor/
mod.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/
4
5//! Asynchronous event reactor.
6//!
7//! This module provides a lightweight wrapper around Linux `epoll` for
8//! multiplexing I/O events. It is optimized for edge-triggered monitoring.
9//! It is intentionally explicit about Linux readiness semantics rather than
10//! hiding them behind a higher-level async runtime abstraction.
11
12use crate::CoreError;
13use crate::error::syscall_ret;
14use std::io::Error as IoError;
15use std::time::Duration;
16
17#[inline(always)]
18fn errno() -> i32 {
19    IoError::last_os_error().raw_os_error().unwrap_or(0)
20}
21
22/// An owned file descriptor that closes on drop.
23///
24/// `Fd` is move-only. Constructing one from a raw descriptor transfers close
25/// ownership to `Fd`; do not also close the raw descriptor elsewhere.
26///
27/// ### Fork Safety
28/// `Fd` instances created by Core usually have `O_CLOEXEC` set. If the process
29/// forks, the descriptor will be inherited by the child but will be closed
30/// automatically upon `exec`. Callers that need a descriptor to survive `exec`
31/// must clear the flag manually.
32pub struct Fd(RawFd);
33
34use std::os::unix::io::{AsRawFd, RawFd};
35
36impl AsRawFd for Fd {
37    fn as_raw_fd(&self) -> RawFd {
38        self.0
39    }
40}
41
42impl Fd {
43    /// Wrap a raw file descriptor.
44    ///
45    /// # Errors
46    /// Returns a [`CoreError`] if the descriptor is negative.
47    #[inline(always)]
48    pub(crate) fn new(fd: RawFd, op: &'static str) -> Result<Self, CoreError> {
49        if fd < 0 {
50            Err(CoreError::sys(errno(), op))
51        } else {
52            Ok(Self(fd))
53        }
54    }
55
56    /// Wrap an owned raw file descriptor.
57    ///
58    /// # Safety
59    /// The caller must guarantee `fd` is valid, open, and uniquely owned by the
60    /// returned `Fd`. Passing a borrowed fd, or closing `fd` after this call,
61    /// can cause double-close or use-after-close bugs.
62    #[inline(always)]
63    pub unsafe fn from_owned_raw_fd(fd: RawFd, op: &'static str) -> Result<Self, CoreError> {
64        Self::new(fd, op)
65    }
66
67    /// Create a non-blocking `eventfd` with `EFD_CLOEXEC`.
68    ///
69    /// The descriptor is created with `FD_CLOEXEC` set.
70    ///
71    /// ### Errors
72    /// - `EINVAL`: `init` is invalid.
73    /// - `EMFILE`: Process limit on open file descriptors hit.
74    /// - `ENFILE`: System-wide limit on open files hit.
75    pub fn eventfd(init: u32) -> Result<Self, CoreError> {
76        let fd = unsafe { libc::eventfd(init, libc::EFD_CLOEXEC | libc::EFD_NONBLOCK) };
77        syscall_ret(fd, "eventfd")?;
78        Self::new(fd, "eventfd")
79    }
80
81    /// Create a non-blocking `timerfd` using `CLOCK_MONOTONIC` with `TFD_CLOEXEC`.
82    ///
83    /// The descriptor is created with `FD_CLOEXEC` set.
84    ///
85    /// ### Errors
86    /// - `EMFILE`: Process limit on open file descriptors hit.
87    /// - `ENFILE`: System-wide limit on open files hit.
88    /// - `ENOMEM`: Insufficient kernel memory.
89    pub fn timerfd() -> Result<Self, CoreError> {
90        let fd = unsafe {
91            libc::timerfd_create(
92                libc::CLOCK_MONOTONIC,
93                libc::TFD_CLOEXEC | libc::TFD_NONBLOCK,
94            )
95        };
96        syscall_ret(fd, "timerfd_create")?;
97        Self::new(fd, "timerfd_create")
98    }
99
100    /// Access the underlying raw file descriptor.
101    ///
102    /// NOTE: This is an escape hatch for low-level interactions. Prefer using
103    /// the safe methods on `Fd` or implementing `AsRawFd`.
104    #[inline(always)]
105    pub(crate) fn raw(&self) -> RawFd {
106        self.0
107    }
108
109    /// Duplicate this descriptor, returning a new owned `Fd`.
110    ///
111    /// Both descriptors share the same open file description, so a `dup` of
112    /// an eventfd remains a single signalable object: a write on either copy
113    /// is observed on the other. Useful for fan-out wakeups (hub keeps one
114    /// copy, a worker reactor owns another).
115    ///
116    /// ### Errors
117    /// - `EBADF`: The source descriptor is invalid.
118    /// - `EMFILE`: The process file descriptor limit is reached.
119    pub fn dup(&self) -> Result<Self, CoreError> {
120        let r = loop {
121            let d = unsafe { libc::dup(self.0) };
122            if d < 0 && errno() == libc::EINTR {
123                continue;
124            }
125            break d;
126        };
127        if r < 0 {
128            let e = errno();
129            Err(CoreError::sys(e, "dup"))
130        } else {
131            // SAFETY: `r` is a freshly duplicated owned descriptor.
132            unsafe { Self::from_owned_raw_fd(r, "dup") }
133        }
134    }
135
136    /// Perform a `dup2` syscall.
137    ///
138    /// ### Errors
139    /// - `EBADF`: The source or target file descriptor is invalid.
140    /// - `EMFILE`: The target descriptor exceeds the process limit.
141    pub fn dup2(&self, target: RawFd) -> Result<(), CoreError> {
142        loop {
143            let r = unsafe { libc::dup2(self.0, target) };
144            if r < 0 {
145                let e = errno();
146                if e == libc::EINTR {
147                    continue;
148                }
149                return syscall_ret(r, "dup2");
150            }
151            return Ok(());
152        }
153    }
154
155    /// Set the `O_NONBLOCK` flag on the descriptor.
156    ///
157    /// ### Errors
158    /// - `EBADF`: The file descriptor is invalid.
159    pub fn set_nonblock(&self) -> Result<(), CoreError> {
160        let flags = unsafe { libc::fcntl(self.0, libc::F_GETFL) };
161        syscall_ret(flags, "fcntl(F_GETFL)")?;
162        let r = unsafe { libc::fcntl(self.0, libc::F_SETFL, flags | libc::O_NONBLOCK) };
163        syscall_ret(r, "fcntl(F_SETFL)")
164    }
165
166    /// Set the `FD_CLOEXEC` flag on the descriptor.
167    ///
168    /// ### Errors
169    /// - `EBADF`: The file descriptor is invalid.
170    pub fn set_cloexec(&self) -> Result<(), CoreError> {
171        let flags = unsafe { libc::fcntl(self.0, libc::F_GETFD) };
172        syscall_ret(flags, "fcntl(F_GETFD)")?;
173        let r = unsafe { libc::fcntl(self.0, libc::F_SETFD, flags | libc::FD_CLOEXEC) };
174        syscall_ret(r, "fcntl(F_SETFD)")
175    }
176
177    /// Read bytes into a mutable slice.
178    ///
179    /// Returns `Ok(None)` if the operation would block (`EAGAIN`).
180    ///
181    /// ### Edge Cases
182    /// - **Zero-length read**: Returns `Ok(Some(0))` immediately.
183    /// - **Partial read**: Returns the number of bytes actually read.
184    ///
185    /// ### Errors
186    /// - `EBADF`: The file descriptor is invalid or not open for reading.
187    /// - `EFAULT`: `buf` points outside the process's address space.
188    /// - `EIO`: Low-level I/O error.
189    pub fn read_slice(&self, buf: &mut [u8]) -> Result<Option<usize>, CoreError> {
190        self.read_raw(buf.as_mut_ptr(), buf.len())
191    }
192
193    /// Seek to an absolute file offset.
194    ///
195    /// ### Errors
196    /// - `EBADF`: The file descriptor is not seekable.
197    /// - `EINVAL`: `offset` is invalid.
198    /// - `EOVERFLOW`: The resulting offset exceeds the off_t range.
199    pub fn seek_set(&self, offset: i64) -> Result<u64, CoreError> {
200        loop {
201            let pos = unsafe { libc::lseek(self.0, offset as libc::off_t, libc::SEEK_SET) };
202            if pos < 0 {
203                let e = errno();
204                if e == libc::EINTR {
205                    continue;
206                }
207                return Err(CoreError::sys(e, "lseek"));
208            }
209            return Ok(pos as u64);
210        }
211    }
212
213    /// Write bytes from a slice.
214    ///
215    /// Returns `Ok(None)` if the operation would block (`EAGAIN`).
216    ///
217    /// ### Edge Cases
218    /// - **Zero-length write**: Returns `Ok(Some(0))` immediately.
219    /// - **Partial write**: Returns the number of bytes actually written.
220    ///
221    /// ### Errors
222    /// - `EBADF`: The file descriptor is invalid or not open for writing.
223    /// - `EFAULT`: `buf` points outside the process's address space.
224    /// - `EPIPE`: The reading end of a pipe or socket was closed.
225    pub fn write_slice(&self, buf: &[u8]) -> Result<Option<usize>, CoreError> {
226        self.write_raw(buf.as_ptr(), buf.len())
227    }
228
229    /// Read a native-endian `u64`, blocking until data is available.
230    ///
231    /// Unlike `read_u64`, this never returns `Ok(None)` — it retries on `EINTR`
232    /// and returns `Err` only on a hard I/O failure. Intended for blocking
233    /// eventfds used as inter-thread notification primitives.
234    pub fn read_u64_blocking(&self) -> Result<u64, CoreError> {
235        let mut bytes = [0u8; std::mem::size_of::<u64>()];
236        loop {
237            let n =
238                unsafe { libc::read(self.0, bytes.as_mut_ptr() as *mut libc::c_void, bytes.len()) };
239            if n == bytes.len() as isize {
240                return Ok(u64::from_ne_bytes(bytes));
241            }
242            if n < 0 {
243                let e = errno();
244                if e == libc::EINTR {
245                    continue;
246                }
247                return Err(CoreError::sys(e, "read_u64_blocking"));
248            }
249            return Err(CoreError::sys(libc::EIO, "read_u64_blocking:short_read"));
250        }
251    }
252
253    /// Read a native-endian `u64`.
254    ///
255    /// Returns `Ok(None)` if the operation would block (`EAGAIN`).
256    pub fn read_u64(&self) -> Result<Option<u64>, CoreError> {
257        let mut bytes = [0u8; std::mem::size_of::<u64>()];
258        match self.read_slice(&mut bytes)? {
259            Some(n) if n == bytes.len() => Ok(Some(u64::from_ne_bytes(bytes))),
260            Some(_) => Err(CoreError::sys(libc::EIO, "read_u64")),
261            None => Ok(None),
262        }
263    }
264
265    /// Write a native-endian `u64`.
266    ///
267    /// Returns `Ok(None)` if the operation would block (`EAGAIN`).
268    pub fn write_u64(&self, value: u64) -> Result<Option<usize>, CoreError> {
269        self.write_slice(&value.to_ne_bytes())
270    }
271
272    /// Arm or disarm a one-shot `timerfd`.
273    ///
274    /// Passing `None` disarms the timer. Zero durations are rounded up to one
275    /// nanosecond so the timer still expires.
276    ///
277    /// ### Errors
278    /// - `EBADF`: The file descriptor is invalid.
279    /// - `EINVAL`: The duration is invalid or not supported by the kernel.
280    pub fn set_timer_oneshot(&self, delay: Option<Duration>) -> Result<(), CoreError> {
281        let mut spec: libc::itimerspec = unsafe { std::mem::zeroed() };
282        if let Some(delay) = delay {
283            let delay = delay.max(Duration::from_nanos(1));
284            spec.it_value.tv_sec = delay.as_secs() as libc::time_t;
285            spec.it_value.tv_nsec = delay.subsec_nanos() as libc::c_long;
286        }
287
288        let ret = unsafe { libc::timerfd_settime(self.raw(), 0, &spec, std::ptr::null_mut()) };
289        syscall_ret(ret, "timerfd_settime")
290    }
291
292    /// Read bytes into a raw buffer.
293    ///
294    /// Internal callers must ensure `buf` points to a valid writable region of
295    /// at least `count` bytes.
296    ///
297    /// Returns `Ok(None)` if the operation would block (`EAGAIN`).
298    pub(crate) fn read_raw(&self, buf: *mut u8, count: usize) -> Result<Option<usize>, CoreError> {
299        loop {
300            let n = unsafe { libc::read(self.0, buf as *mut libc::c_void, count) };
301            if n < 0 {
302                let e = errno();
303                if e == libc::EINTR {
304                    continue;
305                }
306                if e == libc::EAGAIN || e == libc::EWOULDBLOCK {
307                    return Ok(None);
308                }
309                return Err(CoreError::sys(e, "read"));
310            }
311            return Ok(Some(n as usize));
312        }
313    }
314
315    /// Write bytes from a raw buffer.
316    ///
317    /// Internal callers must ensure `buf` points to a valid readable region of
318    /// at least `count` bytes.
319    ///
320    /// Returns `Ok(None)` if the operation would block (`EAGAIN`).
321    pub(crate) fn write_raw(
322        &self,
323        buf: *const u8,
324        count: usize,
325    ) -> Result<Option<usize>, CoreError> {
326        loop {
327            let n = unsafe { libc::write(self.0, buf as *const libc::c_void, count) };
328            if n < 0 {
329                let e = errno();
330                if e == libc::EINTR {
331                    continue;
332                }
333                if e == libc::EAGAIN || e == libc::EWOULDBLOCK {
334                    return Ok(None);
335                }
336                return Err(CoreError::sys(e, "write"));
337            }
338            return Ok(Some(n as usize));
339        }
340    }
341}
342
343impl Drop for Fd {
344    fn drop(&mut self) {
345        if self.0 >= 0 {
346            unsafe {
347                libc::close(self.0);
348            }
349        }
350    }
351}
352
353/// An opaque token representing a registered file descriptor.
354#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
355pub struct Token(u64);
356
357#[allow(dead_code)]
358impl Token {
359    #[inline(always)]
360    pub(crate) fn new(val: u64) -> Self {
361        Self(val)
362    }
363
364    #[inline(always)]
365    pub(crate) fn val(&self) -> u64 {
366        self.0
367    }
368}
369
370/// A readiness event generated by the reactor.
371#[derive(Clone, Copy, Debug)]
372pub struct Event {
373    /// Token associated with the ready descriptor.
374    pub token: Token,
375    /// Descriptor is ready for reading (`EPOLLIN`).
376    pub readable: bool,
377    /// Descriptor has priority data or an exceptional condition (`EPOLLPRI`).
378    pub priority: bool,
379    /// Descriptor is ready for writing (`EPOLLOUT`).
380    pub writable: bool,
381    /// Indicates an error condition (`EPOLLERR`).
382    ///
383    /// NOTE: For edge-triggered readiness, an error condition often means both
384    /// readable and writable are set to ensure the handler drains the FD.
385    pub error: bool,
386    /// Indicates a remote hangup (`EPOLLHUP`).
387    pub hangup: bool,
388}
389
390const _: () = assert!(std::mem::size_of::<Event>() == 16);
391const _: () = assert!(std::mem::align_of::<Event>() == 8);
392
393/// A lightweight epoll reactor using edge-triggered monitoring (EPOLLET).
394///
395/// ### Edge-Triggered Contract
396/// Because this reactor uses EPOLLET, all handlers MUST drain their respective
397/// read or write sources until they receive an `EAGAIN` / `EWOULDBLOCK` error
398/// (represented as `Ok(None)` in the `Fd` helpers).
399///
400/// Failure to drain a source will result in missing future readiness events
401/// for that file descriptor until it is re-registered or another event occurs.
402///
403/// ### Fork Safety
404/// The `Reactor` owns an `epoll` descriptor which is `O_CLOEXEC`. After an
405/// `exec` call in a child process, the reactor and all its registrations are
406/// lost. If the child continues without `exec`, it shares the same epoll
407/// instance, which is generally unsafe and requires careful coordination.
408///
409/// # Example
410/// ```no_run
411/// # use coreshift_core::reactor::{Reactor, Fd, Event};
412/// # fn example(fd: Fd) -> Result<(), Box<dyn std::error::Error>> {
413/// let mut reactor = Reactor::new()?;
414/// let token = reactor.add(&fd, true, false)?;
415///
416/// let mut events = Vec::new();
417/// loop {
418///     reactor.wait(&mut events, 64, -1)?;
419///     for ev in &events {
420///         if ev.token == token {
421///             // Drain fd...
422///         }
423///     }
424/// }
425/// # Ok(())
426/// # }
427/// ```
428pub struct Reactor {
429    epfd: RawFd,
430    next_token: u64,
431    events_buf: Vec<libc::epoll_event>,
432    signalfd: Option<Fd>,
433    signalfd_previous_mask: Option<libc::sigset_t>,
434    /// Token for the signalfd (if initialized).
435    sigchld_token: Option<Token>,
436    /// Token for the inotify fd (if initialized).
437    inotify_token: Option<Token>,
438}
439
440impl Reactor {
441    /// Create a new epoll reactor.
442    ///
443    /// ### Errors
444    /// - `EMFILE`: Process limit on open file descriptors hit.
445    /// - `ENFILE`: System-wide limit on open files hit.
446    /// - `ENOMEM`: Insufficient kernel memory.
447    pub fn new() -> Result<Self, CoreError> {
448        let epfd = unsafe { libc::epoll_create1(libc::EPOLL_CLOEXEC) };
449        syscall_ret(epfd, "epoll_create1")?;
450        Ok(Self {
451            epfd,
452            next_token: 1,
453            events_buf: Vec::with_capacity(64),
454            signalfd: None,
455            signalfd_previous_mask: None,
456            sigchld_token: None,
457            inotify_token: None,
458        })
459    }
460
461    /// Initialize inotify and add it to the reactor.
462    ///
463    /// ### Errors
464    /// - `EMFILE`: Process limit on open file descriptors hit.
465    /// - `ENFILE`: System-wide limit on open files hit.
466    /// - `ENOMEM`: Insufficient kernel memory.
467    /// - `EPERM`: Permission denied to create inotify instance.
468    pub fn setup_inotify(&mut self) -> Result<(Fd, Token), CoreError> {
469        let fd = unsafe { libc::inotify_init1(libc::IN_CLOEXEC | libc::IN_NONBLOCK) };
470        syscall_ret(fd, "inotify_init1")?;
471
472        let fd_obj = Fd::new(fd, "inotify")?;
473        let token = self.add(&fd_obj, true, false)?;
474        self.inotify_token = Some(token);
475
476        Ok((fd_obj, token))
477    }
478
479    /// Initialize signalfd for SIGCHLD and add it to the reactor.
480    ///
481    /// The previous current-thread signal mask is restored when the reactor is
482    /// dropped.
483    ///
484    /// ### Errors
485    /// - `EBADF`: The provided file descriptor is invalid.
486    /// - `EINVAL`: Signal mask is invalid or already set up.
487    /// - `EMFILE`: Process limit on open file descriptors hit.
488    pub fn setup_signalfd(&mut self) -> Result<Token, CoreError> {
489        if self.signalfd.is_some() {
490            return Err(CoreError::sys(
491                libc::EINVAL,
492                "setup_signalfd already initialized",
493            ));
494        }
495
496        let mut mask: libc::sigset_t = unsafe { std::mem::zeroed() };
497        unsafe { libc::sigemptyset(&mut mask) };
498        unsafe { libc::sigaddset(&mut mask, libc::SIGCHLD) };
499
500        let mut previous_mask: libc::sigset_t = unsafe { std::mem::zeroed() };
501        let r = unsafe { libc::pthread_sigmask(libc::SIG_BLOCK, &mask, &mut previous_mask) };
502        if r != 0 {
503            return Err(CoreError::sys(r, "pthread_sigmask(SIG_BLOCK)"));
504        }
505
506        let sfd = unsafe { libc::signalfd(-1, &mask, libc::SFD_NONBLOCK | libc::SFD_CLOEXEC) };
507        if let Err(err) = syscall_ret(sfd, "signalfd") {
508            let _ = unsafe {
509                libc::pthread_sigmask(libc::SIG_SETMASK, &previous_mask, std::ptr::null_mut())
510            };
511            return Err(err);
512        }
513
514        let fd = Fd::new(sfd, "signalfd")?;
515        let token = match self.add(&fd, true, false) {
516            Ok(token) => token,
517            Err(err) => {
518                let _ = unsafe {
519                    libc::pthread_sigmask(libc::SIG_SETMASK, &previous_mask, std::ptr::null_mut())
520                };
521                return Err(err);
522            }
523        };
524
525        self.signalfd = Some(fd);
526        self.signalfd_previous_mask = Some(previous_mask);
527        self.sigchld_token = Some(token);
528
529        Ok(token)
530    }
531
532    /// Drain the internal signalfd buffer.
533    pub fn drain_signalfd(&self) -> Result<(), CoreError> {
534        if let Some(fd) = &self.signalfd {
535            let mut buf = [0u8; std::mem::size_of::<libc::signalfd_siginfo>()];
536            loop {
537                match fd.read_slice(&mut buf) {
538                    Ok(Some(n)) if n < buf.len() => break,
539                    Ok(Some(_)) => continue,
540                    Ok(None) => break,
541                    Err(e) => return Err(e),
542                }
543            }
544        }
545        Ok(())
546    }
547
548    /// Register a file descriptor with the reactor.
549    ///
550    /// This assigns a new unique token for the descriptor and enables
551    /// edge-triggered monitoring.
552    #[inline(always)]
553    pub fn add(&mut self, fd: &Fd, readable: bool, writable: bool) -> Result<Token, CoreError> {
554        let token = Token(self.next_token);
555        self.next_token += 1;
556        self.add_with_token(fd.raw(), token, readable, writable, false)?;
557        Ok(token)
558    }
559
560    /// Register a file descriptor for priority readiness (EPOLLPRI).
561    #[inline(always)]
562    pub fn add_priority(&mut self, fd: &Fd) -> Result<Token, CoreError> {
563        let token = Token(self.next_token);
564        self.next_token += 1;
565        self.add_with_token(fd.raw(), token, false, false, true)?;
566        Ok(token)
567    }
568
569    /// Register a file descriptor with custom epoll flags.
570    ///
571    /// This allows registration with flags like `EPOLLONESHOT` or explicit
572    /// control over `EPOLLET`.
573    ///
574    /// # Example
575    /// ```no_run
576    /// # use coreshift_core::reactor::{Reactor, Fd};
577    /// let mut reactor = Reactor::new().unwrap();
578    /// let fd = Fd::eventfd(0).unwrap();
579    /// reactor.add_with_flags(&fd, (libc::EPOLLIN | libc::EPOLLONESHOT) as u32).unwrap();
580    /// ```
581    #[inline(always)]
582    pub fn add_with_flags(&mut self, fd: &Fd, flags: u32) -> Result<Token, CoreError> {
583        let token = Token(self.next_token);
584        self.next_token += 1;
585        let mut ev = libc::epoll_event {
586            events: flags,
587            u64: token.0,
588        };
589        let r = unsafe { libc::epoll_ctl(self.epfd, libc::EPOLL_CTL_ADD, fd.raw(), &mut ev) };
590        syscall_ret(r, "epoll_ctl_add")?;
591        Ok(token)
592    }
593
594    #[inline(always)]
595    pub(crate) fn add_with_token(
596        &mut self,
597        raw_fd: RawFd,
598        token: Token,
599        readable: bool,
600        writable: bool,
601        priority: bool,
602    ) -> Result<(), CoreError> {
603        let mut events = libc::EPOLLET as u32;
604        if readable {
605            events |= libc::EPOLLIN as u32;
606        }
607        if writable {
608            events |= libc::EPOLLOUT as u32;
609        }
610        if priority {
611            events |= libc::EPOLLPRI as u32;
612        }
613        let mut ev = libc::epoll_event {
614            events,
615            u64: token.0,
616        };
617        let r = unsafe { libc::epoll_ctl(self.epfd, libc::EPOLL_CTL_ADD, raw_fd, &mut ev) };
618        syscall_ret(r, "epoll_ctl_add")?;
619        Ok(())
620    }
621
622    /// Remove a file descriptor from the reactor.
623    #[inline(always)]
624    pub fn del(&self, fd: &Fd) -> Result<(), CoreError> {
625        self.del_raw(fd.raw())
626    }
627
628    /// Remove a raw descriptor from the reactor.
629    ///
630    /// NOTE: This is an escape hatch for low-level interactions. Prefer using
631    /// [`del`](Self::del).
632    #[inline(always)]
633    pub(crate) fn del_raw(&self, raw: RawFd) -> Result<(), CoreError> {
634        loop {
635            let ret = unsafe {
636                libc::epoll_ctl(self.epfd, libc::EPOLL_CTL_DEL, raw, std::ptr::null_mut())
637            };
638            if ret == -1 {
639                let e = errno();
640                if e == libc::EINTR {
641                    continue;
642                }
643                return Err(CoreError::sys(e, "epoll_ctl_del"));
644            }
645            return Ok(());
646        }
647    }
648
649    /// Wait for events.
650    ///
651    /// This function blocks until at least one event is ready or the timeout
652    /// expires. Ready events are appended to the `buffer`.
653    ///
654    /// ### Timeout Contract
655    /// - `-1`: Block indefinitely until an event occurs or a signal interrupts.
656    /// - `0`: Return immediately, even if no events are ready.
657    /// - `> 0`: Wait for up to the specified number of milliseconds.
658    ///
659    /// Returns the number of events received.
660    #[inline(always)]
661    pub fn wait(
662        &mut self,
663        buffer: &mut Vec<Event>,
664        max_events: usize,
665        timeout: i32,
666    ) -> Result<usize, CoreError> {
667        buffer.clear();
668
669        if max_events == 0 {
670            return Ok(0);
671        }
672
673        // Ensure buffer has enough capacity
674        if buffer.capacity() < max_events {
675            buffer.reserve(max_events.saturating_sub(buffer.len()));
676        }
677
678        if self.events_buf.capacity() < max_events {
679            self.events_buf
680                .reserve(max_events.saturating_sub(self.events_buf.len()));
681        }
682
683        let n = unsafe {
684            libc::epoll_wait(
685                self.epfd,
686                self.events_buf.as_mut_ptr(),
687                max_events as i32,
688                timeout,
689            )
690        };
691
692        if n > 0 {
693            unsafe {
694                self.events_buf.set_len(n as usize);
695            }
696            for i in 0..n as usize {
697                let ev = self.events_buf[i];
698                let is_read = (ev.events & libc::EPOLLIN as u32) != 0;
699                let is_priority = (ev.events & libc::EPOLLPRI as u32) != 0;
700                let is_write = (ev.events & libc::EPOLLOUT as u32) != 0;
701                let is_err = (ev.events & libc::EPOLLERR as u32) != 0;
702                let is_hup = (ev.events & libc::EPOLLHUP as u32) != 0;
703
704                buffer.push(Event {
705                    token: Token(ev.u64),
706                    readable: is_read || is_err,
707                    priority: is_priority || is_err,
708                    writable: is_write || is_err,
709                    error: is_err,
710                    hangup: is_hup,
711                });
712            }
713            return Ok(n as usize);
714        }
715
716        if n < 0 {
717            let e = errno();
718            if e == libc::EINTR {
719                return Ok(0);
720            }
721            return Err(CoreError::sys(e, "epoll_wait"));
722        }
723        Ok(0)
724    }
725
726    /// Return the raw epoll file descriptor.
727    ///
728    /// NOTE: This is an escape hatch for low-level interactions.
729    #[allow(dead_code)]
730    pub(crate) fn fd(&self) -> RawFd {
731        self.epfd
732    }
733}
734
735impl Drop for Reactor {
736    fn drop(&mut self) {
737        if let Some(mask) = self.signalfd_previous_mask.take() {
738            let _ =
739                unsafe { libc::pthread_sigmask(libc::SIG_SETMASK, &mask, std::ptr::null_mut()) };
740        }
741        if self.epfd >= 0 {
742            unsafe {
743                libc::close(self.epfd);
744            }
745        }
746    }
747}