Skip to main content

canic_memory/api/
mod.rs

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