The Memory Trait
Stable structures are responsible for managing their own memory.
To provide maximum flexibility, the library introduces the Memory
trait:
#![allow(unused)] fn main() { pub trait Memory { /// Equivalent to WebAssembly memory.size. fn size(&self) -> u64; /// Equivalent to WebAssembly memory.grow. /// Returns the previous size, or -1 if the grow fails. fn grow(&self, pages: u64) -> i64; /// Copies bytes from this memory to the heap (in Wasm, memory 0). /// Panics or traps if out of bounds. fn read(&self, offset: u64, dst: &mut [u8]); /// Writes bytes from the heap (in Wasm, memory 0) to this memory. /// Panics or traps if out of bounds. fn write(&self, offset: u64, src: &[u8]); } }
The Memory
trait intentionally models a WebAssembly memory instance.
This design choice ensures consistency with the interface of memories available to canisters.
It also provides future compatibility with potential multi-memory support in canisters.
Panics
⚠️ read
and write
assume the caller will not access memory outside the current size.
If the range [offset … offset + len)
exceeds available memory, the call panics (in native tests) or traps (in a Wasm canister).
Callers must store and check data lengths themselves or use higher-level containers such as StableVec
.
Available Memory Implementations
The library provides several implementations of the Memory
trait, each designed for specific use cases:
Ic0StableMemory
: Stores data in the Internet Computer's stable memoryVectorMemory
: An in-memory implementation backed by a RustVec<u8>
DefaultMemoryImpl
: A smart implementation that automatically selects the appropriate memory backend:- Uses
Ic0StableMemory
when running in an Internet Computer canister (wasm32 target) - Falls back to
VectorMemory
in other environments (like tests or non-IC contexts)
- Uses
Additional implementations such as FileMemory
and RestrictedMemory
exist but are less commonly used.
In most cases, you should use DefaultMemoryImpl
as your memory implementation.
Usage Example
Here's how to initialize a stable BTreeMap
using DefaultMemoryImpl
:
#![allow(unused)] fn main() { use ic_stable_structures::{BTreeMap, DefaultMemoryImpl}; let mut map: BTreeMap<u64, u64, _> = BTreeMap::init(DefaultMemoryImpl::default()); }
Important: Stable structures cannot share memories. Each memory must be dedicated to a single stable structure.
While the above example works correctly, it demonstrates a potential issue: the BTreeMap
will use the entire stable memory.
This becomes problematic when trying to use multiple stable structures.
For example, the following code will fail in a canister:
#![allow(unused)] fn main() { use ic_stable_structures::{BTreeMap, DefaultMemoryImpl}; let mut map_1: BTreeMap<u64, u64, _> = BTreeMap::init(DefaultMemoryImpl::default()); let mut map_2: BTreeMap<u64, u64, _> = BTreeMap::init(DefaultMemoryImpl::default()); map_1.insert(1, 2); map_2.insert(1, 3); assert_eq!(map_1.get(&1), Some(2)); // This assertion fails. }
The code fails because both map_1
and map_2
are using the same stable memory.
This causes changes in one map to affect or corrupt the other.
To solve this problem, the library provides the MemoryManager, which creates up to 255 virtual memories from a single memory instance. We'll explore this solution in the next section.