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}