use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::{Arc, Mutex};
pub struct RefCountable;
impl RefCountable {
pub fn create<T: ?Sized + 'static>(item: Arc<T>, release: impl FnOnce() + Send + 'static) -> RefCounted<T> {
RefCounted { item: Mutex::new(Some(item)), counter: Arc::new(RefCounter::new(Box::new(release))) }
}
}
struct RefCounter {
release: Mutex<Option<Box<dyn FnOnce() + Send>>>,
refs: AtomicI32,
}
impl RefCounter {
fn new(release: Box<dyn FnOnce() + Send>) -> Self {
Self { release: Mutex::new(Some(release)), refs: AtomicI32::new(1) }
}
fn try_add_ref(&self) -> bool {
let mut old = self.refs.load(Ordering::SeqCst);
loop {
if old == 0 {
return false;
}
match self.refs.compare_exchange(old, old + 1, Ordering::SeqCst, Ordering::SeqCst) {
Ok(_) => return true,
Err(current) => old = current,
}
}
}
fn release(&self) {
let old = self.refs.fetch_sub(1, Ordering::SeqCst);
if old == 1 {
let release = self.release.lock().unwrap().take();
if let Some(release) = release {
release();
}
}
}
fn ref_count(&self) -> i32 {
self.refs.load(Ordering::SeqCst)
}
}
pub struct RefCounted<T: ?Sized + 'static> {
item: Mutex<Option<Arc<T>>>,
counter: Arc<RefCounter>,
}
impl<T: ?Sized + 'static> RefCounted<T> {
pub fn dispose(&self) {
let item = self.item.lock().unwrap().take();
if item.is_some() {
self.counter.release();
}
}
pub fn item(&self) -> Arc<T> {
let item = self.item.lock().unwrap().clone();
item.unwrap_or_else(|| Self::disposed())
}
pub fn clone_ref(&self) -> RefCounted<T> {
self.clone_as(|item| item)
}
pub fn clone_as<TResult: ?Sized + 'static>(&self, cast: impl FnOnce(Arc<T>) -> Arc<TResult>) -> RefCounted<TResult> {
let item = self.item.lock().unwrap().clone();
let Some(item) = item else { Self::disposed() };
if !self.counter.try_add_ref() {
Self::disposed();
}
RefCounted { item: Mutex::new(Some(cast(item))), counter: self.counter.clone() }
}
pub fn is_alive(&self) -> bool {
self.item.lock().unwrap().is_some()
}
pub fn ref_count(&self) -> i32 {
if !self.is_alive() {
Self::disposed();
}
self.counter.ref_count()
}
fn disposed() -> ! {
panic!("Cannot access a disposed object: RefCounted<{}>", std::any::type_name::<T>())
}
}
impl<T: ?Sized + 'static> Drop for RefCounted<T> {
fn drop(&mut self) {
self.dispose();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn releases_when_the_last_reference_is_disposed() {
let released = Arc::new(AtomicI32::new(0));
let r = released.clone();
let first = RefCountable::create(Arc::new(5), move || {
r.fetch_add(1, Ordering::SeqCst);
});
assert_eq!(1, first.ref_count());
let second = first.clone_ref();
assert_eq!(2, first.ref_count());
assert_eq!(5, *second.item());
first.dispose();
first.dispose();
assert!(!first.is_alive());
assert!(second.is_alive());
assert_eq!(1, second.ref_count());
assert_eq!(0, released.load(Ordering::SeqCst));
let third = second.clone_as(|item| item as Arc<dyn std::any::Any + Send + Sync>);
second.dispose();
assert_eq!(0, released.load(Ordering::SeqCst));
drop(third);
assert_eq!(1, released.load(Ordering::SeqCst));
}
#[test]
#[should_panic(expected = "disposed")]
fn item_of_a_disposed_reference_panics() {
let reference = RefCountable::create(Arc::new(1), || {});
reference.dispose();
reference.item();
}
#[test]
#[should_panic(expected = "disposed")]
fn cloning_a_disposed_reference_panics() {
let reference = RefCountable::create(Arc::new(1), || {});
reference.dispose();
reference.clone_ref();
}
}