canic_core/memory/runtime/mod.rs
1//! Module: memory::runtime
2//!
3//! Responsibility: coordinate eager TLS initialization and memory bootstrap readiness.
4//! Does not own: stable schema definitions, allocation policy, or lifecycle hooks.
5//! Boundary: macros and lifecycle call this before stable-memory-backed statics are used.
6
7use std::sync::Mutex;
8
9// -----------------------------------------------------------------------------
10// Eager TLS
11// -----------------------------------------------------------------------------
12// Internal registry of "TLS touch" functions.
13//
14// Each function must be a plain `fn()` pointer (not a closure). When invoked,
15// the function must perform a `.with(|_| {})` on a thread_local! static.
16// This guarantees that the TLS slot is *initialized eagerly*, not lazily, so
17// stable memory pages or other backing buffers are allocated in a deterministic
18// order before any canister entry points are executed.
19//
20// These functions are registered by the `eager_static!` macro via
21// `defer_tls_initializer()`, and run once during process startup by
22// `init_eager_tls()`.
23// -----------------------------------------------------------------------------
24
25static CANIC_EAGER_TLS: Mutex<Vec<fn()>> = Mutex::new(Vec::new());
26#[cfg(any(test, debug_assertions))]
27static TEST_BOOTSTRAP_HOOK: Mutex<Option<fn()>> = Mutex::new(None);
28
29/// Run all deferred TLS initializers and clear the registry.
30///
31/// This drains the internal queue of initializer functions and invokes
32/// each *exactly once*. The use of `std::mem::take` ensures:
33///
34/// - the vector is fully emptied before we run any initializers
35/// - we drop the borrow before calling user code (prevents borrow panics)
36/// - functions cannot be re-run accidentally
37/// - reentrant modifications of the queue become visible *after* this call
38///
39/// This should be invoked before any IC canister lifecycle hooks (init, update,
40/// heartbeat, etc.) so that thread-local caches are in a fully-initialized state
41/// before the canister performs memory-dependent work.
42///
43/// # Panics
44///
45/// Panics if the process-local eager TLS registry mutex is poisoned.
46pub fn init_eager_tls() {
47 let funcs = {
48 let mut funcs = CANIC_EAGER_TLS.lock().expect("eager tls queue poisoned");
49 std::mem::take(&mut *funcs)
50 };
51
52 debug_assert!(
53 CANIC_EAGER_TLS
54 .lock()
55 .expect("eager tls queue poisoned")
56 .is_empty(),
57 "CANIC_EAGER_TLS was modified during init_eager_tls() execution"
58 );
59
60 for f in funcs {
61 f();
62 }
63}
64
65/// Return whether memory access is currently allowed during bootstrap.
66#[must_use]
67pub fn is_memory_bootstrap_ready() -> bool {
68 ic_memory::runtime::is_default_memory_manager_bootstrapped()
69}
70
71/// Panic if a stable-memory slot is touched before memory bootstrap is ready.
72///
73/// # Panics
74///
75/// Panics when the default memory manager has not been bootstrapped before the
76/// stable-memory slot identified by `label` and `id` is accessed. In tests and
77/// debug builds, an installed bootstrap hook is run first and the function only
78/// panics if memory remains unbootstrapped after that hook.
79pub fn assert_memory_bootstrap_ready(label: &str, id: u8) {
80 if is_memory_bootstrap_ready() {
81 return;
82 }
83
84 #[cfg(any(test, debug_assertions))]
85 {
86 run_test_bootstrap_hook();
87 if is_memory_bootstrap_ready() {
88 return;
89 }
90 }
91
92 panic!(
93 "stable memory slot '{label}' (id {id}) accessed before memory bootstrap; call ic_memory::bootstrap_default_memory_manager_with_policy(...) first"
94 );
95}
96
97/// Register a TLS initializer function for eager execution.
98///
99/// This is called by the `eager_static!` macro. The function pointer `f`
100/// must be a zero-argument function (`fn()`) that performs a `.with(|_| {})`
101/// on the thread-local static it is meant to initialize.
102///
103/// # Panics
104///
105/// Panics if the process-local eager TLS registry mutex is poisoned.
106pub fn defer_tls_initializer(f: fn()) {
107 CANIC_EAGER_TLS
108 .lock()
109 .expect("eager tls queue poisoned")
110 .push(f);
111}
112
113/// Install a test-only hook that can run the crate's normal memory bootstrap
114/// before host unit tests first touch macro-backed stable memory.
115///
116/// # Panics
117///
118/// Panics if the process-local test bootstrap hook mutex is poisoned.
119#[cfg(any(test, debug_assertions))]
120pub fn install_test_bootstrap_hook(hook: fn()) {
121 *TEST_BOOTSTRAP_HOOK
122 .lock()
123 .expect("test bootstrap hook poisoned") = Some(hook);
124}
125
126/// Return whether a test bootstrap hook has been installed.
127///
128/// # Panics
129///
130/// Panics if the process-local test bootstrap hook mutex is poisoned.
131#[cfg(any(test, debug_assertions))]
132#[must_use]
133pub fn has_test_bootstrap_hook() -> bool {
134 TEST_BOOTSTRAP_HOOK
135 .lock()
136 .expect("test bootstrap hook poisoned")
137 .is_some()
138}
139
140#[cfg(any(test, debug_assertions))]
141fn run_test_bootstrap_hook() {
142 let hook = *TEST_BOOTSTRAP_HOOK
143 .lock()
144 .expect("test bootstrap hook poisoned");
145 if let Some(hook) = hook {
146 hook();
147 }
148}
149
150// -----------------------------------------------------------------------------
151// Tests
152// -----------------------------------------------------------------------------
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use std::sync::{
158 Mutex,
159 atomic::{AtomicU32, Ordering},
160 };
161
162 static COUNT: AtomicU32 = AtomicU32::new(0);
163 static TEST_LOCK: Mutex<()> = Mutex::new(());
164
165 fn clear_test_queues() {
166 CANIC_EAGER_TLS
167 .lock()
168 .expect("eager tls queue poisoned")
169 .clear();
170 }
171
172 fn bump() {
173 COUNT.fetch_add(1, Ordering::SeqCst);
174 }
175
176 #[test]
177 fn init_eager_tls_runs_and_clears_queue() {
178 let _guard = TEST_LOCK.lock().expect("test lock poisoned");
179 clear_test_queues();
180 COUNT.store(0, Ordering::SeqCst);
181 CANIC_EAGER_TLS
182 .lock()
183 .expect("eager tls queue poisoned")
184 .push(bump);
185 init_eager_tls();
186 let first = COUNT.load(Ordering::SeqCst);
187 assert_eq!(first, 1);
188
189 // second call sees empty queue
190 init_eager_tls();
191 let second = COUNT.load(Ordering::SeqCst);
192 assert_eq!(second, 1);
193 }
194}