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 ///
50 /// # Panics
51 ///
52 /// Panics if a private growth-accounting invariant is broken or backing
53 /// memory panics.
54 pub fn grow(&self, pages: u64) -> Result<u64, super::RuntimeGrowError> {
55 use super::RuntimeGrowError;
56 let mut allocated = self
57 .growth
58 .allocated_buckets
59 .try_borrow_mut()
60 .map_err(|_| RuntimeGrowError::ReentrantAccess)?;
61 let old_pages = self.memory.size();
62 let new_pages = old_pages
63 .checked_add(pages)
64 .ok_or(RuntimeGrowError::ArithmeticOverflow)?;
65 let bucket_pages = u64::from(self.growth.bucket_size_pages);
66 let extra = new_pages.div_ceil(bucket_pages) - old_pages.div_ceil(bucket_pages);
67 let total = u64::from(*allocated)
68 .checked_add(extra)
69 .ok_or(RuntimeGrowError::ArithmeticOverflow)?;
70 if total > u64::from(super::layout::BUCKET_CAPACITY) {
71 return Err(RuntimeGrowError::BucketExhausted {
72 required_buckets: total,
73 capacity: super::layout::BUCKET_CAPACITY,
74 });
75 }
76 #[expect(
77 clippy::cast_possible_truncation,
78 reason = "admission bounds total by the u16 bucket capacity"
79 )]
80 let total_buckets = total as u16;
81 let required_pages = 1 + total * bucket_pages;
82 let physical_pages = self.growth.backing.size();
83 if required_pages > physical_pages
84 && self.growth.backing.grow(required_pages - physical_pages) < 0
85 {
86 return Err(RuntimeGrowError::BackingRefused {
87 additional_pages: required_pages - physical_pages,
88 });
89 }
90 // The pinned manager's only refusal is bucket exhaustion, already
91 // checked above while all handles share this exclusive reservation.
92 // Physical capacity is reserved before it assigns any buckets.
93 assert_eq!(
94 self.memory.grow(pages),
95 old_pages.cast_signed(),
96 "preflighted manager growth returns the previous virtual extent"
97 );
98 *allocated = total_buckets;
99 Ok(old_pages)
100 }
101}
102
103impl<M: Memory> Memory for RuntimeMemory<M> {
104 fn size(&self) -> u64 {
105 self.memory.size()
106 }
107 fn grow(&self, pages: u64) -> i64 {
108 // The upstream trait fixes the sentinel contract. Direct calls use the
109 // inherent typed method; virtual extents fit in i64 by bucket capacity.
110 Self::grow(self, pages).map_or(-1, u64::cast_signed)
111 }
112 fn read(&self, offset: u64, dst: &mut [u8]) {
113 self.memory.read(offset, dst);
114 }
115 #[expect(
116 unsafe_code,
117 reason = "delegate the upstream raw-read contract unchanged"
118 )]
119 unsafe fn read_unsafe(&self, offset: u64, dst: *mut u8, count: usize) {
120 // SAFETY: The caller supplies a valid destination disjoint from this
121 // memory and its backing. Forwarding preserves the pointer and count;
122 // VirtualMemory owns bucket translation and initializes the destination
123 // on success. After a panic, initialization must not be assumed.
124 unsafe { self.memory.read_unsafe(offset, dst, count) }
125 }
126 fn write(&self, offset: u64, src: &[u8]) {
127 self.memory.write(offset, src);
128 }
129}