Skip to main content

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}