use std::{cell::RefCell, rc::Rc};
use crate::WASM_PAGE_SIZE_BYTES;
pub trait Memory {
fn size(&self) -> u64;
fn grow(&self, pages: u64) -> i64;
fn read(&self, offset: u64, bytes: &mut [u8]);
#[expect(
unsafe_code,
reason = "The raw destination is an explicit caller-owned boundary."
)]
unsafe fn read_unsafe(&self, offset: u64, dst: *mut u8, count: usize) {
admit_range(offset, count, self.size());
let mut scratch = [0_u8; 1024];
let mut copied = 0;
while copied < count {
let chunk = (count - copied).min(scratch.len());
let source_offset = offset + u64::try_from(copied).expect("admitted offset");
self.read(source_offset, &mut scratch[..chunk]);
unsafe {
std::ptr::copy_nonoverlapping(scratch.as_ptr(), dst.add(copied), chunk);
}
copied += chunk;
}
}
fn write(&self, offset: u64, bytes: &[u8]);
}
impl<M: Memory + ?Sized> Memory for Rc<M> {
fn size(&self) -> u64 {
(**self).size()
}
fn grow(&self, pages: u64) -> i64 {
(**self).grow(pages)
}
fn read(&self, offset: u64, bytes: &mut [u8]) {
(**self).read(offset, bytes);
}
#[expect(
unsafe_code,
reason = "Forwarding preserves the caller's raw-read obligations."
)]
unsafe fn read_unsafe(&self, offset: u64, dst: *mut u8, count: usize) {
unsafe { (**self).read_unsafe(offset, dst, count) };
}
fn write(&self, offset: u64, bytes: &[u8]) {
(**self).write(offset, bytes);
}
}
impl<M: Memory + ?Sized> Memory for &M {
fn size(&self) -> u64 {
(**self).size()
}
fn grow(&self, pages: u64) -> i64 {
(**self).grow(pages)
}
fn read(&self, offset: u64, bytes: &mut [u8]) {
(**self).read(offset, bytes);
}
#[expect(
unsafe_code,
reason = "Forwarding preserves the caller's raw-read obligations."
)]
unsafe fn read_unsafe(&self, offset: u64, dst: *mut u8, count: usize) {
unsafe { (**self).read_unsafe(offset, dst, count) };
}
fn write(&self, offset: u64, bytes: &[u8]) {
(**self).write(offset, bytes);
}
}
pub type VectorMemory = Rc<RefCell<Vec<u8>>>;
#[cfg(not(target_arch = "wasm32"))]
pub type DefaultMemoryImpl = VectorMemory;
impl Memory for RefCell<Vec<u8>> {
fn size(&self) -> u64 {
u64::try_from(self.borrow().len()).expect("native byte length fits u64")
/ WASM_PAGE_SIZE_BYTES
}
fn grow(&self, pages: u64) -> i64 {
let mut bytes = self.borrow_mut();
let length = u64::try_from(bytes.len()).expect("native byte length fits u64");
let old_pages = length / WASM_PAGE_SIZE_BYTES;
let Ok(previous) = i64::try_from(old_pages) else {
return -1;
};
if pages == 0 {
return previous;
}
if !length.is_multiple_of(WASM_PAGE_SIZE_BYTES) {
return -1;
}
let Some(new_length) = old_pages
.checked_add(pages)
.and_then(|total| total.checked_mul(WASM_PAGE_SIZE_BYTES))
.and_then(|total| usize::try_from(total).ok())
else {
return -1;
};
let additional = new_length - bytes.len();
if bytes.try_reserve_exact(additional).is_err() {
return -1;
}
bytes.resize(new_length, 0);
previous
}
fn read(&self, offset: u64, bytes: &mut [u8]) {
let backing = self.borrow();
let range = native_range(offset, bytes.len(), backing.len());
bytes.copy_from_slice(&backing[range]);
}
#[expect(
unsafe_code,
reason = "Direct copying initializes caller-owned raw output without a temporary."
)]
unsafe fn read_unsafe(&self, offset: u64, dst: *mut u8, count: usize) {
let backing = self.borrow();
let range = native_range(offset, count, backing.len());
unsafe {
std::ptr::copy_nonoverlapping(backing[range].as_ptr(), dst, count);
}
}
fn write(&self, offset: u64, bytes: &[u8]) {
let mut backing = self.borrow_mut();
let range = native_range(offset, bytes.len(), backing.len());
backing[range].copy_from_slice(bytes);
}
}
fn admit_range(offset: u64, count: usize, pages: u64) {
let extent = pages
.checked_mul(WASM_PAGE_SIZE_BYTES)
.expect("memory extent overflow");
let count = u64::try_from(count).expect("memory access length overflow");
let end = offset.checked_add(count).expect("memory access overflow");
assert!(end <= extent, "memory access outside complete pages");
}
fn native_range(offset: u64, count: usize, length: usize) -> std::ops::Range<usize> {
let pages = u64::try_from(length).expect("native byte length fits u64") / WASM_PAGE_SIZE_BYTES;
admit_range(offset, count, pages);
let start = usize::try_from(offset).expect("admitted native offset");
start..start + count
}
#[cfg(target_arch = "wasm32")]
#[derive(Clone, Copy, Default)]
pub struct DefaultMemoryImpl {
_private: (),
}
#[cfg(target_arch = "wasm32")]
impl Memory for DefaultMemoryImpl {
fn size(&self) -> u64 {
ic_platform_runtime::ic0::stable64_size()
}
fn grow(&self, pages: u64) -> i64 {
let old_pages = self.size();
let Ok(previous) = i64::try_from(old_pages) else {
return -1;
};
if pages == 0 {
return previous;
}
if old_pages
.checked_add(pages)
.and_then(|total| total.checked_mul(WASM_PAGE_SIZE_BYTES))
.is_none()
{
return -1;
}
i64::try_from(ic_platform_runtime::ic0::stable64_grow(pages)).unwrap_or(-1)
}
fn read(&self, offset: u64, bytes: &mut [u8]) {
if bytes.is_empty() {
admit_range(offset, 0, self.size());
return;
}
ic_platform_runtime::ic0::stable64_read(bytes, offset);
}
#[expect(
unsafe_code,
reason = "MaybeUninit exposes raw writable bytes to the safe Runtime boundary."
)]
unsafe fn read_unsafe(&self, offset: u64, dst: *mut u8, count: usize) {
if count == 0 {
admit_range(offset, 0, self.size());
return;
}
let bytes = unsafe {
std::slice::from_raw_parts_mut(dst.cast::<std::mem::MaybeUninit<u8>>(), count)
};
ic_platform_runtime::ic0::stable64_read_uninit(bytes, offset);
}
fn write(&self, offset: u64, bytes: &[u8]) {
if bytes.is_empty() {
admit_range(offset, 0, self.size());
return;
}
ic_platform_runtime::ic0::stable64_write(bytes, offset);
}
}
#[cfg(test)]
mod contract_tests;