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