Skip to main content

monocoque_core/
io.rs

1//! Owned-buffer I/O helpers shared by every runtime backend.
2//!
3//! The runtime adapters (`rt::tokio`, `rt::smol`, and the native `rt::compio`
4//! path) all speak compio's owned-buffer read/write contract. Two pieces of
5//! that contract are fiddly enough that duplicating them per backend invites
6//! drift:
7//!
8//! - declaring how many bytes a read initialized in a buffer's spare capacity,
9//!   which is the workspace's single owned-buffer `unsafe` operation
10//!   ([`fill_read`]); and
11//! - building borrowed `IoSlice`s over an owned vectored buffer for one
12//!   `writev` ([`with_vectored_slices`]).
13//!
14//! Centralizing both here gives each backend one call site instead of its own
15//! copy, and keeps the lone `set_buf_init` `unsafe` behind a single documented
16//! contract rather than repeated in every adapter.
17//!
18//! Like `tcp`, this module encapsulates its `unsafe` behind a safe API, so it
19//! opts back into `unsafe_code` that the crate otherwise denies.
20#![allow(unsafe_code)]
21
22use bytes::BytesMut;
23use compio_buf::{BufResult, IoBufMut, IoVectoredBuf};
24use smallvec::SmallVec;
25use std::io::{self, IoSlice};
26use std::mem::MaybeUninit;
27
28/// Size of a read scratch slab.
29///
30/// Doubles as the ceiling `read_buffer_size` clamps to and the minimum capacity
31/// [`take_read_buffer`] grows a stash to. Equal to the old arena page size, so
32/// the per-read buffer bound and its resident footprint are unchanged.
33pub const READ_SLAB_SIZE: usize = 64 * 1024;
34
35/// Frame-size threshold below which a received frame is copied out of the
36/// shared read slab into a right-sized allocation instead of being frozen in
37/// place.
38///
39/// Freezing a small frame shares the whole [`READ_SLAB_SIZE`] slab by refcount,
40/// so a single small frame that stays queued (a slow consumer, a lagging SUB)
41/// pins 64 KiB. Copying frames below this threshold releases the slab
42/// immediately; larger frames stay zero-copy, where the per-byte copy would
43/// outweigh the footprint saving.
44pub const COPY_OUT_THRESHOLD: usize = 4 * 1024;
45
46/// Take a read-sized scratch buffer from the front of `stash`, leaving the
47/// remaining tail in `stash` to hand out on the next call.
48///
49/// This replaces the old bump-pointer read arena: successive reads carve
50/// `read_size` chunks off one `READ_SLAB_SIZE` allocation until it is used up,
51/// then a fresh slab is allocated. Freezing a returned buffer shares that
52/// slab's allocation (via `bytes` refcounting), so a lagging consumer pins the
53/// slab exactly as the arena page did.
54///
55/// # Safety
56///
57/// The returned buffer reports `read_size` initialized bytes that are in fact
58/// uninitialized, so it can be handed to an owned-buffer read without
59/// zero-filling first. The caller must pass it straight to a read and
60/// `truncate` it to the number of bytes actually read before freezing,
61/// inspecting, or otherwise exposing its contents.
62pub unsafe fn take_read_buffer(stash: &mut BytesMut, read_size: usize) -> BytesMut {
63    if stash.capacity() < read_size {
64        *stash = BytesMut::with_capacity(read_size.max(READ_SLAB_SIZE));
65    }
66    if stash.len() < read_size {
67        // SAFETY: `read_size` is within capacity per the check above. The
68        // function's contract requires the caller to overwrite these bytes via
69        // the read and truncate to the real count before exposing them.
70        unsafe { stash.set_len(read_size) };
71    }
72    let tail = stash.split_off(read_size);
73    std::mem::replace(stash, tail)
74}
75
76/// Read into an owned buffer's spare capacity, then declare the bytes written
77/// as initialized.
78///
79/// This is the one place in the workspace that calls `set_len` (from compio's
80/// `SetLen`). Every runtime backend routes its read path through here, so the
81/// single owned-buffer `unsafe` block lives behind one documented contract
82/// instead of a copy per adapter.
83///
84/// `read` is an async closure handed the buffer's uninitialized spare capacity
85/// as `&mut [MaybeUninit<u8>]`; it performs the actual read into the front of
86/// that slice: tokio wraps it in a `ReadBuf`, smol hands it to `recv`, so the
87/// read mechanism stays with the backend while the buffer bookkeeping stays
88/// here. `AsyncFnOnce` lets the returned future borrow the spare slice, which a
89/// plain `FnOnce` returning a future cannot.
90///
91/// # Contract
92///
93/// On `Ok(n)`, `read` must have initialized exactly the first `n` bytes of the
94/// slice it was given (`n` never exceeds the slice length). `set_buf_init(n)`
95/// then declares precisely those bytes live, matching what was written. A
96/// backend that reported more bytes than it wrote, or wrote them anywhere but
97/// the front of the slice, would break this contract and the `unsafe` below.
98pub async fn fill_read<B, F>(mut buf: B, read: F) -> BufResult<usize, B>
99where
100    B: IoBufMut,
101    F: AsyncFnOnce(&mut [MaybeUninit<u8>]) -> io::Result<usize>,
102{
103    // Scope the spare-capacity borrow to the read so `buf` is free to mutate
104    // again once the count is known.
105    let outcome = {
106        let spare = buf.as_uninit();
107        read(spare).await
108    };
109    match outcome {
110        Ok(n) => {
111            // SAFETY: per this function's contract, `read` initialized exactly
112            // the first `n` bytes of the spare slice it was handed. Declaring
113            // that same length initialized matches what was actually written.
114            unsafe {
115                buf.set_len(n);
116            }
117            BufResult(Ok(n), buf)
118        }
119        Err(e) => BufResult(Err(e), buf),
120    }
121}
122
123/// Build borrowed `IoSlice`s over the initialized bytes of each buffer in an
124/// owned vectored buffer and hand them to `f` for a single vectored write.
125///
126/// The slices borrow `buf`, so they stay valid only for the duration of `f`.
127/// A `SmallVec` keeps the common case (a frame header plus a handful of frames)
128/// off the heap; it spills to a `Vec` only past 16 buffers. Centralized here so
129/// the smol `writev` path and the instruction-count bench share one builder.
130pub fn with_vectored_slices<B, R>(buf: &B, f: impl FnOnce(&[IoSlice<'_>]) -> R) -> R
131where
132    B: IoVectoredBuf,
133{
134    let slices: SmallVec<[IoSlice<'_>; 16]> = buf.iter_slice().map(IoSlice::new).collect();
135    f(&slices)
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn small_frame_copy_releases_the_slab() {
144        use bytes::Bytes;
145
146        // One 64 KiB slab, fully initialized so we can read pointers safely.
147        let mut stash = BytesMut::new();
148        stash.resize(READ_SLAB_SIZE, 0);
149        let slab_ptr = stash.as_ptr();
150
151        // Freezing a small frame in place shares the slab: the frozen Bytes
152        // points into the original 64 KiB allocation, pinning all of it.
153        let mut a = unsafe { take_read_buffer(&mut stash, 8192) };
154        a.truncate(10);
155        let a_ptr = a.as_ptr();
156        assert_eq!(
157            a_ptr, slab_ptr,
158            "chunk should be carved from the slab front"
159        );
160        let frozen = a.freeze();
161        assert_eq!(
162            frozen.as_ptr(),
163            slab_ptr,
164            "freeze() shares the slab allocation"
165        );
166
167        // Copying a small frame out yields a fresh, right-sized allocation that
168        // does not reference the slab, so the slab is free once the chunk drops.
169        let mut b = unsafe { take_read_buffer(&mut stash, 8192) };
170        b.truncate(10);
171        let b_ptr = b.as_ptr();
172        let copied = Bytes::copy_from_slice(&b);
173        assert_ne!(
174            copied.as_ptr(),
175            b_ptr,
176            "copy_from_slice must allocate off the slab, not alias it"
177        );
178        // The 10-byte frame above is well below the copy-out threshold, so the
179        // read path takes the copy branch for it.
180        let _ = COPY_OUT_THRESHOLD;
181    }
182
183    #[test]
184    fn take_read_buffer_over_slab_size_does_not_panic_or_corrupt() {
185        // Regression (migrated from the old arena): a read_size larger than the
186        // slab size once overflowed the backing page and panicked in freeze()
187        // with "range end out of bounds". take_read_buffer must grow to fit.
188        let mut stash = BytesMut::with_capacity(READ_SLAB_SIZE);
189        let read_size = READ_SLAB_SIZE + 1024;
190        // SAFETY: bookkeeping-only test; we truncate to the full length and
191        // never rely on the (uninitialized) contents.
192        let mut buf = unsafe { take_read_buffer(&mut stash, read_size) };
193        assert_eq!(buf.len(), read_size);
194        buf.truncate(read_size);
195        assert_eq!(buf.freeze().len(), read_size);
196    }
197
198    #[test]
199    fn take_read_buffer_splits_front_and_keeps_tail() {
200        let mut stash = BytesMut::with_capacity(READ_SLAB_SIZE);
201        // SAFETY: bookkeeping-only test; contents are never inspected.
202        let buf = unsafe { take_read_buffer(&mut stash, 256) };
203        assert_eq!(buf.len(), 256);
204        assert_eq!(stash.len(), 0);
205        assert!(stash.capacity() >= READ_SLAB_SIZE - 256);
206    }
207
208    #[test]
209    fn take_read_buffer_reuses_one_slab_across_reads() {
210        // Successive sub-slab reads carve from the same allocation, the way the
211        // bump-pointer arena shared a page: the stash tail shrinks by exactly
212        // read_size each time, with no fresh 64 KiB allocation.
213        let mut stash = BytesMut::with_capacity(READ_SLAB_SIZE);
214        // SAFETY: bookkeeping-only test; contents are never inspected.
215        let _first = unsafe { take_read_buffer(&mut stash, 4096) };
216        assert_eq!(stash.capacity(), READ_SLAB_SIZE - 4096);
217        // SAFETY: bookkeeping-only test.
218        let _second = unsafe { take_read_buffer(&mut stash, 4096) };
219        assert_eq!(stash.capacity(), READ_SLAB_SIZE - 8192);
220    }
221}