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