//! Helpers for reference counting //! //! Two main purposes: //! //! * Consistent handling of overflow and underflow //! * Assurance of incrementing/decrementing as appropriate, //! including in combination with a slotmap containing the referenced data. //! //! The caller is responsible for making sure that the *right instance*'s //! [`Count`] is passed to the methods on [`Ref`]. // // There are no separate tests for this module. Many of the tests would want to // exercise the `Ref`s drop bomb, which is troublesome since it's panic in drop, // which they're making Rust treat as an abort upstream. // (This scheme did detect a bug or two during development testing, // so the drop bomb is known to work.) // // Anyway, these functions are mostly newtype veneers over existing functionality. // They're tested by the MemoryQuotaTracker's tests. use crate::internal_prelude::*; /// Local alias for the counter type pub(crate) type RawCount = u32; /// Decrement a refcount and maybe remove a corresponding slotmap entry /// /// ```rust,ignore /// fn slotmap_dec_ref!( /// slotmap: &mut SlotMap, /// ref_: Ref, /// refcount: &mut Count, // (typically) borrows from slotmap /// ) /// ``` // // This macro is a bit out-of-position, up here, because we want to be able to link // to it in our rustdocs. macro_rules! slotmap_dec_ref { { $slotmap:expr, $ref_:expr, $refcount:expr } => { { { use $crate::refcount::*; let key: Ref<_> = $ref_; let refcount: &mut Count<_> = $refcount; if let Some(Garbage(key)) = key.dispose(refcount) { let slotmap: &mut SlotMap<_, _> = $slotmap; let removed = slotmap.remove(key).expect("entry vanished or wrong key passed?!"); Some(Garbage(removed)) } else { None } } } } } /// A reference count, counting references with id type `K` #[derive(Default, Educe, Ord, PartialOrd, Eq, PartialEq, Deref)] #[educe(Debug)] pub(crate) struct Count { /// Actual count of references #[deref] count: RawCount, /// Bind to the specific key type // K is generally Send + Sync + 'static so we don't care about variance etc. #[educe(Debug(ignore))] marker: PhantomData, } /// An copy of a [`slotmap_careful::Key`] `K`, which is counted by a `RefCount` /// /// Ie, a key of type `K` with the property that it /// keeps the refcounted data structure alive. /// /// Must always be deleted using [`dispose`](Ref::dispose), not dropped. /// In tests, dropping a `RefCounted` will panic. /// /// The `Default` value does *not* contribute to a reference count, /// and is fine to drop. #[derive(Deref, Educe)] #[educe(Debug, Default, Ord, Eq, PartialEq)] pub(crate) struct Ref { /// Actual key (without generics) #[deref] raw_key: K, /// Bind to the specific key type #[educe(Debug(ignore))] marker: PhantomData, /// Drop bomb /// /// Also forces `Ref` not to be Clone #[educe(Debug(ignore), Ord(ignore), Eq(ignore), PartialEq(ignore))] #[allow(dead_code)] bomb: DropBombCondition, } // educe's Ord is open-coded and triggers clippy::non_canonical_partial_ord_impl impl PartialOrd for Ref { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } // Ideally we'd assert_not_impl on Ref but it has generics assert_not_impl_any!(DropBombCondition: Clone); /// Error: refcount overflowed #[derive(Debug, Clone, Error, Eq, PartialEq)] #[error("memory tracking refcount overflowed")] pub(crate) struct Overflow; /// Something which has become garbage /// /// Often used within `Option`, for clarity. Examples: /// /// * Key whose reference count has reached zero - see [`Ref::dispose`] /// * Value removed from a SlotMap - see [`slotmap_dec_ref!`] #[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] pub(crate) struct Garbage(pub(crate) K); impl Count { /// Make a new refcount with a specified value const fn new_raw(count: RawCount) -> Self { Count { count, marker: PhantomData, } } /// Obtain this counter as a `usize` /// /// (Reference counts are `u32`, so this might be a conversion.) pub(crate) fn as_usize(&self) -> usize { // On a 16-bit platform this could theoretically overflow, // but there would have to be >2^16 clones, which would be impossible. let r: u32 = **self; r as usize } } /// Increment this refcount, but don't care about any [`Ref`]s fn inc_raw(c: &mut RawCount) -> Result<(), Overflow> { *c = c.checked_add(1).ok_or(Overflow)?; Ok(()) } /// Decrement this refcount, but don't care about any [`Ref`]s /// /// Returns `Some(Garbage(()))` if the count reached zero. fn dec_raw(c: &mut RawCount) -> Option> { *c = c .checked_sub(1) // if this happens, our data structure is corrupted, very bad .expect("refcount underflow"); (*c == 0).then_some(Garbage(())) } impl Ref { /// Create a refcounted reference `Ref` from an un-counted key, incrementing the count pub(crate) fn new(key: K, count: &mut Count) -> Result { inc_raw(&mut count.count)?; Ok(Ref::from_raw(key)) } /// Creates a null `Ref`, which doesn't refer to any slot (lookups always give `None`) pub(crate) fn null() -> Self { Ref::from_raw(K::null()) } /// Internal function for creating a `Ref` fn from_raw(raw_key: K) -> Self { Ref { raw_key, marker: PhantomData, bomb: DropBombCondition::new_armed(), } } /// Dispose of a refcounted reference `Ref`, decrementing the count /// /// If the count reaches zero, the raw key is returned; /// the caller should remove the corresponding data from the data structure. pub(crate) fn dispose(mut self, refcount: &mut Count) -> Option> { let was = mem::take(&mut self.raw_key); assert!(!was.is_null()); dec_raw(&mut refcount.count).map(|_: Garbage<()>| Garbage(was)) } /// Dispose of a refcounted reference whose container no longer exists /// /// # CORRECTNESS /// /// This just forgets the reference, without decrementing any refcount. /// If the container *does* still exist, a ref count ref will be leaked. pub(crate) fn dispose_container_destroyed(mut self) { let _: K = mem::take(&mut self.raw_key); } } impl DefaultExtTake for Ref {} /// Insert a new entry into a slotmap using refcounted keys /// /// `value_maker` should take the provided `Count`, /// and incorporate it into a new value. /// /// On return, the entry will be in the map, and there will be one reference, /// which is returned. /// /// There is no corresponding `slotmap_remove` in this module. /// Use [`Ref::dispose`] and handle any [`Garbage`] it returns. pub(crate) fn slotmap_insert( slotmap: &mut SlotMap, value_maker: impl FnOnce(Count) -> V, ) -> Ref { let (ref_, ()) = slotmap_try_insert(slotmap, move |refcount| { Ok::<_, Void>((value_maker(refcount), ())) }) .void_unwrap(); ref_ } /// Insert a new entry into a slotmap using refcounted keys, fallibly and with extra data /// /// Like [`slotmap_insert`] but: /// * `value_maker` can also return extra return data `RD` to the caller /// * `value_maker` is allowed to fail. /// /// On successful return, the entry will be in the map, and /// the new `Ref` is returned along with the data `D`. pub(crate) fn slotmap_try_insert( slotmap: &mut SlotMap, value_maker: impl FnOnce(Count) -> Result<(V, RD), E>, ) -> Result<(Ref, RD), E> { let refcount = Count::new_raw(1); let (value, data) = value_maker(refcount)?; let raw_key = slotmap.insert(value); let ref_ = Ref { raw_key, marker: PhantomData, bomb: DropBombCondition::new_armed(), }; Ok((ref_, data)) } /// Unconditionally remove en entry from the slotmap, given a strong ref /// /// Other references to this entry will become dangling. pub(crate) fn slotmap_remove_early( slotmap: &mut SlotMap, key: Ref, ) -> Option { let r = slotmap.remove(*key); // Correctness: this becomes a reference to a missing entry key.dispose_container_destroyed(); r } #[cfg(test)] impl Drop for Ref { fn drop(&mut self) { drop_bomb_disarm_assert!(self.bomb, self.raw_key.is_null(),); } } impl From for Error { fn from(_overflow: Overflow) -> Error { internal!("reference count overflow in memory tracking (out-of-control subsystem?)").into() } } #[cfg(test)] mod test { // @@ begin test lint list maintained by maint/add_warning @@ #![allow(clippy::bool_assert_comparison)] #![allow(clippy::clone_on_copy)] #![allow(clippy::dbg_macro)] #![allow(clippy::mixed_attributes_style)] #![allow(clippy::print_stderr)] #![allow(clippy::print_stdout)] #![allow(clippy::single_char_pattern)] #![allow(clippy::unwrap_used)] #![allow(clippy::unchecked_time_subtraction)] #![allow(clippy::useless_vec)] #![allow(clippy::needless_pass_by_value)] #![allow(clippy::string_slice)] // See arti#2571 //! #![allow(clippy::let_and_return)] // TODO this lint is annoying and we should disable it use super::*; slotmap_careful::new_key_type! { struct Id; } #[derive(Eq, PartialEq, Debug)] struct Record { refcount: Count, } type Map = SlotMap; fn setup() -> (Map, Ref) { let mut map = Map::default(); let ref_ = slotmap_insert(&mut map, |refcount| Record { refcount }); (map, ref_) } #[test] fn good() { let (mut map, ref1) = setup(); let ent = map.get_mut(*ref1).unwrap(); let ref2 = Ref::new(*ref1, &mut ent.refcount).unwrap(); let g1: Option> = slotmap_dec_ref!(&mut map, ref1, &mut ent.refcount); assert_eq!(g1, None); let ent = map.get_mut(*ref2).unwrap(); let g2: Option> = slotmap_dec_ref!(&mut map, ref2, &mut ent.refcount); assert!(g2.is_some()); } #[test] fn try_insert_fail() { let mut map = Map::default(); let () = slotmap_try_insert::<_, _, _, String>(&mut map, |_refcount| Err(())).unwrap_err(); } #[test] fn drop_ref_without_decrement() { let (_map, mut ref1) = setup(); let h = ref1.bomb.make_simulated(); drop(ref1); h.expect_exploded(); } }