Skip to main content

canic_core/memory/runtime/
mod.rs

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