pub struct OpaquePool { /* private fields */ }Expand description
A type-erased object pool with stable memory addresses and dual handle system.
OpaquePool stores objects of any type that matches a std::alloc::Layout specified at
pool creation time. All stored values remain at stable memory addresses throughout their
lifetime, making it safe to create pointers and pinned references to them.
The pool provides two handle types for different access patterns:
PooledMut<T>: Exclusive handles for safe removal that prevent double-usePooled<T>: Shared handles that can be copied freely for multiple references
§Key Features
- Type erasure: Store different types with the same layout in one pool
- Stable addresses: Values never move once inserted, enabling safe pointer usage
- Dual handle system: Choose between exclusive safety or shared flexibility
- Builder pattern: Flexible configuration via
OpaquePool::builder() - Dynamic growth: Automatic capacity expansion with manual shrinking available
- Drop policies: Configurable behavior when dropping pools with remaining items
- Layout verification: Debug builds verify type compatibility automatically
- Deref support: Direct value access through
std::ops::Derefandstd::ops::DerefMut - Pinning support: Safe
std::pin::Pinaccess to stored values
§Memory Management
The pool manages memory through high-density slab allocation, automatically growing as
needed. Use shrink_to_fit() to release unused capacity after
removing items. The pool never holds references to its contents, allowing you to control
aliasing and maintain Rust’s borrowing rules.
§Examples
Basic usage with exclusive handles:
use opaque_pool::OpaquePool;
let mut pool = OpaquePool::builder().layout_of::<String>().build();
// Insert a value and get an exclusive handle
// SAFETY: String matches the layout used to create the pool
let item = unsafe { pool.insert("Hello, World!".to_string()) };
// Access the value directly through Deref
assert_eq!(&*item, "Hello, World!");
assert_eq!(item.len(), 13);
// Remove safely - the handle is consumed, preventing reuse
pool.remove_mut(item);Shared access pattern:
use opaque_pool::OpaquePool;
let mut pool = OpaquePool::builder().layout_of::<u64>().build();
// SAFETY: u64 matches the layout used to create the pool
let item = unsafe { pool.insert(42_u64) };
// Convert to shared handle for copying
let shared = item.into_shared();
let shared_copy = shared; // Can copy freely
// Access the value
assert_eq!(*shared_copy, 42);
// Removal requires unsafe (caller ensures no other copies are used)
// SAFETY: No other copies of the handle will be used after this call
unsafe { pool.remove(&shared_copy) };§Thread Safety
The pool is thread-mobile (Send) and can be moved between threads, but it is not
thread-safe (Sync) and cannot be shared between threads without additional synchronization.
Handles inherit the thread safety properties of their contained type T.
Implementations§
Source§impl OpaquePool
impl OpaquePool
Sourcepub fn builder() -> OpaquePoolBuilder
pub fn builder() -> OpaquePoolBuilder
Creates a builder for configuring and constructing an OpaquePool.
This how you can create an OpaquePool. You must specify an item memory layout
using either .layout() or .layout_of::<T>() before calling .build().
§Example
use std::alloc::Layout;
use opaque_pool::OpaquePool;
// Create a pool for storing u64 values using explicit layout.
let layout = Layout::new::<u64>();
let pool = OpaquePool::builder().layout(layout).build();
assert_eq!(pool.len(), 0);
assert!(pool.is_empty());
assert_eq!(pool.item_layout(), layout);
// Create a pool for storing u32 values using type-based layout.
let pool = OpaquePool::builder().layout_of::<u32>().build();Sourcepub fn item_layout(&self) -> Layout
pub fn item_layout(&self) -> Layout
Returns the memory layout used by items in this pool.
§Example
use std::alloc::Layout;
use opaque_pool::OpaquePool;
let layout = Layout::new::<u128>();
let pool = OpaquePool::builder().layout(layout).build();
assert_eq!(pool.item_layout(), layout);
assert_eq!(pool.item_layout().size(), std::mem::size_of::<u128>());Sourcepub fn len(&self) -> usize
pub fn len(&self) -> usize
The number of values that have been inserted into the pool.
§Example
use std::alloc::Layout;
use opaque_pool::OpaquePool;
let mut pool = OpaquePool::builder().layout_of::<String>().build();
assert_eq!(pool.len(), 0);
// SAFETY: String matches the layout used to create the pool.
let pooled1 = unsafe { pool.insert("First".to_string()) };
assert_eq!(pool.len(), 1);
// SAFETY: String matches the layout used to create the pool.
let pooled2 = unsafe { pool.insert("Second".to_string()) };
assert_eq!(pool.len(), 2);
pool.remove_mut(pooled1);
assert_eq!(pool.len(), 1);Sourcepub fn capacity(&self) -> usize
pub fn capacity(&self) -> usize
The number of values the pool can accommodate without additional resource allocation.
This is the total capacity, including any existing items. The capacity will grow
automatically when insert() is called and insufficient capacity is available.
§Example
use std::alloc::Layout;
use opaque_pool::OpaquePool;
let mut pool = OpaquePool::builder().layout_of::<String>().build();
// New pool starts with zero capacity.
assert_eq!(pool.capacity(), 0);
// Inserting values may increase capacity.
// SAFETY: String matches the layout used to create the pool.
let pooled = unsafe { pool.insert("Test".to_string()) };
assert!(pool.capacity() > 0);
assert!(pool.capacity() >= pool.len());Sourcepub fn is_empty(&self) -> bool
pub fn is_empty(&self) -> bool
Whether the pool has no inserted values.
An empty pool may still be holding unused memory capacity.
§Example
use std::alloc::Layout;
use opaque_pool::OpaquePool;
let mut pool = OpaquePool::builder().layout_of::<String>().build();
assert!(pool.is_empty());
// SAFETY: String matches the layout used to create the pool.
let pooled = unsafe { pool.insert("Test".to_string()) };
assert!(!pool.is_empty());
pool.remove_mut(pooled);
assert!(pool.is_empty());Sourcepub fn reserve(&mut self, additional: usize)
pub fn reserve(&mut self, additional: usize)
Reserves capacity for at least additional more items to be inserted in the pool.
The pool may reserve more space to speculatively avoid frequent reallocations.
After calling reserve, capacity will be greater than or equal to
self.len() + additional. Does nothing if capacity is already sufficient.
§Example
use std::alloc::Layout;
use opaque_pool::OpaquePool;
let mut pool = OpaquePool::builder().layout_of::<String>().build();
// Reserve space for 10 more items
pool.reserve(10);
assert!(pool.capacity() >= 10);
// SAFETY: String matches the layout used to create the pool.
let pooled = unsafe { pool.insert("Test".to_string()) };
// Reserve additional space on top of existing items
pool.reserve(5);
assert!(pool.capacity() >= pool.len() + 5);Sourcepub fn shrink_to_fit(&mut self)
pub fn shrink_to_fit(&mut self)
Shrinks the pool’s memory usage by dropping unused capacity.
This method reduces the pool’s memory footprint by removing unused capacity where possible. Items currently in the pool are preserved.
The pool’s capacity may be reduced, but all existing handles remain valid.
§Example
use std::alloc::Layout;
use opaque_pool::OpaquePool;
let mut pool = OpaquePool::builder().layout_of::<String>().build();
// Insert some items to create slabs
// SAFETY: String matches the layout used to create the pool.
let pooled1 = unsafe { pool.insert("First".to_string()) };
// SAFETY: String matches the layout used to create the pool.
let pooled2 = unsafe { pool.insert("Second".to_string()) };
let initial_capacity = pool.capacity();
// Remove all items
pool.remove_mut(pooled1);
pool.remove_mut(pooled2);
// Capacity remains the same until we shrink
assert_eq!(pool.capacity(), initial_capacity);
// Shrink to fit reduces capacity
pool.shrink_to_fit();
assert!(pool.capacity() <= initial_capacity);Sourcepub unsafe fn insert<T>(&mut self, value: T) -> PooledMut<T>
pub unsafe fn insert<T>(&mut self, value: T) -> PooledMut<T>
Inserts a value into the pool and returns a handle that acts as the key and supplies a pointer to the item.
The returned Pooled<T> provides direct access to the memory via Pooled::ptr().
Accessing this pointer from unsafe code is the only way to use the inserted value.
The Pooled<T> may be returned to the pool via remove() to free the memory and
drop the value. Behavior of the pool if dropped when non-empty is determined
by the pool’s drop policy.
§Example
use std::alloc::Layout;
use opaque_pool::OpaquePool;
let mut pool = OpaquePool::builder().layout_of::<String>().build();
// SAFETY: String matches the layout used to create the pool.
let item = unsafe { pool.insert("Hello, World!".to_string()) };
// Read data back.
// SAFETY: The pointer is valid for String reads/writes and we have exclusive access.
let value = unsafe { item.ptr().as_ref() };
assert_eq!(value, "Hello, World!");
// Removal does not require unsafe code if using the original handle.
pool.remove_mut(item);§Panics
In debug builds, panics if the layout of T does not match the pool’s item layout.
§Safety
The caller must ensure that:
- The layout of
Tmatches the pool’s item layout. - If
Tcontains any references or other lifetime-dependent data, those lifetimes are valid for the entire duration that the value may remain in the pool. Since access to pool contents is only possible through unsafe code, the caller is responsible for ensuring that no use-after-free conditions occur.
In debug builds, the layout requirement is checked with an assertion.
Sourcepub unsafe fn insert_with<T>(
&mut self,
f: impl FnOnce(&mut MaybeUninit<T>),
) -> PooledMut<T>
pub unsafe fn insert_with<T>( &mut self, f: impl FnOnce(&mut MaybeUninit<T>), ) -> PooledMut<T>
Inserts a value into the pool using in-place initialization and returns an exclusive handle to it.
This method is designed for partial object initialization scenarios where only some fields
of a struct need to be initialized, while others remain as MaybeUninit. This can provide
significant performance benefits by avoiding unnecessary memory writes to uninitialized fields.
This method is not generally faster than insert() for fully-initialized types.
Use it only when you need to create objects with some fields intentionally left uninitialized.
The returned PooledMut<T> provides direct access to the memory via PooledMut::ptr().
Accessing this pointer from unsafe code is the only way to use the inserted value.
The PooledMut<T> may be returned to the pool via remove_mut() or
remove_unpin_mut() to free the memory. These operations consume the handle,
making reuse impossible and eliminating double-free risks.
To get a copyable Pooled<T> handle for sharing, use PooledMut::into_shared().
§Example: Partial initialization
use std::alloc::Layout;
use std::mem::MaybeUninit;
use opaque_pool::OpaquePool;
// A struct with some fields that start uninitialized by design
struct PartialData {
// This field is always initialized
id: u32,
// This field starts uninitialized and will be filled later
buffer: MaybeUninit<[u8; 1024]>,
}
let mut pool = OpaquePool::builder().layout_of::<PartialData>().build();
// Using insert_with() to initialize only the `id` field, leaving `buffer` uninitialized.
// This avoids writing 1024 bytes of uninitialized data that would happen with insert().
// SAFETY: PartialData matches the layout used to create the pool.
let item = unsafe {
pool.insert_with(|uninit: &mut MaybeUninit<PartialData>| {
let ptr = uninit.as_mut_ptr();
// Only initialize the `id` field, leaving `buffer` as MaybeUninit
unsafe {
std::ptr::addr_of_mut!((*ptr).id).write(42);
// Note: We intentionally do NOT initialize `buffer`
}
})
};
// Later, when we need to use the buffer, we can initialize it:
// SAFETY: The pointer is valid and we have exclusive access.
unsafe {
let data = item.ptr().as_mut();
data.buffer.write([0u8; 1024]);
}
pool.remove_mut(item);§Panics
In debug builds, panics if the layout of T does not match the pool’s item layout.
§Safety
The caller must ensure that:
- The layout of
Tmatches the pool’s item layout. - The closure properly initializes the
MaybeUninit<T>before returning. - If
Tcontains any references or other lifetime-dependent data, those lifetimes are valid for the entire duration that the value may remain in the pool. Since access to pool contents is only possible through unsafe code, the caller is responsible for ensuring that no use-after-free conditions occur.
In debug builds, the layout requirement is checked with an assertion.
Sourcepub unsafe fn remove<T: ?Sized>(&mut self, pooled: &Pooled<T>)
pub unsafe fn remove<T: ?Sized>(&mut self, pooled: &Pooled<T>)
Removes a value previously inserted into the pool.
The value is dropped and the memory becomes available for future insertions. There is no way to remove an item from the pool without dropping it.
Note: Consider using Self::remove_mut() or Self::remove_unpin_mut() with PooledMut<T>
handles instead, which provide safer removal without requiring unsafe code.
§Example
use std::alloc::Layout;
use opaque_pool::OpaquePool;
let mut pool = OpaquePool::builder().layout_of::<String>().build();
// SAFETY: String matches the layout used to create the pool.
let pooled = unsafe { pool.insert("Test".to_string()) }.into_shared();
assert_eq!(pool.len(), 1);
// Remove the value.
// SAFETY: pooled (and any of its copies) has not been used to remove an item before.
unsafe { pool.remove(&pooled) };
assert_eq!(pool.len(), 0);
assert!(pool.is_empty());
// For safer removal, consider:
// let pooled_mut = unsafe { pool.insert("Test".to_string()) };
// pool.remove_mut(pooled_mut); // Safe, consumes the handle§Panics
Panics if the handle is not associated with an existing item in this pool.
§Safety
A Pooled<T> handle can only be used to remove an item from the pool once.
Using the same handle (or any of its copies) to remove an item multiple times
will result in undefined behavior due to double-free or use-after-free issues.
Sourcepub unsafe fn remove_unpin<T: Unpin>(&mut self, pooled: &Pooled<T>) -> T
pub unsafe fn remove_unpin<T: Unpin>(&mut self, pooled: &Pooled<T>) -> T
Removes a value from the pool and returns it, without dropping it.
This method moves the value out of the pool and returns ownership to the caller. The pool slot is marked as vacant and becomes available for future insertions.
Note: Consider using Self::remove_unpin_mut() with PooledMut<T> handles instead,
which provides safer removal without requiring unsafe code.
§Example
use std::alloc::Layout;
use opaque_pool::OpaquePool;
let mut pool = OpaquePool::builder().layout_of::<String>().build();
// SAFETY: String matches the layout used to create the pool.
let pooled = unsafe { pool.insert("Test".to_string()) }.into_shared();
assert_eq!(pool.len(), 1);
// Remove and extract the value.
// SAFETY: pooled (and any of its copies) has not been used to remove an item before.
let extracted = unsafe { pool.remove_unpin(&pooled) };
assert_eq!(extracted, "Test");
assert_eq!(pool.len(), 0);
assert!(pool.is_empty());
// For safer removal, consider:
// let pooled_mut = unsafe { pool.insert("Test".to_string()) };
// let extracted = pool.remove_unpin_mut(pooled_mut); // Safe, consumes the handle§Safety
The caller must guarantee that the pooled handle has not been used for removal before. Using the same pooled handle multiple times may result in undefined behavior.
§Panics
Panics if the handle is not associated with an existing item in this pool. Panics if the handle has been type-erased to a zero-sized type.
Sourcepub fn remove_mut<T: ?Sized>(&mut self, pooled_mut: PooledMut<T>)
pub fn remove_mut<T: ?Sized>(&mut self, pooled_mut: PooledMut<T>)
Removes a value previously inserted into the pool using an exclusive handle.
This method provides safe removal without requiring unsafe code, since the
PooledMut<T> handle can only be used once. The handle is consumed by this operation,
making reuse impossible and eliminating double-free risks.
The value is dropped and the memory becomes available for future insertions.
§Example
use std::alloc::Layout;
use opaque_pool::OpaquePool;
let mut pool = OpaquePool::builder().layout_of::<String>().build();
// SAFETY: String matches the layout used to create the pool.
let item = unsafe { pool.insert("Test".to_string()) };
assert_eq!(pool.len(), 1);
// Remove the value safely.
pool.remove_mut(item);
assert_eq!(pool.len(), 0);
assert!(pool.is_empty());§Panics
Panics if the handle is not associated with an existing item in this pool.
Sourcepub fn remove_unpin_mut<T: Unpin>(&mut self, pooled_mut: PooledMut<T>) -> T
pub fn remove_unpin_mut<T: Unpin>(&mut self, pooled_mut: PooledMut<T>) -> T
Removes a value from the pool using an exclusive handle and returns it, without dropping it.
This method provides safe removal and extraction without requiring unsafe code, since the
PooledMut<T> handle can only be used once. The handle is consumed by this operation,
making reuse impossible and eliminating double-free risks.
The value is moved out of the pool and returned to the caller. The pool slot is marked as vacant and becomes available for future insertions.
§Example
use std::alloc::Layout;
use opaque_pool::OpaquePool;
let mut pool = OpaquePool::builder().layout_of::<String>().build();
// SAFETY: String matches the layout used to create the pool.
let item = unsafe { pool.insert("Test".to_string()) };
assert_eq!(pool.len(), 1);
// Remove and extract the value safely.
let extracted = pool.remove_unpin_mut(item);
assert_eq!(extracted, "Test");
assert_eq!(pool.len(), 0);
assert!(pool.is_empty());§Panics
Panics if the handle is not associated with an existing item in this pool. Panics if the handle has been type-erased to a zero-sized type.