Skip to main content

commonware_runtime/iobuf/
mod.rs

1//! Buffer types for I/O operations.
2//!
3//! `IoBuf` and `IoBufMut` store readable/writable cursor state directly in the
4//! public handle. Allocation ownership lives in a compact tagged owner
5//! reference: runtime-owned heap buffers keep a header inside their own
6//! allocation (in front of the data for low-alignment mutable buffers, at the
7//! tail for high-alignment ones and adopted vecs), pooled buffers keep their
8//! owner record in a per-slot side table owned by the size class,
9//! caller-supplied `Vec<u8>` values converted to immutable buffers are adopted
10//! into the native heap form when their spare capacity allows (mutable
11//! conversions copy to preserve the caller's capacity), and caller-supplied
12//! [`Bytes`] values are held zero-copy by a small external owner. This keeps
13//! `bytes::Buf` and `bytes::BufMut` hot paths as simple pointer/length
14//! arithmetic. `owner.rs` documents the owner model.
15//!
16//! Throughout this module, "native" means runtime-owned storage whose owner
17//! supports zero-copy mutable recovery through [`IoBuf::try_into_mut`]: heap
18//! allocations (front or tail header), pooled buffers, and adopted vecs, as
19//! opposed to external `Bytes` and `'static` views.
20//!
21//! # Conversions
22//!
23//! Every `From` conversion into [`IoBuf`] or [`IoBufs`] is zero-copy: the
24//! payload is never copied. Most conversions require at most one small owner
25//! allocation. A `Vec<u8>` that cannot host an inline owner may require two
26//! small metadata allocations: one for `bytes` shared ownership and one for
27//! the external owner. Conversions into [`IoBufMut`] or [`IoBufsMut`] are
28//! zero-copy where the source allocation can back a mutable handle and copy
29//! otherwise. Each mutable conversion documents which one it is, and
30//! conversions out of the handles document their cost on each impl.
31//!
32//! Because untracked heap buffers embed their owner header in the same
33//! allocation, a power-of-two capacity request may land in the allocator's
34//! next size bin. Pooled buffers do not pay this: their side-table record
35//! keeps the data allocation exactly the class size.
36//!
37//! Public types:
38//! - [`IoBuf`]: Immutable byte buffer
39//! - [`IoBufMut`]: Mutable byte buffer
40//! - [`IoBufs`]: Container for one or more immutable buffers
41//! - [`IoBufsMut`]: Container for one or more mutable buffers
42//! - [`BufferPool`]: Pool of reusable, aligned buffers
43//! - [`Builder`]: Assembles [`IoBufs`] from inline writes and zero-copy pieces
44//!
45//! # Examples
46//!
47//! The core lifecycle: fill a fixed-capacity mutable buffer, freeze it into
48//! cheaply cloneable immutable views, and recover the mutable handle (with
49//! its spare capacity) once the views are gone:
50//!
51//! ```
52//! use commonware_runtime::{Buf, BufMut, IoBuf, IoBufMut};
53//!
54//! let mut buf = IoBufMut::with_capacity(8);
55//! buf.put_slice(b"abcdef");
56//!
57//! let frozen: IoBuf = buf.freeze();
58//! let head = frozen.slice(..3);
59//! assert_eq!(head, b"abc"[..]);
60//!
61//! // A live view shares the owner, so recovery declines.
62//! let frozen = frozen.try_into_mut().unwrap_err();
63//! drop(head);
64//!
65//! // Unique again: the mutable handle returns with its spare capacity.
66//! let mut recovered = frozen.try_into_mut().unwrap();
67//! assert_eq!(recovered.as_ref(), b"abcdef");
68//! assert_eq!(recovered.capacity(), 8);
69//! recovered.put_slice(b"gh");
70//! ```
71//!
72//! [`Bytes`]: bytes::Bytes
73
74mod buf;
75mod bufs;
76mod owner;
77mod pool;
78
79pub use buf::{IoBuf, IoBufMut};
80pub use bufs::{Builder, EncodeExt, IoBufs, IoBufsMut};
81use crossbeam_utils::CachePadded;
82pub use pool::{
83    BufferPool, BufferPoolClassConfig, BufferPoolConfig, BufferPoolThreadCache, PoolError,
84};
85use std::mem::align_of;
86
87/// Returns the system page size.
88///
89/// On Unix systems, queries the actual page size via `sysconf`.
90/// On WebAssembly, defaults to 4KB.
91#[allow(clippy::missing_const_for_fn)]
92pub fn page_size() -> usize {
93    #[cfg(unix)]
94    {
95        // SAFETY: sysconf is safe to call.
96        let size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
97        if size <= 0 {
98            4096 // Safe fallback if sysconf fails
99        } else {
100            size as usize
101        }
102    }
103
104    #[cfg(not(unix))]
105    {
106        4096
107    }
108}
109
110/// Returns the cache line size for the current architecture.
111pub const fn cache_line_size() -> usize {
112    align_of::<CachePadded<u8>>()
113}
114
115/// Panics for cursor or write operations that run past the available region.
116///
117/// Outlined so the `Buf`/`BufMut` fast paths inline as a compare, a branch,
118/// and a memcpy, mirroring the panic helpers in `bytes`.
119#[cold]
120#[inline(never)]
121fn panic_advance(requested: usize, available: usize) -> ! {
122    panic!("cannot advance past end of buffer: requested {requested}, available {available}");
123}
124
125/// Benchmark-only access to internal pool machinery.
126///
127/// Raw pooled buffers reference owner metadata stored by their freelist.
128/// Taking one requires the caller to keep that freelist alive, and returning
129/// one requires proof that it came from the target freelist:
130///
131/// ```compile_fail,E0133
132/// use commonware_runtime::iobuf::bench::{Freelist, PooledBuffer};
133///
134/// fn return_buffer(freelist: &Freelist, buffer: PooledBuffer) {
135///     freelist.put(buffer);
136/// }
137/// ```
138#[doc(hidden)]
139#[cfg(feature = "bench")]
140pub mod bench {
141    pub use super::owner::{PooledBuffer, PooledOwner};
142    use std::{
143        alloc::Layout,
144        num::{NonZeroU32, NonZeroUsize},
145    };
146
147    /// Raw freelist access for benchmarks.
148    ///
149    /// Pooled buffers carry no type-level identity for their originating
150    /// freelist, so return operations are unsafe at this public boundary.
151    pub struct Freelist(super::pool::Freelist);
152
153    impl Freelist {
154        /// Creates a fixed-capacity benchmark freelist.
155        pub fn new(
156            capacity: NonZeroU32,
157            parallelism: NonZeroUsize,
158            layout: Layout,
159            prefill: bool,
160        ) -> Self {
161            Self(super::pool::Freelist::new(
162                capacity,
163                parallelism,
164                layout,
165                prefill,
166            ))
167        }
168
169        /// Returns one available pooled buffer.
170        ///
171        /// ```compile_fail,E0133
172        /// use commonware_runtime::iobuf::bench::Freelist;
173        ///
174        /// fn take(freelist: &Freelist) {
175        ///     let _buffer = freelist.take();
176        /// }
177        /// ```
178        ///
179        /// # Safety
180        ///
181        /// The returned buffer references owner metadata stored by this
182        /// freelist. This freelist must remain alive until the buffer is
183        /// returned here or deallocated.
184        #[inline]
185        pub unsafe fn take(&self) -> Option<PooledBuffer> {
186            self.0.take()
187        }
188
189        /// Returns up to `max` available pooled buffers to `on_entry`.
190        ///
191        /// `on_entry` must not panic. A panic can strand claimed buffers
192        /// outside the freelist and leak their allocations.
193        ///
194        /// ```compile_fail,E0133
195        /// use commonware_runtime::iobuf::bench::Freelist;
196        ///
197        /// fn take_batch(freelist: &Freelist) {
198        ///     freelist.take_batch(1, |_| {});
199        /// }
200        /// ```
201        ///
202        /// # Safety
203        ///
204        /// Every buffer passed to `on_entry` references owner metadata stored
205        /// by this freelist. This freelist must remain alive until all such
206        /// buffers are returned here or deallocated.
207        #[inline]
208        pub unsafe fn take_batch(&self, max: usize, on_entry: impl FnMut(PooledBuffer)) -> usize {
209            self.0.take_batch(max, on_entry)
210        }
211
212        /// Returns one pooled buffer to this freelist.
213        ///
214        /// # Safety
215        ///
216        /// `buffer` must have been taken from this freelist, and its slot must
217        /// not already be available here.
218        #[inline]
219        pub unsafe fn put(&self, buffer: PooledBuffer) {
220            self.0.put(buffer);
221        }
222
223        /// Returns several pooled buffers to this freelist.
224        ///
225        /// If the iterator panics, buffers already accepted by this method may
226        /// leak.
227        ///
228        /// # Safety
229        ///
230        /// Every buffer must have been taken from this freelist. Their slots
231        /// must be unique within the batch and unavailable in the freelist.
232        #[inline]
233        pub unsafe fn put_batch(&self, buffers: impl IntoIterator<Item = PooledBuffer>) {
234            self.0.put_batch(buffers);
235        }
236
237        /// Drops every currently available pooled buffer.
238        #[inline]
239        pub fn drain(&self) -> usize {
240            self.0.drain()
241        }
242    }
243}