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}