concinnity_core/ecs/access_check.rs
1//! Debug-build access validation hooks. `PipelineContext` accessors report each
2//! touch here; the client installs a hook that asserts the touch against the
3//! stepping system's declared [`Access`] (tracked
4//! client-side, since this crate is no_std and holds no per-thread state).
5//! `World::step` announces which system is stepping through the second hook,
6//! for the same reason: the tracking is per-thread and belongs to the host.
7//! Compiled out of release builds entirely: release parallel-safety never rests
8//! on these checks, only on the executor handing conflicting systems to
9//! different waves.
10#![cfg(debug_assertions)]
11
12use core::any::TypeId;
13use core::sync::atomic::{AtomicPtr, Ordering};
14
15use crate::ecs::Access;
16
17/// What a context accessor touched. Component ids are the registry
18/// discriminants; resources and events report their `TypeId` for the client's
19/// id registry to resolve.
20pub enum Touch {
21 /// A component column was read.
22 ComponentRead {
23 /// The component's registry discriminant.
24 id: u8,
25 /// The component type's name, for the assertion message.
26 type_name: &'static str,
27 },
28 /// A component column was written.
29 ComponentWrite {
30 /// The component's registry discriminant.
31 id: u8,
32 /// The component type's name, for the assertion message.
33 type_name: &'static str,
34 },
35 /// Structural change (push/insert/remove/despawn/drain): reorders columns
36 /// and the join index, so it requires exclusive access.
37 Structural {
38 /// The operation that changed structure.
39 op: &'static str,
40 },
41 /// A resource or event queue was touched.
42 Resource {
43 /// The resource type's id, resolved by the client's id registry.
44 type_id: TypeId,
45 /// The resource type's name, for the assertion message.
46 type_name: &'static str,
47 /// `true` for a write, `false` for a read.
48 write: bool,
49 },
50 /// Compiled-payload access mutates the blob store's residency.
51 Blob {
52 /// The operation that touched the blob store.
53 op: &'static str,
54 },
55}
56
57type Hook = fn(&Touch);
58
59/// Marks the stepping system's declared access active, or clears it. The
60/// client keeps the per-thread state a no_std crate cannot.
61type ActiveHook = fn(Option<(Access, &'static str)>);
62
63static HOOK: AtomicPtr<()> = AtomicPtr::new(core::ptr::null_mut());
64static ACTIVE_HOOK: AtomicPtr<()> = AtomicPtr::new(core::ptr::null_mut());
65
66/// Install the process-wide validation hook. Idempotent by usage (the client
67/// installs one hook once); the last install wins.
68pub fn install(hook: Hook) {
69 HOOK.store(hook as *mut (), Ordering::Release);
70}
71
72/// Install the process-wide hook `World::step` announces each system's declared
73/// access through. Installed alongside [`install`]; without it every touch is
74/// reported against no active system and passes.
75pub fn install_active(hook: ActiveHook) {
76 ACTIVE_HOOK.store(hook as *mut (), Ordering::Release);
77}
78
79#[inline]
80pub(crate) fn touch(t: Touch) {
81 let raw = HOOK.load(Ordering::Acquire);
82 if raw.is_null() {
83 return;
84 }
85 // SAFETY: the pointer only ever holds a `Hook` stored by `install`.
86 let hook: Hook = unsafe { core::mem::transmute::<*mut (), Hook>(raw) };
87 hook(&t);
88}
89
90/// Announce the system whose step is running, or clear it with `None`. A no-op
91/// until a client installs the hook.
92pub fn set_active(active: Option<(Access, &'static str)>) {
93 let raw = ACTIVE_HOOK.load(Ordering::Acquire);
94 if raw.is_null() {
95 return;
96 }
97 // SAFETY: the pointer only ever holds an `ActiveHook` stored by
98 // `install_active`.
99 let hook: ActiveHook = unsafe { core::mem::transmute::<*mut (), ActiveHook>(raw) };
100 hook(active);
101}