Skip to main content

expando/
lib.rs

1// Copyright 2024 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use starnix_sync::{ExpandoLock, LockDepMutex};
6use std::any::{Any, TypeId};
7use std::collections::BTreeMap;
8use std::marker::{Send, Sync};
9use std::ops::Deref;
10use std::sync::Arc;
11
12/// A spot in an `Expando`.
13///
14/// Holds a value of type `Arc<T>`.
15#[derive(Debug)]
16struct ExpandoSlot {
17    value: Arc<dyn Any + Send + Sync>,
18}
19
20impl ExpandoSlot {
21    fn new(value: Arc<dyn Any + Send + Sync>) -> Self {
22        ExpandoSlot { value }
23    }
24
25    fn downcast<T: Any + Send + Sync>(&self) -> Option<Arc<T>> {
26        self.value.clone().downcast::<T>().ok()
27    }
28}
29
30/// A lazy collection of values of every type.
31///
32/// An Expando contains a single instance of every type. The values are instantiated lazily
33/// when accessed. Useful for letting modules add their own state to context objects without
34/// requiring the context object itself to know about the types in every module.
35///
36/// Typically the type a module uses in the Expando will be private to that module, which lets
37/// the module know that no other code is accessing its slot on the expando.
38#[derive(Debug, Default)]
39pub struct Expando {
40    properties: LockDepMutex<BTreeMap<TypeId, ExpandoSlot>, ExpandoLock>,
41}
42
43impl Expando {
44    /// Get the slot in the expando associated with the given type.
45    ///
46    /// The slot is added to the expando lazily but the same instance is returned every time the
47    /// expando is queried for the same type.
48    pub fn get<T: Any + Send + Sync + Default + 'static>(&self) -> Arc<T> {
49        self.get_or_init(T::default)
50    }
51
52    /// Get the slot in the expando associated with the given type, running `init` to initialize
53    /// the slot if needed.
54    ///
55    /// The slot is added to the expando lazily but the same instance is returned every time the
56    /// expando is queried for the same type.
57    pub fn get_or_init<T: Any + Send + Sync + 'static>(&self, init: impl FnOnce() -> T) -> Arc<T> {
58        self.get_or_try_init::<T, ()>(|| Ok(init())).expect("infallible initializer")
59    }
60
61    /// Get the slot in the expando associated with the given type, running `try_init` to initialize
62    /// the slot if needed. Returns an error only if `try_init` returns an error.
63    ///
64    /// The slot is added to the expando lazily but the same instance is returned every time the
65    /// expando is queried for the same type.
66    pub fn get_or_try_init<T: Any + Send + Sync + 'static, E>(
67        &self,
68        try_init: impl FnOnce() -> Result<T, E>,
69    ) -> Result<Arc<T>, E> {
70        let type_id = TypeId::of::<T>();
71
72        // Acquire the lock each time we want to look at the map so that user-provided initializer
73        // can use the expando too.
74        if let Some(slot) = self.properties.lock().get(&type_id) {
75            assert_eq!(type_id, slot.value.deref().type_id());
76            return Ok(slot.downcast().expect("downcast of expando slot was successful"));
77        }
78
79        // Initialize the new value without holding the lock.
80        let newly_init = Arc::new(try_init()?);
81
82        // Only insert the newly-initialized value if no other threads got there first.
83        let mut properties = self.properties.lock();
84        let slot = properties.entry(type_id).or_insert_with(|| ExpandoSlot::new(newly_init));
85        assert_eq!(type_id, slot.value.deref().type_id());
86        Ok(slot.downcast().expect("downcast of expando slot was successful"))
87    }
88
89    /// Get the slot in the expando associated with the given type if it has previously been
90    /// initialized.
91    pub fn peek<T: Any + Send + Sync + 'static>(&self) -> Option<Arc<T>> {
92        let properties = self.properties.lock();
93        let type_id = TypeId::of::<T>();
94        let slot = properties.get(&type_id)?;
95        assert_eq!(type_id, slot.value.deref().type_id());
96        Some(slot.downcast().expect("downcast of expando slot was successful"))
97    }
98
99    /// Remove the provided type from the expando if it is present.
100    pub fn remove<T: Any + Send + Sync + 'static>(&self) -> Option<Arc<T>> {
101        let mut properties = self.properties.lock();
102        let type_id = TypeId::of::<T>();
103        let slot = properties.remove(&type_id)?;
104        assert_eq!(type_id, slot.value.deref().type_id());
105        Some(slot.downcast().expect("downcast of expando slot was successful"))
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use starnix_sync::Mutex;
113
114    #[derive(Debug, Default)]
115    struct MyStruct {
116        counter: Mutex<i32>,
117    }
118
119    #[test]
120    fn basic_test() {
121        let expando = Expando::default();
122        let first = expando.get::<MyStruct>();
123        assert_eq!(*first.counter.lock(), 0);
124        *first.counter.lock() += 1;
125        let second = expando.get::<MyStruct>();
126        assert_eq!(*second.counter.lock(), 1);
127    }
128
129    #[test]
130    fn user_initializer() {
131        let expando = Expando::default();
132        let first = expando.get_or_init(|| String::from("hello"));
133        assert_eq!(first.as_str(), "hello");
134        let second = expando.get_or_init(|| String::from("world"));
135        assert_eq!(
136            second.as_str(),
137            "hello",
138            "expando must have preserved value from original initializer"
139        );
140        assert_eq!(Arc::as_ptr(&first), Arc::as_ptr(&second));
141    }
142
143    #[test]
144    fn nested_user_initializer() {
145        let expando = Expando::default();
146        let first = expando.get_or_init(|| expando.get::<u32>().to_string());
147        assert_eq!(first.as_str(), "0");
148        let second = expando.get_or_init(|| expando.get::<u32>().to_string());
149        assert_eq!(Arc::as_ptr(&first), Arc::as_ptr(&second));
150    }
151
152    #[test]
153    fn failed_init_can_be_retried() {
154        let expando = Expando::default();
155        let failed = expando.get_or_try_init::<String, String>(|| Err(String::from("oops")));
156        assert_eq!(failed.unwrap_err().as_str(), "oops");
157
158        let succeeded = expando.get_or_try_init::<String, String>(|| Ok(String::from("hurray")));
159        assert_eq!(succeeded.unwrap().as_str(), "hurray");
160    }
161
162    #[test]
163    fn peek_works() {
164        let expando = Expando::default();
165        assert_eq!(expando.peek::<String>(), None);
166        let from_init = expando.get_or_init(|| String::from("hello"));
167        let from_peek = expando.peek::<String>().unwrap();
168        assert_eq!(from_peek.as_str(), "hello");
169        assert_eq!(Arc::as_ptr(&from_init), Arc::as_ptr(&from_peek));
170    }
171
172    #[test]
173    fn remove_works() {
174        let expando = Expando::default();
175        assert_eq!(expando.peek::<String>(), None);
176        let from_init = expando.get_or_init(|| String::from("hello"));
177        let removed = expando.remove::<String>().unwrap();
178        assert_eq!(removed.as_str(), "hello");
179        assert_eq!(Arc::as_ptr(&from_init), Arc::as_ptr(&removed));
180        assert_eq!(expando.peek::<String>(), None);
181    }
182}