Skip to main content

hclient_rt/
futures_io.rs

1use hyper::rt::ReadBufCursor;
2use std::fmt::Debug;
3use std::pin::Pin;
4use std::task::{Context, Poll};
5
6/// Bridges `futures_io::{AsyncRead, AsyncWrite}` → `hyper::rt::{Read, Write}`.
7///
8/// hyper-util only ships `TokioIo`; `smol-hyper` 0.1.1 has been dead since
9/// 2023-12-29 and bridges in the opposite direction. Without this bridge,
10/// the smol backend doesn't exist.
11///
12/// The implementation is **`unsafe`-free**: it reads into a scratch buffer
13/// and copies out through the safe `ReadBufCursor::put_slice` — exactly the
14/// technique `hyper::rt::Read`'s documentation recommends. The cost is one
15/// copy per read; zero-copy would require `unsafe`
16/// (`ReadBufCursor::as_mut`/`advance`), and the crate declares
17/// `#![forbid(unsafe_code)]` — deliberately deferred.
18pub struct FuturesIo<S> {
19    inner: S,
20    /// The buffer is allocated and zeroed ONCE, in [`FuturesIo::new`] — not
21    /// on every `poll_read`.
22    ///
23    /// An earlier draft of this task kept it as `[0u8; SCRATCH]` on the
24    /// stack inside `poll_read`. Measured (`rustc -O --emit=asm` on an
25    /// isolated reproduction of both versions outside the rest of the
26    /// crate): the stack variant calls `memset` on EVERY invocation
27    /// (`subq $4096,%rsp` twice — that's the 8192 bytes — then `callq
28    /// *memset@GOTPCREL`), whereas the struct-field version makes no such
29    /// call at all — the reading function is called directly on an
30    /// already-ready buffer. The allocation and the single zeroing happen
31    /// in `new()` (`__rust_alloc_zeroed`), once per connection, not once
32    /// per read. `poll_read` is the hot path of every request on smol; a
33    /// stray `memset` there is not a hypothetical cost.
34    scratch: Box<[u8]>,
35}
36
37/// Buffer size. 8 KiB is hyper's typical read size, so no extra iterations
38/// result.
39const SCRATCH: usize = 8 * 1024;
40
41impl<S> FuturesIo<S> {
42    pub fn new(inner: S) -> Self {
43        Self {
44            inner,
45            scratch: vec![0u8; SCRATCH].into_boxed_slice(),
46        }
47    }
48
49    pub fn into_inner(self) -> S {
50        self.inner
51    }
52
53    pub fn get_ref(&self) -> &S {
54        &self.inner
55    }
56}
57
58// A hand-written `Debug`, not `#[derive]`: `derive` would dump all 8 KiB of
59// `scratch` as a list of numbers on every format call — useless and noisy
60// in logs. The same technique is already used in
61// `hclient_core::RequestBody` (length instead of contents).
62impl<S: Debug> Debug for FuturesIo<S> {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        f.debug_struct("FuturesIo")
65            .field("inner", &self.inner)
66            .field("scratch_len", &self.scratch.len())
67            .finish()
68    }
69}
70
71impl<S: futures_io::AsyncRead + Unpin> hyper::rt::Read for FuturesIo<S> {
72    fn poll_read(
73        mut self: Pin<&mut Self>,
74        cx: &mut Context<'_>,
75        mut buf: ReadBufCursor<'_>,
76    ) -> Poll<std::io::Result<()>> {
77        let want = buf.remaining().min(self.scratch.len());
78        if want == 0 {
79            return Poll::Ready(Ok(()));
80        }
81        // Destructure into disjoint fields explicitly: `inner` and
82        // `scratch` are borrowed at the same time, but independently of
83        // each other.
84        let Self { inner, scratch } = &mut *self;
85        let n = std::task::ready!(Pin::new(inner).poll_read(cx, &mut scratch[..want]))?;
86        buf.put_slice(&scratch[..n]);
87        Poll::Ready(Ok(()))
88    }
89}
90
91impl<S: futures_io::AsyncWrite + Unpin> hyper::rt::Write for FuturesIo<S> {
92    fn poll_write(
93        mut self: Pin<&mut Self>,
94        cx: &mut Context<'_>,
95        buf: &[u8],
96    ) -> Poll<std::io::Result<usize>> {
97        Pin::new(&mut self.inner).poll_write(cx, buf)
98    }
99
100    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
101        Pin::new(&mut self.inner).poll_flush(cx)
102    }
103
104    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
105        Pin::new(&mut self.inner).poll_close(cx)
106    }
107
108    fn poll_write_vectored(
109        mut self: Pin<&mut Self>,
110        cx: &mut Context<'_>,
111        bufs: &[std::io::IoSlice<'_>],
112    ) -> Poll<std::io::Result<usize>> {
113        Pin::new(&mut self.inner).poll_write_vectored(cx, bufs)
114    }
115
116    /// `futures_io::AsyncWrite` gives no way to ask `S` whether its
117    /// vectored write is efficient: the trait has no `is_write_vectored`
118    /// method at all — only `poll_write`, `poll_write_vectored`,
119    /// `poll_flush`, `poll_close`. The default `poll_write_vectored`
120    /// implementation in `futures-io` 0.3.33 writes only the first
121    /// non-empty buffer (`futures_io::AsyncWrite::poll_write_vectored`,
122    /// checked against the dependency's source); for any `S` that hasn't
123    /// overridden it, a vectored write silently degrades into one ordinary
124    /// write — more syscalls, not fewer.
125    ///
126    /// hyper documents this method as a promise of an "efficient"
127    /// `poll_write_vectored` implementation and branches on it to decide
128    /// whether to coalesce buffers before writing. Returning `true` here
129    /// would assert something we have no way to back up — and a capability
130    /// that lies is worse than one that's absent, because the calling code
131    /// (here, hyper itself) branches on it. The honest, conservative
132    /// answer is `false`.
133    ///
134    /// If a concrete `S` shows up for which vectored writes are provably
135    /// efficient, the right path is a separate constructor with an
136    /// explicit opt-in, not an optimistic default here. There is no such
137    /// consumer today, so adding one would be speculative API with no
138    /// caller.
139    fn is_write_vectored(&self) -> bool {
140        false
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use futures_executor::block_on;
148    use std::future::poll_fn;
149    use std::pin::Pin;
150    use std::task::Context;
151    use std::task::Poll;
152
153    /// A source that hands out data in chunks, to catch partial reads.
154    struct Chunked {
155        data: Vec<u8>,
156        at: usize,
157        step: usize,
158    }
159    impl futures_io::AsyncRead for Chunked {
160        fn poll_read(
161            mut self: Pin<&mut Self>,
162            _: &mut Context<'_>,
163            buf: &mut [u8],
164        ) -> Poll<std::io::Result<usize>> {
165            let n = self.step.min(buf.len()).min(self.data.len() - self.at);
166            buf[..n].copy_from_slice(&self.data[self.at..self.at + n]);
167            self.at += n;
168            Poll::Ready(Ok(n))
169        }
170    }
171
172    fn read_all(mut io: FuturesIo<Chunked>) -> Vec<u8> {
173        let mut out = Vec::new();
174        let mut store = [0u8; 8];
175        loop {
176            let mut rb = hyper::rt::ReadBuf::new(&mut store);
177            let poll = block_on(poll_fn(|cx| {
178                hyper::rt::Read::poll_read(Pin::new(&mut io), cx, rb.unfilled())
179            }));
180            poll.unwrap();
181            let filled = rb.filled().to_vec();
182            if filled.is_empty() {
183                return out;
184            }
185            out.extend_from_slice(&filled);
186        }
187    }
188
189    #[test]
190    fn forwards_bytes_through_partial_reads() {
191        let io = FuturesIo::new(Chunked {
192            data: b"hello world".to_vec(),
193            at: 0,
194            step: 3,
195        });
196        assert_eq!(read_all(io), b"hello world");
197    }
198
199    #[test]
200    fn never_writes_more_than_remaining() {
201        // step is larger than the buffer's capacity: put_slice must not panic.
202        let io = FuturesIo::new(Chunked {
203            data: vec![7u8; 64],
204            at: 0,
205            step: 64,
206        });
207        assert_eq!(read_all(io).len(), 64);
208    }
209
210    #[test]
211    fn into_inner_round_trips() {
212        let io = FuturesIo::new(Chunked {
213            data: vec![],
214            at: 0,
215            step: 1,
216        });
217        let c = io.into_inner();
218        assert_eq!(c.step, 1);
219    }
220
221    #[test]
222    fn read_request_larger_than_scratch_buffer_does_not_panic() {
223        // `buf.remaining()` is controlled by the caller (hyper) and can be
224        // larger than `scratch.len()` — `want` must be clamped to the
225        // buffer's size, or `&mut scratch[..want]` indexes out of bounds
226        // and panics. This was previously untested by anything:
227        // `never_writes_more_than_remaining` uses an 8-byte caller buffer,
228        // which is always smaller than `SCRATCH`, so removing the
229        // `.min(..)` wouldn't have been caught there.
230        let len = super::SCRATCH + 137;
231        let data: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
232        let mut io = FuturesIo::new(Chunked {
233            data: data.clone(),
234            at: 0,
235            step: len,
236        });
237        let mut out = Vec::new();
238        let mut store = vec![0u8; len];
239        loop {
240            let mut rb = hyper::rt::ReadBuf::new(&mut store);
241            let poll = block_on(poll_fn(|cx| {
242                hyper::rt::Read::poll_read(Pin::new(&mut io), cx, rb.unfilled())
243            }));
244            poll.unwrap();
245            let filled = rb.filled().to_vec();
246            if filled.is_empty() {
247                break;
248            }
249            out.extend_from_slice(&filled);
250        }
251        assert_eq!(out, data);
252    }
253
254    /// An `AsyncWrite` stub that's always "successful" — needed only to
255    /// check `is_write_vectored()`, not write behavior.
256    struct NullWrite;
257    impl futures_io::AsyncWrite for NullWrite {
258        fn poll_write(
259            self: Pin<&mut Self>,
260            _: &mut Context<'_>,
261            buf: &[u8],
262        ) -> Poll<std::io::Result<usize>> {
263            Poll::Ready(Ok(buf.len()))
264        }
265        fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<std::io::Result<()>> {
266            Poll::Ready(Ok(()))
267        }
268        fn poll_close(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<std::io::Result<()>> {
269            Poll::Ready(Ok(()))
270        }
271    }
272
273    #[test]
274    fn is_write_vectored_reports_false_not_an_unverifiable_claim() {
275        // `futures_io::AsyncWrite` gives no way to ask `S` whether its
276        // vectored write is efficient — so `true` here would be an
277        // assertion we have no way to back up. See the doc comment on the
278        // impl.
279        let io = FuturesIo::new(NullWrite);
280        assert!(!hyper::rt::Write::is_write_vectored(&io));
281    }
282}