Skip to main content

monocoque_core/
rt.rs

1//! Runtime facade: the single place that names a concrete async runtime.
2//!
3//! Monocoque's whole socket stack is generic over the I/O traits from
4//! `compio::io` (an owned-buffer, completion-style interface that maps cleanly
5//! onto `io_uring`). Those traits, and the buffer types in `compio::buf`, are the
6//! abstraction the rest of the code is written against, and they stay the same
7//! no matter which runtime drives the sockets.
8//!
9//! What actually differs between runtimes is small: how you open a connection,
10//! how you spawn a task, and how you arm a timer. This module collects exactly
11//! those pieces behind one set of names so the rest of the crate (and the ZMTP
12//! layer above it) never mentions `compio` or `tokio` directly.
13//!
14//! Pick the backend with a Cargo feature:
15//!
16//! - `runtime-compio` (default): native `io_uring` through compio.
17//! - `runtime-tokio`: tokio, with a thin stream adapter that implements the
18//!   same `compio::io` traits by reading straight into the owned buffer's
19//!   memory, so there is no extra copy on the data path.
20//!
21//! Exactly one of the two must be enabled.
22
23// The tokio adapter declares initialized buffer length after a read, which is an
24// unsafe operation on the owned-buffer contract. Kept local to this module.
25#![allow(unsafe_code)]
26
27#[cfg(all(feature = "runtime-compio", feature = "runtime-tokio"))]
28compile_error!(
29    "monocoque: enable exactly one runtime backend, not both \
30     (`runtime-compio` or `runtime-tokio`)"
31);
32
33#[cfg(not(any(feature = "runtime-compio", feature = "runtime-tokio")))]
34compile_error!(
35    "monocoque: no runtime backend selected; enable `runtime-compio` (default) \
36     or `runtime-tokio`"
37);
38
39// ─────────────────────────────────────────────────────────────────────────────
40// compio backend
41// ─────────────────────────────────────────────────────────────────────────────
42
43#[cfg(feature = "runtime-compio")]
44mod backend {
45    use std::future::Future;
46
47    pub use compio::net::{TcpListener, TcpStream, ToSocketAddrsAsync as ToSocketAddrs};
48    pub use compio::time::{sleep, timeout};
49
50    /// Owned read half of a split TCP stream.
51    pub type OwnedReadHalf = compio::net::OwnedReadHalf<TcpStream>;
52    /// Owned write half of a split TCP stream.
53    pub type OwnedWriteHalf = compio::net::OwnedWriteHalf<TcpStream>;
54
55    #[cfg(unix)]
56    pub use compio::net::{UnixListener, UnixStream};
57
58    /// Handle to a spawned task. Kept alive keeps the task running; dropping it
59    /// detaches under compio.
60    pub type JoinHandle<T> = compio::runtime::Task<T>;
61
62    /// Spawn a task on the current runtime and return its handle.
63    #[inline]
64    pub fn spawn<F>(fut: F) -> JoinHandle<F::Output>
65    where
66        F: Future + 'static,
67    {
68        compio::runtime::spawn(fut)
69    }
70
71    /// Spawn a task and let it run on its own, discarding the handle.
72    #[inline]
73    pub fn spawn_detached<F>(fut: F)
74    where
75        F: Future + 'static,
76        F::Output: 'static,
77    {
78        compio::runtime::spawn(fut).detach();
79    }
80
81    /// Run a blocking closure off the async executor and await its result.
82    #[inline]
83    pub async fn spawn_blocking<F, T>(f: F) -> T
84    where
85        F: FnOnce() -> T + Send + Sync + 'static,
86        T: Send + 'static,
87    {
88        compio::runtime::spawn_blocking(f).await
89    }
90
91    /// Await a spawned task and return its output.
92    ///
93    /// Normalizes the difference between backends: compio's task awaits directly
94    /// to the output, so this is just the await.
95    #[inline]
96    pub async fn join<T>(handle: JoinHandle<T>) -> T {
97        handle.await
98    }
99
100    /// A self-contained runtime owned by a single thread.
101    ///
102    /// Used by worker threads that drive their own event loop independently of
103    /// the caller's runtime (for example the publisher's fan-out workers).
104    pub struct LocalRuntime {
105        inner: compio::runtime::Runtime,
106    }
107
108    impl LocalRuntime {
109        /// Build a new single-threaded runtime.
110        pub fn new() -> std::io::Result<Self> {
111            Ok(Self {
112                inner: compio::runtime::Runtime::new()?,
113            })
114        }
115
116        /// Run a future to completion on this runtime, blocking the thread.
117        pub fn block_on<F: Future>(&self, fut: F) -> F::Output {
118            self.inner.block_on(fut)
119        }
120    }
121}
122
123// ─────────────────────────────────────────────────────────────────────────────
124// tokio backend
125// ─────────────────────────────────────────────────────────────────────────────
126
127// When both backends are enabled the guard above is the real error; gating the
128// tokio module out here keeps that message from being buried under a duplicate
129// `backend` definition.
130#[cfg(all(feature = "runtime-tokio", not(feature = "runtime-compio")))]
131mod backend {
132    use compio_buf::{BufResult, IoBuf, IoBufMut};
133    // `AsyncRead` is consumed only inside the macro-generated impls below, which
134    // the unused-import lint does not attribute back to this import.
135    #[allow(unused_imports)]
136    use compio_io::{AsyncRead, AsyncWrite};
137    use std::future::{Future, poll_fn};
138    use std::io;
139    use std::net::SocketAddr;
140    use std::pin::Pin;
141    // Bring tokio's write trait into scope under an alias so its poll methods are
142    // callable on concrete tokio streams without colliding with the compio
143    // `AsyncWrite` the adapters implement. Reads go through `read_into`, whose
144    // bound already carries the read methods.
145    use tokio::io::{AsyncWrite as TokioAsyncWrite, ReadBuf};
146
147    pub use tokio::net::ToSocketAddrs;
148    pub use tokio::time::{sleep, timeout};
149
150    /// Handle to a spawned task. Dropping it detaches under tokio.
151    pub type JoinHandle<T> = tokio::task::JoinHandle<T>;
152
153    /// Spawn a task on the current runtime and return its handle.
154    ///
155    /// Tasks are spawned onto the local set so they may hold `!Send` state,
156    /// matching compio's thread-per-core model. The caller must therefore run on
157    /// a current-thread runtime inside a `LocalSet` (see [`LocalRuntime`]).
158    #[inline]
159    pub fn spawn<F>(fut: F) -> JoinHandle<F::Output>
160    where
161        F: Future + 'static,
162        F::Output: 'static,
163    {
164        tokio::task::spawn_local(fut)
165    }
166
167    /// Spawn a task and let it run on its own, discarding the handle.
168    #[inline]
169    pub fn spawn_detached<F>(fut: F)
170    where
171        F: Future + 'static,
172        F::Output: 'static,
173    {
174        drop(tokio::task::spawn_local(fut));
175    }
176
177    /// Run a blocking closure off the async executor and await its result.
178    ///
179    /// Panics propagate, matching the compio backend where a panicking blocking
180    /// task aborts the await rather than yielding a value.
181    #[inline]
182    pub async fn spawn_blocking<F, T>(f: F) -> T
183    where
184        F: FnOnce() -> T + Send + Sync + 'static,
185        T: Send + 'static,
186    {
187        tokio::task::spawn_blocking(f)
188            .await
189            .expect("blocking task panicked")
190    }
191
192    /// Await a spawned task and return its output.
193    ///
194    /// Normalizes the difference between backends: tokio's `JoinHandle` awaits to
195    /// a `Result`, so this unwraps the join error (a panicked or cancelled task)
196    /// to match compio, where the panic simply propagates through the await.
197    #[inline]
198    pub async fn join<T>(handle: JoinHandle<T>) -> T {
199        handle
200            .await
201            .expect("spawned task panicked or was cancelled")
202    }
203
204    /// A self-contained runtime owned by a single thread.
205    ///
206    /// Used by worker threads that drive their own event loop independently of
207    /// the caller's runtime (for example the publisher's fan-out workers). A
208    /// current-thread tokio runtime matches the single-threaded compio one and
209    /// lets `spawn`/`spawn_detached` run within `block_on`.
210    pub struct LocalRuntime {
211        inner: tokio::runtime::Runtime,
212        local: tokio::task::LocalSet,
213    }
214
215    impl LocalRuntime {
216        /// Build a new single-threaded runtime with a local task set, so that
217        /// `spawn`/`spawn_detached` can run `!Send` tasks within `block_on`.
218        pub fn new() -> std::io::Result<Self> {
219            let inner = tokio::runtime::Builder::new_current_thread()
220                .enable_all()
221                .build()?;
222            Ok(Self {
223                inner,
224                local: tokio::task::LocalSet::new(),
225            })
226        }
227
228        /// Run a future to completion on this runtime, blocking the thread.
229        ///
230        /// Drives the future and any locally spawned tasks to completion.
231        pub fn block_on<F: Future>(&self, fut: F) -> F::Output {
232            self.local.block_on(&self.inner, fut)
233        }
234    }
235
236    /// Read the spare capacity of an owned buffer from a tokio source.
237    ///
238    /// Mirrors compio's owned-buffer read contract: bytes land in the buffer's
239    /// backing memory (no intermediate copy) and the count read is reported back
240    /// through `set_buf_init`. `ReadBuf::uninit` keeps this sound over arena
241    /// pages whose capacity is not yet initialized.
242    async fn read_into<R, B>(reader: &mut R, mut buf: B) -> BufResult<usize, B>
243    where
244        R: tokio::io::AsyncRead + Unpin,
245        B: IoBufMut,
246    {
247        let spare = buf.as_mut_slice();
248        let mut read_buf = ReadBuf::uninit(spare);
249        let result = poll_fn(|cx| Pin::new(&mut *reader).poll_read(cx, &mut read_buf)).await;
250        match result {
251            Ok(()) => {
252                let n = read_buf.filled().len();
253                // SAFETY: tokio initialized exactly `n` bytes in the buffer's
254                // backing memory via `ReadBuf`; declaring that length initialized
255                // matches what was actually written.
256                unsafe {
257                    buf.set_buf_init(n);
258                }
259                BufResult(Ok(n), buf)
260            }
261            Err(e) => BufResult(Err(e), buf),
262        }
263    }
264
265    /// Write the initialized bytes of an owned buffer to a tokio sink.
266    async fn write_from<W, B>(writer: &mut W, buf: B) -> BufResult<usize, B>
267    where
268        W: tokio::io::AsyncWrite + Unpin,
269        B: IoBuf,
270    {
271        let slice = buf.as_slice();
272        let result = poll_fn(|cx| Pin::new(&mut *writer).poll_write(cx, slice)).await;
273        match result {
274            Ok(n) => BufResult(Ok(n), buf),
275            Err(e) => BufResult(Err(e), buf),
276        }
277    }
278
279    /// Generate a compio-style stream adapter over a tokio I/O type.
280    ///
281    /// The macro wires up the `compio::io` read/write traits plus the raw-fd
282    /// accessor the TCP tuning helpers rely on, so each concrete tokio type
283    /// (full stream, split halves, Unix variants) shares one implementation.
284    macro_rules! impl_compio_io {
285        (read $ty:ty) => {
286            impl AsyncRead for $ty {
287                async fn read<B: IoBufMut>(&mut self, buf: B) -> BufResult<usize, B> {
288                    read_into(&mut self.inner, buf).await
289                }
290            }
291        };
292        (write $ty:ty) => {
293            impl AsyncWrite for $ty {
294                async fn write<B: IoBuf>(&mut self, buf: B) -> BufResult<usize, B> {
295                    write_from(&mut self.inner, buf).await
296                }
297
298                async fn flush(&mut self) -> io::Result<()> {
299                    poll_fn(|cx| Pin::new(&mut self.inner).poll_flush(cx)).await
300                }
301
302                async fn shutdown(&mut self) -> io::Result<()> {
303                    poll_fn(|cx| Pin::new(&mut self.inner).poll_shutdown(cx)).await
304                }
305            }
306        };
307        (raw_fd $ty:ty) => {
308            #[cfg(unix)]
309            impl std::os::unix::io::AsRawFd for $ty {
310                fn as_raw_fd(&self) -> std::os::unix::io::RawFd {
311                    self.inner.as_raw_fd()
312                }
313            }
314        };
315    }
316
317    // ── TCP ──────────────────────────────────────────────────────────────────
318
319    /// Tokio TCP stream wearing the `compio::io` interface.
320    #[derive(Debug)]
321    pub struct TcpStream {
322        inner: tokio::net::TcpStream,
323    }
324
325    /// Owned read half of a split [`TcpStream`].
326    #[derive(Debug)]
327    pub struct OwnedReadHalf {
328        inner: tokio::net::tcp::OwnedReadHalf,
329    }
330
331    /// Owned write half of a split [`TcpStream`].
332    #[derive(Debug)]
333    pub struct OwnedWriteHalf {
334        inner: tokio::net::tcp::OwnedWriteHalf,
335    }
336
337    impl TcpStream {
338        /// Open a TCP connection to `addr`.
339        pub async fn connect<A: ToSocketAddrs>(addr: A) -> io::Result<Self> {
340            let inner = tokio::net::TcpStream::connect(addr).await?;
341            Ok(Self { inner })
342        }
343
344        /// Local address this stream is bound to.
345        pub fn local_addr(&self) -> io::Result<SocketAddr> {
346            self.inner.local_addr()
347        }
348
349        /// Address of the remote peer.
350        pub fn peer_addr(&self) -> io::Result<SocketAddr> {
351            self.inner.peer_addr()
352        }
353
354        /// Split into owned read and write halves.
355        pub fn into_split(self) -> (OwnedReadHalf, OwnedWriteHalf) {
356            let (r, w) = self.inner.into_split();
357            (OwnedReadHalf { inner: r }, OwnedWriteHalf { inner: w })
358        }
359    }
360
361    /// Tokio TCP listener returning [`TcpStream`] adapters.
362    #[derive(Debug)]
363    pub struct TcpListener {
364        inner: tokio::net::TcpListener,
365    }
366
367    impl TcpListener {
368        /// Bind a listening socket to `addr`.
369        pub async fn bind<A: ToSocketAddrs>(addr: A) -> io::Result<Self> {
370            let inner = tokio::net::TcpListener::bind(addr).await?;
371            Ok(Self { inner })
372        }
373
374        /// Accept the next inbound connection.
375        pub async fn accept(&self) -> io::Result<(TcpStream, SocketAddr)> {
376            let (stream, addr) = self.inner.accept().await?;
377            Ok((TcpStream { inner: stream }, addr))
378        }
379
380        /// Local address this listener is bound to.
381        pub fn local_addr(&self) -> io::Result<SocketAddr> {
382            self.inner.local_addr()
383        }
384    }
385
386    impl_compio_io!(read TcpStream);
387    impl_compio_io!(write TcpStream);
388    impl_compio_io!(raw_fd TcpStream);
389    // The split halves intentionally have no raw-fd accessor: TCP tuning is
390    // applied to the whole stream before it is split, and tokio's halves do not
391    // expose the descriptor.
392    impl_compio_io!(read OwnedReadHalf);
393    impl_compio_io!(write OwnedWriteHalf);
394
395    // ── Unix domain sockets ───────────────────────────────────────────────────
396
397    #[cfg(unix)]
398    pub use unix::{UnixListener, UnixStream};
399
400    #[cfg(unix)]
401    mod unix {
402        use super::{
403            AsyncRead, AsyncWrite, BufResult, IoBuf, IoBufMut, Pin, TokioAsyncWrite, io, poll_fn,
404            read_into, write_from,
405        };
406        use std::os::unix::io::AsRawFd;
407        use std::path::Path;
408
409        /// Tokio Unix stream wearing the `compio::io` interface.
410        #[derive(Debug)]
411        pub struct UnixStream {
412            inner: tokio::net::UnixStream,
413        }
414
415        impl UnixStream {
416            /// Connect to a Unix domain socket at `path`.
417            pub async fn connect<P: AsRef<Path>>(path: P) -> io::Result<Self> {
418                let inner = tokio::net::UnixStream::connect(path).await?;
419                Ok(Self { inner })
420            }
421
422            /// Local (bound) address of this stream, if any.
423            pub fn local_addr(&self) -> io::Result<tokio::net::unix::SocketAddr> {
424                self.inner.local_addr()
425            }
426
427            /// Peer address of this stream, if any.
428            pub fn peer_addr(&self) -> io::Result<tokio::net::unix::SocketAddr> {
429                self.inner.peer_addr()
430            }
431        }
432
433        /// Tokio Unix listener returning [`UnixStream`] adapters.
434        #[derive(Debug)]
435        pub struct UnixListener {
436            inner: tokio::net::UnixListener,
437        }
438
439        impl UnixListener {
440            /// Bind a Unix domain socket listener at `path`.
441            ///
442            /// Async to mirror the compio backend, where binding awaits; tokio's
443            /// bind is synchronous, so this just wraps it.
444            #[allow(clippy::unused_async)] // signature parity with the compio backend
445            pub async fn bind<P: AsRef<Path>>(path: P) -> io::Result<Self> {
446                let inner = tokio::net::UnixListener::bind(path)?;
447                Ok(Self { inner })
448            }
449
450            /// Accept the next inbound connection.
451            pub async fn accept(&self) -> io::Result<(UnixStream, tokio::net::unix::SocketAddr)> {
452                let (stream, addr) = self.inner.accept().await?;
453                Ok((UnixStream { inner: stream }, addr))
454            }
455
456            /// Local address this listener is bound to.
457            pub fn local_addr(&self) -> io::Result<tokio::net::unix::SocketAddr> {
458                self.inner.local_addr()
459            }
460        }
461
462        impl AsyncRead for UnixStream {
463            async fn read<B: IoBufMut>(&mut self, buf: B) -> BufResult<usize, B> {
464                read_into(&mut self.inner, buf).await
465            }
466        }
467
468        impl AsyncWrite for UnixStream {
469            async fn write<B: IoBuf>(&mut self, buf: B) -> BufResult<usize, B> {
470                write_from(&mut self.inner, buf).await
471            }
472
473            async fn flush(&mut self) -> io::Result<()> {
474                poll_fn(|cx| Pin::new(&mut self.inner).poll_flush(cx)).await
475            }
476
477            async fn shutdown(&mut self) -> io::Result<()> {
478                poll_fn(|cx| Pin::new(&mut self.inner).poll_shutdown(cx)).await
479            }
480        }
481
482        impl AsRawFd for UnixStream {
483            fn as_raw_fd(&self) -> std::os::unix::io::RawFd {
484                self.inner.as_raw_fd()
485            }
486        }
487    }
488}
489
490pub use backend::*;