Skip to main content

ferroui_base/utilities/
ref_countable.rs

1use std::sync::atomic::{AtomicI32, Ordering};
2use std::sync::{Arc, Mutex};
3
4/// Creates explicitly counted references; see [`RefCounted`].
5pub struct RefCountable;
6
7impl RefCountable {
8    /// Creates the first counted reference to `item`. `release` runs once,
9    /// when the last reference is disposed (or dropped).
10    pub fn create<T: ?Sized + 'static>(item: Arc<T>, release: impl FnOnce() + Send + 'static) -> RefCounted<T> {
11        RefCounted { item: Mutex::new(Some(item)), counter: Arc::new(RefCounter::new(Box::new(release))) }
12    }
13}
14
15struct RefCounter {
16    release: Mutex<Option<Box<dyn FnOnce() + Send>>>,
17    refs: AtomicI32,
18}
19
20impl RefCounter {
21    fn new(release: Box<dyn FnOnce() + Send>) -> Self {
22        Self { release: Mutex::new(Some(release)), refs: AtomicI32::new(1) }
23    }
24
25    fn try_add_ref(&self) -> bool {
26        let mut old = self.refs.load(Ordering::SeqCst);
27        loop {
28            if old == 0 {
29                return false;
30            }
31            match self.refs.compare_exchange(old, old + 1, Ordering::SeqCst, Ordering::SeqCst) {
32                Ok(_) => return true,
33                Err(current) => old = current,
34            }
35        }
36    }
37
38    fn release(&self) {
39        let old = self.refs.fetch_sub(1, Ordering::SeqCst);
40        if old == 1 {
41            let release = self.release.lock().unwrap().take();
42            if let Some(release) = release {
43                release();
44            }
45        }
46    }
47
48    fn ref_count(&self) -> i32 {
49        self.refs.load(Ordering::SeqCst)
50    }
51}
52
53/// An explicitly counted reference to a shared resource.
54///
55/// Every reference is disposed on its own; the resource is released when the
56/// last one is disposed. A reference that is dropped without having been
57/// disposed releases its count as well.
58///
59/// References are shared between the UI thread and the render thread: the
60/// count is atomic, as in the original.
61pub struct RefCounted<T: ?Sized + 'static> {
62    item: Mutex<Option<Arc<T>>>,
63    counter: Arc<RefCounter>,
64}
65
66impl<T: ?Sized + 'static> RefCounted<T> {
67    /// Releases this reference.
68    pub fn dispose(&self) {
69        let item = self.item.lock().unwrap().take();
70        if item.is_some() {
71            self.counter.release();
72        }
73    }
74
75    /// The referenced resource. Panics if this reference has been disposed.
76    pub fn item(&self) -> Arc<T> {
77        // The lock is released before a disposed reference panics.
78        let item = self.item.lock().unwrap().clone();
79        item.unwrap_or_else(|| Self::disposed())
80    }
81
82    /// Creates another reference to the resource. Panics if this reference
83    /// has been disposed.
84    pub fn clone_ref(&self) -> RefCounted<T> {
85        self.clone_as(|item| item)
86    }
87
88    /// Creates another reference to the resource, viewed as another type.
89    /// Panics if this reference has been disposed.
90    pub fn clone_as<TResult: ?Sized + 'static>(&self, cast: impl FnOnce(Arc<T>) -> Arc<TResult>) -> RefCounted<TResult> {
91        // Snapshot the item so it does not matter if the cast disposes this
92        // reference in the meantime.
93        let item = self.item.lock().unwrap().clone();
94        let Some(item) = item else { Self::disposed() };
95
96        // Try to add a reference to the counter; if it fails, the item is
97        // released.
98        if !self.counter.try_add_ref() {
99            Self::disposed();
100        }
101
102        RefCounted { item: Mutex::new(Some(cast(item))), counter: self.counter.clone() }
103    }
104
105    /// Whether this reference has not been disposed.
106    pub fn is_alive(&self) -> bool {
107        self.item.lock().unwrap().is_some()
108    }
109
110    /// The number of live references to the resource. Panics if this
111    /// reference has been disposed.
112    pub fn ref_count(&self) -> i32 {
113        if !self.is_alive() {
114            Self::disposed();
115        }
116        self.counter.ref_count()
117    }
118
119    fn disposed() -> ! {
120        panic!("Cannot access a disposed object: RefCounted<{}>", std::any::type_name::<T>())
121    }
122}
123
124impl<T: ?Sized + 'static> Drop for RefCounted<T> {
125    fn drop(&mut self) {
126        self.dispose();
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    // Not from upstream.
133    use super::*;
134
135    #[test]
136    fn releases_when_the_last_reference_is_disposed() {
137        let released = Arc::new(AtomicI32::new(0));
138        let r = released.clone();
139        let first = RefCountable::create(Arc::new(5), move || {
140            r.fetch_add(1, Ordering::SeqCst);
141        });
142        assert_eq!(1, first.ref_count());
143
144        let second = first.clone_ref();
145        assert_eq!(2, first.ref_count());
146        assert_eq!(5, *second.item());
147
148        first.dispose();
149        first.dispose();
150        assert!(!first.is_alive());
151        assert!(second.is_alive());
152        assert_eq!(1, second.ref_count());
153        assert_eq!(0, released.load(Ordering::SeqCst));
154
155        let third = second.clone_as(|item| item as Arc<dyn std::any::Any + Send + Sync>);
156        second.dispose();
157        assert_eq!(0, released.load(Ordering::SeqCst));
158        drop(third);
159        assert_eq!(1, released.load(Ordering::SeqCst));
160    }
161
162    #[test]
163    #[should_panic(expected = "disposed")]
164    fn item_of_a_disposed_reference_panics() {
165        let reference = RefCountable::create(Arc::new(1), || {});
166        reference.dispose();
167        reference.item();
168    }
169
170    #[test]
171    #[should_panic(expected = "disposed")]
172    fn cloning_a_disposed_reference_panics() {
173        let reference = RefCountable::create(Arc::new(1), || {});
174        reference.dispose();
175        reference.clone_ref();
176    }
177}