OpaquePool

Struct OpaquePool 

Source
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-use
  • Pooled<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::Deref and std::ops::DerefMut
  • Pinning support: Safe std::pin::Pin access 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

Source

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();
Source

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>());
Source

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);
Source

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());
Source

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());
Source

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);
Source

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);
Source

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 T matches the pool’s item layout.
  • If T contains 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.

Source

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 T matches the pool’s item layout.
  • The closure properly initializes the MaybeUninit<T> before returning.
  • If T contains 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.

Source

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.

Source

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.

Source

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.

Source

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.

Trait Implementations§

Source§

impl Debug for OpaquePool

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.