canic-memory 0.38.3

Canic — a canister orchestration and management toolkit for the Internet Computer
Documentation
pub mod registry;

use crate::registry::MemoryRegistryError;
use std::sync::{
    Mutex,
    atomic::{AtomicBool, Ordering},
};

// -----------------------------------------------------------------------------
// CANIC_EAGER_TLS
// -----------------------------------------------------------------------------
// Internal registry of "TLS touch" functions.
//
// Each function must be a plain `fn()` pointer (not a closure). When invoked,
// the function must perform a `.with(|_| {})` on a thread_local! static.
// This guarantees that the TLS slot is *initialized eagerly*, not lazily, so
// stable memory pages or other backing buffers are allocated in a deterministic
// order before any canister entry points are executed.
//
// These functions are registered by the `eager_static!` macro via
// `defer_tls_initializer()`, and run once during process startup by
// `init_eager_tls()`.
// -----------------------------------------------------------------------------

static CANIC_EAGER_TLS: Mutex<Vec<fn()>> = Mutex::new(Vec::new());
static CANIC_EAGER_TLS_RUNNING: AtomicBool = AtomicBool::new(false);
static CANIC_EAGER_INIT: Mutex<Vec<fn()>> = Mutex::new(Vec::new());
#[cfg(any(test, debug_assertions))]
static TEST_BOOTSTRAP_HOOK: Mutex<Option<fn()>> = Mutex::new(None);

/// Run all deferred TLS initializers and clear the registry.
///
/// This drains the internal queue of initializer functions and invokes
/// each *exactly once*. The use of `std::mem::take` ensures:
///
/// - the vector is fully emptied before we run any initializers
/// - we drop the borrow before calling user code (prevents borrow panics)
/// - functions cannot be re-run accidentally
/// - reentrant modifications of the queue become visible *after* this call
///
/// This should be invoked before any IC canister lifecycle hooks (init, update,
/// heartbeat, etc.) so that thread-local caches are in a fully-initialized state
/// before the canister performs memory-dependent work.
pub fn init_eager_tls() {
    ///
    /// RunningGuard
    ///

    struct RunningGuard;

    impl Drop for RunningGuard {
        fn drop(&mut self) {
            CANIC_EAGER_TLS_RUNNING.store(false, Ordering::SeqCst);
        }
    }

    CANIC_EAGER_TLS_RUNNING.store(true, Ordering::SeqCst);
    let _running_guard = RunningGuard;

    let funcs = {
        let mut funcs = CANIC_EAGER_TLS.lock().expect("eager tls queue poisoned");
        std::mem::take(&mut *funcs)
    };

    debug_assert!(
        CANIC_EAGER_TLS
            .lock()
            .expect("eager tls queue poisoned")
            .is_empty(),
        "CANIC_EAGER_TLS was modified during init_eager_tls() execution"
    );

    for f in funcs {
        f();
    }
}

/// Run all registered eager-init hooks and clear the registry.
///
/// This drains the internal queue of eager-init functions and invokes each
/// exactly once. Canic uses this during synchronous lifecycle bootstrap before
/// the memory registry is committed, so explicit range declarations can be
/// collected without opening stable-memory handles.
pub fn run_registered_eager_init() {
    let funcs = {
        let mut funcs = CANIC_EAGER_INIT.lock().expect("eager init queue poisoned");
        std::mem::take(&mut *funcs)
    };

    debug_assert!(
        CANIC_EAGER_INIT
            .lock()
            .expect("eager init queue poisoned")
            .is_empty(),
        "CANIC_EAGER_INIT was modified during eager init execution"
    );

    for f in funcs {
        f();
    }
}

/// Return whether eager TLS initializers are currently being executed.
#[must_use]
pub fn is_eager_tls_initializing() -> bool {
    CANIC_EAGER_TLS_RUNNING.load(Ordering::SeqCst)
}

/// Return whether memory access is currently allowed during bootstrap.
#[must_use]
pub fn is_memory_bootstrap_ready() -> bool {
    registry::MemoryRegistryRuntime::is_initialized()
}

/// Panic if a stable-memory slot is touched before memory bootstrap is ready.
pub fn assert_memory_bootstrap_ready(label: &str, id: u8) {
    if is_memory_bootstrap_ready() {
        return;
    }

    #[cfg(any(test, debug_assertions))]
    {
        run_test_bootstrap_hook();
        if is_memory_bootstrap_ready() {
            return;
        }
    }

    panic!(
        "stable memory slot '{label}' (id {id}) accessed before memory bootstrap; call init_eager_tls() and MemoryRegistryRuntime::init(...) first"
    );
}

