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