proxima_primitives/stream/mod.rs
1//! Bidirectional byte-stream primitive. Trait bound is `futures::io`
2//! (not `tokio::io`) so non-tokio backends — AF_XDP, DPDK, glommio,
3//! smol — can implement without a tokio dep. Tokio backends use the
4//! `tokio-util::compat` shim at the impl boundary.
5//!
6//! Two-tier by design (principle 3): every trait in this crate is built on
7//! `std::io::{Error, Result}` and/or `futures::io`'s `AsyncRead`/`AsyncWrite`,
8//! none of which has a `core`/`alloc` analog (`futures-io`'s items only exist
9//! under its own `std` feature; see `proxima_core::io`'s doc comment
10//! for why a no_std async IO seam needs an associated-`Error` type instead
11//! of `std::io::Error`). The surface is therefore gated behind
12//! `#[cfg(feature = "std")]` in full; the alloc tier keeps only the
13//! tier-agnostic pieces — [`TcpBindOptions`] and the [`BindAddr`]/
14//! [`PeerInfo`] re-export.
15
16#[cfg(feature = "std")]
17use std::io;
18
19#[cfg(feature = "std")]
20use core::future::Future;
21#[cfg(feature = "std")]
22use core::marker::PhantomData;
23#[cfg(feature = "std")]
24use core::net::SocketAddr;
25#[cfg(feature = "std")]
26use core::pin::Pin;
27#[cfg(feature = "std")]
28use core::task::{Context, Poll};
29
30#[cfg(feature = "std")]
31use futures::io::{AsyncRead, AsyncWrite};
32
33// gated on alloc (not left unconditional): `crate::pipe::endpoint` only
34// exists under pipe's own alloc feature, so a bare no_std+no-alloc build
35// must not reference it.
36#[cfg(feature = "alloc")]
37pub use crate::pipe::endpoint::{BindAddr, PeerInfo};
38
39#[cfg(feature = "std")]
40mod datagram_batch_ext;
41#[cfg(feature = "std")]
42pub use datagram_batch_ext::DatagramSocketBatchExt;
43
44/// Default `listen()` backlog when the caller does not specify one.
45pub const DEFAULT_LISTEN_BACKLOG: u32 = 1024;
46
47#[cfg(feature = "std")]
48pub trait StreamConnection: AsyncRead + AsyncWrite + Send + Unpin + 'static {
49 fn peer(&self) -> Option<PeerInfo>;
50}
51
52// A boxed connection is itself a connection: `futures::io` already impls
53// AsyncRead/AsyncWrite for `Box<T: ?Sized + …>`, so a listener that wants to
54// accept a caller-supplied wrapper (e.g. a cipher-wrapped socket) can take a
55// `Box<dyn StreamConnection>` as its `C` without naming the concrete type.
56#[cfg(feature = "std")]
57impl StreamConnection for Box<dyn StreamConnection> {
58 fn peer(&self) -> Option<PeerInfo> {
59 (**self).peer()
60 }
61}
62
63#[cfg(feature = "std")]
64pub trait StreamListener: Send + Sync + 'static {
65 type Conn: StreamConnection;
66
67 fn poll_accept(&self, cx: &mut Context<'_>) -> Poll<io::Result<Self::Conn>>;
68
69 fn local_addr(&self) -> Option<BindAddr>;
70}
71
72#[cfg(feature = "std")]
73// an open set of transports crosses this object-safe boundary.
74pub type ConnectFuture<'lifetime, Connection> =
75 Pin<Box<dyn Future<Output = io::Result<Connection>> + Send + 'lifetime>>;
76
77#[cfg(feature = "std")]
78pub trait StreamUpstream: Send + Sync + 'static {
79 type Conn: StreamConnection;
80
81 fn connect_future(&self) -> ConnectFuture<'_, Self::Conn>;
82}
83
84#[cfg(feature = "std")]
85pub trait StreamListenerExt: StreamListener {
86 fn accept(&self) -> Accept<'_, Self> {
87 Accept { listener: self }
88 }
89}
90
91#[cfg(feature = "std")]
92impl<T: StreamListener + ?Sized> StreamListenerExt for T {}
93
94#[cfg(feature = "std")]
95pub trait StreamUpstreamExt: StreamUpstream {
96 fn connect(&self) -> Connect<'_, Self> {
97 Connect {
98 future: self.connect_future(),
99 upstream: PhantomData,
100 }
101 }
102}
103
104#[cfg(feature = "std")]
105impl<T: StreamUpstream + ?Sized> StreamUpstreamExt for T {}
106
107/// Runtime-agnostic factory that builds a [`StreamUpstream`] dialing a
108/// Unix-domain socket path — the Unix sibling of [`AcceptorFactory`], for
109/// the dispatch sites (`StreamPassthroughUpstream`) that pick a backend by
110/// transport name rather than binding a listener.
111///
112/// `Conn` is pinned to `Box<dyn StreamConnection>` so the trait stays
113/// object-safe and one backend's upstream (prime's `PrimeUnixConnection`,
114/// tokio's `TokioUnixConnection`) can sit behind the same `Arc<dyn
115/// UnixUpstreamFactory>` field as the other — the same erasure boundary
116/// `Box<dyn StreamConnection>`'s own `StreamConnection` impl above already
117/// established, one level up.
118///
119/// `connect` is cheap and synchronous, mirroring `StreamUpstream` itself:
120/// building the upstream value never touches the network — the actual dial
121/// happens lazily when the returned value's connection future is polled.
122#[cfg(feature = "std")]
123pub trait UnixUpstreamFactory: Send + Sync + 'static {
124 fn connect(
125 &self,
126 path: std::path::PathBuf,
127 ) -> std::sync::Arc<dyn StreamUpstream<Conn = Box<dyn StreamConnection>>>;
128}
129
130#[cfg(feature = "std")]
131pub struct Accept<'lifetime, L: StreamListener + ?Sized> {
132 listener: &'lifetime L,
133}
134
135#[cfg(feature = "std")]
136impl<L: StreamListener + ?Sized> Future for Accept<'_, L> {
137 type Output = io::Result<L::Conn>;
138
139 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
140 self.listener.poll_accept(cx)
141 }
142}
143
144#[cfg(feature = "std")]
145pub struct Connect<'lifetime, U: StreamUpstream + ?Sized> {
146 future: ConnectFuture<'lifetime, U::Conn>,
147 upstream: PhantomData<&'lifetime U>,
148}
149
150#[cfg(feature = "std")]
151impl<U: StreamUpstream + ?Sized> Future for Connect<'_, U> {
152 type Output = io::Result<U::Conn>;
153
154 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
155 self.future.as_mut().poll(cx)
156 }
157}
158
159/// Listen-socket bind options shared by every runtime backend.
160#[derive(Debug, Clone, Copy)]
161pub struct TcpBindOptions {
162 /// `listen()` queue depth.
163 pub backlog: u32,
164 /// Request `SO_REUSEPORT` so each core can bind its own lane. Prime
165 /// honors a single-core listener today; tokio fans out per core.
166 pub reuseport: bool,
167 /// Kernel passive-open queue depth for TCP Fast Open (RFC 7413,
168 /// Linux). `None` leaves TFO off; non-Linux platforms ignore it.
169 pub tcp_fastopen: Option<u32>,
170}
171
172impl Default for TcpBindOptions {
173 fn default() -> Self {
174 Self {
175 backlog: DEFAULT_LISTEN_BACKLOG,
176 reuseport: false,
177 tcp_fastopen: None,
178 }
179 }
180}
181
182/// A bound TCP acceptor pinned to the worker that created it. `Send` (so
183/// it can be held across awaits in a `Send` bootstrap future) but NOT
184/// `Sync`: prime's reactor-backed listener retains its registration's
185/// worker identity and rejects polling on a different worker. Cross-thread
186/// disposal queues deregistration with the owner before closing the socket.
187/// The accept surface therefore requires exclusive access for polling.
188#[cfg(feature = "std")]
189pub trait TcpAcceptor: Send {
190 /// Accept the next connection, yielding a boxed `StreamConnection`.
191 /// Must be polled on the worker that produced this acceptor.
192 fn poll_accept(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<Box<dyn StreamConnection>>>;
193
194 /// The local address the listener bound to.
195 fn local_addr(&self) -> io::Result<SocketAddr>;
196}
197
198/// Runtime-agnostic factory that binds a [`TcpAcceptor`] on the calling
199/// worker. Injected into the serve path so a listener loop can obtain a
200/// prime- or tokio-backed acceptor without naming the concrete runtime.
201///
202/// The factory is `Send + Sync` (it is shared via `Arc` and only builds
203/// acceptors); the acceptor it produces is `Send`-but-`!Sync` and lives
204/// on one worker. `bind` MUST be called from within a future already
205/// running on the target runtime's worker — prime needs `CURRENT_REACTOR`
206/// live, tokio needs its reactor entered.
207#[cfg(feature = "std")]
208pub trait AcceptorFactory: Send + Sync + 'static {
209 fn bind(&self, addr: SocketAddr, options: TcpBindOptions) -> io::Result<Box<dyn TcpAcceptor>>;
210}
211
212/// A bound datagram (UDP) socket pinned to the worker that created it.
213/// `Send`-but-`!Sync` for the same reason as [`TcpAcceptor`]: prime's reactor
214/// registration is per-worker. A QUIC/h3 listener demuxes many connections over
215/// this one socket, so poll it only on the worker that produced it.
216#[cfg(feature = "std")]
217pub trait DatagramSocket: Send {
218 /// Receive one datagram into `buf`; yields `(len, sender)`.
219 fn poll_recv_from(
220 &mut self,
221 cx: &mut Context<'_>,
222 buf: &mut [u8],
223 ) -> Poll<io::Result<(usize, SocketAddr)>>;
224
225 /// Send `buf` to `peer`; `Pending` when the kernel send buffer is full.
226 fn poll_send_to(
227 &mut self,
228 cx: &mut Context<'_>,
229 buf: &[u8],
230 peer: SocketAddr,
231 ) -> Poll<io::Result<usize>>;
232
233 /// Receive up to `min(bufs.len(), out_meta.len())` datagrams at once,
234 /// writing each datagram's `(len, sender)` into `out_meta` and returning
235 /// the count. The default loops [`poll_recv_from`](Self::poll_recv_from);
236 /// a backend that can pull many datagrams per syscall (prime's `recvmmsg`)
237 /// overrides this to amortize the kernel-entry cost across the burst.
238 fn poll_recv_batch(
239 &mut self,
240 cx: &mut Context<'_>,
241 bufs: &mut [&mut [u8]],
242 out_meta: &mut [(usize, SocketAddr)],
243 ) -> Poll<io::Result<usize>> {
244 let want = bufs.len().min(out_meta.len());
245 let mut count = 0;
246 while count < want {
247 // reborrow the slot buffer to a plain &mut [u8] for the single recv.
248 let slot: &mut [u8] = &mut *bufs[count];
249 match self.poll_recv_from(cx, slot) {
250 Poll::Ready(Ok((len, peer))) => {
251 out_meta[count] = (len, peer);
252 count += 1;
253 }
254 Poll::Ready(Err(_)) if count > 0 => return Poll::Ready(Ok(count)),
255 Poll::Ready(Err(err)) => return Poll::Ready(Err(err)),
256 Poll::Pending if count > 0 => return Poll::Ready(Ok(count)),
257 Poll::Pending => return Poll::Pending,
258 }
259 }
260 Poll::Ready(Ok(count))
261 }
262
263 /// Send all `packets` (each `(bytes, peer)`) in as few syscalls as
264 /// possible, returning the count accepted. The default loops
265 /// [`poll_send_to`](Self::poll_send_to); prime overrides with `sendmmsg`.
266 fn poll_send_batch(
267 &mut self,
268 cx: &mut Context<'_>,
269 packets: &[(&[u8], SocketAddr)],
270 ) -> Poll<io::Result<usize>> {
271 let mut sent = 0;
272 while sent < packets.len() {
273 let (bytes, peer) = packets[sent];
274 match self.poll_send_to(cx, bytes, peer) {
275 Poll::Ready(Ok(_)) => sent += 1,
276 Poll::Ready(Err(_)) if sent > 0 => return Poll::Ready(Ok(sent)),
277 Poll::Ready(Err(err)) => return Poll::Ready(Err(err)),
278 Poll::Pending if sent > 0 => return Poll::Ready(Ok(sent)),
279 Poll::Pending => return Poll::Pending,
280 }
281 }
282 Poll::Ready(Ok(sent))
283 }
284
285 /// The local address the socket bound to.
286 fn local_addr(&self) -> io::Result<SocketAddr>;
287}
288
289/// Runtime-agnostic factory that binds a [`DatagramSocket`] on the calling
290/// worker — the UDP sibling of [`AcceptorFactory`]. Injected into the serve path
291/// so a QUIC/h3 listener obtains a prime- or tokio-backed UDP socket without
292/// naming the concrete runtime. `bind` MUST be called from within a future
293/// already running on the target runtime's worker.
294#[cfg(feature = "std")]
295pub trait DatagramFactory: Send + Sync + 'static {
296 fn bind(&self, addr: SocketAddr) -> io::Result<Box<dyn DatagramSocket>>;
297
298 /// Name of the runtime backend this factory's `bind` output must be
299 /// polled on (`"prime"`, `"tokio"`, ...). The default `"unknown"` is
300 /// for factories that don't declare one — test doubles and in-memory
301 /// fakes never need to, since they have no reactor affinity to
302 /// violate. Real backends override this so a caller-assembled
303 /// `RuntimeSelection` (every field is `pub`) can be checked for a
304 /// factory/runtime mismatch BEFORE the first poll, rather than
305 /// discovering it via an opaque failure deep inside the wrong
306 /// reactor (e.g. prime's `CURRENT_REACTOR is null` when its
307 /// `UdpSocket` is driven from a tokio worker).
308 fn backend_name(&self) -> &'static str {
309 "unknown"
310 }
311}