Skip to main content

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.
66pub fn is_memory_bootstrap_ready() -> Result<bool, ic_memory::RuntimeStateError> {
67    ic_memory::is_default_memory_manager_bootstrapped()
68}
69
70/// Panic if a stable-memory slot is touched before memory bootstrap is ready.
71///
72/// # Panics
73///
74/// Panics when the default memory manager has not been bootstrapped before the
75/// stable-memory slot identified by `label` and `id` is accessed. In tests and
76/// debug builds, an installed bootstrap hook is run first and the function only
77/// panics if memory remains unbootstrapped after that hook.
78pub fn assert_memory_bootstrap_ready(label: &str, id: u8) {
79    match is_memory_bootstrap_ready() {
80        Ok(true) => return,
81        Ok(false) => {}
82        Err(error) => {
83            panic!(
84                "stable memory slot '{label}' (id {id}) could not inspect memory bootstrap: {error}"
85            );
86        }
87    }
88
89    #[cfg(any(test, debug_assertions))]
90    {
91        run_test_bootstrap_hook();
92        match is_memory_bootstrap_ready() {
93            Ok(true) => return,
94            Ok(false) => {}
95            Err(error) => {
96                panic!(
97                    "stable memory slot '{label}' (id {id}) could not inspect memory bootstrap after the test hook: {error}"
98                );
99            }
100        }
101    }
102
103    panic!(
104        "stable memory slot '{label}' (id {id}) accessed before memory bootstrap; call ic_memory::bootstrap_default_memory_manager_with_policy(...) first"
105    );
106}
107
108/// Register a TLS initializer function for eager execution.
109///
110/// This is called by the `eager_static!` macro. The function pointer `f`
111/// must be a zero-argument function (`fn()`) that performs a `.with(|_| {})`
112/// on the thread-local static it is meant to initialize.
113///
114/// # Panics
115///
116/// Panics if the process-local eager TLS registry mutex is poisoned.
117pub fn defer_tls_initializer(f: fn()) {
118    CANIC_EAGER_TLS
119        .lock()
120        .expect("eager tls queue poisoned")
121        .push(f);
122}
123
124/// Install a test-only hook that can run the crate's normal memory bootstrap
125/// before host unit tests first touch macro-backed stable memory.
126///
127/// # Panics
128///
129/// Panics if the process-local test bootstrap hook mutex is poisoned.
130#[cfg(any(test, debug_assertions))]
131pub fn install_test_bootstrap_hook(hook: fn()) {
132    *TEST_BOOTSTRAP_HOOK
133        .lock()
134        .expect("test bootstrap hook poisoned") = Some(hook);
135}
136
137/// Return whether a test bootstrap hook has been installed.
138///
139/// # Panics
140///
141/// Panics if the process-local test bootstrap hook mutex is poisoned.
142#[cfg(any(test, debug_assertions))]
143#[must_use]
144pub fn has_test_bootstrap_hook() -> bool {
145    TEST_BOOTSTRAP_HOOK
146        .lock()
147        .expect("test bootstrap hook poisoned")
148        .is_some()
149}
150
151#[cfg(any(test, debug_assertions))]
152fn run_test_bootstrap_hook() {
153    let hook = *TEST_BOOTSTRAP_HOOK
154        .lock()
155        .expect("test bootstrap hook poisoned");
156    if let Some(hook) = hook {
157        hook();
158    }
159}
160
161// -----------------------------------------------------------------------------
162// Tests
163// -----------------------------------------------------------------------------
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use std::sync::{
169        Mutex,
170        atomic::{AtomicU32, Ordering},
171    };
172
173    static COUNT: AtomicU32 = AtomicU32::new(0);
174    static TEST_LOCK: Mutex<()> = Mutex::new(());
175
176    fn clear_test_queues() {
177        CANIC_EAGER_TLS
178            .lock()
179            .expect("eager tls queue poisoned")
180            .clear();
181    }
182
183    fn bump() {
184        COUNT.fetch_add(1, Ordering::SeqCst);
185    }
186
187    #[test]
188    fn init_eager_tls_runs_and_clears_queue() {
189        let _guard = TEST_LOCK.lock().expect("test lock poisoned");
190        clear_test_queues();
191        COUNT.store(0, Ordering::SeqCst);
192        CANIC_EAGER_TLS
193            .lock()
194            .expect("eager tls queue poisoned")
195            .push(bump);
196        init_eager_tls();
197        let first = COUNT.load(Ordering::SeqCst);
198        assert_eq!(first, 1);
199
200        // second call sees empty queue
201        init_eager_tls();
202        let second = COUNT.load(Ordering::SeqCst);
203        assert_eq!(second, 1);
204    }
205}