canic_memory/
registry.rs

1//! NOTE: All stable registry access is TLS-thread-local.
2//! This ensures atomicity on the IC’s single-threaded execution model.
3use crate::manager::MEMORY_MANAGER;
4use candid::CandidType;
5use canic_cdk::structures::{
6    BTreeMap as StableBTreeMap, DefaultMemoryImpl,
7    memory::{MemoryId, VirtualMemory},
8};
9use canic_macros::impl_storable_bounded;
10use canic_types::BoundedString256;
11use canic_utils::time::now_secs;
12use serde::{Deserialize, Serialize};
13use std::cell::RefCell;
14use thiserror::Error as ThisError;
15
16///
17/// Reserved for the registry system itself
18///
19pub const MEMORY_REGISTRY_ID: u8 = 0;
20pub const MEMORY_RANGES_ID: u8 = 1;
21
22//
23// MEMORY_REGISTRY
24//
25
26thread_local! {
27    static MEMORY_REGISTRY: RefCell<StableBTreeMap<u8, MemoryRegistryEntry, VirtualMemory<DefaultMemoryImpl>>> =
28        RefCell::new(StableBTreeMap::init(
29            MEMORY_MANAGER.with_borrow(|this| {
30                this.get(MemoryId::new(MEMORY_REGISTRY_ID))
31            }),
32        ));
33}
34
35//
36// MEMORY_RANGES
37//
38
39thread_local! {
40    static MEMORY_RANGES: RefCell<StableBTreeMap<String, MemoryRange, VirtualMemory<DefaultMemoryImpl>>> =
41        RefCell::new(StableBTreeMap::init(
42            MEMORY_MANAGER.with_borrow(|mgr| {
43                mgr.get(MemoryId::new(MEMORY_RANGES_ID))
44            }),
45        ));
46}
47
48//
49// PENDING_REGISTRATIONS
50//
51// Queue of memory registrations produced during TLS initialization
52// Each entry is (id, crate_name, label).
53// These are deferred until `flush_pending_registrations()` is called,
54// which validates and inserts them into the global MemoryRegistry.
55//
56
57thread_local! {
58    static PENDING_REGISTRATIONS: RefCell<Vec<(u8, &'static str, &'static str)>> = const {
59        RefCell::new(Vec::new())
60    };
61}
62
63// public as it gets called from macros
64pub fn defer_register(id: u8, crate_name: &'static str, label: &'static str) {
65    PENDING_REGISTRATIONS.with(|q| {
66        q.borrow_mut().push((id, crate_name, label));
67    });
68}
69
70/// Drain (and clear) all pending registrations.
71/// Intended to be called from the ops layer during init/post-upgrade.
72#[must_use]
73pub fn drain_pending_registrations() -> Vec<(u8, &'static str, &'static str)> {
74    PENDING_REGISTRATIONS.with(|q| q.borrow_mut().drain(..).collect())
75}
76
77//
78// PENDING_RANGES
79//
80
81thread_local! {
82    pub static PENDING_RANGES: RefCell<Vec<(&'static str, u8, u8)>> = const {
83        RefCell::new(Vec::new())
84    };
85}
86
87// public as it gets called from macros
88pub fn defer_reserve_range(crate_name: &'static str, start: u8, end: u8) {
89    PENDING_RANGES.with(|q| q.borrow_mut().push((crate_name, start, end)));
90}
91
92/// Drain (and clear) all pending ranges.
93/// Intended to be called from the ops layer during init/post-upgrade.
94#[must_use]
95pub fn drain_pending_ranges() -> Vec<(&'static str, u8, u8)> {
96    PENDING_RANGES.with(|q| q.borrow_mut().drain(..).collect())
97}
98
99///
100/// MemoryRegistryError
101///
102
103#[derive(Debug, ThisError)]
104pub enum MemoryRegistryError {
105    #[error("ID {0} is already registered with type {1}, tried to register type {2}")]
106    AlreadyRegistered(u8, String, String),
107
108    #[error("crate `{0}` already has a reserved range")]
109    DuplicateRange(String),
110
111    #[error("crate `{0}` provided invalid range {1}-{2} (start > end)")]
112    InvalidRange(String, u8, u8),
113
114    #[error("crate `{0}` attempted to register ID {1}, but it is outside its allowed ranges")]
115    OutOfRange(String, u8),
116
117    #[error("crate `{0}` range {1}-{2} overlaps with crate `{3}` range {4}-{5}")]
118    Overlap(String, u8, u8, String, u8, u8),
119
120    #[error("crate `{0}` has not reserved any memory range")]
121    NoRange(String),
122}
123
124///
125/// MemoryRange
126///
127#[derive(Clone, Debug, Deserialize, Serialize)]
128pub struct MemoryRange {
129    pub crate_key: BoundedString256,
130    pub start: u8,
131    pub end: u8,
132    pub created_at: u64,
133}
134
135impl MemoryRange {
136    #[must_use]
137    pub(crate) fn new(crate_key: &str, start: u8, end: u8) -> Self {
138        Self {
139            crate_key: BoundedString256::new(crate_key),
140            start,
141            end,
142            created_at: now_secs(),
143        }
144    }
145
146    #[must_use]
147    pub fn contains(&self, id: u8) -> bool {
148        (self.start..=self.end).contains(&id)
149    }
150}
151
152impl_storable_bounded!(MemoryRange, 320, false);
153
154///
155/// MemoryRegistryEntry
156///
157
158#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
159pub struct MemoryRegistryEntry {
160    pub label: BoundedString256,
161    pub created_at: u64,
162}
163
164impl MemoryRegistryEntry {
165    #[must_use]
166    pub(crate) fn new(label: &str) -> Self {
167        Self {
168            label: BoundedString256::new(label),
169            created_at: now_secs(),
170        }
171    }
172}
173
174impl_storable_bounded!(MemoryRegistryEntry, 320, false);
175
176///
177/// MemoryRegistryView
178///
179
180pub type MemoryRegistryView = Vec<(u8, MemoryRegistryEntry)>;
181
182///
183/// MemoryRegistry
184///
185
186pub struct MemoryRegistry;
187
188impl MemoryRegistry {
189    /// Register an ID, enforcing crate’s allowed range.
190    ///
191    /// Pure domain/model-level function:
192    /// - no logging
193    /// - no unwrap
194    /// - no mapping to `crate::Error`
195    pub fn register(id: u8, crate_name: &str, label: &str) -> Result<(), MemoryRegistryError> {
196        let crate_key = crate_name.to_string();
197
198        // 1. Check reserved range
199        let range = MEMORY_RANGES.with_borrow(|ranges| ranges.get(&crate_key));
200        match range {
201            None => {
202                return Err(MemoryRegistryError::NoRange(crate_key));
203            }
204            Some(r) if !r.contains(id) => {
205                return Err(MemoryRegistryError::OutOfRange(crate_key, id));
206            }
207            Some(_) => {
208                // OK, continue
209            }
210        }
211
212        // 2. Check already registered
213        let existing = MEMORY_REGISTRY.with_borrow(|map| map.get(&id));
214        if let Some(existing) = existing {
215            if existing.label.as_ref() != label {
216                return Err(MemoryRegistryError::AlreadyRegistered(
217                    id,
218                    existing.label.to_string(),
219                    label.to_string(),
220                ));
221            }
222
223            // idempotent case
224            return Ok(());
225        }
226
227        // 3. Insert
228        MEMORY_REGISTRY.with_borrow_mut(|map| {
229            map.insert(id, MemoryRegistryEntry::new(label));
230        });
231
232        Ok(())
233    }
234
235    /// Reserve a block of memory IDs for a crate.
236    ///
237    /// Pure domain/model-level function, no logging or unwrap.
238    pub fn reserve_range(crate_name: &str, start: u8, end: u8) -> Result<(), MemoryRegistryError> {
239        if start > end {
240            return Err(MemoryRegistryError::InvalidRange(
241                crate_name.to_string(),
242                start,
243                end,
244            ));
245        }
246
247        let crate_key = crate_name.to_string();
248
249        // 1. Check for conflicts (existing ranges)
250        let conflict = MEMORY_RANGES.with_borrow(|ranges| {
251            if ranges.contains_key(&crate_key) {
252                return Some(MemoryRegistryError::DuplicateRange(crate_key.clone()));
253            }
254
255            for entry in ranges.iter() {
256                let other_crate = entry.key();
257                let other_range = entry.value();
258
259                if !(end < other_range.start || start > other_range.end) {
260                    return Some(MemoryRegistryError::Overlap(
261                        crate_key.clone(),
262                        start,
263                        end,
264                        other_crate.clone(),
265                        other_range.start,
266                        other_range.end,
267                    ));
268                }
269            }
270
271            None
272        });
273
274        if let Some(err) = conflict {
275            return Err(err);
276        }
277
278        // 2. Insert
279        MEMORY_RANGES.with_borrow_mut(|ranges| {
280            let range = MemoryRange::new(crate_name, start, end);
281            ranges.insert(crate_name.to_string(), range);
282        });
283
284        Ok(())
285    }
286
287    #[must_use]
288    pub fn get(id: u8) -> Option<MemoryRegistryEntry> {
289        MEMORY_REGISTRY.with_borrow(|map| map.get(&id))
290    }
291
292    #[must_use]
293    pub fn export() -> MemoryRegistryView {
294        MEMORY_REGISTRY.with_borrow(|map| {
295            map.iter()
296                .map(|entry| (*entry.key(), entry.value()))
297                .collect()
298        })
299    }
300
301    #[must_use]
302    pub fn export_ranges() -> Vec<(String, MemoryRange)> {
303        MEMORY_RANGES.with_borrow(|ranges| {
304            ranges
305                .iter()
306                .map(|e| (e.key().clone(), e.value()))
307                .collect()
308        })
309    }
310}
311
312#[cfg(test)]
313pub(crate) fn reset_for_tests() {
314    MEMORY_REGISTRY.with_borrow_mut(StableBTreeMap::clear);
315    MEMORY_RANGES.with_borrow_mut(StableBTreeMap::clear);
316    PENDING_REGISTRATIONS.with(|q| q.borrow_mut().clear());
317    PENDING_RANGES.with(|q| q.borrow_mut().clear());
318}
319
320///
321/// TESTS
322///
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn reserve_range_happy_path_and_reject_overlap() {
330        reset_for_tests();
331        MemoryRegistry::reserve_range("crate_a", 10, 20).unwrap();
332
333        // Overlap with existing should error
334        let err = MemoryRegistry::reserve_range("crate_b", 15, 25).unwrap_err();
335        matches!(err, MemoryRegistryError::Overlap(_, _, _, _, _, _));
336
337        // Disjoint should succeed
338        MemoryRegistry::reserve_range("crate_b", 30, 40).unwrap();
339
340        let ranges = MemoryRegistry::export_ranges();
341        assert_eq!(ranges.len(), 2);
342    }
343
344    #[test]
345    fn reserve_range_rejects_invalid_order() {
346        reset_for_tests();
347        let err = MemoryRegistry::reserve_range("crate_a", 5, 4).unwrap_err();
348        matches!(err, MemoryRegistryError::InvalidRange(_, _, _));
349        assert!(MemoryRegistry::export_ranges().is_empty());
350    }
351
352    #[test]
353    fn register_id_requires_range_and_checks_bounds() {
354        reset_for_tests();
355        MemoryRegistry::reserve_range("crate_a", 1, 3).unwrap();
356
357        // Out of range
358        let err = MemoryRegistry::register(5, "crate_a", "Foo").unwrap_err();
359        matches!(err, MemoryRegistryError::OutOfRange(_, _));
360
361        // Happy path
362        MemoryRegistry::register(2, "crate_a", "Foo").unwrap();
363
364        // Idempotent same label
365        MemoryRegistry::register(2, "crate_a", "Foo").unwrap();
366
367        // Different label should error
368        let err = MemoryRegistry::register(2, "crate_a", "Bar").unwrap_err();
369        matches!(err, MemoryRegistryError::AlreadyRegistered(_, _, _));
370
371        let view = MemoryRegistry::export();
372        assert_eq!(view.len(), 1);
373        assert_eq!(view[0].0, 2);
374    }
375
376    #[test]
377    fn pending_queues_drain_in_order() {
378        reset_for_tests();
379        defer_reserve_range("crate_a", 1, 2);
380        defer_reserve_range("crate_b", 3, 4);
381        defer_register(1, "crate_a", "A1");
382        defer_register(3, "crate_b", "B3");
383
384        let ranges = drain_pending_ranges();
385        assert_eq!(ranges, vec![("crate_a", 1, 2), ("crate_b", 3, 4)]);
386        let regs = drain_pending_registrations();
387        assert_eq!(regs, vec![(1, "crate_a", "A1"), (3, "crate_b", "B3")]);
388
389        // queues are empty after drain
390        assert!(drain_pending_ranges().is_empty());
391        assert!(drain_pending_registrations().is_empty());
392    }
393}