/// Register a TLS initializer function for eager execution.
///
/// This is called by the `eager_static!` macro. The function pointer `f`
/// must be a zero-argument function (`fn()`) that performs a `.with(|_| {})`
/// on the thread-local static it is meant to initialize.
pub fn defer_tls_initializer(f: fn()) {
    CANIC_EAGER_TLS
        .lock()
        .expect("eager tls queue poisoned")
        .push(f);
}

/// Register an eager-init function for lifecycle bootstrap execution.
pub fn defer_eager_init(f: fn()) {
    CANIC_EAGER_INIT
        .lock()
        .expect("eager init queue poisoned")
        .push(f);
}

/// Install a test-only hook that can run the crate's normal memory bootstrap
/// before host unit tests first touch macro-backed stable memory.
#[cfg(any(test, debug_assertions))]
pub fn install_test_bootstrap_hook(hook: fn()) {
    *TEST_BOOTSTRAP_HOOK
        .lock()
        .expect("test bootstrap hook poisoned") = Some(hook);
}

/// Return whether a test bootstrap hook has been installed.
#[cfg(any(test, debug_assertions))]
#[must_use]
pub fn has_test_bootstrap_hook() -> bool {
    TEST_BOOTSTRAP_HOOK
        .lock()
        .expect("test bootstrap hook poisoned")
        .is_some()
}

#[cfg(any(test, debug_assertions))]
fn run_test_bootstrap_hook() {
    let hook = *TEST_BOOTSTRAP_HOOK
        .lock()
        .expect("test bootstrap hook poisoned");
    if let Some(hook) = hook {
        hook();
    }
}

///
/// MemoryRuntimeApi
///
/// High-level runtime bootstrap facade used by the supported public API.

pub struct MemoryRuntimeApi;

impl MemoryRuntimeApi {
    /// Bootstrap eager-init hooks, the memory registry, and eager TLS for the given owner range.
    pub fn bootstrap_registry(
        crate_name: &'static str,
        start: u8,
        end: u8,
    ) -> Result<registry::MemoryRegistryInitSummary, MemoryRegistryError> {
        run_registered_eager_init();
        let summary = registry::MemoryRegistryRuntime::init(Some((crate_name, start, end)))?;
        init_eager_tls();
        Ok(summary)
    }

    /// Bootstrap eager-init hooks, flush deferred registry state, and then
    /// initialize eager TLS without reserving a new owner range.
    pub fn bootstrap_registry_without_range()
    -> Result<registry::MemoryRegistryInitSummary, MemoryRegistryError> {
        run_registered_eager_init();
        let summary = registry::MemoryRegistryRuntime::init(None)?;
        init_eager_tls();
        Ok(summary)
    }
}

///
/// TESTS
///

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::{
        Mutex,
        atomic::{AtomicU32, Ordering},
    };

    static COUNT: AtomicU32 = AtomicU32::new(0);
    static TEST_LOCK: Mutex<()> = Mutex::new(());

    fn bump() {
        COUNT.fetch_add(1, Ordering::SeqCst);
    }

    #[test]
    fn init_eager_tls_runs_and_clears_queue() {
        let _guard = TEST_LOCK.lock().expect("test lock poisoned");
        COUNT.store(0, Ordering::SeqCst);
        CANIC_EAGER_TLS
            .lock()
            .expect("eager tls queue poisoned")
            .push(bump);
        init_eager_tls();
        let first = COUNT.load(Ordering::SeqCst);
        assert_eq!(first, 1);

        // second call sees empty queue
        init_eager_tls();
        let second = COUNT.load(Ordering::SeqCst);
        assert_eq!(second, 1);
    }

    #[test]
    fn run_registered_eager_init_runs_and_clears_queue() {
        let _guard = TEST_LOCK.lock().expect("test lock poisoned");
        COUNT.store(0, Ordering::SeqCst);
        CANIC_EAGER_INIT
            .lock()
            .expect("eager init queue poisoned")
            .push(bump);
        run_registered_eager_init();
        let first = COUNT.load(Ordering::SeqCst);
        assert_eq!(first, 1);

        // second call sees empty queue
        run_registered_eager_init();
        let second = COUNT.load(Ordering::SeqCst);
        assert_eq!(second, 1);
    }
}