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}