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