Skip to main content

rune/runtime/
globals.rs

1//! Storage for static items declared in a unit.
2
3use core::cell::RefCell;
4use core::fmt;
5
6use rust_alloc::rc::Rc;
7
8use crate::alloc::clone::TryClone;
9use crate::alloc::{self, Box, Vec};
10use crate::hash::ToTypeHash;
11use crate::runtime::{Unit, Value};
12use crate::sync::Arc;
13use crate::{Hash, ItemBuf};
14
15/// Storage for the static items declared by a [`Unit`].
16///
17/// A unit assigns every `static` item it declares a zero-indexed slot. The
18/// values living in those slots are *not* stored in the unit itself, they are
19/// stored here and handed to a virtual machine when it is called. This means a
20/// single compiled unit can back any number of independent states.
21///
22/// Every slot keeps track of whether it has been initialized. A slot which has
23/// an initializer is lazily initialized the first time a script reads it, but a
24/// caller can also assign a value up front, in which case the initializer is
25/// never evaluated.
26///
27/// The storage is a cheaply cloned handle. Cloning it does not copy the slots,
28/// it produces a second handle to the same storage, which is how a caller can
29/// keep reading and writing static items while a virtual machine using them is
30/// running.
31///
32/// # Examples
33///
34/// ```
35/// use rune::{Context, Diagnostics, Source, Sources, Vm};
36/// use rune::runtime::Globals;
37/// use rune::sync::Arc;
38///
39/// let context = Context::with_default_modules()?;
40/// let runtime = Arc::try_new(context.runtime()?)?;
41///
42/// let mut sources = Sources::new();
43/// sources.insert(Source::memory(r#"
44/// static COUNTER = 0;
45///
46/// pub fn main() {
47///     COUNTER += 1;
48/// }
49/// "#)?)?;
50///
51/// let mut diagnostics = Diagnostics::new();
52///
53/// let unit = rune::prepare(&mut sources)
54///     .with_context(&context)
55///     .with_diagnostics(&mut diagnostics)
56///     .build()?;
57///
58/// let unit = Arc::try_new(unit)?;
59/// let globals = Globals::new(unit.clone())?;
60///
61/// let mut vm = Vm::new(runtime, unit).with_globals(globals.clone());
62/// vm.call(["main"], ())?;
63/// vm.call(["main"], ())?;
64///
65/// let counter = globals.get(["COUNTER"])?.expect("COUNTER to be initialized");
66/// assert_eq!(rune::from_value::<i64>(counter)?, 2);
67/// # Ok::<_, rune::support::Error>(())
68/// ```
69#[derive(Default, Clone)]
70pub struct Globals {
71    /// `None` means that no storage has been configured. This exists so that
72    /// [`Globals::empty`] can be a `const fn`, which in turn keeps [`Vm::new`]
73    /// allocation free.
74    ///
75    /// The handle is non-atomically counted, since the storage holds [`Value`]s
76    /// and can therefore never be shared across threads anyway.
77    ///
78    /// [`Vm::new`]: crate::runtime::Vm::new
79    inner: Option<Rc<GlobalsInner>>,
80}
81
82/// The shared allocation backing a [`Globals`] handle.
83///
84/// This is only public to the crate so that the thread-local environment can
85/// store it as a raw pointer, the same way it stores the unit and the context.
86pub(crate) struct GlobalsInner {
87    unit: Arc<Unit>,
88    /// One slot per static declared by the unit. The number of statics is fixed
89    /// by the unit, so this is sized once and never grows.
90    ///
91    /// An uninitialized slot holds [`Value::empty`].
92    slots: RefCell<Box<[Value]>>,
93}
94
95impl Globals {
96    /// Construct an empty storage which has no slots.
97    ///
98    /// This is what a [`Vm`] is constructed with. Reading or writing a static
99    /// through it always errors, so a unit which declares statics needs real
100    /// storage through [`Vm::with_globals`].
101    ///
102    /// [`Vm`]: crate::runtime::Vm
103    /// [`Vm::with_globals`]: crate::runtime::Vm::with_globals
104    #[inline]
105    pub const fn empty() -> Self {
106        Self { inner: None }
107    }
108
109    /// Construct storage for all the statics declared by the given unit, with
110    /// every slot uninitialized.
111    pub fn new(unit: Arc<Unit>) -> alloc::Result<Self> {
112        let len = unit.globals_len();
113
114        let mut slots = Vec::try_with_capacity(len)?;
115
116        for _ in 0..len {
117            slots.try_push(Value::empty())?;
118        }
119
120        Ok(Self {
121            inner: Some(Rc::new(GlobalsInner {
122                unit,
123                slots: RefCell::new(slots.try_into_boxed_slice()?),
124            })),
125        })
126    }
127
128    /// Test if this storage has been configured for a unit.
129    ///
130    /// Note that this is not the same as [`is_empty`], since a unit which
131    /// declares no statics gets configured storage with no slots in it.
132    ///
133    /// [`is_empty`]: Globals::is_empty
134    #[inline]
135    pub fn is_configured(&self) -> bool {
136        self.inner.is_some()
137    }
138
139    /// The number of slots in this storage.
140    #[inline]
141    pub fn len(&self) -> usize {
142        match &self.inner {
143            Some(inner) => inner.slots.borrow().len(),
144            None => 0,
145        }
146    }
147
148    /// Test if this storage has no slots.
149    #[inline]
150    pub fn is_empty(&self) -> bool {
151        self.len() == 0
152    }
153
154    /// Test if two handles refer to the same storage.
155    #[inline]
156    pub fn is_same(&self, other: &Self) -> bool {
157        match (&self.inner, &other.inner) {
158            (Some(a), Some(b)) => Rc::ptr_eq(a, b),
159            (None, None) => true,
160            _ => false,
161        }
162    }
163
164    /// Test if this storage was constructed for the given unit.
165    #[inline]
166    pub fn is_same_unit(&self, unit: &Arc<Unit>) -> bool {
167        match &self.inner {
168            Some(inner) => Arc::ptr_eq(&inner.unit, unit),
169            None => false,
170        }
171    }
172
173    /// Look up the slot assigned to the given static item.
174    pub fn slot(&self, name: impl ToTypeHash) -> Result<usize, GlobalsError> {
175        let inner = self.inner()?;
176        let hash = name.to_type_hash();
177
178        inner
179            .unit
180            .global_slot(&hash)
181            .ok_or(GlobalsError::new(GlobalsErrorKind::MissingStatic {
182                hash,
183                item: name.to_item().ok().flatten(),
184            }))
185    }
186
187    /// Read the value of a static item, if it has been initialized.
188    ///
189    /// Note that this does not evaluate the initializer of an uninitialized
190    /// static, since doing so requires a runtime. A static which has an
191    /// initializer but has never been read by a script therefore reads as
192    /// `None` here.
193    #[inline]
194    pub fn get(&self, name: impl ToTypeHash) -> Result<Option<Value>, GlobalsError> {
195        let slot = self.slot(name)?;
196        Ok(self.get_at(slot))
197    }
198
199    /// Write the value of a static item.
200    #[inline]
201    pub fn set(&self, name: impl ToTypeHash, value: Value) -> Result<(), GlobalsError> {
202        let slot = self.slot(name)?;
203        self.set_at(slot, value)
204    }
205
206    /// Reset a static item back to being uninitialized.
207    ///
208    /// The next script which reads it will evaluate its initializer again, if
209    /// it has one.
210    #[inline]
211    pub fn clear(&self, name: impl ToTypeHash) -> Result<(), GlobalsError> {
212        let slot = self.slot(name)?;
213        self.clear_at(slot)
214    }
215
216    /// Read the value in the given slot, if it has been initialized.
217    #[inline]
218    pub fn get_at(&self, slot: usize) -> Option<Value> {
219        self.try_get_at(slot).ok().flatten()
220    }
221
222    /// Write the value in the given slot.
223    #[inline]
224    pub fn set_at(&self, slot: usize, value: Value) -> Result<(), GlobalsError> {
225        self.replace_at(slot, value)
226    }
227
228    /// Reset the given slot back to being uninitialized.
229    #[inline]
230    pub fn clear_at(&self, slot: usize) -> Result<(), GlobalsError> {
231        self.replace_at(slot, Value::empty())
232    }
233
234    /// Test if the given slot has been initialized.
235    #[inline]
236    pub fn is_initialized(&self, slot: usize) -> bool {
237        let Some(inner) = &self.inner else {
238            return false;
239        };
240
241        let slots = inner.slots.borrow();
242
243        match slots.get(slot) {
244            Some(value) => !value.is_empty(),
245            None => false,
246        }
247    }
248
249    /// Read the value in the given slot, distinguishing a slot which is out of
250    /// bounds from one which is merely uninitialized.
251    ///
252    /// The virtual machine needs to tell those apart, since the former means
253    /// the storage doesn't belong to the unit being run and the latter means
254    /// the initializer still has to be evaluated.
255    pub(crate) fn try_get_at(&self, slot: usize) -> Result<Option<Value>, GlobalsError> {
256        let inner = self.inner()?;
257        let slots = inner.slots.borrow();
258
259        let Some(value) = slots.get(slot) else {
260            return Err(GlobalsError::new(GlobalsErrorKind::SlotOutOfBounds {
261                slot,
262                len: slots.len(),
263            }));
264        };
265
266        if value.is_empty() {
267            return Ok(None);
268        }
269
270        Ok(Some(value.clone()))
271    }
272
273    /// Write a slot, returning an error if it is out of bounds.
274    fn replace_at(&self, slot: usize, value: Value) -> Result<(), GlobalsError> {
275        let inner = self.inner()?;
276        let mut slots = inner.slots.borrow_mut();
277        let len = slots.len();
278
279        let Some(existing) = slots.get_mut(slot) else {
280            return Err(GlobalsError::new(GlobalsErrorKind::SlotOutOfBounds {
281                slot,
282                len,
283            }));
284        };
285
286        *existing = value;
287        Ok(())
288    }
289
290    /// Deconstruct this handle into its shared allocation.
291    #[inline]
292    pub(crate) fn into_inner(self) -> Option<Rc<GlobalsInner>> {
293        self.inner
294    }
295
296    /// Construct a handle from a shared allocation.
297    #[inline]
298    pub(crate) fn from_inner(inner: Option<Rc<GlobalsInner>>) -> Self {
299        Self { inner }
300    }
301
302    #[inline]
303    fn inner(&self) -> Result<&GlobalsInner, GlobalsError> {
304        match &self.inner {
305            Some(inner) => Ok(inner),
306            None => Err(GlobalsError::new(GlobalsErrorKind::Missing)),
307        }
308    }
309}
310
311impl TryClone for Globals {
312    #[inline]
313    fn try_clone(&self) -> alloc::Result<Self> {
314        Ok(self.clone())
315    }
316}
317
318impl fmt::Debug for Globals {
319    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
320        let mut f = f.debug_struct("Globals");
321
322        match &self.inner {
323            Some(inner) => f.field("len", &inner.slots.borrow().len()),
324            None => f.field("len", &0usize),
325        };
326
327        f.finish_non_exhaustive()
328    }
329}
330
331/// An error raised when accessing static item storage.
332#[derive(Debug)]
333pub struct GlobalsError {
334    kind: GlobalsErrorKind,
335}
336
337impl GlobalsError {
338    #[inline]
339    const fn new(kind: GlobalsErrorKind) -> Self {
340        Self { kind }
341    }
342}
343
344impl fmt::Display for GlobalsError {
345    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
346        match &self.kind {
347            GlobalsErrorKind::Missing => {
348                write!(f, "No static item storage has been configured")
349            }
350            GlobalsErrorKind::MissingStatic { hash, item } => match item {
351                Some(item) => write!(f, "Missing static item `{item}`"),
352                None => write!(f, "Missing static item with hash {hash}"),
353            },
354            GlobalsErrorKind::SlotOutOfBounds { slot, len } => {
355                write!(f, "Static slot {slot} is out of bounds 0-{len}")
356            }
357        }
358    }
359}
360
361impl core::error::Error for GlobalsError {}
362
363#[derive(Debug)]
364enum GlobalsErrorKind {
365    Missing,
366    MissingStatic { hash: Hash, item: Option<ItemBuf> },
367    SlotOutOfBounds { slot: usize, len: usize },
368}