Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 148 additions & 41 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,21 @@ use generic_array::{GenericArray, sequence::GenericSequence};
pub use generic_array::typenum::consts;
pub use generic_array::ArrayLength;

mod slotmap;
pub use slotmap::SlotMap;

use generic_array::typenum::Unsigned;

/// Access key to a Slots instance
///
/// Keys must only be used with the Slots they were generated from; using them for any other access
/// is a logic error and results in immediate or later panics from the Slot's functions. Erroneous
/// use is largely caught by type constraints; additional runtime constraints can be enabled using
/// the `verify_owner` feature.
///
/// Unless `verify_owner` is used, a Key is pointer-sized.
///
/// Keys can only be created by a Slot's [`store`](struct.Slots.html#method.store) method.
pub struct Key<IT, N> {
#[cfg(feature = "verify_owner")]
owner_id: usize,
Expand All @@ -79,11 +92,31 @@ impl<IT, N> Key<IT, N>
}
}

/// The underlying index of the key
///
/// A key's index can be used in fallible access using the
/// [`try_read()`](struct.Slots.html#method.try_read) method.
pub fn index(&self) -> usize {
self.index
}
}

/// Internal element of `RawSlots` instances
///
/// This struct is not expected to be used by code outside the slots crate, except to define
/// suitable array sizes that are `ArrayLength<Entry<IT>>` as required by the
/// [`RawSlots`](struct.RawSlots.html) and [`Slots`](struct.Slots.html) generics
/// for usages that are generic over the length of the used slots:
///
/// ```
/// # use slots::*;
/// fn examine<IT, N>(slots: &Slots<IT, N>, keys: &[Key<IT, N>])
/// where
/// N: slots::ArrayLength<slots::Entry<IT>>,
/// {
/// unimplemented!();
/// }
/// ```
pub struct Entry<IT>(EntryInner<IT>);

