Skip to main content

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}