blob: e7dbaeb5a3da645fc9aa11d5b516d8c010542647 [file] [log] [blame]
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::guard::{Downcast, LookupByHandle, ReadGuard, ReadWriteGuard};
use crate::shard::{
BoxedAnyProvider, HandleMapShard, ShardAllocationError, ShardValue, ValueProvider,
};
use crate::Handle;
use core::any::Any;
use core::convert::Infallible;
use core::fmt::Debug;
use core::marker::PhantomData;
use core::ops::{Deref, DerefMut};
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
/// Error raised when attempting to allocate into a full handle-map.
#[derive(Debug)]
pub struct HandleMapFullError;
/// Error raised when the entry for a given [`Handle`] doesn't exist.
#[derive(Debug)]
pub struct HandleNotPresentError;
/// Errors which may be raised while attempting to allocate
/// a handle from contents given by a (fallible) value-provider.
#[derive(Debug)]
pub enum HandleMapTryAllocateError<E: Debug> {
/// The call to the value-provider for the allocation failed.
ValueProviderFailed(E),
/// We couldn't reserve a spot for the allocation, because
/// the handle-map was full.
HandleMapFull,
}
/// FFI-transmissible structure expressing the dimensions
/// (max # of allocatable slots, number of shards) of a handle-map
/// to be used upon initialization.
#[repr(C)]
#[derive(Clone, Copy)]
pub struct HandleMapDimensions {
/// The number of shards which are employed
/// by the associated handle-map.
pub num_shards: u8,
/// The maximum number of active handles which may be
/// stored within the associated handle-map.
pub max_active_handles: u32,
}
/// A thread-safe mapping from "handle"s [like pointers, but safer]
/// to underlying structures, supporting allocations, reads, writes,
/// and deallocations of objects behind handles.
pub struct HandleMap<T> {
/// Internal type-erased implementation. This allows most of the code to be shared for
/// code-size reasons.
erased: ErasedHandleMap,
/// Keep `T` around so that we can downcast values from the erased implementation.
_t: PhantomData<T>,
}
impl<T: Any + Send + Sync + 'static> HandleMap<T> {
/// Creates a new handle-map with the given `HandleMapDimensions`.
pub fn with_dimensions(dimensions: HandleMapDimensions) -> Self {
Self {
erased: ErasedHandleMap::with_dimensions(dimensions),
_t: PhantomData,
}
}
/// Allocates a new object within the given handle-map, returning
/// a handle to the location it was stored at. This operation
/// may fail if attempting to allocate over the `dimensions.max_active_handles`
/// limit imposed on the handle-map, in which case this method
/// will return a `HandleMapFullError`.
///
/// If you want the passed closure to be able to possibly fail, see
/// [`Self::try_allocate`] instead.
pub fn allocate(
&self,
initial_value_provider: impl FnOnce() -> T,
) -> Result<Handle, HandleMapFullError> {
let value_provider =
BoxedAnyProvider(move || Ok::<_, Infallible>(initial_value_provider()));
self.erased
.try_allocate_impl(value_provider)
.map_err(|e| match e {
HandleMapTryAllocateError::ValueProviderFailed(never) => match never {},
HandleMapTryAllocateError::HandleMapFull => HandleMapFullError,
})
}
/// Attempts to allocate a new object within the given handle-map, returning
/// a handle to the location it was stored at.
///
/// This operation may fail if attempting to allocate over the `dimensions.max_active_handles`
/// limit imposed on the handle-map, in which case this method will return a
/// `HandleMapTryAllocateError::HandleMapFull` and the given `initial_value_provider` will not
/// be run.
///
/// The passed initial-value provider may also fail, in which case this will return the error
/// wrapped in `HandleMapTryAllocateError::ValueProviderFailed`.
///
/// If your initial-value provider is infallible, see [`Self::allocate`] instead.
pub fn try_allocate<E: Debug>(
&self,
initial_value_provider: impl FnOnce() -> Result<T, E>,
) -> Result<Handle, HandleMapTryAllocateError<E>> {
let value_provider = BoxedAnyProvider(initial_value_provider);
self.erased.try_allocate_impl(value_provider)
}
/// Gets a read-only reference to an object within the given handle-map,
/// if the given handle is present. Otherwise, returns [`HandleNotPresentError`].
pub fn get(
&self,
handle: Handle,
) -> Result<impl Deref<Target = T> + '_, HandleNotPresentError> {
self.erased
.get(handle)
.map(|read_guard| read_guard.map(Downcast::<T>::new()))
}
/// Gets a read+write reference to an object within the given handle-map,
/// if the given handle is present. Otherwise, returns [`HandleNotPresentError`].
pub fn get_mut(
&self,
handle: Handle,
) -> Result<impl DerefMut<Target = T> + '_, HandleNotPresentError> {
self.erased
.get_mut(handle)
.map(|read_write_guard| read_write_guard.map(Downcast::<T>::new()))
}
/// Removes the object pointed to by the given handle in
/// the handle-map, returning the removed object if it
/// exists. Otherwise, returns [`HandleNotPresentError`].
pub fn deallocate(&self, handle: Handle) -> Result<T, HandleNotPresentError> {
// Values should always match T
#[allow(clippy::expect_used)]
let downcast = |boxed: ShardValue| {
*boxed
.downcast::<T>()
.expect("Handle map value has invalid type")
};
self.erased.deallocate(handle).map(downcast)
}
/// Gets the actual number of elements stored in the entire map.
pub fn get_current_allocation_count(&self) -> u32 {
self.erased.get_current_allocation_count()
}
/// Sets the new-handle-id counter to the given value.
/// Only suitable for tests.
#[cfg(test)]
pub(crate) fn set_new_handle_id_counter(&mut self, value: u64) {
self.erased.set_new_handle_id_counter(value)
}
}
/// Internal type-erased handle map implementation.
struct ErasedHandleMap {
/// The dimensions of this handle-map
dimensions: HandleMapDimensions,
/// The individually-lockable "shards" of the handle-map,
/// among which the keys will be roughly uniformly-distributed.
handle_map_shards: Box<[HandleMapShard]>,
/// An atomically-incrementing counter which tracks the
/// next handle ID which allocations will attempt to use.
new_handle_id_counter: AtomicU64,
/// An atomic integer roughly tracking the number of
/// currently-outstanding allocated entries in this
/// handle-map among all [`HandleMapShard`]s.
outstanding_allocations_counter: AtomicU32,
}
impl ErasedHandleMap {
/// Creates a new handle-map with the given `HandleMapDimensions`.
fn with_dimensions(dimensions: HandleMapDimensions) -> Self {
let mut handle_map_shards = Vec::with_capacity(dimensions.num_shards as usize);
for _ in 0..dimensions.num_shards {
handle_map_shards.push(HandleMapShard::default());
}
let handle_map_shards = handle_map_shards.into_boxed_slice();
Self {
dimensions,
handle_map_shards,
new_handle_id_counter: AtomicU64::new(0),
outstanding_allocations_counter: AtomicU32::new(0),
}
}
/// Try allocate using a type erased ValueProvider.
fn try_allocate_impl<E: Debug>(
&self,
mut value_provider: impl ValueProvider<E>,
) -> Result<Handle, HandleMapTryAllocateError<E>> {
loop {
// Increment the new-handle-ID counter using relaxed memory ordering,
// since the only invariant that we want to enforce is that concurrently-running
// threads always get distinct new handle-ids.
let new_handle_id = self.new_handle_id_counter.fetch_add(1, Ordering::Relaxed);
let new_handle = Handle::from_id(new_handle_id);
let shard_allocate_result = self.get_shard_from_handle(new_handle).try_allocate(
new_handle,
value_provider,
&self.outstanding_allocations_counter,
self.dimensions.max_active_handles,
);
match shard_allocate_result {
Ok(_) => {
return Ok(new_handle);
}
Err(ShardAllocationError::ValueProviderFailed(e)) => {
return Err(HandleMapTryAllocateError::ValueProviderFailed(e))
}
Err(ShardAllocationError::ExceedsAllocationLimit) => {
return Err(HandleMapTryAllocateError::HandleMapFull);
}
Err(ShardAllocationError::EntryOccupied(thrown_back_provider)) => {
// We need to do the whole thing again with a new ID
value_provider = thrown_back_provider;
}
}
}
}
fn get(&self, handle: Handle) -> Result<ReadGuard<LookupByHandle>, HandleNotPresentError> {
self.get_shard_from_handle(handle).get(handle)
}
fn get_mut(
&self,
handle: Handle,
) -> Result<ReadWriteGuard<LookupByHandle>, HandleNotPresentError> {
self.get_shard_from_handle(handle).get_mut(handle)
}
/// Removes the object pointed to by the given handle in
/// the handle-map, returning the removed object if it
/// exists. Otherwise, returns [`HandleNotPresentError`].
fn deallocate(&self, handle: Handle) -> Result<ShardValue, HandleNotPresentError> {
self.get_shard_from_handle(handle)
.deallocate(handle, &self.outstanding_allocations_counter)
}
/// Lookup the shard containing the given handle.
fn get_shard_from_handle(&self, handle: Handle) -> &HandleMapShard {
let shard_index = handle.get_shard_index(self.dimensions.num_shards);
#[allow(clippy::expect_used)]
self.handle_map_shards
.get(shard_index)
// This should never occur due to `get_shard_index` respecting the given `num_shards`.
.expect("shard_index is always in range")
}
/// Gets the actual number of elements stored in the entire map.
pub fn get_current_allocation_count(&self) -> u32 {
self.outstanding_allocations_counter.load(Ordering::Relaxed)
}
/// Sets the new-handle-id counter to the given value.
/// Only suitable for tests.
#[cfg(test)]
pub(crate) fn set_new_handle_id_counter(&mut self, value: u64) {
self.new_handle_id_counter = AtomicU64::new(value);
}
}