Skip to main content

ic_storage/
memory.rs

1//! Storage-owned backing interface and native/IC implementations.
2use crate::WASM_PAGE_SIZE_BYTES as PAGE;
3use std::{cell::RefCell, rc::Rc};
4
5///
6/// Memory
7///
8/// Page-addressed backing for Storage. Growth returns the previous page count,
9/// or -1 without changing memory. Successful growth adds zero-filled pages.
10/// Reads and writes must reject the complete out-of-bounds range before IO.
11/// Implementing this trait alone does not grant allocation authority.
12///
13pub trait Memory {
14    /// Current extent in 65,536-byte pages.
15    fn size(&self) -> u64;
16    /// Add zero-filled pages, or return -1 on refusal.
17    fn grow(&self, pages: u64) -> i64;
18    /// Read an admitted byte range.
19    fn read(&self, offset: u64, bytes: &mut [u8]);
20    /// Read into a potentially uninitialized destination, following ICSS's hook.
21    ///
22    /// Every implementation must initialize all `count` bytes on successful
23    /// return. A failed read may leave a partially initialized destination.
24    /// The default supports existing backings through [`Self::read`]; overrides
25    /// can avoid zeroing bytes that backing IO will overwrite.
26    ///
27    /// # Safety
28    /// The caller must provide an exclusive writable `count`-byte range valid
29    /// for a Rust slice. It must not overlap this memory's state or read source.
30    #[inline]
31    #[expect(
32        unsafe_code,
33        reason = "ICSS raw-read contract admits an exclusive destination and guarantees initialization"
34    )]
35    unsafe fn read_unsafe(&self, offset: u64, dst: *mut u8, count: usize) {
36        // Adapted from ICSS 0.7.2 Memory::read_unsafe (Apache-2.0).
37        // SAFETY: the caller admits the range; zeroing makes the u8 slice valid.
38        unsafe {
39            std::ptr::write_bytes(dst, 0, count);
40            self.read(offset, std::slice::from_raw_parts_mut(dst, count));
41        }
42    }
43    /// Write an admitted byte range. Native writes need not be transactional.
44    fn write(&self, offset: u64, bytes: &[u8]);
45}
46
47impl<M: Memory + ?Sized> Memory for Rc<M> {
48    fn size(&self) -> u64 {
49        self.as_ref().size()
50    }
51    fn grow(&self, pages: u64) -> i64 {
52        self.as_ref().grow(pages)
53    }
54    fn read(&self, offset: u64, bytes: &mut [u8]) {
55        self.as_ref().read(offset, bytes);
56    }
57    #[expect(
58        unsafe_code,
59        reason = "Forward the caller's admitted destination and initialization contract"
60    )]
61    unsafe fn read_unsafe(&self, offset: u64, dst: *mut u8, count: usize) {
62        // SAFETY: the same destination and count retain the caller's guarantees.
63        unsafe { self.as_ref().read_unsafe(offset, dst, count) }
64    }
65    fn write(&self, offset: u64, bytes: &[u8]) {
66        self.as_ref().write(offset, bytes);
67    }
68}
69impl<M: Memory + ?Sized> Memory for &M {
70    fn size(&self) -> u64 {
71        (**self).size()
72    }
73    fn grow(&self, pages: u64) -> i64 {
74        (**self).grow(pages)
75    }
76    fn read(&self, offset: u64, bytes: &mut [u8]) {
77        (**self).read(offset, bytes);
78    }
79    #[expect(
80        unsafe_code,
81        reason = "Forward the caller's admitted destination and initialization contract"
82    )]
83    unsafe fn read_unsafe(&self, offset: u64, dst: *mut u8, count: usize) {
84        // SAFETY: the same destination and count retain the caller's guarantees.
85        unsafe { (**self).read_unsafe(offset, dst, count) }
86    }
87    fn write(&self, offset: u64, bytes: &[u8]) {
88        (**self).write(offset, bytes);
89    }
90}
91
92/// Shared native backing; clones identify the same physical bytes.
93pub type VectorMemory = Rc<RefCell<Vec<u8>>>;
94impl Memory for RefCell<Vec<u8>> {
95    fn size(&self) -> u64 {
96        self.borrow().len() as u64 / PAGE
97    }
98    fn grow(&self, pages: u64) -> i64 {
99        let previous = self.size();
100        let Some(length) = previous
101            .checked_add(pages)
102            .and_then(|value| value.checked_mul(PAGE))
103            .and_then(|value| usize::try_from(value).ok())
104        else {
105            return -1;
106        };
107        let mut bytes = self.borrow_mut();
108        let additional = length - bytes.len();
109        if bytes.try_reserve(additional).is_err() {
110            return -1;
111        }
112        bytes.resize(length, 0);
113        i64::try_from(previous).expect("native page count")
114    }
115    fn read(&self, offset: u64, bytes: &mut [u8]) {
116        let start = usize::try_from(offset).expect("native offset");
117        let end = start
118            .checked_add(bytes.len())
119            .expect("native range overflow");
120        bytes.copy_from_slice(&self.borrow()[start..end]);
121    }
122    #[expect(
123        unsafe_code,
124        reason = "Copy a fully admitted source into the caller's exclusive non-overlapping destination"
125    )]
126    unsafe fn read_unsafe(&self, offset: u64, dst: *mut u8, count: usize) {
127        let start = usize::try_from(offset).expect("native offset");
128        let end = start.checked_add(count).expect("native range overflow");
129        let backing = self.borrow();
130        let source = &backing[start..end];
131        // SAFETY: the source is admitted before copying. The caller guarantees
132        // writable, non-overlapping space for every byte; u8 alignment is one.
133        unsafe { std::ptr::copy_nonoverlapping(source.as_ptr(), dst, count) }
134    }
135    fn write(&self, offset: u64, bytes: &[u8]) {
136        let start = usize::try_from(offset).expect("native offset");
137        let end = start
138            .checked_add(bytes.len())
139            .expect("native range overflow");
140        self.borrow_mut()[start..end].copy_from_slice(bytes);
141    }
142}
143
144/// Native default backing for fixture and host integration.
145#[cfg(not(target_arch = "wasm32"))]
146pub type DefaultMemoryImpl = VectorMemory;
147
148///
149/// `DefaultMemoryImpl`
150///
151/// Canister stable memory through IC Platform's owned system-call boundary.
152///
153#[cfg(target_arch = "wasm32")]
154#[derive(Clone, Copy, Default)]
155pub struct DefaultMemoryImpl {
156    _private: (),
157}
158#[cfg(target_arch = "wasm32")]
159impl Memory for DefaultMemoryImpl {
160    fn size(&self) -> u64 {
161        ic_platform_runtime::ic0::stable64_size()
162    }
163    fn grow(&self, pages: u64) -> i64 {
164        let previous = ic_platform_runtime::ic0::stable64_grow(pages);
165        if previous == u64::MAX {
166            -1
167        } else {
168            i64::try_from(previous).expect("IC page count")
169        }
170    }
171    fn read(&self, offset: u64, bytes: &mut [u8]) {
172        ic_platform_runtime::ic0::stable64_read(bytes, offset);
173    }
174    #[expect(
175        unsafe_code,
176        reason = "Bridge the caller's admitted raw destination to Platform's safe initialization helper"
177    )]
178    unsafe fn read_unsafe(&self, offset: u64, dst: *mut u8, count: usize) {
179        // SAFETY: the caller admits the slice's validity and exclusivity;
180        // MaybeUninit permits its original contents. Platform initializes the
181        // complete destination or traps, satisfying the raw-read postcondition.
182        let bytes = unsafe {
183            std::slice::from_raw_parts_mut(dst.cast::<std::mem::MaybeUninit<u8>>(), count)
184        };
185        ic_platform_runtime::ic0::stable64_read_uninit(bytes, offset);
186    }
187    fn write(&self, offset: u64, bytes: &[u8]) {
188        ic_platform_runtime::ic0::stable64_write(bytes, offset);
189    }
190}
191
192#[cfg(test)]
193mod tests;