blob: bb420e3a2ba0cdf2a3717db21d736b1b9376b4b5 [file] [log] [blame]
// Copyright 2023 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.
//! A thread-safe implementation of a map for managing object handles,
//! a safer alternative to raw pointers for FFI interop.
#![forbid(unsafe_code)]
#![deny(missing_docs)]
use std::boxed::Box;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::vec::Vec;
pub mod declare_handle_map;
mod guard;
pub(crate) mod shard;
#[cfg(test)]
mod tests;
pub use guard::{ObjectReadGuardImpl, ObjectReadWriteGuardImpl};
use shard::{HandleMapShard, ShardAllocationError};
/// An individual handle to be given out by a [`HandleMap`].
/// This representation is untyped, and just a wrapper
/// around a handle-id, in contrast to implementors of `HandleLike`.
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Handle {
handle_id: u64,
}
impl From<&Handle> for Handle {
fn from(handle: &Handle) -> Self {
*handle
}
}
impl Handle {
/// Constructs a handle wrapping the given ID.
///
/// No validity checks are done on the wrapped ID
/// to ensure that the given ID is active in
/// any specific handle-map, and the type
/// of the handle is not represented.
///
/// As a result, this method is only useful for
/// allowing access to handles from an FFI layer.
pub fn from_id(handle_id: u64) -> Self {
Self { handle_id }
}
/// Gets the ID for this handle.
///
/// Since the underlying handle is un-typed,`
/// this method is only suitable for
/// transmitting handles across an FFI layer.
pub fn get_id(&self) -> u64 {
self.handle_id
}
/// Derives the shard index from the handle id
fn get_shard_index(&self, num_shards: u8) -> usize {
(self.handle_id % (num_shards as u64)) as usize
}
}
/// 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;
/// 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: Send + Sync> {
/// 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<T>]>,
/// 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<T: Send + Sync> HandleMap<T> {
/// Creates a new handle-map with the given `HandleMapDimensions`.
pub 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),
}
}
}
impl<T: Send + Sync> HandleMap<T> {
/// 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`.
pub fn allocate(
&self,
initial_value_provider: impl FnOnce() -> T,
) -> Result<Handle, HandleMapFullError> {
let mut initial_value_provider = initial_value_provider;
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_index = new_handle.get_shard_index(self.dimensions.num_shards);
// Now, check the shard to see if we can actually allocate into it.
let shard_allocate_result = self.handle_map_shards[shard_index].try_allocate(
new_handle,
initial_value_provider,
&self.outstanding_allocations_counter,
self.dimensions.max_active_handles,
);
match shard_allocate_result {
Ok(_) => {
return Ok(new_handle);
}
Err(ShardAllocationError::ExceedsAllocationLimit) => {
return Err(HandleMapFullError);
}
Err(ShardAllocationError::EntryOccupied(thrown_back_provider)) => {
// We need to do the whole thing again with a new ID
initial_value_provider = thrown_back_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<ObjectReadGuardImpl<T>, HandleNotPresentError> {
let shard_index = handle.get_shard_index(self.dimensions.num_shards);
self.handle_map_shards[shard_index].get(handle)
}
/// 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<ObjectReadWriteGuardImpl<T>, HandleNotPresentError> {
let shard_index = handle.get_shard_index(self.dimensions.num_shards);
self.handle_map_shards[shard_index].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`].
pub fn deallocate(&self, handle: Handle) -> Result<T, HandleNotPresentError> {
let shard_index = handle.get_shard_index(self.dimensions.num_shards);
self.handle_map_shards[shard_index]
.deallocate(handle, &self.outstanding_allocations_counter)
}
/// Gets the actual number of elements stored in the entire map.
/// Only suitable for single-threaded sections of tests.
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.handle_map_shards.iter().map(|s| s.len()).sum()
}
/// 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);
}
}
/// Externally-facing trait for things which behave like handle-map handles
/// with a globally-defined handle-map for the type.
pub trait HandleLike: Sized {
/// The underlying object type pointed-to by this handle
type Object: Send + Sync;
/// Tries to allocate a new handle using the given provider
/// to construct the underlying stored object as a new
/// entry into the global handle table for this type.
fn allocate(
initial_value_provider: impl FnOnce() -> Self::Object,
) -> Result<Self, HandleMapFullError>;
/// Gets a RAII read-guard on the contents behind this handle.
fn get(&self) -> Result<ObjectReadGuardImpl<Self::Object>, HandleNotPresentError>;
/// Gets a RAII read-write guard on the contents behind this handle.
fn get_mut(&self) -> Result<ObjectReadWriteGuardImpl<Self::Object>, HandleNotPresentError>;
/// Deallocates the contents behind this handle.
fn deallocate(self) -> Result<Self::Object, HandleNotPresentError>;
}