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::*;