canic-memory
Shared stable-memory helpers you can drop into any IC canister, even if you are not using the rest of Canic. It keeps you honest about which IDs you use and makes TLS-backed stable structures initialize in a predictable order.
What you get:
- Reserve and validate a per-crate stable-memory ID range.
- One place to register stable structures (
ic_memory!+ registry). - Eager TLS init so thread-locals that allocate memory are ready before entrypoints.
- Zero dependency on the
caniccrate (onlycanic-utilsandcanic-cdk).
Sample boot logs when everything is wired correctly:
17:27:24.796 [...] [Init] 🔧 --------------------- 'canic v0.6.x -----------------------
17:27:24.796 [...] [Init] 🏁 init: root (Prime)
17:27:24.796 [...] [Memory] 💾 memory.range: canic-core [5-30] (15/26 slots used)
17:27:24.796 [...] [Wasm] 📄 registry.insert: app (1013.10 KB)
...
17:27:26.879 [...] [CanisterLifecycle] ⚡ create_canister: nssc3-p7777-77777-aaawa-cai (5.000 TC)
17:27:27.549 [...] [Init] 🏁 init: app
17:27:27.549 [...] [Memory] 💾 memory.range: canic-core [5-30] (15/26 slots used)
Modules
manager— thread-localMemoryManager<DefaultMemoryImpl>used by all helpers.registry— range reservation + ID registry with pending queues for macro-driven registration.ops— helper to flush pending reservations/registrations into the registry during startup.runtime— eager TLS initialization helper.macros—ic_memory!,ic_memory_range!,eager_static!,eager_init!.
Quick start
Add the crate to your Cargo.toml:
= { = true }
Reserve a range and declare a memory slot
// Reserve IDs 10–19 for this crate (usually in a module's init or ctor).
ic_memory_range!;
// Declare a stable-memory slot at ID 10 and wrap it in a stable BTreeMap.
use ic_memory;
use ;
use RefCell;
thread_local!
Flush pending registrations during startup
Call the ops helper once during init/post-upgrade to validate ranges and apply any pending registrations queued by macros. Repeated calls are allowed when the initial range is identical; conflicts return a MemoryRegistryError.
use MemoryRegistryOps;
init_memory will:
- reserve the optional initial range,
- apply all pending range reservations,
- apply all pending ID registrations (sorted),
- return a summary of ranges/entries for logging or inspection.
Eagerly initialize thread-locals that allocate memory
Why bother? thread_local! values are lazy. If a stable BTreeMap (or similar) spins up the first time an endpoint is called, you get:
- unpredictable init order (especially across upgrades),
- memory allocations happening under a user call instead of during init,
- possible panics if the registry/ranges were not flushed yet.
eager_static! and eager_init! make TLS setup a deliberate part of your init flow: run init_eager_tls() → run eager_init! blocks → flush the registry. After that, every endpoint starts with the same, prebuilt memory layout.
use ;
use RefCell;
eager_static!
eager_init!;
Error handling
The registry surfaces MemoryRegistryError for:
- duplicate ranges, overlapping ranges, invalid range (start > end)
- crate keys or labels longer than 256 bytes
- registration outside the crate's reserved ranges
- conflicting registrations on an ID with a different label
- missing range for the crate
Handle these at init time so your canister fails fast on invalid memory layout.
Testing helpers
registry::reset_for_tests() clears the registry and pending queues to keep unit tests isolated. Example:
Notes
- The macros automatically namespace memory IDs by crate (
CARGO_PKG_NAME) when validating ranges. - If you don't want an initial range, omit it and rely solely on
ic_memory_range!calls beforeinit_memory. - Consumers outside Canic can import only
canic-memorypluscanic-utilsandcanic-cdk; the rest of the stack is optional.