blob: a4531be7e09012a3769d61981fdd323eb7bfec31 [file] [edit]
// Copyright 2026 The Fuchsia Authors
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT
#![no_std]
use bitmap::{Bitmap, FixedStorage, RawBitmapGeneric};
use ksync::{KMutex, guarded, lock};
use pin_init::{PinInit, pin_init};
use zx_status::Status;
// Allocates architecture-specific resource IDs.
//
// IDs of type `T` will be allocated in the range [`MIN_ID`, `MAX_ID`).
//
// `N` is the number of `usize` words required to back the bitmap for `MAX_ID` bits.
// We must pass `N` explicitly because stable Rust does not support using generic
// parameters (like `MAX_ID`) in const operations within type signatures (e.g.
// `FixedStorage<{ (MAX_ID - 1) / 64 + 1 }>`). Doing so requires the unstable
// `generic_const_exprs` feature.
#[guarded]
pub struct IdAllocator<
T: Copy + Ord + TryFrom<usize> + TryInto<usize> + 'static,
const MAX_ID: usize,
const MIN_ID: usize,
const N: usize,
> {
#[mutex]
mutex: KMutex,
/// Hint for where the next search starts. Held as a `usize` because it is
/// only ever a bitmap index; the C++ holds a `T` and widens it implicitly.
#[guarded_by(mutex)]
next: usize,
#[guarded_by(mutex)]
bitmap: RawBitmapGeneric<FixedStorage<N>>,
}
impl<
T: Copy + Ord + TryFrom<usize> + TryInto<usize> + 'static,
const MAX_ID: usize,
const MIN_ID: usize,
const N: usize,
> IdAllocator<T, MAX_ID, MIN_ID, N>
{
const _STATIC_ASSERT: () = {
assert!(MAX_ID > MIN_ID, "MAX_ID must be greater than MIN_ID");
let required_words =
if MAX_ID == 0 { 0 } else { (MAX_ID - 1) / (usize::BITS as usize) + 1 };
assert!(
N == required_words,
"N must be exactly the number of words required to hold MAX_ID bits"
);
};
pub fn init() -> impl PinInit<Self, Status> {
let () = Self::_STATIC_ASSERT;
pin_init!(Self {
mutex <- KMutex::init(),
next: MIN_ID.into(),
bitmap: {
let mut bitmap = RawBitmapGeneric::default();
bitmap.reset(MAX_ID)?;
bitmap
}.into(),
_: {
// Runtime check that every allocatable id fits in T. MAX_ID is an
// exclusive bound, so the largest id ever handed out is MAX_ID - 1;
// requiring MAX_ID itself to fit would reject a full-width range
// (e.g. T = u16 with MAX_ID = 65536).
if T::try_from(MAX_ID - 1).is_err() {
return Err(Status::OUT_OF_RANGE);
}
}
}? Status)
}
pub fn reset(&self, max_id: T) -> Result<(), Status> {
let max_id_usize = max_id.try_into().map_err(|_| Status::OUT_OF_RANGE)?;
if max_id_usize <= MIN_ID || max_id_usize > MAX_ID {
return Err(Status::OUT_OF_RANGE);
}
lock!(let mut guard = self.lock_mutex());
guard.as_mut().bitmap_mut().reset(max_id_usize)
}
pub fn try_alloc(&self) -> Result<T, Status> {
lock!(let mut guard = self.lock_mutex());
let fields = guard.as_mut().fields_mut();
let next_usize = *fields.next;
let mut get_result = fields.bitmap.get(next_usize, MAX_ID);
if get_result.all_set {
get_result = fields.bitmap.get(MIN_ID, next_usize);
if get_result.all_set {
return Err(Status::NO_RESOURCES);
}
}
let first_unset = get_result.first_unset;
// The bitmap returned this index as unset, so this should not fail.
fields.bitmap.set_one(first_unset)?;
// Unreachable: `first_unset` is below `MAX_ID` and `init()` checked that
// `MAX_ID - 1` fits in `T`. The C++ uses an infallible `static_cast`.
let val = T::try_from(first_unset).map_err(|_| Status::OUT_OF_RANGE)?;
// Update next
let next_val_usize = (first_unset + 1) % MAX_ID;
*fields.next = if next_val_usize == 0 { MIN_ID } else { next_val_usize };
Ok(val)
}
pub fn free(&self, id: T) -> Result<(), Status> {
lock!(let mut guard = self.lock_mutex());
let id_usize = id.try_into().map_err(|_| Status::INVALID_ARGS)?;
if !guard.bitmap().get_one(id_usize) {
return Err(Status::INVALID_ARGS);
}
guard.as_mut().bitmap_mut().clear_one(id_usize)
}
}
#[cfg(test)]
mod tests {
use super::*;
use pin_init::stack_try_pin_init;
#[test]
fn test_id_allocator_alloc_and_free() {
const K_MAX_ID: usize = 8;
const K_MIN_ID: usize = 1;
stack_try_pin_init!(let allocator = IdAllocator::<u8, 254, K_MIN_ID, 4>::init());
let allocator = allocator.unwrap();
// Reset to invalid value, before using a valid value.
assert_eq!(allocator.reset(K_MIN_ID as u8), Err(Status::OUT_OF_RANGE));
assert_eq!(allocator.reset(u8::MAX), Err(Status::OUT_OF_RANGE));
assert_eq!(allocator.reset(K_MAX_ID as u8), Ok(()));
// Allocate all IDs.
for i in K_MIN_ID..K_MAX_ID {
assert_eq!(allocator.try_alloc(), Ok(i as u8));
}
// Allocate when no IDs are free.
assert_eq!(allocator.try_alloc(), Err(Status::NO_RESOURCES));
// Free an ID that was just allocated.
const K_FREE_ID: u8 = (K_MAX_ID / 2) as u8;
assert_eq!(allocator.free(K_FREE_ID), Ok(()));
// Free an ID that was already freed.
assert_eq!(allocator.free(K_FREE_ID), Err(Status::INVALID_ARGS));
// Free an invalid ID.
assert_eq!(allocator.free((K_MAX_ID + 1) as u8), Err(Status::INVALID_ARGS));
}
}