simple_someip/buffer_pool.rs
1//! Fixed-capacity pool of byte buffers with claim/release semantics,
2//! mirroring the channel pools in this module. A `BufferPool` is declared as
3//! a `static` (bare-metal) or held behind an `Arc` (std/tokio) by the
4//! consumer; each claim hands out one slot as a `BufferLease` (a raw
5//! `NonNull` slice whose exclusivity is enforced by the per-slot
6//! `AtomicBool`, not the borrow checker) that returns the slot on drop.
7//!
8//! Synchronization uses per-slot `AtomicBool` compare-exchange so the same
9//! code is valid on the bare-metal target and on std without requiring a
10//! `critical-section` implementation.
11
12use core::cell::UnsafeCell;
13use core::ops::{Deref, DerefMut};
14use core::ptr::NonNull;
15use core::sync::atomic::{AtomicBool, Ordering};
16
17/// Fixed-capacity pool of `LEN`-byte buffers. Declare as a `static` and call
18/// [`Self::claim`] to obtain a [`BufferLease`].
19///
20/// # Const-constructible
21///
22/// `BufferPool::new()` is `const fn`, so pools can be declared as `static`
23/// items initialized at link time with no runtime cost.
24///
25/// # Minimum slot length
26///
27/// `LEN` must be at least 16 bytes — the size of a SOME/IP header — or the
28/// client silently drops all inbound and rejects all sends. A compile-time
29/// `const` assertion in [`Self::new`] enforces this floor. 16 is only the
30/// absolute header minimum: in practice a slot must hold the largest expected
31/// message (header + payload), realistically one full UDP datagram (see
32/// [`crate::UDP_BUFFER_SIZE`]).
33///
34/// # Synchronization
35///
36/// Each slot has an independent `AtomicBool` claimed flag. `claim()` scans
37/// for the first free slot and atomically claims it via
38/// `compare_exchange(false, true, AcqRel, Acquire)`. `Drop` releases via
39/// `store(false, Release)`. No global lock is taken; claim and release are
40/// individually linearizable.
41pub struct BufferPool<const SLOTS: usize, const LEN: usize> {
42 // `UnsafeCell` because claims hand out raw `NonNull` slices into this
43 // store; the per-slot `claimed` AtomicBool (not the borrow checker) is
44 // what guarantees at most one live lease per slot.
45 store: UnsafeCell<[[u8; LEN]; SLOTS]>,
46 // One atomic flag per slot; `true` = slot is currently claimed.
47 claimed: [AtomicBool; SLOTS],
48}
49
50// SAFETY: `BufferPool` is Sync because:
51// - `claimed` is an array of `AtomicBool`, which is already Sync.
52// - Access to `store` is strictly gated: a slot's bytes are only touched
53// while its `claimed` flag is held (compare_exchange'd to true), which
54// ensures at most one live `&mut` per slot at any time.
55unsafe impl<const SLOTS: usize, const LEN: usize> Sync for BufferPool<SLOTS, LEN> {}
56
57impl<const SLOTS: usize, const LEN: usize> BufferPool<SLOTS, LEN> {
58 /// Create a new, empty pool. All slots are free.
59 ///
60 /// # Panics (compile-time)
61 ///
62 /// A `const` assertion rejects `LEN < 16` at compile time: a slot must be
63 /// large enough to hold a 16-byte SOME/IP header, otherwise the client
64 /// silently drops all inbound and rejects all sends.
65 #[must_use]
66 pub const fn new() -> Self {
67 // Compile-time floor: a slot must hold at least a SOME/IP header.
68 // Placed in `const {}` so it is evaluated during const-eval of every
69 // monomorphization that constructs a pool (e.g. the `static` init).
70 const {
71 assert!(
72 LEN >= 16,
73 "BufferPool slot must hold at least a 16-byte SOME/IP header"
74 );
75 };
76 Self {
77 store: UnsafeCell::new([[0u8; LEN]; SLOTS]),
78 claimed: [const { AtomicBool::new(false) }; SLOTS],
79 }
80 }
81
82 /// Claim a free slot, returning a [`BufferLease`], or `None` if all
83 /// `SLOTS` are in use.
84 ///
85 /// The returned buffer is zeroed before hand-out so a reused slot never
86 /// leaks the previous tenant's bytes.
87 pub fn claim(&'static self) -> Option<BufferLease> {
88 let (buf, flag) = self.try_claim_slot()?;
89 Some(BufferLease {
90 buf,
91 len: LEN,
92 flag,
93 // Truly-'static pool (bare-metal static-pool path): nothing to
94 // keep alive — the backing store outlives the lease via `'static`.
95 // No allocation on this path. The `_owner` field only exists when
96 // `alloc` is available; under `bare_metal` it is cfg'd out.
97 #[cfg(feature = "_alloc")]
98 _owner: None,
99 })
100 }
101
102 /// Scan for a free slot and atomically claim it. On success returns the
103 /// `NonNull` start-of-slot pointer and a `NonNull` to that slot's claimed
104 /// flag; on exhaustion returns `None`.
105 ///
106 /// Both pointers reference memory owned by `self`; the caller is
107 /// responsible for keeping `self` alive for as long as the pointers are
108 /// used (via `'static` or an `Arc` clone held in the `BufferLease`).
109 fn try_claim_slot(&self) -> Option<(NonNull<u8>, NonNull<AtomicBool>)> {
110 for (idx, flag) in self.claimed.iter().enumerate() {
111 // Attempt to atomically claim this slot.
112 if flag
113 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
114 .is_ok()
115 {
116 // SAFETY: we just won the compare_exchange on `claimed[idx]`,
117 // so no other live lease references slot `idx`. We derive the
118 // slot pointer by raw-pointer arithmetic to avoid forming a
119 // `&mut` to the whole array (which would alias already-claimed
120 // slots).
121 let slot_ptr = unsafe { self.store.get().cast::<u8>().add(idx * LEN) };
122 // Zero the freshly-claimed slot so a reused slot never leaks
123 // the previous tenant's bytes.
124 //
125 // SAFETY: `slot_ptr` is the start of slot `idx`, in bounds for
126 // `LEN` bytes, and we hold the exclusive claim on it; no other
127 // reference aliases these bytes.
128 unsafe { core::ptr::write_bytes(slot_ptr, 0, LEN) };
129 // SAFETY: `slot_ptr` derives from `self.store.get()` (non-null)
130 // plus an in-bounds offset; `flag` is an element of the
131 // `claimed` array (non-null).
132 let buf = unsafe { NonNull::new_unchecked(slot_ptr) };
133 let flag = NonNull::from(flag);
134 return Some((buf, flag));
135 }
136 }
137 None
138 }
139}
140
141#[cfg(feature = "_alloc")]
142impl<const SLOTS: usize, const LEN: usize> BufferPool<SLOTS, LEN> {
143 /// Claim a free slot from an `Arc`-backed pool, returning a [`BufferLease`]
144 /// that holds an `Arc` clone to keep the pool alive for the lease's
145 /// lifetime, or `None` if all `SLOTS` are in use.
146 ///
147 /// This is the heap-backed counterpart to [`Self::claim`]: the static-pool
148 /// path uses `&'static self` and stores `_owner: None`; this path stores
149 /// `_owner: Some(arc.clone())` so the pool's backing store (and the slot's
150 /// claimed flag) stay valid until the last lease and provider drop. Only
151 /// compiled where `alloc` is available (the `_alloc` feature), so the
152 /// bare-metal `client,bare_metal` build stays allocation-free.
153 pub fn claim_arc(self: &alloc::sync::Arc<Self>) -> Option<BufferLease> {
154 let (buf, flag) = self.try_claim_slot()?;
155 Some(BufferLease {
156 buf,
157 len: LEN,
158 flag,
159 // Keep the Arc'd pool alive for the lease's lifetime. The pool is
160 // `Send + Sync` (see the `unsafe impl Sync` above and the `Send`
161 // bounds on its contents), so `Arc<Self>` coerces to
162 // `Arc<dyn Any + Send + Sync>`.
163 _owner: Some(self.clone()),
164 })
165 }
166}
167
168impl<const SLOTS: usize, const LEN: usize> Default for BufferPool<SLOTS, LEN> {
169 fn default() -> Self {
170 Self::new()
171 }
172}
173
174impl<const SLOTS: usize, const LEN: usize> core::fmt::Debug for BufferPool<SLOTS, LEN> {
175 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
176 f.debug_struct("BufferPool")
177 .field("slots", &SLOTS)
178 .field("len", &LEN)
179 .finish_non_exhaustive()
180 }
181}
182
183/// RAII handle to one claimed buffer from a [`BufferPool`].
184///
185/// Derefs to `[u8]` for read/write access. Returns the slot to its pool on
186/// drop.
187///
188/// # Two backing strategies
189///
190/// - **Static pool** (bare-metal): the pool is a `&'static BufferPool`, so the
191/// slot's memory and claimed flag are valid for the program's whole life.
192/// `_owner` is `None`; no allocation.
193/// - **`Arc`-backed pool** (tokio/std): the pool lives behind an `Arc`. The
194/// lease holds an `Arc` clone in `_owner` so the slot's memory and flag stay
195/// valid until the last lease *and* the provider drop, at which point the
196/// pool is freed — no per-client leak.
197pub struct BufferLease {
198 /// Start of the claimed slot.
199 buf: NonNull<u8>,
200 /// Length of the slot, in bytes.
201 len: usize,
202 /// This slot's claimed flag in the owning pool.
203 flag: NonNull<AtomicBool>,
204 /// Keeps an `Arc`-backed pool alive for the lease's lifetime; `None` for a
205 /// truly-`'static` (bare-metal) pool. Drop order: the flag is cleared
206 /// first (the raw pointer is still valid via `_owner` for the Arc case, or
207 /// `'static` for the static case), then `_owner` drops, releasing the
208 /// pool's last reference if this was the final holder.
209 #[cfg(feature = "_alloc")]
210 _owner: Option<alloc::sync::Arc<dyn core::any::Any + Send + Sync>>,
211}
212
213// SAFETY: `BufferLease` is `Send` because:
214// - The lease owns exclusive access to its slot, enforced by the pool's
215// per-slot `AtomicBool` (won via compare_exchange in `try_claim_slot`); no
216// other live lease can reference the same slot bytes.
217// - The raw `NonNull<u8>` / `NonNull<AtomicBool>` pointers are themselves
218// `Send` only by this `unsafe impl`; they reference memory kept alive
219// either by `'static` (static path) or by the `Arc` in `_owner`, which is
220// `Arc<dyn Any + Send + Sync>` and hence `Send`. Sending the lease to
221// another thread moves all of these together, so the slot's memory and
222// flag remain valid and exclusively owned.
223unsafe impl Send for BufferLease {}
224
225impl Deref for BufferLease {
226 type Target = [u8];
227 fn deref(&self) -> &[u8] {
228 // SAFETY: `buf` points at the start of an exclusively-claimed slot of
229 // `len` bytes, kept alive by `_owner`/`'static`. We hold `&self`, so
230 // an immutable slice is sound (no concurrent `&mut` exists — the
231 // claimed flag guarantees a single live lease per slot).
232 unsafe { core::slice::from_raw_parts(self.buf.as_ptr(), self.len) }
233 }
234}
235
236impl DerefMut for BufferLease {
237 fn deref_mut(&mut self) -> &mut [u8] {
238 // SAFETY: as in `deref`, plus we hold `&mut self`, so a mutable slice
239 // is the unique reference to these bytes.
240 unsafe { core::slice::from_raw_parts_mut(self.buf.as_ptr(), self.len) }
241 }
242}
243
244impl Drop for BufferLease {
245 fn drop(&mut self) {
246 // Release the slot atomically. The flag memory is still valid here:
247 // for the static path it is `'static`; for the Arc path `_owner` (which
248 // drops *after* this block) still holds a live reference to the pool.
249 // Any subsequent claim that acquires this flag will see the updated
250 // store state.
251 //
252 // SAFETY: `flag` references this slot's `AtomicBool` inside the pool,
253 // valid for the reasons above.
254 unsafe { self.flag.as_ref() }.store(false, Ordering::Release);
255 // `_owner` (if any) drops after this, releasing the pool's last
256 // reference when this is the final holder.
257 }
258}