Skip to main content

dcp/reactor/
mod.rs

1//! Platform-native reactor abstraction for async I/O.
2//!
3//! Provides a unified API for platform-specific I/O mechanisms:
4//! - Linux: io_uring (preferred) or epoll (fallback)
5//! - macOS: kqueue
6//! - Windows: IOCP
7//!
8//! The reactor trait abstracts over these implementations to provide
9//! a consistent interface for high-performance async I/O operations.
10
11use std::io;
12use std::time::Duration;
13
14mod epoll;
15mod iocp;
16mod iouring;
17mod kqueue;
18
19pub use epoll::EpollReactor;
20pub use iocp::IocpReactor;
21pub use iouring::IoUringReactor;
22pub use kqueue::KqueueReactor;
23
24/// Token for identifying registered file descriptors
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub struct Token(pub usize);
27
28/// Interest flags for I/O events
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct Interest {
31    /// Interested in read events
32    pub readable: bool,
33    /// Interested in write events
34    pub writable: bool,
35}
36
37impl Interest {
38    /// Interest in readable events only
39    pub const READABLE: Interest = Interest {
40        readable: true,
41        writable: false,
42    };
43
44    /// Interest in writable events only
45    pub const WRITABLE: Interest = Interest {
46        readable: false,
47        writable: true,
48    };
49
50    /// Interest in both readable and writable events
51    pub const BOTH: Interest = Interest {
52        readable: true,
53        writable: true,
54    };
55}
56
57/// I/O event returned by poll
58#[derive(Debug, Clone)]
59pub struct Event {
60    /// Token identifying the source
61    pub token: Token,
62    /// Whether the source is readable
63    pub readable: bool,
64    /// Whether the source is writable
65    pub writable: bool,
66    /// Whether an error occurred
67    pub error: bool,
68    /// Whether the connection was closed
69    pub closed: bool,
70}
71
72impl Event {
73    /// Create a new event
74    pub fn new(token: Token) -> Self {
75        Self {
76            token,
77            readable: false,
78            writable: false,
79            error: false,
80            closed: false,
81        }
82    }
83
84    /// Set readable flag
85    pub fn with_readable(mut self) -> Self {
86        self.readable = true;
87        self
88    }
89
90    /// Set writable flag
91    pub fn with_writable(mut self) -> Self {
92        self.writable = true;
93        self
94    }
95
96    /// Set error flag
97    pub fn with_error(mut self) -> Self {
98        self.error = true;
99        self
100    }
101
102    /// Set closed flag
103    pub fn with_closed(mut self) -> Self {
104        self.closed = true;
105        self
106    }
107}
108
109/// Completion result for async operations
110#[derive(Debug)]
111pub struct Completion {
112    /// Token identifying the operation
113    pub token: Token,
114    /// Result of the operation (bytes transferred or error)
115    pub result: io::Result<usize>,
116}
117
118/// Platform-agnostic reactor trait
119///
120/// Provides a unified API for async I/O across different platforms.
121/// Implementations use platform-specific mechanisms for optimal performance.
122pub trait Reactor: Send + Sync {
123    /// Poll for I/O events
124    ///
125    /// Blocks until events are available or timeout expires.
126    /// Returns a list of events that occurred.
127    fn poll(&mut self, timeout: Option<Duration>) -> io::Result<Vec<Event>>;
128
129    /// Register interest in a file descriptor
130    ///
131    /// Returns a token that identifies this registration.
132    fn register(&mut self, fd: RawFd, interest: Interest) -> io::Result<Token>;
133
134    /// Modify interest for a registered file descriptor
135    fn modify(&mut self, token: Token, interest: Interest) -> io::Result<()>;
136
137    /// Deregister a file descriptor
138    fn deregister(&mut self, token: Token) -> io::Result<()>;
139
140    /// Submit an async read operation (for io_uring/IOCP)
141    ///
142    /// For poll-based reactors, this is a no-op and reads should be
143    /// performed after receiving a readable event.
144    fn submit_read(&mut self, _token: Token, _buf: &mut [u8]) -> io::Result<Option<Completion>> {
145        Ok(None)
146    }
147
148    /// Submit an async write operation (for io_uring/IOCP)
149    ///
150    /// For poll-based reactors, this is a no-op and writes should be
151    /// performed after receiving a writable event.
152    fn submit_write(&mut self, _token: Token, _buf: &[u8]) -> io::Result<Option<Completion>> {
153        Ok(None)
154    }
155
156    /// Check if this reactor supports true async I/O (io_uring/IOCP)
157    ///
158    /// If true, submit_read/submit_write can be used for zero-copy I/O.
159    /// If false, use poll() and perform I/O after receiving events.
160    fn supports_async_io(&self) -> bool {
161        false
162    }
163
164    /// Get the reactor type name for debugging
165    fn name(&self) -> &'static str;
166}
167
168/// Raw file descriptor type (platform-specific)
169#[cfg(unix)]
170pub type RawFd = std::os::unix::io::RawFd;
171
172#[cfg(windows)]
173pub type RawFd = std::os::windows::io::RawSocket;
174
175/// Reactor configuration
176#[derive(Debug, Clone)]
177pub struct ReactorConfig {
178    /// Maximum number of events to return per poll
179    pub max_events: usize,
180    /// Whether to prefer io_uring on Linux (if available)
181    pub prefer_io_uring: bool,
182}
183
184impl Default for ReactorConfig {
185    fn default() -> Self {
186        Self {
187            max_events: 1024,
188            prefer_io_uring: true,
189        }
190    }
191}
192
193/// Create a platform-appropriate reactor
194///
195/// On Linux: tries io_uring first, falls back to epoll
196/// On macOS: uses kqueue
197/// On Windows: uses IOCP
198pub fn create_reactor(config: ReactorConfig) -> io::Result<Box<dyn Reactor>> {
199    #[cfg(target_os = "linux")]
200    {
201        if config.prefer_io_uring {
202            match IoUringReactor::new(config.max_events) {
203                Ok(reactor) => return Ok(Box::new(reactor)),
204                Err(_) => {
205                    // Fall back to epoll
206                }
207            }
208        }
209        Ok(Box::new(EpollReactor::new(config.max_events)?))
210    }
211
212    #[cfg(target_os = "macos")]
213    {
214        Ok(Box::new(KqueueReactor::new(config.max_events)?))
215    }
216
217    #[cfg(target_os = "windows")]
218    {
219        Ok(Box::new(IocpReactor::new(config.max_events)?))
220    }
221
222    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
223    {
224        Err(io::Error::new(
225            io::ErrorKind::Unsupported,
226            "No reactor implementation for this platform",
227        ))
228    }
229}
230
231/// Create a reactor with default configuration
232pub fn create_default_reactor() -> io::Result<Box<dyn Reactor>> {
233    create_reactor(ReactorConfig::default())
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn test_interest_constants() {
242        assert!(Interest::READABLE.readable);
243        assert!(!Interest::READABLE.writable);
244
245        assert!(!Interest::WRITABLE.readable);
246        assert!(Interest::WRITABLE.writable);
247
248        assert!(Interest::BOTH.readable);
249        assert!(Interest::BOTH.writable);
250    }
251
252    #[test]
253    fn test_event_builder() {
254        let event = Event::new(Token(42)).with_readable().with_writable();
255
256        assert_eq!(event.token, Token(42));
257        assert!(event.readable);
258        assert!(event.writable);
259        assert!(!event.error);
260        assert!(!event.closed);
261    }
262
263    #[test]
264    fn test_token_equality() {
265        assert_eq!(Token(1), Token(1));
266        assert_ne!(Token(1), Token(2));
267    }
268
269    #[test]
270    fn test_reactor_config_default() {
271        let config = ReactorConfig::default();
272        assert_eq!(config.max_events, 1024);
273        assert!(config.prefer_io_uring);
274    }
275
276    #[test]
277    fn test_create_default_reactor() {
278        // This should succeed on all platforms
279        let result = create_default_reactor();
280        // On Windows, we get IOCP stub; on Linux, epoll; on macOS, kqueue
281        // All should succeed
282        assert!(result.is_ok());
283
284        let reactor = result.unwrap();
285        // Verify we got a valid reactor
286        let name = reactor.name();
287        assert!(!name.is_empty());
288    }
289
290    #[test]
291    fn test_create_reactor_with_config() {
292        let config = ReactorConfig {
293            max_events: 512,
294            prefer_io_uring: false, // Force epoll on Linux
295        };
296
297        let result = create_reactor(config);
298        assert!(result.is_ok());
299    }
300}