From 13ec9a3f0a11653b2c08fa33da42b67d05d6c909 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:11:12 +0100 Subject: [PATCH] feat: add no_std support Add std (default) and alloc feature flags. The core cache functionality works in no_std environments using new_static and static_cache!. Dynamic allocation via Cache::new requires the alloc feature. The stats feature now implies alloc since it uses Box and Arc. --- Cargo.toml | 10 ++++++--- src/lib.rs | 59 +++++++++++++++++++++++++++++++++------------------- src/stats.rs | 8 +++---- 3 files changed, 48 insertions(+), 29 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aedf051..a2b9384 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["fixed", "cache", "hash", "concurrent", "lock-free"] categories = ["caching", "concurrency", "data-structures"] [dependencies] -equivalent = "1" +equivalent = { version = "1", default-features = false } rapidhash = { version = "4", default-features = false, optional = true } typeid = { version = "1", default-features = false, optional = true } @@ -20,7 +20,11 @@ typeid = { version = "1", default-features = false, optional = true } rapidhash = { version = "4", default-features = false } [features] -default = [] +default = ["std"] + +std = ["alloc", "rapidhash?/std"] +alloc = [] + rapidhash = ["dep:rapidhash"] nightly = ["rapidhash?/nightly"] -stats = ["dep:typeid"] +stats = ["alloc", "dep:typeid"] diff --git a/src/lib.rs b/src/lib.rs index 70a7283..a138fb0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,22 @@ #![doc = include_str!("../README.md")] #![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(not(feature = "std"), no_std)] #![allow(clippy::new_without_default)] -use equivalent::Equivalent; -use std::{ +#[cfg(feature = "alloc")] +extern crate alloc; + +use core::{ cell::UnsafeCell, convert::Infallible, fmt, hash::{BuildHasher, Hash}, marker::PhantomData, - mem::MaybeUninit, - sync::atomic::{AtomicUsize, Ordering}, + mem::{self, MaybeUninit}, + ptr, + sync::atomic::{self, AtomicUsize, Ordering}, }; +use equivalent::Equivalent; #[cfg(feature = "stats")] mod stats; @@ -57,9 +62,15 @@ const VERSION_MASK: usize = ((1usize << VERSION_BITS) - 1) << VERSION_SHIFT; const VERSION_INCREMENT: usize = 1usize << VERSION_SHIFT; #[cfg(feature = "rapidhash")] -type DefaultBuildHasher = std::hash::BuildHasherDefault>; -#[cfg(not(feature = "rapidhash"))] +type DefaultBuildHasher = core::hash::BuildHasherDefault>; +#[cfg(all(not(feature = "rapidhash"), feature = "std"))] type DefaultBuildHasher = std::hash::RandomState; +#[cfg(all(not(feature = "rapidhash"), not(feature = "std")))] +type DefaultBuildHasher = core::hash::BuildHasherDefault; + +#[cfg(all(not(feature = "rapidhash"), not(feature = "std")))] +#[doc(hidden)] +pub enum NoDefaultHasher {} /// Configuration trait for [`Cache`]. /// @@ -145,6 +156,7 @@ pub struct Cache>, + #[cfg(feature = "alloc")] drop: bool, epoch: AtomicUsize, _config: PhantomData, @@ -179,14 +191,16 @@ where /// - is not a power of two. /// - isn't at least 4. // See len_assertion for why. + #[cfg(feature = "alloc")] + #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] pub fn new(num: usize, build_hasher: S) -> Self { Self::len_assertion(num); - let layout = std::alloc::Layout::array::>(num).unwrap(); - let ptr = unsafe { std::alloc::alloc_zeroed(layout) }; + let layout = alloc::alloc::Layout::array::>(num).unwrap(); + let ptr = unsafe { alloc::alloc::alloc_zeroed(layout) }; if ptr.is_null() { - std::alloc::handle_alloc_error(layout); + alloc::alloc::handle_alloc_error(layout); } - let entries = std::ptr::slice_from_raw_parts(ptr.cast::>(), num); + let entries = ptr::slice_from_raw_parts(ptr.cast::>(), num); Self::new_inner(entries, build_hasher, true) } @@ -223,11 +237,13 @@ where #[inline] const fn new_inner(entries: *const [Bucket<(K, V)>], build_hasher: S, drop: bool) -> Self { + let _ = drop; Self { entries, build_hasher, #[cfg(feature = "stats")] stats: None, + #[cfg(feature = "alloc")] drop, epoch: AtomicUsize::new(0), _config: PhantomData, @@ -247,7 +263,7 @@ where #[inline] const fn index_mask(&self) -> usize { let n = self.capacity(); - unsafe { std::hint::assert_unchecked(n.is_power_of_two()) }; + unsafe { core::hint::assert_unchecked(n.is_power_of_two()) }; n - 1 } @@ -295,7 +311,7 @@ where // - We don't need to drop existing entries since we're zeroing the ALIVE_BIT. // - Concurrent readers will see either the old state or zeros (empty). unsafe { - std::ptr::write_bytes( + ptr::write_bytes( self.entries.cast_mut().cast::>(), 0, self.entries.len(), @@ -356,7 +372,7 @@ where // Skip fence on x86. Thanks to TSO, these loads are never reordered. if !cfg!(any(target_arch = "x86_64", target_arch = "x86")) { - std::sync::atomic::fence(Ordering::Acquire); + atomic::fence(Ordering::Acquire); } if seq1 == bucket.tag.load(Ordering::Acquire) && key.equivalent(&ck) { @@ -452,7 +468,7 @@ where // directly into the heap instead. It seems we get it to realize // this most consistently if we put this critical line into it's // own function instead of inlining it into the surrounding code. - unsafe { core::ptr::write(ptr, f()) }; + unsafe { ptr::write(ptr, f()) }; } let Some(locked) = bucket.try_lock_ret(None, true) else { @@ -468,7 +484,7 @@ where if C::STATS && cfg!(feature = "stats") { #[cfg(feature = "stats")] if is_alive { - let (old_key, old_value) = std::ptr::replace(data, make_entry()); + let (old_key, old_value) = ptr::replace(data, make_entry()); if let Some(stats) = &self.stats { stats.record_insert(&(*data).0, &(*data).1, Some((&old_key, &old_value))); } @@ -480,7 +496,7 @@ where } } else { if Self::NEEDS_DROP && is_alive { - std::ptr::drop_in_place(data); + ptr::drop_in_place(data); } do_write(data, make_entry); } @@ -531,14 +547,14 @@ where where F: FnOnce(&K) -> Result, { - let mut key = std::mem::ManuallyDrop::new(key); + let mut key = mem::ManuallyDrop::new(key); let mut read = false; let r = self.get_or_try_insert_with_ref(&*key, f, |k| { read = true; - unsafe { std::ptr::read(k) } + unsafe { ptr::read(k) } }); if !read { - unsafe { std::mem::ManuallyDrop::drop(&mut key) } + unsafe { mem::ManuallyDrop::drop(&mut key) } } r } @@ -598,9 +614,10 @@ where impl Drop for Cache { fn drop(&mut self) { + #[cfg(feature = "alloc")] if self.drop { // SAFETY: `Drop` has exclusive access. - drop(unsafe { Box::from_raw(self.entries.cast_mut()) }); + drop(unsafe { alloc::boxed::Box::from_raw(self.entries.cast_mut()) }); } } } @@ -621,7 +638,7 @@ pub struct Bucket { } impl Bucket { - const NEEDS_DROP: bool = std::mem::needs_drop::(); + const NEEDS_DROP: bool = mem::needs_drop::(); // TODO: Not entirely correct. const IMPLS_COPY: bool = !Self::NEEDS_DROP; diff --git a/src/stats.rs b/src/stats.rs index 4f5563a..86e070b 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -1,11 +1,9 @@ -use std::{ +use alloc::{boxed::Box, sync::Arc}; +use core::{ any::TypeId, fmt, marker::PhantomData, - sync::{ - Arc, - atomic::{AtomicU64, Ordering}, - }, + sync::atomic::{AtomicU64, Ordering}, }; /// A type-erased reference that can be downcast even for non-`'static` types.