Skip to main content

ic_memory/runtime/
backing.rs

1use ic_stable_structures::{Memory, memory_manager::VirtualMemory};
2use std::{cell::RefCell, rc::Rc};
3
4// Backing, immutable geometry and one live bucket count for the sole manager,
5// shared by the runtime and all handles, including the ledger. The count is
6// seeded from validated metadata on reopen and updated only after grow.
7pub(super) struct GrowthState<M: Memory> {
8    pub backing: Rc<M>,
9    pub bucket_size_pages: u16,
10    pub allocated_buckets: RefCell<u16>,
11}
12
13///
14/// RuntimeMemory
15///
16/// Cloneable virtual memory opened through a runtime's committed authority.
17/// Implements [`Memory`] for stable structures without exposing the backing
18/// memory or an alternate manager. Cloning does not require `M: Clone`.
19/// Reads delegate to the upstream memory implementation, including its
20/// optimized support for uninitialized destinations through `Memory::read_unsafe`.
21/// Growth reserves physical capacity before assigning manager buckets. Ordinary
22/// backing refusal, arithmetic overflow and bucket exhaustion return typed errors
23/// without changing virtual extents or manager metadata. Backing implementations
24/// must obey the `Memory` contract; traps and partial writes are not transactions
25/// on native memory. Typed collections retain their own failure behavior.
26///
27
28pub struct RuntimeMemory<M: Memory> {
29    pub(super) memory: VirtualMemory<Rc<M>>,
30    pub(super) growth: Rc<GrowthState<M>>,
31}
32
33impl<M: Memory> Clone for RuntimeMemory<M> {
34    fn clone(&self) -> Self {
35        Self {
36            memory: self.memory.clone(),
37            growth: Rc::clone(&self.growth),
38        }
39    }
40}
41
42impl<M: Memory> RuntimeMemory<M> {
43    /// Grow by the requested pages, returning the previous virtual page count.
44    ///
45    /// Capacity is reserved before assigning manager buckets. Ordinary refusal
46    /// preserves virtual extents and manager metadata and permits retry. The
47    /// upstream [`Memory::grow`] adapter translates errors into its required
48    /// `-1` sentinel; direct runtime callers receive [`super::RuntimeGrowError`].
49    pub fn grow(&self, pages: u64) -> Result<u64, super::RuntimeGrowError> {
50        use super::RuntimeGrowError;
51        let mut allocated = self
52            .growth
53            .allocated_buckets
54            .try_borrow_mut()
55            .map_err(|_| RuntimeGrowError::ReentrantAccess)?;
56        let old_pages = self.memory.size();
57        let new_pages = old_pages
58            .checked_add(pages)
59            .ok_or(RuntimeGrowError::ArithmeticOverflow)?;
60        let bucket_pages = u64::from(self.growth.bucket_size_pages);
61        let extra = new_pages.div_ceil(bucket_pages) - old_pages.div_ceil(bucket_pages);
62        let total = u64::from(*allocated)
63            .checked_add(extra)
64            .ok_or(RuntimeGrowError::ArithmeticOverflow)?;
65        if total > u64::from(super::layout::BUCKET_CAPACITY) {
66            return Err(RuntimeGrowError::BucketExhausted {
67                required_buckets: total,
68                capacity: super::layout::BUCKET_CAPACITY,
69            });
70        }
71        #[expect(
72            clippy::cast_possible_truncation,
73            reason = "admission bounds total by the u16 bucket capacity"
74        )]
75        let total_buckets = total as u16;
76        let required_pages = 1 + total * bucket_pages;
77        let physical_pages = self.growth.backing.size();
78        if required_pages > physical_pages
79            && self.growth.backing.grow(required_pages - physical_pages) < 0
80        {
81            return Err(RuntimeGrowError::BackingRefused {
82                additional_pages: required_pages - physical_pages,
83            });
84        }
85        let previous =
86            u64::try_from(self.memory.grow(pages)).map_err(|_| RuntimeGrowError::ManagerRefused)?;
87        *allocated = total_buckets;
88        Ok(previous)
89    }
90}
91
92impl<M: Memory> Memory for RuntimeMemory<M> {
93    fn size(&self) -> u64 {
94        self.memory.size()
95    }
96    fn grow(&self, pages: u64) -> i64 {
97        // The upstream trait fixes the sentinel contract. Direct calls use the
98        // inherent typed method; virtual extents fit in i64 by bucket capacity.
99        Self::grow(self, pages).map_or(-1, u64::cast_signed)
100    }
101    fn read(&self, offset: u64, dst: &mut [u8]) {
102        self.memory.read(offset, dst);
103    }
104    #[expect(
105        unsafe_code,
106        reason = "delegate the upstream raw-read contract unchanged"
107    )]
108    unsafe fn read_unsafe(&self, offset: u64, dst: *mut u8, count: usize) {
109        // SAFETY: The caller supplies a valid destination disjoint from this
110        // memory and its backing. Forwarding preserves the pointer and count;
111        // VirtualMemory owns bucket translation and initializes the destination
112        // on success. After a panic, initialization must not be assumed.
113        unsafe { self.memory.read_unsafe(offset, dst, count) }
114    }
115    fn write(&self, offset: u64, src: &[u8]) {
116        self.memory.write(offset, src);
117    }
118}