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/// Take a read-sized scratch buffer from the front of `stash`, leaving the
36/// remaining tail in `stash` to hand out on the next call.
37///
38/// This replaces the old bump-pointer read arena: successive reads carve
39/// `read_size` chunks off one `READ_SLAB_SIZE` allocation until it is used up,
40/// then a fresh slab is allocated. Freezing a returned buffer shares that
41/// slab's allocation (via `bytes` refcounting), so a lagging consumer pins the
42/// slab exactly as the arena page did.
43///
44/// # Safety
45///
46/// The returned buffer reports `read_size` initialized bytes that are in fact
47/// uninitialized, so it can be handed to an owned-buffer read without
48/// zero-filling first. The caller must pass it straight to a read and
49/// `truncate` it to the number of bytes actually read before freezing,
50/// inspecting, or otherwise exposing its contents.
51pub unsafe fn take_read_buffer(stash: &mut BytesMut, read_size: usize) -> BytesMut {
52    if stash.capacity() < read_size {
53        *stash = BytesMut::with_capacity(read_size.max(READ_SLAB_SIZE));
54    }
55    if stash.len() < read_size {
56        // SAFETY: `read_size` is within capacity per the check above. The
57        // function's contract requires the caller to overwrite these bytes via
58        // the read and truncate to the real count before exposing them.
59        unsafe { stash.set_len(read_size) };
60    }
61    let tail = stash.split_off(read_size);
62    std::mem::replace(stash, tail)
63}
64
65/// Read into an owned buffer's spare capacity, then declare the bytes written
66/// as initialized.
67///
68/// This is the one place in the workspace that calls `set_len` (from compio's
69/// `SetLen`). Every runtime backend routes its read path through here, so the
70/// single owned-buffer `unsafe` block lives behind one documented contract
71/// instead of a copy per adapter.
72///
73/// `read` is an async closure handed the buffer's uninitialized spare capacity
74/// as `&mut [MaybeUninit<u8>]`; it performs the actual read into the front of
75/// that slice: tokio wraps it in a `ReadBuf`, smol hands it to `recv`, so the
76/// read mechanism stays with the backend while the buffer bookkeeping stays
77/// here. `AsyncFnOnce` lets the returned future borrow the spare slice, which a
78/// plain `FnOnce` returning a future cannot.
79///
80/// # Contract
81///
82/// On `Ok(n)`, `read` must have initialized exactly the first `n` bytes of the
83/// slice it was given (`n` never exceeds the slice length). `set_buf_init(n)`
84/// then declares precisely those bytes live, matching what was written. A
85/// backend that reported more bytes than it wrote, or wrote them anywhere but
86/// the front of the slice, would break this contract and the `unsafe` below.
87pub async fn fill_read<B, F>(mut buf: B, read: F) -> BufResult<usize, B>
88where
89    B: IoBufMut,
90    F: AsyncFnOnce(&mut [MaybeUninit<u8>]) -> io::Result<usize>,
91{
92    // Scope the spare-capacity borrow to the read so `buf` is free to mutate
93    // again once the count is known.
94    let (outcome, spare_len) = {
95        let spare = buf.as_uninit();
96        let spare_len = spare.len();
97        (read(spare).await, spare_len)
98    };
99    match outcome {
100        Ok(n) => {
101            // A backend that reports more bytes than the spare slice held would
102            // make the set_len below declare uninitialized (or out-of-bounds)
103            // memory live. Catch that contract violation in debug builds.
104            debug_assert!(
105                n <= spare_len,
106                "read reported {n} bytes but the spare slice was only {spare_len}"
107            );
108            // SAFETY: per this function's contract, `read` initialized exactly
109            // the first `n` bytes of the spare slice it was handed. Declaring
110            // that same length initialized matches what was actually written.
111            unsafe {
112                buf.set_len(n);
113            }
114            BufResult(Ok(n), buf)
115        }
116        Err(e) => BufResult(Err(e), buf),
117    }
118}
119
120/// Build borrowed `IoSlice`s over the initialized bytes of each buffer in an
121/// owned vectored buffer and hand them to `f` for a single vectored write.
122///
123/// The slices borrow `buf`, so they stay valid only for the duration of `f`.
124/// A `SmallVec` keeps the common case (a frame header plus a handful of frames)
125/// off the heap; it spills to a `Vec` only past 16 buffers. Centralized here so
126/// the smol `writev` path and the instruction-count bench share one builder.
127pub fn with_vectored_slices<B, R>(buf: &B, f: impl FnOnce(&[IoSlice<'_>]) -> R) -> R
128where
129    B: IoVectoredBuf,
130{
131    let slices: SmallVec<[IoSlice<'_>; 16]> = buf.iter_slice().map(IoSlice::new).collect();
132    f(&slices)
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn reclaim_makes_slab_use_track_bytes_read_and_freezes_only_read_bytes() {
141        // One 64 KiB slab, fully initialized so we can read pointers/contents.
142        let mut stash = BytesMut::new();
143        stash.resize(READ_SLAB_SIZE, 0);
144        let slab_ptr = stash.as_ptr();
145
146        // Carve a read buffer, "read" 10 bytes into it, and reclaim the unused
147        // tail back into the slab (the read_raw hot path).
148        let mut buf = unsafe { take_read_buffer(&mut stash, 8192) };
149        assert_eq!(buf.as_ptr(), slab_ptr, "carved from the slab front");
150        buf[..10].copy_from_slice(b"0123456789");
151        buf.truncate(10);
152        let mut reclaimed = buf.split_off(10);
153        // Scratch len so unsplit does not discard the empty tail (see read_raw).
154        // SAFETY: uninitialized scratch, overwritten before the next read.
155        unsafe {
156            reclaimed.set_len(reclaimed.capacity());
157        }
158        reclaimed.unsplit(std::mem::take(&mut stash));
159        stash = reclaimed;
160
161        // Freezing exposes exactly the 10 bytes read - never any of the
162        // uninitialized carve.
163        let frozen = buf.freeze();
164        assert_eq!(frozen.as_ref(), b"0123456789");
165        assert_eq!(
166            frozen.as_ptr(),
167            slab_ptr,
168            "frozen frame aliases the slab head"
169        );
170
171        // The reclaim returned the unused bytes: the next carve advances by the
172        // 10 bytes read, not the full 8192 carve, and reuses the same slab.
173        let next = unsafe { take_read_buffer(&mut stash, 8192) };
174        assert_eq!(
175            next.as_ptr() as usize,
176            slab_ptr as usize + 10,
177            "the next carve reuses the reclaimed space (slab use tracks bytes read)"
178        );
179    }
180
181    #[test]
182    fn take_read_buffer_over_slab_size_does_not_panic_or_corrupt() {
183        // Regression (migrated from the old arena): a read_size larger than the
184        // slab size once overflowed the backing page and panicked in freeze()
185        // with "range end out of bounds". take_read_buffer must grow to fit.
186        let mut stash = BytesMut::with_capacity(READ_SLAB_SIZE);
187        let read_size = READ_SLAB_SIZE + 1024;
188        // SAFETY: bookkeeping-only test; we truncate to the full length and
189        // never rely on the (uninitialized) contents.
190        let mut buf = unsafe { take_read_buffer(&mut stash, read_size) };
191        assert_eq!(buf.len(), read_size);
192        buf.truncate(read_size);
193        assert_eq!(buf.freeze().len(), read_size);
194    }
195
196    #[test]
197    fn take_read_buffer_splits_front_and_keeps_tail() {
198        let mut stash = BytesMut::with_capacity(READ_SLAB_SIZE);
199        // SAFETY: bookkeeping-only test; contents are never inspected.
200        let buf = unsafe { take_read_buffer(&mut stash, 256) };
201        assert_eq!(buf.len(), 256);
202        assert_eq!(stash.len(), 0);
203        assert!(stash.capacity() >= READ_SLAB_SIZE - 256);
204    }
205
206    #[test]
207    fn take_read_buffer_reuses_one_slab_across_reads() {
208        // Successive sub-slab reads carve from the same allocation, the way the
209        // bump-pointer arena shared a page: the stash tail shrinks by exactly
210        // read_size each time, with no fresh 64 KiB allocation.
211        let mut stash = BytesMut::with_capacity(READ_SLAB_SIZE);
212        // SAFETY: bookkeeping-only test; contents are never inspected.
213        let _first = unsafe { take_read_buffer(&mut stash, 4096) };
214        assert_eq!(stash.capacity(), READ_SLAB_SIZE - 4096);
215        // SAFETY: bookkeeping-only test.
216        let _second = unsafe { take_read_buffer(&mut stash, 4096) };
217        assert_eq!(stash.capacity(), READ_SLAB_SIZE - 8192);
218    }
219}