Skip to main content

canic_memory/api/
mod.rs

1#[cfg(target_arch = "wasm32")]
2use crate::ledger;
3use crate::{
4    cdk::structures::{
5        DefaultMemoryImpl,
6        memory::{MemoryId, VirtualMemory},
7    },
8    manager::MEMORY_MANAGER,
9    registry::{
10        MemoryRange, MemoryRangeAuthority, MemoryRegistry, MemoryRegistryError, defer_register,
11        defer_register_with_key_metadata,
12    },
13    runtime::{MemoryRuntimeApi, registry::MemoryRegistryRuntime},
14};
15
16///
17/// MemoryApi
18///
19/// Supported facade for memory bootstrap, dynamic slot registration, and
20/// registry inspection.
21
22pub struct MemoryApi;
23
24///
25/// MemoryInspection
26///
27/// Read-only description of the owner range for one memory ID.
28
29#[derive(Clone, Debug, Eq, PartialEq)]
30pub struct MemoryInspection {
31    /// Stable-memory ID being inspected.
32    pub id: u8,
33    /// Crate name that reserved the range containing `id`.
34    pub owner: String,
35    /// Reserved range containing `id`.
36    pub range: MemoryRange,
37    /// Registered slot label for `id`, when the ID has already been registered.
38    pub label: Option<String>,
39    /// ABI-stable key for `id`, when the ID has already been registered.
40    pub stable_key: Option<String>,
41    /// Optional in-place schema version metadata for diagnostics.
42    pub schema_version: Option<u32>,
43    /// Optional opaque schema fingerprint metadata for diagnostics.
44    pub schema_fingerprint: Option<String>,
45}
46
47///
48/// RegisteredMemory
49///
50/// Read-only description of one registered stable-memory slot.
51
52#[derive(Clone, Debug, Eq, PartialEq)]
53pub struct RegisteredMemory {
54    /// Registered stable-memory ID.
55    pub id: u8,
56    /// Crate name that owns the slot's reserved range.
57    pub owner: String,
58    /// Reserved range containing `id`.
59    pub range: MemoryRange,
60    /// Human-readable slot label supplied by the registering crate.
61    pub label: String,
62    /// ABI-stable key that owns this memory ID permanently.
63    pub stable_key: String,
64    /// Optional in-place schema version metadata for diagnostics.
65    pub schema_version: Option<u32>,
66    /// Optional opaque schema fingerprint metadata for diagnostics.
67    pub schema_fingerprint: Option<String>,
68}
69
70///
71/// LedgerSnapshot
72///
73/// Read-only snapshot of the persisted ABI ledger.
74
75#[derive(Clone, Debug, Eq, PartialEq)]
76pub struct LedgerSnapshot {
77    /// Canonical allocation authority ranges recorded by the persisted ABI ledger.
78    pub authorities: Vec<MemoryRangeAuthority>,
79    /// Historical owner ranges recorded by the persisted ABI ledger.
80    pub ranges: Vec<(String, MemoryRange)>,
81    /// Historical memory ID records recorded by the persisted ABI ledger.
82    pub entries: Vec<(u8, crate::registry::MemoryRegistryEntry)>,
83}
84
85impl MemoryApi {
86    /// Bootstrap eager TLS, eager-init hooks, and the caller's initial reserved range.
87    pub fn bootstrap_owner_range(
88        crate_name: &'static str,
89        start: u8,
90        end: u8,
91    ) -> Result<(), MemoryRegistryError> {
92        let _ = MemoryRuntimeApi::bootstrap_registry(crate_name, start, end)?;
93        Ok(())
94    }
95
96    /// Bootstrap eager TLS, eager-init hooks, and flush deferred registry state
97    /// without reserving a new owner range.
98    pub fn bootstrap_pending() -> Result<(), MemoryRegistryError> {
99        let _ = MemoryRuntimeApi::bootstrap_registry_without_range()?;
100        Ok(())
101    }
102
103    /// Declare one legacy-key stable-memory ID for bootstrap validation.
104    ///
105    /// This queues metadata only. It does not open the underlying virtual memory.
106    pub fn declare(id: u8, crate_name: &str, label: &str) -> Result<(), MemoryRegistryError> {
107        if MemoryRegistryRuntime::is_initialized() {
108            return Err(MemoryRegistryError::RegistrationAfterBootstrap {
109                ranges: 0,
110                registrations: 1,
111            });
112        }
113
114        defer_register(id, crate_name, label)
115    }
116
117    /// Declare one explicit-key stable-memory ID for bootstrap validation.
118    ///
119    /// This queues metadata only. It does not open the underlying virtual memory.
120    pub fn declare_with_key(
121        id: u8,
122        crate_name: &str,
123        label: &str,
124        stable_key: &str,
125    ) -> Result<(), MemoryRegistryError> {
126        Self::declare_with_key_metadata(id, crate_name, label, stable_key, None, None)
127    }
128
129    /// Declare one explicit-key stable-memory ID with optional schema metadata.
130    ///
131    /// Schema metadata is informational in 0.38 and does not affect allocation
132    /// ownership. This queues metadata only. It does not open virtual memory.
133    pub fn declare_with_key_metadata(
134        id: u8,
135        crate_name: &str,
136        label: &str,
137        stable_key: &str,
138        schema_version: Option<u32>,
139        schema_fingerprint: Option<&str>,
140    ) -> Result<(), MemoryRegistryError> {
141        if MemoryRegistryRuntime::is_initialized() {
142            return Err(MemoryRegistryError::RegistrationAfterBootstrap {
143                ranges: 0,
144                registrations: 1,
145            });
146        }
147
148        defer_register_with_key_metadata(
149            id,
150            crate_name,
151            label,
152            stable_key,
153            schema_version,
154            schema_fingerprint,
155        )
156    }
157
158    /// Open one already-validated stable-memory ID and return its virtual memory handle.
159    ///
160    /// The ID must have been declared before bootstrap and accepted by the
161    /// sealed runtime declaration snapshot. This is not a dynamic allocation API.
162    pub fn register(
163        id: u8,
164        crate_name: &str,
165        label: &str,
166    ) -> Result<VirtualMemory<DefaultMemoryImpl>, MemoryRegistryError> {
167        if !MemoryRegistryRuntime::is_initialized() {
168            return Err(MemoryRegistryError::RegistryNotBootstrapped);
169        }
170
171        if let Some(entry) = MemoryRegistry::get(id)
172            && entry.crate_name == crate_name
173            && entry.label == label
174        {
175            return Ok(open_memory(id));
176        }
177
178        Err(MemoryRegistryError::RegistrationAfterBootstrap {
179            ranges: 0,
180            registrations: 1,
181        })
182    }
183
184    /// Open one already-validated stable-memory ID using its explicit ABI-stable key.
185    pub fn register_with_key(
186        id: u8,
187        _crate_name: &str,
188        _label: &str,
189        stable_key: &str,
190    ) -> Result<VirtualMemory<DefaultMemoryImpl>, MemoryRegistryError> {
191        if !MemoryRegistryRuntime::is_initialized() {
192            return Err(MemoryRegistryError::RegistryNotBootstrapped);
193        }
194
195        if let Some(entry) = MemoryRegistry::get(id)
196            && entry.stable_key == stable_key
197        {
198            return Ok(open_memory(id));
199        }
200
201        Err(MemoryRegistryError::RegistrationAfterBootstrap {
202            ranges: 0,
203            registrations: 1,
204        })
205    }
206
207    /// Inspect who currently owns one memory id and whether it is registered.
208    #[must_use]
209    pub fn inspect(id: u8) -> Option<MemoryInspection> {
210        let range = MemoryRegistry::export_range_entries()
211            .into_iter()
212            .find(|entry| entry.range.contains(id))?;
213        let entry = MemoryRegistry::get(id);
214        let label = entry.as_ref().map(|entry| entry.label.clone());
215        let stable_key = entry.as_ref().map(|entry| entry.stable_key.clone());
216        let schema_version = entry.as_ref().and_then(|entry| entry.schema_version);
217        let schema_fingerprint = entry.and_then(|entry| entry.schema_fingerprint);
218
219        Some(MemoryInspection {
220            id,
221            owner: range.owner,
222            range: range.range,
223            label,
224            stable_key,
225            schema_version,
226            schema_fingerprint,
227        })
228    }
229
230    /// List every registered memory slot with owner/range/label context.
231    #[must_use]
232    pub fn registered() -> Vec<RegisteredMemory> {
233        MemoryRegistry::export_ids_by_range()
234            .into_iter()
235            .flat_map(|snapshot| {
236                snapshot
237                    .entries
238                    .into_iter()
239                    .map(move |(id, entry)| RegisteredMemory {
240                        id,
241                        owner: snapshot.owner.clone(),
242                        range: snapshot.range,
243                        label: entry.label,
244                        stable_key: entry.stable_key,
245                        schema_version: entry.schema_version,
246                        schema_fingerprint: entry.schema_fingerprint,
247                    })
248            })
249            .collect()
250    }
251
252    /// List all registered memory slots for one owner.
253    #[must_use]
254    pub fn registered_for_owner(owner: &str) -> Vec<RegisteredMemory> {
255        Self::registered()
256            .into_iter()
257            .filter(|entry| entry.owner == owner)
258            .collect()
259    }
260
261    /// Find one registered memory slot by owner and label.
262    #[must_use]
263    pub fn find(owner: &str, label: &str) -> Option<RegisteredMemory> {
264        Self::registered()
265            .into_iter()
266            .find(|entry| entry.owner == owner && entry.label == label)
267    }
268
269    /// Read the persisted ABI ledger without relying on current registry reconstruction.
270    pub fn ledger_snapshot() -> Result<LedgerSnapshot, MemoryRegistryError> {
271        #[cfg(target_arch = "wasm32")]
272        {
273            let snapshot = ledger::try_diagnostic_snapshot()?;
274            Ok(LedgerSnapshot {
275                authorities: snapshot.authorities,
276                ranges: snapshot.ranges,
277                entries: snapshot.entries,
278            })
279        }
280
281        #[cfg(not(target_arch = "wasm32"))]
282        {
283            Ok(LedgerSnapshot {
284                authorities: MemoryRegistry::try_export_historical_authorities()?,
285                ranges: MemoryRegistry::try_export_historical_ranges()?,
286                entries: MemoryRegistry::try_export_historical()?,
287            })
288        }
289    }
290}
291
292// Open a registered virtual memory slot through the shared manager.
293fn open_memory(id: u8) -> VirtualMemory<DefaultMemoryImpl> {
294    MEMORY_MANAGER.with_borrow_mut(|mgr| mgr.get(MemoryId::new(id)))
295}
296
297///
298/// TESTS
299///
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use crate::registry::{
305        MemoryRegistryError, defer_register, defer_reserve_range, reset_for_tests,
306    };
307
308    #[test]
309    fn register_memory_opens_validated_memory_for_reserved_slot() {
310        reset_for_tests();
311        defer_reserve_range("crate_a", 100, 102).expect("defer range");
312        defer_register(101, "crate_a", "slot").expect("defer register");
313        MemoryApi::bootstrap_pending().expect("bootstrap registry");
314
315        let _memory = MemoryApi::register(101, "crate_a", "slot").expect("open memory");
316    }
317
318    #[test]
319    fn register_memory_is_idempotent_for_same_entry() {
320        reset_for_tests();
321        defer_reserve_range("crate_a", 100, 102).expect("defer range");
322        defer_register(101, "crate_a", "slot").expect("defer register");
323        MemoryApi::bootstrap_pending().expect("bootstrap registry");
324        let _ = MemoryApi::register(101, "crate_a", "slot").expect("first open succeeds");
325
326        let _ = MemoryApi::register(101, "crate_a", "slot").expect("second open succeeds");
327    }
328
329    #[test]
330    fn register_with_key_opens_validated_explicit_key() {
331        reset_for_tests();
332        defer_reserve_range("crate_a", 100, 102).expect("defer range");
333        MemoryApi::declare_with_key(101, "crate_a", "slot", "app.crate_a.slot.v1")
334            .expect("defer register");
335        MemoryApi::bootstrap_pending().expect("bootstrap registry");
336
337        let _memory = MemoryApi::register_with_key(101, "crate_a", "slot", "app.crate_a.slot.v1")
338            .expect("open memory");
339    }
340
341    #[test]
342    fn declare_with_key_metadata_records_schema_metadata() {
343        reset_for_tests();
344        defer_reserve_range("crate_a", 100, 102).expect("defer range");
345        MemoryApi::declare_with_key_metadata(
346            101,
347            "crate_a",
348            "slot",
349            "app.crate_a.slot.v1",
350            Some(3),
351            Some("sha256:abc123"),
352        )
353        .expect("defer register");
354        MemoryApi::bootstrap_pending().expect("bootstrap registry");
355
356        let registered = MemoryApi::find("crate_a", "slot").expect("registered memory");
357        assert_eq!(registered.schema_version, Some(3));
358        assert_eq!(
359            registered.schema_fingerprint.as_deref(),
360            Some("sha256:abc123")
361        );
362
363        let snapshot = MemoryApi::ledger_snapshot().expect("ledger snapshot");
364        let (_, entry) = snapshot
365            .entries
366            .into_iter()
367            .find(|(id, _)| *id == 101)
368            .expect("ledger entry");
369        assert_eq!(entry.schema_version, Some(3));
370        assert_eq!(entry.schema_fingerprint.as_deref(), Some("sha256:abc123"));
371    }
372
373    #[test]
374    fn declare_memory_does_not_open_before_bootstrap() {
375        reset_for_tests();
376
377        MemoryApi::declare_with_key(101, "crate_a", "slot", "app.crate_a.slot.v1")
378            .expect("declare memory");
379
380        assert!(MemoryRegistry::get(101).is_none());
381    }
382
383    #[test]
384    fn declare_memory_rejects_after_bootstrap_seal() {
385        reset_for_tests();
386        MemoryApi::bootstrap_owner_range("crate_a", 100, 102).expect("bootstrap registry");
387
388        let err = MemoryApi::declare_with_key(101, "crate_a", "slot", "app.crate_a.slot.v1")
389            .expect_err("late declaration should fail");
390        assert!(matches!(
391            err,
392            MemoryRegistryError::RegistrationAfterBootstrap {
393                ranges: 0,
394                registrations: 1,
395            }
396        ));
397    }
398
399    #[test]
400    fn register_memory_rejects_before_bootstrap_validation() {
401        reset_for_tests();
402
403        let Err(err) = MemoryApi::register(100, "crate_a", "slot") else {
404            panic!("opening before bootstrap must fail")
405        };
406        assert!(matches!(err, MemoryRegistryError::RegistryNotBootstrapped));
407    }
408
409    #[test]
410    fn register_memory_rejects_new_claim_after_bootstrap_seal() {
411        reset_for_tests();
412        MemoryApi::bootstrap_owner_range("crate_a", 100, 102).expect("bootstrap registry");
413
414        let Err(err) = MemoryApi::register(101, "crate_a", "slot") else {
415            panic!("new registration after bootstrap must fail")
416        };
417        assert!(matches!(
418            err,
419            MemoryRegistryError::RegistrationAfterBootstrap {
420                ranges: 0,
421                registrations: 1,
422            }
423        ));
424    }
425
426    #[test]
427    fn bootstrap_pending_flushes_deferred_state() {
428        reset_for_tests();
429        defer_reserve_range("crate_a", 100, 102).expect("defer range");
430        defer_register(101, "crate_a", "slot").expect("defer register");
431
432        MemoryApi::bootstrap_pending().expect("bootstrap pending");
433
434        assert!(MemoryRegistry::export_ranges().contains(&(
435            "crate_a".to_string(),
436            MemoryRange {
437                start: 100,
438                end: 102
439            }
440        )));
441        let entries = MemoryRegistry::export();
442        assert!(entries.iter().any(|(id, entry)| {
443            *id == 101 && entry.crate_name == "crate_a" && entry.label == "slot"
444        }));
445    }
446
447    #[test]
448    fn inspect_memory_returns_reserved_owner_without_label() {
449        reset_for_tests();
450        MemoryApi::bootstrap_owner_range("crate_a", 100, 102).expect("bootstrap registry");
451
452        let inspection = MemoryApi::inspect(101).expect("reserved slot should inspect");
453        assert_eq!(inspection.owner, "crate_a");
454        assert_eq!(
455            inspection.range,
456            MemoryRange {
457                start: 100,
458                end: 102
459            }
460        );
461        assert_eq!(inspection.label, None);
462    }
463
464    #[test]
465    fn inspect_memory_returns_registered_label() {
466        reset_for_tests();
467        defer_reserve_range("crate_a", 100, 102).expect("defer range");
468        defer_register(101, "crate_a", "slot").expect("defer register");
469        MemoryApi::bootstrap_pending().expect("bootstrap registry");
470
471        let inspection = MemoryApi::inspect(101).expect("registered slot should inspect");
472        assert_eq!(inspection.owner, "crate_a");
473        assert_eq!(
474            inspection.range,
475            MemoryRange {
476                start: 100,
477                end: 102
478            }
479        );
480        assert_eq!(inspection.label.as_deref(), Some("slot"));
481        assert_eq!(
482            inspection.stable_key.as_deref(),
483            Some("legacy.crate_a.slot.v1")
484        );
485    }
486
487    #[test]
488    fn inspect_memory_returns_none_for_unowned_id() {
489        reset_for_tests();
490        assert_eq!(MemoryApi::inspect(99), None);
491    }
492
493    #[test]
494    fn registered_memories_lists_registered_slots_with_owner_context() {
495        reset_for_tests();
496        defer_reserve_range("crate_a", 100, 102).expect("defer range A");
497        defer_reserve_range("crate_b", 110, 112).expect("defer range B");
498        defer_register(101, "crate_a", "slot_a").expect("defer register A");
499        defer_register(111, "crate_b", "slot_b").expect("defer register B");
500        MemoryApi::bootstrap_pending().expect("bootstrap registry");
501
502        let registrations = MemoryApi::registered();
503        assert_eq!(registrations.len(), 3);
504        assert!(registrations.contains(&RegisteredMemory {
505            id: 101,
506            owner: "crate_a".to_string(),
507            range: MemoryRange {
508                start: 100,
509                end: 102
510            },
511            label: "slot_a".to_string(),
512            stable_key: "legacy.crate_a.slot_a.v1".to_string(),
513            schema_version: None,
514            schema_fingerprint: None,
515        }));
516        assert!(registrations.contains(&RegisteredMemory {
517            id: 111,
518            owner: "crate_b".to_string(),
519            range: MemoryRange {
520                start: 110,
521                end: 112
522            },
523            label: "slot_b".to_string(),
524            stable_key: "legacy.crate_b.slot_b.v1".to_string(),
525            schema_version: None,
526            schema_fingerprint: None,
527        }));
528    }
529
530    #[test]
531    fn registered_memories_for_owner_filters_to_owner() {
532        reset_for_tests();
533        defer_reserve_range("crate_a", 100, 102).expect("defer range A");
534        defer_reserve_range("crate_b", 110, 112).expect("defer range B");
535        defer_register(101, "crate_a", "slot_a").expect("defer register A");
536        defer_register(111, "crate_b", "slot_b").expect("defer register B");
537        MemoryApi::bootstrap_pending().expect("bootstrap registry");
538
539        let registrations = MemoryApi::registered_for_owner("crate_a");
540        assert_eq!(
541            registrations,
542            vec![RegisteredMemory {
543                id: 101,
544                owner: "crate_a".to_string(),
545                range: MemoryRange {
546                    start: 100,
547                    end: 102
548                },
549                label: "slot_a".to_string(),
550                stable_key: "legacy.crate_a.slot_a.v1".to_string(),
551                schema_version: None,
552                schema_fingerprint: None,
553            }]
554        );
555    }
556
557    #[test]
558    fn find_registered_memory_returns_match_for_owner_and_label() {
559        reset_for_tests();
560        defer_reserve_range("crate_a", 100, 102).expect("defer range");
561        defer_register(101, "crate_a", "slot_a").expect("defer register");
562        MemoryApi::bootstrap_pending().expect("bootstrap registry");
563
564        let registration = MemoryApi::find("crate_a", "slot_a").expect("slot should exist");
565        assert_eq!(
566            registration,
567            RegisteredMemory {
568                id: 101,
569                owner: "crate_a".to_string(),
570                range: MemoryRange {
571                    start: 100,
572                    end: 102
573                },
574                label: "slot_a".to_string(),
575                stable_key: "legacy.crate_a.slot_a.v1".to_string(),
576                schema_version: None,
577                schema_fingerprint: None,
578            }
579        );
580    }
581
582    #[test]
583    fn find_registered_memory_returns_none_when_missing() {
584        reset_for_tests();
585        MemoryApi::bootstrap_owner_range("crate_a", 100, 102).expect("bootstrap registry");
586        assert_eq!(MemoryApi::find("crate_a", "slot_a"), None);
587    }
588
589    #[test]
590    fn ledger_snapshot_reads_historical_records() {
591        reset_for_tests();
592        defer_reserve_range("crate_a", 100, 102).expect("defer range");
593        defer_register(101, "crate_a", "slot").expect("defer register");
594        MemoryApi::bootstrap_pending().expect("bootstrap registry");
595
596        let snapshot = MemoryApi::ledger_snapshot().expect("ledger snapshot");
597        assert!(snapshot.authorities.iter().any(|authority| {
598            authority.owner == "canic.framework"
599                && authority.range == MemoryRange { start: 0, end: 99 }
600        }));
601        assert!(snapshot.authorities.iter().any(|authority| {
602            authority.owner == "applications"
603                && authority.range
604                    == MemoryRange {
605                        start: 100,
606                        end: 254,
607                    }
608        }));
609        assert!(snapshot.ranges.iter().any(|(owner, range)| {
610            owner == "crate_a"
611                && *range
612                    == MemoryRange {
613                        start: 100,
614                        end: 102,
615                    }
616        }));
617        assert!(snapshot.entries.iter().any(|(id, entry)| {
618            *id == 101
619                && entry.crate_name == "crate_a"
620                && entry.label == "slot"
621                && entry.stable_key == "legacy.crate_a.slot.v1"
622        }));
623    }
624}