ic-memory
ic-memory helps Internet Computer canisters avoid opening the wrong stable
memory after an upgrade.
It remembers this mapping forever:
logical store -> physical stable-memory slot
If a future version tries to move that store to a different slot, or reuse that
slot for a different store, ic-memory rejects the layout before stable-memory
handles are opened.
Why Use It?
Use ic-memory when a canister has more than one stable store and the layout
can change over time.
It is most useful for frameworks, generated canisters, multi-store apps, plugin systems, and canister families that evolve across releases.
You probably do not need it for a tiny canister with one hand-written stable structure and a fixed layout.
The Bug
Version 1 ships with:
app.users.v1 -> MemoryManager ID 100
app.orders.v1 -> MemoryManager ID 101
A later upgrade accidentally ships with:
app.users.v1 -> MemoryManager ID 101
app.orders.v1 -> MemoryManager ID 100
That can still compile. It can even install.
But now the canister may open orders data as users data, and users data as
orders data. ic-memory catches that mismatch first.
Quick Start
Declare both direct dependencies:
[]
= "0.12.1"
= "0.7.2"
Declare the MemoryManager IDs your crate owns. A shared compile-time constant keeps the explicit authority identical across the range and each key:
const MEMORY_AUTHORITY: &str = "icydb.test_db";
ic_memory_range!;
The authority string is explicit stable policy metadata. It is not persisted allocation identity; the stable key and memory ID fill that role. Use the same authority value for the package's range and key declarations, and do not derive it from a Cargo package name or module path.
Open stable structures through ic_memory_key!:
use RefCell;
thread_local!
Bootstrap once per concrete memory runtime before touching stable data:
That is the normal path.
The default runtime API is exported from the crate root. It is one
thread-local MemoryRuntime<DefaultMemoryImpl>, so every native thread owns an
independent backing memory, lifecycle, committed capability, and diagnostic
view. On IC Wasm, execution is single-threaded and the same TLS object naturally
has canister-instance lifetime.
Use helpers such as
ic_memory::bootstrap_default_memory_manager(),
ic_memory::bootstrap_default_memory_manager_with_policy(...),
ic_memory::committed_allocations(),
ic_memory::open_default_memory_manager_memory(...), and the macros shown
above; implementation modules are private.
The no-argument bootstrap helper uses ic-memory's built-in versioned policy
identity. A custom policy implements both AllocationPolicy and
RuntimeBootstrapPolicy; its static identity must change whenever its
configuration or semantics change.
Multi-Crate Composition
Every crate registers into the same linked declaration registry. Crates do not need to import or name each other:
The linked program seals one immutable, canonical declaration snapshot.
Bootstrap supplies that snapshot to the calling thread's default runtime,
recovers and commits that runtime's allocation ledger, and publishes committed
allocations into that runtime only. TLS-backed stores open when your code first
touches the thread_local!.
Duplicate stable keys, duplicate MemoryManager IDs, overlapping ranges, and out-of-range declarations fail before stable structures open.
ic-memory follows the ic-stable-structures::MemoryManager ID domain exactly:
IDs 0..=254 are usable, and ID 255 is always the unallocated sentinel. It is
not an application slot and cannot be declared or reserved.
The default runtime reserves MemoryManager IDs 0..=9 and stable keys under
ic_memory.* for allocation-governance records. The ledger itself lives at ID
0; it remains in the durable ledger for recovery, but public runtime helpers
do not publish or open that internal allocation as application memory.
Range claims are authoritative in the default runtime. If a crate registers
ic_memory_range!, its declared memories must stay inside that range. Framework
adapters that want their own range policy, such as Canic, should register only
the ranges they want ic-memory to enforce and put the rest in their policy
adapter.
The committed allocation state is an in-memory capability published into one runtime only after that runtime's stable-cell persistence succeeds. It is not a serde payload and should not be treated as configuration.
Explicit Runtimes
Frameworks and tests that own backing memory directly should use
MemoryRuntime<M> as the canonical API:
use ;
let declarations = sealed_declaration_snapshot?;
let mut runtime = new;
runtime.bootstrap?;
let rows = runtime.open_memory?;
let diagnostics = runtime.diagnostic_export?;
Each runtime owns all facts derived from backing_memory: recovery, ledger
cell, lifecycle, committed allocations, opens, diagnostics, and live sizes.
Multiple runtimes share only the immutable linked declaration snapshot. A
failed bootstrap publishes no capability, and repeated bootstrap on the same
runtime object is idempotent only when the snapshot and
RuntimeBootstrapPolicy::runtime_bootstrap_identity() match the successful
bootstrap. A changed snapshot or policy identity returns a typed error without
touching the ledger. Policy implementations should change their identity
whenever policy configuration or semantics change.
There is intentionally no public reset API. Native tests should construct a new explicit runtime or use the naturally independent default TLS runtime; changing global flags cannot reset a concrete stable-memory instance safely.
Diagnostics
Use default_memory_manager_doctor_report() for operator-facing preflight and
runtime diagnostics. It returns a typed error if the default TLS runtime is
re-entered. Otherwise it can be called before or after bootstrap and reports the
stable-cell status, protected commit recovery state, recovered ledger export,
registered declarations, range authority, validation preflight, and live
MemoryManager slot sizes when they can be recovered.
Diagnostic failures carry stable DiagnosticCode values alongside their
human-readable messages for operator automation.
Use default_memory_manager_commit_recovery_diagnostic() when you only need
commit-slot presence and validity, the selected authoritative generation, and
any corruption or ambiguity error.
Stable Keys
Stable keys are permanent logical store names. They should describe ownership and purpose, not the current memory ID.
namespace.component.store_or_role.vN
Examples:
use StableKey;
parse.expect;
parse.expect;
parse.expect;
Changing a key creates a new logical allocation identity. If the durable store is the same, keep the stable key and update schema metadata instead.
Schema metadata is optional diagnostic metadata for the in-place store schema.
Construct it with SchemaMetadata::new(Some(version)); version 0 is reserved
for absence and is rejected.
More Detail
The short version:
declare ranges
register stable stores
seal linked declarations
bootstrap once per memory runtime
only then open stable memory
Framework authors and policy adapters should read
ADVANCED.md.
The non-negotiable invariants are recorded in
SAFETY.md. The
protocol whitepaper lives in
whitepaper/src/SUMMARY.md
and builds as an mdBook with make maintainer-build.
ic-memory is early infrastructure extracted from Canic. It owns allocation
governance, not schema migration, endpoint routing, authorization, or data
semantics.