Skip to main content

myrmic_sdk/host_functions/
in_memory.rs

1use core::cell::{Ref, RefCell, RefMut};
2
3/// Transient, cell-local storage for state that must not be persisted, such as host resource
4/// handles (sockets, subscriptions, scan sessions, ...) held across handler invocations.
5///
6/// Unlike `State`, `InMemory` never touches the data layer: the wrapped value only lives for as
7/// long as the cell instance is loaded and is lost on restart. It is typically stored in a
8/// `static` and accessed through [`InMemory::with`].
9///
10/// Wrap the value in an [`Option`] and declare it with [`InMemory::empty`] when it only becomes
11/// available at runtime, such as a handle returned by a host call.
12pub struct InMemory<C> {
13    value: RefCell<C>,
14}
15
16// SAFETY: The `InMemory` is only used in a single-threaded context, so it is safe to implement
17// `Sync` for it. The `RefCell` ensures that the inner value cannot be accessed re-entrantly.
18unsafe impl<C> Sync for InMemory<C> {}
19
20impl<C> InMemory<C> {
21    /// Wraps `value` in a new `InMemory`.
22    pub const fn new(value: C) -> Self {
23        Self {
24            value: RefCell::new(value),
25        }
26    }
27
28    /// Runs `f` with mutable access to the wrapped value.
29    ///
30    /// This covers the common case of reading or updating the value in a single expression. If
31    /// you need to interleave the access with control flow in the caller, such as an early
32    /// `return`, a `return` inside `f` only exits `f` itself; use [`InMemory::try_borrow_mut`]
33    /// or [`InMemory::try_borrow`] instead, since the guard they return lives in the caller's
34    /// own scope.
35    ///
36    /// # Errors
37    ///
38    /// Returns an error if `with` is called re-entrantly, i.e. from within another call to
39    /// `with`/`upsert_with`/`try_borrow`/`try_borrow_mut` on the same `InMemory` (for example,
40    /// from a callback invoked by `f`).
41    pub fn with<R>(&self, f: impl FnOnce(&mut C) -> R) -> crate::Result<R> {
42        let mut value = self.try_borrow_mut()?;
43
44        Ok(f(&mut value))
45    }
46
47    /// Immutably borrows the wrapped value.
48    ///
49    /// Prefer this over [`InMemory::with`] when the access needs to be interleaved with control
50    /// flow in the caller, since the returned guard lives in the caller's own scope rather than
51    /// inside a closure.
52    ///
53    /// # Errors
54    ///
55    /// Returns an error if the value is currently mutably borrowed, i.e. from a re-entrant call
56    /// to `with`/`upsert_with`/`try_borrow_mut` (for example, from within a callback).
57    pub fn try_borrow(&self) -> crate::Result<Ref<'_, C>> {
58        self.value
59            .try_borrow()
60            .map_err(|_| "In-memory value was already mutably borrowed. Do not use re-entrantly.")
61    }
62
63    /// Mutably borrows the wrapped value.
64    ///
65    /// Prefer this over [`InMemory::with`] when the access needs to be interleaved with control
66    /// flow in the caller, since the returned guard lives in the caller's own scope rather than
67    /// inside a closure.
68    ///
69    /// # Errors
70    ///
71    /// Returns an error if the value is already borrowed, i.e. from a re-entrant call to
72    /// `with`/`upsert_with`/`try_borrow`/`try_borrow_mut` (for example, from within a callback).
73    pub fn try_borrow_mut(&self) -> crate::Result<RefMut<'_, C>> {
74        self.value
75            .try_borrow_mut()
76            .map_err(|_| "In-memory value was already borrowed. Do not use re-entrantly.")
77    }
78}
79
80impl<T> InMemory<Option<T>> {
81    /// Declares a `InMemory` that starts out without a value.
82    ///
83    /// Use this for a value that can only be built at runtime, such as a handle returned by a
84    /// host call. The [`Option`] is part of the stored type, so [`InMemory::with`] hands the
85    /// closure an `&mut Option<T>` and the caller deals with exactly one level of optionality:
86    ///
87    /// ```
88    /// # use myrmic_sdk::InMemory;
89    /// # struct ScanHandle;
90    /// # impl ScanHandle { fn stop(self) -> myrmic_sdk::Result<()> { Ok(()) } }
91    /// # fn demo(scan: ScanHandle) -> myrmic_sdk::Result<()> {
92    /// static SCAN: InMemory<Option<ScanHandle>> = InMemory::empty();
93    ///
94    /// SCAN.with(|slot| *slot = Some(scan))?;
95    ///
96    /// if let Some(scan) = SCAN.with(Option::take)? {
97    ///     scan.stop()?;
98    /// }
99    /// # Ok(())
100    /// # }
101    /// # demo(ScanHandle).unwrap();
102    /// ```
103    ///
104    /// When `T` implements [`Default`], [`InMemory::upsert_with`] hands the closure an `&mut T`
105    /// instead, inserting the default value first.
106    pub const fn empty() -> Self {
107        Self::new(None)
108    }
109}
110
111impl<T> InMemory<Option<T>>
112where
113    T: Default,
114{
115    /// Runs `f` with mutable access to the wrapped value, inserting `T::default()` first if there
116    /// is no value yet.
117    ///
118    /// This mirrors the `upsert` family on `State` for a value that is never persisted. Since the
119    /// insert guarantees a value, `f` receives an `&mut T` and its result is not wrapped in an
120    /// [`Option`], which makes this the more direct way to build a value up field by field across
121    /// several invocations:
122    ///
123    /// ```
124    /// # use myrmic_sdk::InMemory;
125    /// # #[derive(Default)]
126    /// # struct Session { scan: Option<u32> }
127    /// # fn demo(scan: u32) -> myrmic_sdk::Result<()> {
128    /// static SESSION: InMemory<Option<Session>> = InMemory::empty();
129    ///
130    /// SESSION.upsert_with(|session| session.scan = Some(scan))?;
131    /// # Ok(())
132    /// # }
133    /// # demo(7).unwrap();
134    /// ```
135    ///
136    /// # Errors
137    ///
138    /// Returns an error if `upsert_with` is called re-entrantly, i.e. from within another call to
139    /// `with`/`upsert_with`/`try_borrow`/`try_borrow_mut` on the same `InMemory` (for example,
140    /// from a callback invoked by `f`).
141    pub fn upsert_with<R>(&self, f: impl FnOnce(&mut T) -> R) -> crate::Result<R> {
142        let mut slot = self.try_borrow_mut()?;
143
144        Ok(f(slot.get_or_insert_default()))
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::InMemory;
151
152    #[derive(Default)]
153    struct Handle {
154        id: u32,
155        retries: u8,
156    }
157
158    #[test]
159    fn present_value_needs_no_option() {
160        static CTX: InMemory<u32> = InMemory::new(7);
161
162        let doubled: u32 = CTX.with(|value| *value * 2).unwrap();
163        assert_eq!(doubled, 14);
164    }
165
166    #[test]
167    fn present_value_updates_individual_fields() {
168        static CTX: InMemory<Handle> = InMemory::new(Handle { id: 1, retries: 0 });
169
170        CTX.with(|handle| handle.retries += 1).unwrap();
171        CTX.with(|handle| handle.id = 9).unwrap();
172
173        let fields = CTX.with(|handle| (handle.id, handle.retries)).unwrap();
174        assert_eq!(fields, (9, 1));
175    }
176
177    #[test]
178    fn empty_slot_sets_and_takes() {
179        static CTX: InMemory<Option<Handle>> = InMemory::empty();
180
181        assert!(CTX.with(|slot| slot.is_none()).unwrap());
182        CTX.with(|slot| *slot = Some(Handle { id: 3, retries: 0 }))
183            .unwrap();
184
185        let taken: Option<Handle> = CTX.with(Option::take).unwrap();
186        assert_eq!(taken.map(|handle| handle.id), Some(3));
187        assert!(CTX.with(|slot| slot.is_none()).unwrap());
188    }
189
190    #[test]
191    fn empty_slot_upserts_individual_fields() {
192        static CTX: InMemory<Option<Handle>> = InMemory::empty();
193
194        // An empty slot is filled field by field, so a value that only becomes complete over
195        // several invocations needs no accessor of its own.
196        CTX.upsert_with(|handle| handle.id = 7).unwrap();
197
198        // The closure's result comes back unwrapped, because the insert guarantees a value.
199        let retries = CTX
200            .upsert_with(|handle| {
201                handle.retries += 1;
202
203                handle.retries
204            })
205            .unwrap();
206        assert_eq!(retries, 1);
207
208        // A field of a value that is already there is updated in place.
209        CTX.with(|slot| {
210            if let Some(handle) = slot {
211                handle.retries += 1;
212            }
213        })
214        .unwrap();
215
216        let fields = CTX
217            .with(|slot| slot.as_ref().map(|handle| (handle.id, handle.retries)))
218            .unwrap();
219        assert_eq!(fields, Some((7, 2)));
220    }
221
222    #[test]
223    fn reentrant_access_is_rejected() {
224        static CTX: InMemory<u32> = InMemory::new(7);
225
226        assert!(CTX.with(|_| CTX.with(|_| ())).unwrap().is_err());
227    }
228}