Skip to main content

dtact_util/stream/
native.rs

1//! Native duplex-pipe backend: a lock-free, in-process byte pipe built
2//! directly on [`crate::lockfree::SpscQueue`] — no OS transport, no
3//! `Mutex`, no per-call heap allocation on the hot path.
4//!
5//! A pipe pair is two [`HalfPipe`]s (one per direction), each a fixed-
6//! capacity SPSC ring buffer plus a pair of [`AtomicWakerSlot`]s: one for
7//! a blocked reader (woken when a writer pushes into an empty-to-nonempty
8//! ring or when the writer drops), one for a blocked writer (woken when a
9//! reader pops from a full-to-nonfull ring or when the reader drops).
10//! `writer_dropped`/`reader_dropped` flags give EOF-on-drop (read side)
11//! and broken-pipe-on-drop (write side) semantics without either side
12//! needing to synchronously coordinate shutdown.
13//!
14//! **Not registered with any OS reactor** — this is purely an in-process
15//! handoff primitive (think `tokio::io::duplex`, not a Unix domain
16//! socket). An OS-transport variant (Unix domain sockets on Linux/macOS,
17//! named pipes on Windows) would be the natural next step for cross-
18//! process use, and isn't implemented in this pass — see the crate-level
19//! notes for what's deferred where.
20
21use crate::lockfree::{AtomicWakerSlot, SpscQueue};
22use std::io;
23#[cfg(feature = "compat")]
24use std::pin::Pin;
25use std::sync::Arc;
26use std::sync::atomic::{AtomicBool, Ordering};
27use std::task::{Context, Poll};
28
29#[repr(align(64))]
30struct HalfPipe {
31    queue: SpscQueue<u8>,
32    read_waker: AtomicWakerSlot,
33    write_waker: AtomicWakerSlot,
34    writer_dropped: AtomicBool,
35    reader_dropped: AtomicBool,
36}
37
38impl HalfPipe {
39    fn new(capacity: usize) -> Self {
40        Self {
41            queue: SpscQueue::new(capacity),
42            read_waker: AtomicWakerSlot::new(),
43            write_waker: AtomicWakerSlot::new(),
44            writer_dropped: AtomicBool::new(false),
45            reader_dropped: AtomicBool::new(false),
46        }
47    }
48}
49
50/// One end of an in-process duplex pipe. Create a connected pair with
51/// [`pair`].
52pub struct DtactStream {
53    tx: Arc<HalfPipe>,
54    rx: Arc<HalfPipe>,
55}
56
57unsafe impl Send for DtactStream {}
58unsafe impl Sync for DtactStream {}
59
60/// Create a connected pair of duplex streams, each with `capacity` bytes
61/// of buffering per direction (rounded up to a power of two — required by
62/// the underlying [`SpscQueue`]).
63#[must_use]
64pub fn pair(capacity: usize) -> (DtactStream, DtactStream) {
65    let capacity = capacity.next_power_of_two().max(1);
66    let a = Arc::new(HalfPipe::new(capacity));
67    let b = Arc::new(HalfPipe::new(capacity));
68    (
69        DtactStream {
70            tx: Arc::clone(&a),
71            rx: Arc::clone(&b),
72        },
73        DtactStream { tx: b, rx: a },
74    )
75}
76
77impl DtactStream {
78    /// Non-blocking poll-based read, usable directly from a hand-rolled
79    /// `Future` or via the `async fn read` convenience below.
80    #[inline]
81    pub fn poll_read(&self, cx: &Context<'_>, buf: &mut [u8]) -> Poll<io::Result<usize>> {
82        // An empty buffer is a trivial no-op success (matching `std`/
83        // `tokio` convention) — must be handled before any `buf[0]`
84        // indexing below, which would otherwise panic for a 0-length
85        // buffer on the waker re-check fast path.
86        if buf.is_empty() {
87            return Poll::Ready(Ok(0));
88        }
89        // Bulk-drain the contiguous occupied run(s) in one shot rather than
90        // popping a byte at a time — one `copy_nonoverlapping` (two at a
91        // wrap) plus a single `head` store instead of N atomic pairs.
92        let n = self.rx.queue.pop_slice(buf);
93        if n > 0 {
94            self.rx.write_waker.take_and_wake();
95            return Poll::Ready(Ok(n));
96        }
97        if self.rx.writer_dropped.load(Ordering::Acquire) && self.rx.queue.is_empty() {
98            return Poll::Ready(Ok(0)); // EOF
99        }
100        self.rx.read_waker.register(cx.waker());
101        // Re-check after registering to close the race where the writer
102        // pushed (or dropped) between our drain above and the register call.
103        let n = self.rx.queue.pop_slice(buf);
104        if n > 0 {
105            self.rx.write_waker.take_and_wake();
106            return Poll::Ready(Ok(n));
107        }
108        if self.rx.writer_dropped.load(Ordering::Acquire) && self.rx.queue.is_empty() {
109            return Poll::Ready(Ok(0));
110        }
111        Poll::Pending
112    }
113
114    /// Non-blocking poll-based write.
115    #[inline]
116    pub fn poll_write(&self, cx: &Context<'_>, buf: &[u8]) -> Poll<io::Result<usize>> {
117        // An empty buffer is a trivial no-op success — must be handled
118        // before any `buf[0]` indexing below, which would otherwise panic
119        // for a 0-length buffer on the waker re-check fast path. Note this
120        // intentionally skips the `reader_dropped` check other branches
121        // make: writing zero bytes never actually touches the pipe, so
122        // there's nothing to report as a broken pipe for.
123        if buf.is_empty() {
124            return Poll::Ready(Ok(0));
125        }
126        if self.tx.reader_dropped.load(Ordering::Acquire) {
127            return Poll::Ready(Err(io::Error::new(
128                io::ErrorKind::BrokenPipe,
129                "dtact-stream: peer dropped its read half",
130            )));
131        }
132        // Bulk-fill the contiguous free run(s) in one shot rather than
133        // pushing a byte at a time.
134        let n = self.tx.queue.push_slice(buf);
135        if n > 0 {
136            self.tx.read_waker.take_and_wake();
137            return Poll::Ready(Ok(n));
138        }
139        self.tx.write_waker.register(cx.waker());
140        if self.tx.reader_dropped.load(Ordering::Acquire) {
141            return Poll::Ready(Err(io::Error::new(
142                io::ErrorKind::BrokenPipe,
143                "dtact-stream: peer dropped its read half",
144            )));
145        }
146        let n = self.tx.queue.push_slice(buf);
147        if n > 0 {
148            self.tx.read_waker.take_and_wake();
149            return Poll::Ready(Ok(n));
150        }
151        Poll::Pending
152    }
153
154    /// Read into `buf`, returning the number of bytes read (`0` = EOF).
155    ///
156    /// # Errors
157    ///
158    /// Returns `io::ErrorKind::BrokenPipe` if the peer's write half has
159    /// been dropped with no more buffered bytes to deliver (analogous to
160    /// EOF on a real pipe, surfaced as `Ok(0)` in that case — see
161    /// `poll_read`'s implementation — so in practice this async wrapper
162    /// itself only ever returns `Ok`; the `Result` exists for symmetry
163    /// with `write`/`write_all` and to leave room for future error paths).
164    pub async fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
165        std::future::poll_fn(|cx| self.poll_read(cx, buf)).await
166    }
167
168    /// Write from `buf`, returning the number of bytes written.
169    ///
170    /// # Errors
171    ///
172    /// Returns `io::ErrorKind::BrokenPipe` if the peer has dropped its
173    /// read half — there is no reader left to ever drain the queue, so
174    /// the write can never succeed.
175    pub async fn write(&self, buf: &[u8]) -> io::Result<usize> {
176        std::future::poll_fn(|cx| self.poll_write(cx, buf)).await
177    }
178
179    /// Write the entirety of `buf`, retrying partial writes.
180    ///
181    /// # Errors
182    ///
183    /// Returns the same `io::ErrorKind::BrokenPipe` as [`Self::write`] if
184    /// the peer drops its read half partway through.
185    pub async fn write_all(&self, mut buf: &[u8]) -> io::Result<()> {
186        while !buf.is_empty() {
187            let n = self.write(buf).await?;
188            buf = &buf[n..];
189        }
190        Ok(())
191    }
192}
193
194impl Drop for DtactStream {
195    fn drop(&mut self) {
196        self.tx.writer_dropped.store(true, Ordering::Release);
197        self.tx.read_waker.take_and_wake();
198        self.rx.reader_dropped.store(true, Ordering::Release);
199        self.rx.write_waker.take_and_wake();
200    }
201}
202
203// =============================================================================
204// COMPAT: futures_io / tokio::io AsyncRead+AsyncWrite
205// =============================================================================
206// DtactStream's poll_read/poll_write already match the shape these traits
207// want, so — unlike `io`/`fs`, which needed a `DtactCompat<T>` wrapper —
208// these impl directly on `DtactStream` itself, gated behind the `compat`
209// feature (pulled in automatically by the `tokio` feature) since that's
210// what brings in the `futures-io`/`tokio` dependencies.
211
212#[cfg(feature = "compat")]
213impl futures_io::AsyncRead for DtactStream {
214    fn poll_read(
215        self: Pin<&mut Self>,
216        cx: &mut Context<'_>,
217        buf: &mut [u8],
218    ) -> Poll<io::Result<usize>> {
219        Self::poll_read(&self, cx, buf)
220    }
221}
222
223#[cfg(feature = "compat")]
224impl futures_io::AsyncWrite for DtactStream {
225    fn poll_write(
226        self: Pin<&mut Self>,
227        cx: &mut Context<'_>,
228        buf: &[u8],
229    ) -> Poll<io::Result<usize>> {
230        Self::poll_write(&self, cx, buf)
231    }
232
233    fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
234        Poll::Ready(Ok(()))
235    }
236
237    fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
238        Poll::Ready(Ok(()))
239    }
240}
241
242#[cfg(feature = "compat")]
243impl tokio::io::AsyncRead for DtactStream {
244    fn poll_read(
245        self: Pin<&mut Self>,
246        cx: &mut Context<'_>,
247        buf: &mut tokio::io::ReadBuf<'_>,
248    ) -> Poll<io::Result<()>> {
249        let unfilled = buf.initialize_unfilled();
250        match Self::poll_read(&self, cx, unfilled) {
251            Poll::Ready(Ok(n)) => {
252                buf.advance(n);
253                Poll::Ready(Ok(()))
254            }
255            Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
256            Poll::Pending => Poll::Pending,
257        }
258    }
259}
260
261#[cfg(feature = "compat")]
262impl tokio::io::AsyncWrite for DtactStream {
263    fn poll_write(
264        self: Pin<&mut Self>,
265        cx: &mut Context<'_>,
266        buf: &[u8],
267    ) -> Poll<io::Result<usize>> {
268        Self::poll_write(&self, cx, buf)
269    }
270
271    fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
272        Poll::Ready(Ok(()))
273    }
274
275    fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
276        Poll::Ready(Ok(()))
277    }
278}