enum EntryInner<IT> {
Expand All @@ -92,50 +125,57 @@ enum EntryInner<IT> {
EmptyLast
}

// Data type that stores values and returns a key that can be used to manipulate
// the stored values.
// Values can be read by anyone but can only be modified using the key.
pub struct Slots<IT, N>
/// An unchecked slab allocator with predetermined size
///
/// The allocator deals out usize handles that can be used to access the data later through a
/// (shared or mutable) reference to the `RawSlots`. All access is fallible, as the handles can be
/// arbitrarily created.
///
/// It is up to slots' users to ensure that the item they intend to access is still identified by
/// that handle, especially as it can have been removed in the meantime, and the handle replaced by
/// a different object.
pub struct RawSlots<IT, N>
where N: ArrayLength<Entry<IT>> + Unsigned {
#[cfg(feature = "verify_owner")]
id: usize,
items: GenericArray<Entry<IT>, N>,
// Could be optimized by making it just usize and relying on free_count to determine its
// validity
next_free: Option<usize>,
free_count: usize
}

/// A type-checked slab allocator with predetermined size
///
/// The allocator deals out [`Key`](struct.Key.html) objects that can be used to access the data
/// later through a (shared or mutable) reference to the Slots. By the interface's design, access
/// is guaranteed to succeed, as the keys are unclonable and consumed to remove an item from the
/// Slots instance.
pub struct Slots<IT, N>
where N: ArrayLength<Entry<IT>> + Unsigned {
#[cfg(feature = "verify_owner")]
id: usize,
raw: RawSlots<IT, N>
}

#[cfg(feature = "verify_owner")]
fn new_instance_id() -> usize {
static COUNTER: AtomicUsize = AtomicUsize::new(0);

COUNTER.fetch_add(1, Ordering::Relaxed)
}

impl<IT, N> Slots<IT, N>
impl<IT, N> RawSlots<IT, N>
where N: ArrayLength<Entry<IT>> + Unsigned {
/// Create an empty raw slot allocator of size `N`.
pub fn new() -> Self {
let size = N::to_usize();

Self {
#[cfg(feature = "verify_owner")]
id: new_instance_id(),
items: GenericArray::generate(|i| Entry(i.checked_sub(1).map(EntryInner::EmptyNext).unwrap_or(EntryInner::EmptyLast))),
next_free: size.checked_sub(1),
free_count: size
}
}

#[cfg(feature = "verify_owner")]
fn verify_key(&self, key: &Key<IT, N>) {
assert!(key.owner_id == self.id, "Key used in wrong instance");
}

#[cfg(not(feature = "verify_owner"))]
fn verify_key(&self, _key: &Key<IT, N>) {
}

pub fn capacity(&self) -> usize {
N::to_usize()
}
Expand Down Expand Up @@ -164,49 +204,116 @@ impl<IT, N> Slots<IT, N>
Some(index)
}

pub fn store(&mut self, item: IT) -> Result<Key<IT, N>, IT> {
/// Put an item into a free slot
///
/// This returns an access index for the stored data in the success case, or hands the unstored
/// item back in case of an error.
pub fn store(&mut self, item: IT) -> Result<usize, IT> {
match self.alloc() {
Some(i) => {
self.items[i] = Entry(EntryInner::Used(item));
Ok(Key::new(self, i))
Ok(i)
}
None => Err(item)
}
}

pub fn take(&mut self, key: Key<IT, N>) -> IT {
self.verify_key(&key);

let taken = core::mem::replace(&mut self.items[key.index], Entry(EntryInner::EmptyLast));
self.free(key.index);
/// Move an item out of a slot, if it exists
pub fn take(&mut self, index: usize) -> Option<IT> {
let taken = core::mem::replace(&mut self.items[index], Entry(EntryInner::EmptyLast));
self.free(index);
match taken.0 {
EntryInner::Used(item) => item,
_ => unreachable!("Invalid key")
EntryInner::Used(item) => Some(item),
_ => None
}
}

pub fn read<T, F>(&self, key: &Key<IT, N>, function: F) -> T where F: FnOnce(&IT) -> T {
self.verify_key(&key);

match self.try_read(key.index, function) {
Some(t) => t,
None => unreachable!("Invalid key")
/// Provide immutable access to an item
///
/// The callback is only run if the given index is currently valid.
pub fn read<T, F>(&self, index: usize, function: F) -> Option<T> where F: FnOnce(&IT) -> T {
match &self.items[index].0 {
EntryInner::Used(item) => Some(function(&item)),
_ => None
}
}

pub fn try_read<T, F>(&self, key: usize, function: F) -> Option<T> where F: FnOnce(&IT) -> T {
match &self.items[key].0 {
EntryInner::Used(item) => Some(function(&item)),
/// Provide mutable access to an item
//
/// The callback is only run if the given index is currently valid.
pub fn modify<T, F>(&mut self, index: usize, function: F) -> Option<T> where F: FnOnce(&mut IT) -> T {
match self.items[index].0 {
EntryInner::Used(ref mut item) => Some(function(item)),
_ => None
}
}
}

pub fn modify<T, F>(&mut self, key: &Key<IT, N>, function: F) -> T where F: FnOnce(&mut IT) -> T {
self.verify_key(&key);
impl<IT, N> Slots<IT, N>
where N: ArrayLength<Entry<IT>> + Unsigned {

match self.items[key.index].0 {
EntryInner::Used(ref mut item) => function(item),
_ => unreachable!("Invalid key")
/// Create an empty slot allocator of size `N`.
///
/// If the `verify_owner` feature is enabled, it will carry a new unique (except for
/// wraparounds) ID which it shares with its keys.
pub fn new() -> Self {
Self {
#[cfg(feature = "verify_owner")]
id: new_instance_id(),
raw: RawSlots::new(),
}
}

#[cfg(feature = "verify_owner")]
fn verify_key(&self, key: &Key<IT, N>) {
assert!(key.owner_id == self.id, "Key used in wrong instance");
}

#[cfg(not(feature = "verify_owner"))]
fn verify_key(&self, _key: &Key<IT, N>) {
}

pub fn capacity(&self) -> usize {
self.raw.capacity()
}

pub fn count(&self) -> usize {
self.raw.count()
}

/// Put an item into a free slot
///
/// This returns an access index for the stored data in the success case, or hands the unstored
/// item back in case of an error.
pub fn store(&mut self, item: IT) -> Result<Key<IT, N>, IT> {
self.raw.store(item).map(|id| Key::new(self, id))
}

/// Move an item out of its slot
pub fn take(&mut self, key: Key<IT, N>) -> IT {
self.verify_key(&key);
self.raw.take(key.index()).expect("Invalid key")
}

/// Provide immutable access to an item
pub fn read<T, F>(&self, key: &Key<IT, N>, function: F) -> T where F: FnOnce(&IT) -> T {
self.verify_key(&key);
self.raw.read(key.index(), function).expect("Invalid key")
}

/// Opportunistic immutable access to an item by its index
///
/// A suitable index can be generated from a [`Key`](struct.Key.html) through its
/// [`index()`](struct.Key.html#method.index) method; unlike the regular access, this can fail if
/// the element has been removed (in which case the function is not run at all), or might have
/// been replaced by a completely unrelated element inbetween.
pub fn try_read<T, F>(&self, index: usize, function: F) -> Option<T> where F: FnOnce(&IT) -> T {
self.raw.read(index, function)
}

/// Provide mutable access to an item
pub fn modify<T, F>(&mut self, key: &Key<IT, N>, function: F) -> T where F: FnOnce(&mut IT) -> T {
self.verify_key(&key);
self.raw.modify(key.index(), function).expect("Invalid key")
}
}
118 changes: 118 additions & 0 deletions src/slotmap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
use crate::{RawSlots, ArrayLength, Entry, Unsigned};

/// A SlotMap on a fixed size array with compact slot identifiers
///
/// A SlotMap is a pool allocator whose handles carry additional generation information.
///
/// This implementation inherits the properties of [`RawSlots`](struct.RawSlots.html) (in
/// particular, unlike other slot maps, it does not aim for contiguous allocation of items or
/// growability).
///
/// It stores the current generation as part of the slot map rather than the slot
/// (for simplicity of implementation; per-slot generations that persist data in the slot can not
/// be built on `RawSlots`), and combines index and generation into a single unsigned handler
/// created by concatenating the truncated generation counter by the width of the actuallly
/// required index.
///
/// It is a drop-in replacement for `RawSlots` with some performance penalty (storing an additional
/// usize per entry and map, and additional checks at runtime) with a largely reduced risk of an
/// old key accidentally matching a new object.
pub struct SlotMap<IT, N>
where
N: ArrayLength<Entry<(usize, IT)>> + Unsigned
{
raw: RawSlots<(usize, IT), N>,
generation: usize,
}

impl<IT, N> SlotMap<IT, N>
where
N: ArrayLength<Entry<(usize, IT)>> + Unsigned
{
// All those pubs could be shared in an interface with RawSlots -- same API, just less likely
// collisions

/// Create an empty slot map of size `N`.
pub fn new() -> Self {
Self {
raw: RawSlots::new(),
generation: 0,
}
}

fn pull_generation(&mut self) -> usize {
self.generation = self.generation.wrapping_add(1);
self.generation
}

/// Number of bits maximally required for an actual index
// technically const
fn shiftsize() -> u32 {
core::mem::size_of::<usize>() as u32 * 8 - (N::to_usize().saturating_sub(1)).leading_zeros()
}

fn build_handle(&mut self, index: usize) -> usize {
let genpart = self.pull_generation() << Self::shiftsize();
index | genpart
}

fn extract_index(handle: usize) -> usize {
// Bits that contain the index
let mask = !(!0 << Self::shiftsize());
handle & mask
}

/// Put an item into a free slot
///
/// This returns an access handle for the stored data in the success case, or hands the unstored
/// item back in case of an error.
pub fn store(&mut self, item: IT) -> Result<usize, IT> {
// If we only stored the trimmed generation in the checking field rather than the full
// index, this could be a bit smoother, but the compiler should be able to optimize this
// into a single write access. (And on the other hand, this eases access checks).
match self.raw.store((0, item)) {
Err((_, item)) => Err(item),
Ok(i) => {
let handle = self.build_handle(i);
self.raw.modify(i, |item| item.0 = handle);
Ok(handle)
}
}
}

/// Move an item out of its slot, if it exists
pub fn take(&mut self, handle: usize) -> Option<IT> {
let index = Self::extract_index(handle);
if self.raw.read(index, |&(check, _)| check == handle) == Some(true) {
Some(self.raw.take(index).expect("Item was checked to be present").1)
} else {
None
}
}

/// Provide immutable access to an item
///
/// The callback is only run if the given handle matches the currently stored item.
pub fn read<T, F>(&self, handle: usize, function: F) -> Option<T> where F: FnOnce(&IT) -> T {
self.raw.read(Self::extract_index(handle), |(check, data)|
if check == &handle {
Some(function(&data))
} else {
None
}
).flatten()
}

/// Provide mutable access to an item
//
/// The callback is only run if the given handle matches the currently stored item.
pub fn modify<T, F>(&mut self, handle: usize, function: F) -> Option<T> where F: FnOnce(&mut IT) -> T {
self.raw.modify(Self::extract_index(handle), |(check, data)|
if check == &handle {
Some(function(data))
} else {
None
}
).flatten()
}
}
Loading