refslots 0.1.0

Reference-counted handles to slots in a slotmap
Documentation
//! # entitymap
//!
//! An entity map built on refslots: a container that stores dynamically typed values and provides
//! reference-counted handles. Run with `cargo run --example=entitymap [--features=recorder]`.
//!
//! - [`Entity<T>`] is a statically typed handle. [`AnyEntity`] is a type-erased handle that can
//!   downcast to an `Entity<T>` when the type is known.
//! - [`EntityMap`] pairs a [`Registry`], which counts handles, with a [`SecondaryMap`] that
//!   associated a value with each slot key.
//!
//! Weak handles (i.e. `WeakEntity<T>` and `AnyWeakEntity`) aren't included in this example but
//! would follow the same pattern as shown for `Entity<T>` and `AnyEntity`.
//!
//! ## Lifecycle
//!
//! Dropping the last handle of an entity doesn't drop its value. It releases the slot and adds it's
//! key to the registry's internal release queue. The map's owner is responsible for calling
//! [`EntityMap::take_released`] to remove the values of the released entities and drop them.
//!
//! Handles can be dropped from any thread, but the map lives in one place. Deferring cleanup to
//! `take_released` keeps value destruction on the owner's thread, and returning the value instead
//! of dropping them in place means a value that itself contains handles is never dropped while the
//! registry is locked.

use parking_lot::RwLock;
use refslots::{Handle, Registry};
use slotmap::SecondaryMap;
use std::{
    any::{Any, TypeId},
    marker::PhantomData,
    ops::Deref,
    sync::Arc,
};

slotmap::new_key_type! {
    /// Identifier for an entity.
    pub struct EntityId;
}

/// Statically typed, reference-counted handle.
pub struct Entity<T> {
    any: AnyEntity,
    phantom: PhantomData<fn(T) -> T>,
}

impl<T: 'static> Clone for Entity<T> {
    fn clone(&self) -> Self {
        Self { any: self.any.clone(), phantom: PhantomData }
    }
}

impl<T: 'static> Deref for Entity<T> {
    type Target = AnyEntity;

    fn deref(&self) -> &Self::Target {
        &self.any
    }
}

/// Dynamically typed, reference-counted handle.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AnyEntity(Handle<EntityId, TypeId>);

impl AnyEntity {
    /// Attempts to downcast into a statically typed handle.
    pub fn downcast<T: 'static>(self) -> Result<Entity<T>, Self> {
        if *self.0.meta() != TypeId::of::<T>() {
            return Err(self);
        }
        Ok(Entity { any: self, phantom: PhantomData })
    }

    pub fn id(&self) -> EntityId {
        self.0.key()
    }
}

/// Map of type-erased entities.
#[derive(Default)]
pub struct EntityMap {
    /// Map of entity values.
    entities: SecondaryMap<EntityId, Box<dyn Any>>,
    /// Reference slot registry.
    registry: Arc<RwLock<Registry<EntityId>>>,
}

impl EntityMap {
    /// Stores a value in the map.
    pub fn insert<T: 'static>(&mut self, value: T) -> Entity<T> {
        let handle = Handle::insert(&self.registry, TypeId::of::<T>());
        self.entities.insert(handle.key(), Box::new(value));
        Entity { any: AnyEntity(handle), phantom: PhantomData }
    }

    /// Returns a reference to the entity's value.
    pub fn get<T: 'static>(&self, entity: &Entity<T>) -> &T {
        self.assert_valid_context(entity);
        let Some(value) = self.entities.get(entity.id()) else {
            panic!("entity: {:?} inaccessible", entity.id());
        };
        value.downcast_ref::<T>().unwrap()
    }

    /// Consumes released entities, removing them from the map to be cleaned up and dropped.
    ///
    /// Values are returned instead of dropped here so that dropping a value which might
    /// contain other handles can't deadlock on the registry's lock. Dropping a value might release
    /// additional handles. Call this in a loop until it returns an empty list.
    pub fn take_released(&mut self) -> Vec<(EntityId, Box<dyn Any>)> {
        let mut registry = self.registry.write();
        let released = registry.take_released().filter_map(|id| {
            // Remove entity values eagerly on collecting released slots.
            //
            // Strictly speaking, eagerly cleaning up values in a SecondaryMap isn't required as
            // they will be lazily overwritten as slotmap indexes are reused. Showing here to
            // demonstrate how eager clean up can be achieved.
            Some((id, self.entities.remove(id)?))
        });
        released.collect()
    }

    fn assert_valid_context(&self, entity: &AnyEntity) {
        assert!(entity.0.check_context(&Arc::downgrade(&self.registry)))
    }
}

fn main() {
    let mut map = EntityMap::default();
    let mut entities = vec![map.insert(42_u8).any, map.insert("foo").any];
    entities.push(map.insert(("bar", entities[1].clone())).any);

    for any in entities.drain(..) {
        if let Ok(entity) = any.clone().downcast::<u8>() {
            println!("{:?} = {:?} :: u8", entity.id(), map.get(&entity));
        }
        if let Ok(entity) = any.clone().downcast::<&str>() {
            println!("{:?} = {:?} :: str", entity.id(), map.get(&entity));
        }
        if let Ok(entity) = any.clone().downcast::<(&str, AnyEntity)>() {
            let (value, inner) = map.get(&entity);
            println!("{:?} = {:?} :: (str, AnyEntity)", entity.id(), (value, inner.id()));
        }
    }

    // Drop all released entities to prevent leaks.
    loop {
        let released = map.take_released();
        if released.is_empty() {
            break;
        }
        println!("dropped {} released slots", released.len());
    }
}