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}