Skip to main content

canic_core/memory/runtime/
mod.rs

1pub mod registry;
2
3use std::sync::{
4    Mutex,
5    atomic::{AtomicBool, Ordering},
6};
7
8// -----------------------------------------------------------------------------
9// CANIC_EAGER_TLS
10// -----------------------------------------------------------------------------
11// Internal registry of "TLS touch" functions.
12//
13// Each function must be a plain `fn()` pointer (not a closure). When invoked,
14// the function must perform a `.with(|_| {})` on a thread_local! static.
15// This guarantees that the TLS slot is *initialized eagerly*, not lazily, so
16// stable memory pages or other backing buffers are allocated in a deterministic
17// order before any canister entry points are executed.
18//
19// These functions are registered by the `eager_static!` macro via
20// `defer_tls_initializer()`, and run once during process startup by
21// `init_eager_tls()`.
22// -----------------------------------------------------------------------------
23
24static CANIC_EAGER_TLS: Mutex<Vec<fn()>> = Mutex::new(Vec::new());
25static CANIC_EAGER_TLS_RUNNING: AtomicBool = AtomicBool::new(false);
26static CANIC_EAGER_INIT: Mutex<Vec<fn()>> = Mutex::new(Vec::new());
27#[cfg(any(test, debug_assertions))]
28static TEST_BOOTSTRAP_HOOK: Mutex<Option<fn()>> = Mutex::new(None);
29
30/// Run all deferred TLS initializers and clear the registry.
31///
32/// This drains the internal queue of initializer functions and invokes
33/// each *exactly once*. The use of `std::mem::take` ensures:
34///
35/// - the vector is fully emptied before we run any initializers
36/// - we drop the borrow before calling user code (prevents borrow panics)
37/// - functions cannot be re-run accidentally
38/// - reentrant modifications of the queue become visible *after* this call
39///
40/// This should be invoked before any IC canister lifecycle hooks (init, update,
41/// heartbeat, etc.) so that thread-local caches are in a fully-initialized state
42/// before the canister performs memory-dependent work.
43pub fn init_eager_tls() {
44    ///
45    /// RunningGuard
46    ///
47
48    struct RunningGuard;
49
50    impl Drop for RunningGuard {
51        fn drop(&mut self) {
52            CANIC_EAGER_TLS_RUNNING.store(false, Ordering::SeqCst);
53        }
54    }
55
56    CANIC_EAGER_TLS_RUNNING.store(true, Ordering::SeqCst);
57    let _running_guard = RunningGuard;
58
59    let funcs = {
60        let mut funcs = CANIC_EAGER_TLS.lock().expect("eager tls queue poisoned");
61        std::mem::take(&mut *funcs)
62    };
63
64    debug_assert!(
65        CANIC_EAGER_TLS
66            .lock()
67            .expect("eager tls queue poisoned")
68            .is_empty(),
69        "CANIC_EAGER_TLS was modified during init_eager_tls() execution"
70    );
71
72    for f in funcs {
73        f();
74    }
75}
76
77/// Run all registered eager-init hooks and clear the registry.
78///
79/// This drains the internal queue of eager-init functions and invokes each
80/// exactly once. Canic uses this during synchronous lifecycle bootstrap before
81/// the memory registry is committed, so explicit memory declarations can be
82/// collected without opening stable-memory handles.
83pub fn run_registered_eager_init() {
84    let funcs = {
85        let mut funcs = CANIC_EAGER_INIT.lock().expect("eager init queue poisoned");
86        std::mem::take(&mut *funcs)
87    };
88
89    debug_assert!(
90        CANIC_EAGER_INIT
91            .lock()
92            .expect("eager init queue poisoned")
93            .is_empty(),
94        "CANIC_EAGER_INIT was modified during eager init execution"
95    );
96
97    for f in funcs {
98        f();
99    }
100}
101
102/// Return whether eager TLS initializers are currently being executed.
103#[must_use]
104pub fn is_eager_tls_initializing() -> bool {
105    CANIC_EAGER_TLS_RUNNING.load(Ordering::SeqCst)
106}
107
108/// Return whether memory access is currently allowed during bootstrap.
109#[must_use]
110pub fn is_memory_bootstrap_ready() -> bool {
111    registry::MemoryRegistryRuntime::validated_allocations().is_ok()
112}
113
114/// Panic if a stable-memory slot is touched before memory bootstrap is ready.
115pub fn assert_memory_bootstrap_ready(label: &str, id: u8) {
116    if is_memory_bootstrap_ready() {
117        return;
118    }
119
120    #[cfg(any(test, debug_assertions))]
121    {
122        run_test_bootstrap_hook();
123        if is_memory_bootstrap_ready() {
124            return;
125        }
126    }
127
128    panic!(
129        "stable memory slot '{label}' (id {id}) accessed before memory bootstrap; call init_eager_tls() and MemoryRegistryRuntime::init(...) first"
130    );
131}
132
133/// Register a TLS initializer function for eager execution.
134///
135/// This is called by the `eager_static!` macro. The function pointer `f`
136/// must be a zero-argument function (`fn()`) that performs a `.with(|_| {})`
137/// on the thread-local static it is meant to initialize.
138pub fn defer_tls_initializer(f: fn()) {
139    CANIC_EAGER_TLS
140        .lock()
141        .expect("eager tls queue poisoned")
142        .push(f);
143}
144
145/// Register an eager-init function for lifecycle bootstrap execution.
146pub fn defer_eager_init(f: fn()) {
147    CANIC_EAGER_INIT
148        .lock()
149        .expect("eager init queue poisoned")
150        .push(f);
151}
152
153/// Install a test-only hook that can run the crate's normal memory bootstrap
154/// before host unit tests first touch macro-backed stable memory.
155#[cfg(any(test, debug_assertions))]
156pub fn install_test_bootstrap_hook(hook: fn()) {
157    *TEST_BOOTSTRAP_HOOK
158        .lock()
159        .expect("test bootstrap hook poisoned") = Some(hook);
160}
161
162/// Return whether a test bootstrap hook has been installed.
163#[cfg(any(test, debug_assertions))]
164#[must_use]
165pub fn has_test_bootstrap_hook() -> bool {
166    TEST_BOOTSTRAP_HOOK
167        .lock()
168        .expect("test bootstrap hook poisoned")
169        .is_some()
170}
171
172#[cfg(any(test, debug_assertions))]
173fn run_test_bootstrap_hook() {
174    let hook = *TEST_BOOTSTRAP_HOOK
175        .lock()
176        .expect("test bootstrap hook poisoned");
177    if let Some(hook) = hook {
178        hook();
179    }
180}
181
182///
183/// TESTS
184///
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use std::sync::{
190        Mutex,
191        atomic::{AtomicU32, Ordering},
192    };
193
194    static COUNT: AtomicU32 = AtomicU32::new(0);
195    static TEST_LOCK: Mutex<()> = Mutex::new(());
196
197    fn clear_test_queues() {
198        CANIC_EAGER_TLS
199            .lock()
200            .expect("eager tls queue poisoned")
201            .clear();
202        CANIC_EAGER_INIT
203            .lock()
204            .expect("eager init queue poisoned")
205            .clear();
206    }
207
208    fn bump() {
209        COUNT.fetch_add(1, Ordering::SeqCst);
210    }
211
212    #[test]
213    fn init_eager_tls_runs_and_clears_queue() {
214        let _guard = TEST_LOCK.lock().expect("test lock poisoned");
215        clear_test_queues();
216        COUNT.store(0, Ordering::SeqCst);
217        CANIC_EAGER_TLS
218            .lock()
219            .expect("eager tls queue poisoned")
220            .push(bump);
221        init_eager_tls();
222        let first = COUNT.load(Ordering::SeqCst);
223        assert_eq!(first, 1);
224
225        // second call sees empty queue
226        init_eager_tls();
227        let second = COUNT.load(Ordering::SeqCst);
228        assert_eq!(second, 1);
229    }
230
231    #[test]
232    fn run_registered_eager_init_runs_and_clears_queue() {
233        let _guard = TEST_LOCK.lock().expect("test lock poisoned");
234        clear_test_queues();
235        COUNT.store(0, Ordering::SeqCst);
236        CANIC_EAGER_INIT
237            .lock()
238            .expect("eager init queue poisoned")
239            .push(bump);
240        run_registered_eager_init();
241        let first = COUNT.load(Ordering::SeqCst);
242        assert_eq!(first, 1);
243
244        // second call sees empty queue
245        run_registered_eager_init();
246        let second = COUNT.load(Ordering::SeqCst);
247        assert_eq!(second, 1);
248    }
249}