odem_rs_util/pool.rs
1//! This module defines an arena allocator for [`Active`] simulation objects
2//! that performs automatic garbage collection on terminated and unreferenced
3//! instances.
4//!
5//! # Overview
6//!
7//! The primary components are:
8//!
9//! - [`Pool`]: The main arena allocator. It manages a backing store and a
10//! list of recyclable memory slots. It must be pinned to guarantee stable
11//! addresses for the active entities it allocates.
12//! - [`Storage`]: A trait that abstracts over the memory source for the pool,
13//! allowing for both fixed-size (`no_alloc`) and dynamically growing
14//! (`alloc`) backing stores.
15//! - [`Gc<A>`]: A wrapper around an `Active` type `A`. When the last
16//! reference to a `Gc` is dropped, it automatically returns its memory slot
17//! to the pool for recycling instead of deallocating it.
18//!
19//! # Examples
20//!
21//! Creating a statically sized pool for `no_alloc` environments.
22//! ```
23//! # use core::pin::pin;
24//! # use odem_rs_util::pool::Pool;
25//! # use odem_rs_core::{config::Config, job::Job, simulator::Sim};
26//! # #[derive(Default, Config)] struct MyConfig;
27//! async fn sim_main(sim: &Sim<MyConfig>) {
28//! // Create a pool for up to five elements.
29//! let pool = pin!(Pool::fixed::<5>());
30//!
31//! // Allocate a new Job and activate it in the simulation.
32//! // The Gc wrapper ensures its memory is recycled when it's done.
33//! sim.activate(pool.alloc(Job::new(async {})));
34//! }
35//! ```
36//!
37//! Creating a dynamically growing pool for `alloc` environments.
38//! ```
39//! # use core::pin::pin;
40//! # use odem_rs_util::pool::Pool;
41//! # use odem_rs_core::{config::Config, job::Job, simulator::Sim};
42//! # #[derive(Default, Config)] struct MyConfig;
43//! async fn sim_main(sim: &Sim<MyConfig>) {
44//! // Create a dynamically growing pool for jobs.
45//! let pool = pin!(Pool::dynamic().with(Job::new));
46//!
47//! // Allocate a new Job and activate it in the simulation.
48//! // The Gc wrapper ensures its memory is recycled when it's done.
49//! sim.activate(pool.alloc(async {}));
50//! }
51//! ```
52//!
53//! [`Arena`]: typed_arena::Arena
54#![allow(missing_docs)]
55
56use core::{
57 cell::Cell,
58 pin::Pin,
59 ptr::NonNull,
60 task::{Context, Poll},
61};
62
63use intrusive_collections::{SinglyLinkedList, SinglyLinkedListLink, UnsafeRef};
64
65use odem_rs_core::{Active, Dispatch, ExitStatus, config::Config, continuation::Share, ptr::Lease};
66
67use crate::error::InsufficientSpace;
68
69#[doc(hidden)]
70mod adapter;
71
72mod backing;
73
74#[cfg(test)]
75mod tests;
76
77pub use adapter::*;
78pub use backing::*;
79
80/* ********************************************************** Arena Allocator */
81
82/// An arena allocator for [Active] simulation objects that performs automatic
83/// garbage collection on terminated and unreferenced instances.
84///
85/// This pool manages a [`Storage`] backend and a `reclaim` list of memory
86/// slots that are ready to be reused.
87///
88/// # Safety Notes
89///
90/// Rearranging the fields here can lead to undefined behavior upon dropping of
91/// the pool. Specifically, dropping `storage` before `reclaim` will invalidate
92/// the slots contained in `reclaim`, leading to undefined behavior when
93/// `reclaim` is dropped later.
94#[pin_project::pin_project]
95pub struct Pool<A, S: ?Sized = dyn Storage<Slot = Gc<A>>, F = ()> {
96 /// A linked list of terminated instances ready to be recycled.
97 reclaim: Cell<SinglyLinkedList<GcAdapter<A>>>,
98 /// A constructor function that constructs the value before it is placed
99 /// inside a slot of the storage.
100 cons: F,
101 /// A backing store used to allocate more instances.
102 #[pin]
103 storage: S,
104}
105
106impl<A> Pool<A> {
107 /// Creates a new pool given a backing store.
108 pub fn new<S>(storage: S) -> Pool<A, S> {
109 Pool {
110 reclaim: Cell::new(SinglyLinkedList::new(GcAdapter::new())),
111 cons: (),
112 storage,
113 }
114 }
115
116 /// Creates a pool with a fixed-size backing store.
117 ///
118 /// This pool is suitable for `no_std` + `no_alloc` environments. It will
119 /// panic if an allocation is attempted when the pool is full.
120 pub fn fixed<const N: usize>() -> Pool<A, Array<Gc<A>, N>> {
121 Pool::new(Array::new())
122 }
123
124 /// Creates a pool with a dynamically growing backing store.
125 #[cfg(feature = "alloc")]
126 #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
127 pub fn dynamic() -> Pool<A, typed_arena::Arena<Gc<A>>> {
128 Pool::new(typed_arena::Arena::new())
129 }
130
131 /// Creates a dynamically growing pool with a specified initial capacity.
132 #[cfg(feature = "alloc")]
133 #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
134 pub fn with_capacity(n: usize) -> Pool<A, typed_arena::Arena<Gc<A>>> {
135 Pool::new(typed_arena::Arena::with_capacity(n))
136 }
137}
138
139impl<A, S: Storage<Slot = Gc<A>>> Pool<A, S> {
140 /// Sets a new constructor function for the pool.
141 pub fn with<'p, B, F>(self, cons: F) -> Pool<A, S, F>
142 where
143 F: Fn(B) -> Lease<'p, A>,
144 A: 'p,
145 {
146 Pool {
147 reclaim: self.reclaim,
148 cons,
149 storage: self.storage,
150 }
151 }
152}
153
154impl<A, S: ?Sized + Storage<Slot = Gc<A>>, F> Pool<A, S, F> {
155 /// Attempts to allocate a new, [`Active`] object.
156 ///
157 /// This method first tries to recycle a slot from the reclaim list. If the
158 /// list is empty, it requests a new slot from the backing store.
159 ///
160 /// This function is panic-safe. If the constructor function `cons` panics,
161 /// the slot (whether recycled or newly allocated) is safely returned to
162 /// the reclaim list, preventing memory leaks.
163 ///
164 /// Returns a `Result` containing the pinned, allocated object, or the
165 /// original `value` if allocation fails (e.g., the backing store is full).
166 #[allow(clippy::mut_from_ref)]
167 pub fn try_alloc<'a, 'p, B>(
168 self: &'a Pin<&mut Self>,
169 value: B,
170 ) -> Result<Pin<&'a mut Lease<'a, Gc<A>>>, InsufficientSpace<B>>
171 where
172 F: sealed::Cons<B, Lease<'p, A>>,
173 A: 'p,
174 {
175 use scopeguard::{ScopeGuard, guard};
176
177 // Try to get a slot from either the reclaim list or the backing store.
178 let mut list = self.reclaim.take();
179 let slot = list.pop_front();
180 self.reclaim.set(list);
181
182 let slot = match slot {
183 Some(item) => {
184 // SAFETY: The `UnsafeRef` points to a valid `Gc` in storage.
185 unsafe { Pin::new_unchecked(&mut *UnsafeRef::into_raw(item)) }
186 }
187 None => {
188 // The reclaim list was empty, so try the backing store.
189 match self.as_ref().project_ref().storage.try_alloc(Gc::new()) {
190 Some(new_slot) => new_slot,
191 None => return Err(InsufficientSpace(value)),
192 }
193 }
194 };
195
196 // We have a slot. Guard it to ensure it's reclaimed on panic.
197 let mut guard = guard(slot, |slot| {
198 // This closure runs if a panic occurs during construction.
199 let mut list = self.reclaim.take();
200 // SAFETY: `slot` points to a valid memory location that was just
201 // allocated but is being abandoned. It's safe to add it to the
202 // reclaim list for future use.
203 list.push_front(unsafe { UnsafeRef::from_raw(slot.get_unchecked_mut() as *mut _) });
204 self.reclaim.set(list);
205 });
206
207 // Perform the potentially panicking operation.
208 guard
209 .as_mut()
210 .fill(&self.reclaim, self.cons.cons(value).into_inner());
211
212 // Success! Disarm the guard and get the slot back.
213 let slot = ScopeGuard::into_inner(guard);
214
215 // SAFETY: Adding the `Lease` here is safe, because this is the
216 // only pinned reference to this slot in existence.
217 Ok(unsafe { Lease::unchecked_new(slot) })
218 }
219
220 /// Allocates a new, [`Active`] object, panicking if allocation fails.
221 ///
222 /// This is a convenience wrapper around [`try_alloc`](Self::try_alloc) that
223 /// panics on allocation failure. This is often acceptable in simulations
224 /// where the pool size is expected to be sufficient.
225 #[allow(clippy::mut_from_ref)]
226 pub fn alloc<'a, 'p, B>(self: &'a Pin<&mut Self>, value: B) -> Pin<&'a mut Lease<'a, Gc<A>>>
227 where
228 F: sealed::Cons<B, Lease<'p, A>>,
229 A: 'p,
230 {
231 self.try_alloc(value).unwrap()
232 }
233
234 /// Returns the total number of allocated slots.
235 pub fn len(&self) -> usize {
236 let list = self.reclaim.take();
237 let reclaimed = list.iter().count();
238 self.reclaim.set(list);
239
240 self.storage.len() - reclaimed
241 }
242
243 /// Returns `true` if no slots have been allocated.
244 pub fn is_empty(&self) -> bool {
245 self.len() == 0
246 }
247}
248
249impl<A, S: Storage<Slot = Gc<A>>> From<S> for Pool<A, S> {
250 fn from(storage: S) -> Self {
251 Pool::new(storage)
252 }
253}
254
255impl<A, S: Storage<Slot = Gc<A>> + Default> Default for Pool<A, S> {
256 fn default() -> Self {
257 Pool::new(S::default())
258 }
259}
260
261/* ****************************************************** `Cons` Helper-Trait */
262
263#[doc(hidden)]
264mod sealed {
265 pub trait Cons<S, T> {
266 fn cons(&self, value: S) -> T;
267 }
268
269 // Implement the identity function for the empty tuple.
270 // This is done to prevent wasting memory for the standard case without
271 // a dedicated constructor function.
272 impl<T> Cons<T, T> for () {
273 #[inline]
274 fn cons(&self, value: T) -> T {
275 value
276 }
277 }
278
279 // Blanket implement the trait for `Fn` closures.
280 impl<S, T, F> Cons<S, T> for F
281 where
282 F: Fn(S) -> T,
283 {
284 #[inline]
285 fn cons(&self, value: S) -> T {
286 self(value)
287 }
288 }
289}
290
291/* ************************************************************ Storage Trait */
292
293/// A trait for abstracting over the memory backend of a [`Pool`].
294///
295/// This allows the `Pool` to be generic over its memory source, supporting
296/// both fixed-size arrays for `no_alloc` environments and dynamic arenas
297/// where a heap is available.
298pub trait Storage {
299 /// Slot-type of the memory pool.
300 ///
301 /// Should be [`Gc<A>`] for some [`Active`] `A`.
302 type Slot;
303
304 /// Allocates a new object, panicking if the backing store is full.
305 #[allow(clippy::mut_from_ref)]
306 fn alloc(self: Pin<&Self>, value: Self::Slot) -> Pin<&mut Self::Slot> {
307 self.try_alloc(value).expect("insufficient storage space")
308 }
309
310 /// Attempts to allocate a new object, returning an error if the store is
311 /// full.
312 #[allow(clippy::mut_from_ref)]
313 fn try_alloc(self: Pin<&Self>, value: Self::Slot) -> Option<Pin<&mut Self::Slot>>;
314
315 /// Returns the number of items currently allocated in the store.
316 fn len(&self) -> usize;
317
318 /// Returns `true` if no items have been allocated from the store.
319 fn is_empty(&self) -> bool {
320 self.len() == 0
321 }
322}
323
324/* ************************************************** Garbage Collecting Type */
325
326/// A wrapper that enables an [`Active`] type to be managed by a [`Pool`].
327///
328/// This enum has two states:
329/// - `Active`: The wrapper contains a live `Active` entity.
330/// - `Reclaim`: The entity has been dropped, and this `Gc` now acts as a node
331/// in the pool's intrusive reclaim list.
332#[pin_project::pin_project(
333 project = GcProj,
334 project_ref = GcProjRef,
335 project_replace = GcProjReplace
336)]
337pub enum Gc<A> {
338 /// The state of a `Gc` when it contains a live, active entity.
339 Active {
340 /// A pointer to the pool's reclaim list.
341 reclaim: NonNull<Cell<SinglyLinkedList<GcAdapter<A>>>>,
342 /// The pinned active entity itself.
343 #[pin]
344 active: A,
345 },
346 /// The state of a `Gc` after its entity has been reclaimed. It now holds
347 /// only a link for the intrusive list.
348 Reclaim(SinglyLinkedListLink),
349}
350
351impl<A> Gc<A> {
352 /// Creates a slot in the empty `Reclaim` state.
353 fn new() -> Self {
354 Self::Reclaim(SinglyLinkedListLink::new())
355 }
356
357 /// Creates a new garbage-collected instance in the `Active` state.
358 fn fill<'a>(
359 mut self: Pin<&'a mut Self>,
360 reclaim: &'a Cell<SinglyLinkedList<GcAdapter<A>>>,
361 active: A,
362 ) {
363 self.set(Self::Active {
364 reclaim: NonNull::from(reclaim),
365 active,
366 });
367 }
368}
369
370impl<A: Dispatch> Dispatch for Gc<A> {
371 /// Polls the underlying `Active` entity.
372 fn poll(self: Pin<&Self>, cx: &mut Context<'_>) -> Poll<ExitStatus> {
373 match self.project_ref() {
374 GcProjRef::Active { active, .. } => active.poll(cx),
375 GcProjRef::Reclaim { .. } => {
376 // This state should be impossible to reach during a poll, as an
377 // entity is only reclaimed after it has finished execution.
378 unreachable!("Cannot poll a reclaimed Gc instance");
379 }
380 }
381 }
382
383 /// Reclaims the entity, returning its memory to the pool.
384 ///
385 /// This method is called when the last `Irc` to this `Gc` is dropped.
386 /// It transitions the enum from the `Active` state to the `Reclaim` state.
387 /// This involves dropping the contained `Active` entity and then pushing
388 /// the `Gc` wrapper itself onto the pool's intrusive reclaim list.
389 fn reclaim(mut self: Pin<&mut Self>) {
390 // Atomically replace the `Active` variant with `Reclaim`. The
391 // `project_replace` gives us ownership of the fields from the previous
392 // `Active` state. The `active` field will be dropped here, running its
393 // destructor as intended.
394 let prev = self.as_mut().project_replace(Gc::new());
395
396 match prev {
397 GcProjReplace::Active { reclaim, .. } => {
398 // Now that the entity is dropped, add its memory slot (which is
399 // `self`, now in the `Reclaim` state) to the reclaim list.
400 let reclaim = unsafe { reclaim.as_ref() };
401
402 // Take temporary ownership of the reclaim list.
403 let mut list = reclaim.take();
404
405 // SAFETY: We have mutable access to `self`, and the adapter
406 // correctly calculates the offset to the link field.
407 list.push_front(unsafe { UnsafeRef::from_raw(self.get_unchecked_mut() as *mut _) });
408
409 // Return the modified list to the pool.
410 reclaim.set(list);
411 }
412 GcProjReplace::Reclaim { .. } => {
413 unreachable!("Cannot reclaim a Gc instance that has already been reclaimed");
414 }
415 }
416 }
417}
418
419impl<C: ?Sized + Config, A: Active<C>> Active<C> for Gc<A> {
420 type Output = A::Output;
421 type Puck<'p>
422 = A::Puck<'p>
423 where
424 A: 'p;
425
426 /// Binds the underlying active entity to the simulation context.
427 fn bind<'p>(this: Pin<&'p mut Lease<'p, Self>>, sctx: &'p Share<C>) -> Self::Puck<'p> {
428 // We project through the `Gc` wrapper to get to the inner `active` field.
429 Active::bind(
430 this.map_pin_mut(|inner| match inner.project() {
431 GcProj::Active { active, .. } => active,
432 GcProj::Reclaim { .. } => {
433 unreachable!("Cannot bind a reclaimed Gc instance");
434 }
435 }),
436 sctx,
437 )
438 }
439}