Skip to main content

scope_local/
scope.rs

1use alloc::{boxed::Box, vec::Vec};
2use core::{
3    ptr::NonNull,
4    sync::atomic::{AtomicUsize, Ordering},
5};
6
7use ax_lazyinit::OnceLock;
8use ax_percpu::CpuPin;
9use ax_sync::PreemptGuard;
10
11use crate::{
12    boxed::ItemBox,
13    item::{Item, Registry},
14};
15
16/// A scope is a collection of items.
17pub struct Scope {
18    items: Box<[ItemBox]>,
19}
20
21impl Scope {
22    /// Creates a new namespace and eagerly initializes every registered item.
23    ///
24    /// Initializers run in the caller's ordinary context. Once this function
25    /// returns, pinned access to the scope performs no allocation or lazy
26    /// initialization.
27    pub fn new() -> Self {
28        let items = Registry
29            .iter()
30            .map(ItemBox::new)
31            .collect::<Vec<_>>()
32            .into_boxed_slice();
33        Self { items }
34    }
35
36    pub(crate) fn get(&self, item: &'static Item) -> &ItemBox {
37        &self.items[item.index()]
38    }
39
40    pub(crate) fn get_mut(&mut self, item: &'static Item) -> &mut ItemBox {
41        &mut self.items[item.index()]
42    }
43
44    fn items_ptr(&self) -> NonNull<ItemBox> {
45        NonNull::new(self.items.as_ptr().cast_mut())
46            .expect("scope-local registry must contain the accessed item")
47    }
48}
49
50impl Default for Scope {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56static GLOBAL_SCOPE: OnceLock<Scope> = OnceLock::new();
57static GLOBAL_SCOPE_STATE: AtomicUsize = AtomicUsize::new(GlobalScopeState::Uninitialized as usize);
58
59#[derive(Clone, Copy, Eq, PartialEq)]
60#[repr(usize)]
61enum GlobalScopeState {
62    Uninitialized,
63    Ready,
64}
65
66struct GlobalInitialization {
67    owner_context: usize,
68    published: bool,
69}
70
71impl GlobalInitialization {
72    fn begin(owner_context: usize) -> Self {
73        Self {
74            owner_context,
75            published: false,
76        }
77    }
78
79    fn publish(mut self, scope: Scope) {
80        GLOBAL_SCOPE.call_once(|| scope);
81        GLOBAL_SCOPE_STATE.store(GlobalScopeState::Ready as usize, Ordering::Release);
82        self.published = true;
83    }
84}
85
86impl Drop for GlobalInitialization {
87    fn drop(&mut self) {
88        if !self.published {
89            let _ = GLOBAL_SCOPE_STATE.compare_exchange(
90                self.owner_context,
91                GlobalScopeState::Uninitialized as usize,
92                Ordering::Release,
93                Ordering::Relaxed,
94            );
95        }
96    }
97}
98
99#[ax_percpu::def_percpu]
100pub(crate) static ACTIVE_SCOPE_PTR: usize = 0;
101
102/// Currently active scope.
103pub struct ActiveScope;
104
105impl ActiveScope {
106    /// Sets the active scope pointer to the given scope.
107    ///
108    /// # Safety
109    ///
110    /// The caller must ensure that the provided `scope` reference is valid for
111    /// the duration in which it is set as the active scope, and that no data
112    /// races or aliasing violations occur.
113    pub unsafe fn set(scope: &Scope) {
114        let _guard = PreemptGuard::new();
115        // SAFETY: the guard prevents migration while the per-CPU pointer is
116        // selected and updated.
117        unsafe {
118            ax_percpu::with_cpu_pin(|pin| Self::set_pinned(scope, pin))
119                .expect("scope-local access requires an installed CPU area")
120        };
121    }
122
123    /// Sets the active scope under an existing CPU pin.
124    ///
125    /// # Safety
126    ///
127    /// `scope` must remain alive until a later pinned replacement or reset.
128    pub unsafe fn set_pinned(scope: &Scope, pin: &CpuPin<'_>) {
129        ACTIVE_SCOPE_PTR.write_current(pin, scope.items_ptr().addr().get());
130    }
131
132    /// Set the active scope to the global scope.
133    pub fn set_global() {
134        let _guard = PreemptGuard::new();
135        // SAFETY: the guard prevents migration while the per-CPU pointer is
136        // cleared.
137        unsafe {
138            ax_percpu::with_cpu_pin(Self::set_global_pinned)
139                .expect("scope-local access requires an installed CPU area")
140        };
141    }
142
143    /// Sets the active scope to global under an existing CPU pin.
144    pub fn set_global_pinned(pin: &CpuPin<'_>) {
145        ACTIVE_SCOPE_PTR.write_current(pin, 0);
146    }
147
148    /// Returns true if the active scope is the global scope.
149    pub fn is_global() -> bool {
150        let _guard = PreemptGuard::new();
151        // SAFETY: the guard prevents migration for the complete read.
152        unsafe {
153            ax_percpu::with_cpu_pin(Self::is_global_pinned)
154                .expect("scope-local access requires an installed CPU area")
155        }
156    }
157
158    /// Returns whether the active scope is global under an existing pin.
159    pub fn is_global_pinned(pin: &CpuPin<'_>) -> bool {
160        ACTIVE_SCOPE_PTR.read_current(pin) == 0
161    }
162
163    pub(crate) fn with_item<R>(
164        item: &'static Item,
165        pin: &CpuPin<'_>,
166        operation: impl for<'access> FnOnce(&'access ItemBox) -> R,
167    ) -> R {
168        Self::try_with_item(item, pin, operation).expect(
169            "scope-local global scope is not initialized; use LocalItem::with before pinned access",
170        )
171    }
172
173    pub(crate) fn try_with_item<R>(
174        item: &'static Item,
175        pin: &CpuPin<'_>,
176        operation: impl for<'access> FnOnce(&'access ItemBox) -> R,
177    ) -> Option<R> {
178        let ptr = ACTIVE_SCOPE_PTR.read_current(pin);
179        let items = if ptr == 0 {
180            GLOBAL_SCOPE.get()?.items_ptr()
181        } else {
182            NonNull::new(ptr as *mut ItemBox)?
183        };
184        let index = item.index();
185        Some(operation(unsafe { items.add(index).as_ref() }))
186    }
187
188    pub(crate) fn initialize_global() {
189        let owner_context = current_context_identity();
190        loop {
191            match GLOBAL_SCOPE_STATE.load(Ordering::Acquire) {
192                state if state == GlobalScopeState::Ready as usize => return,
193                state if state == owner_context => {
194                    panic!("scope-local global scope initialization is already in progress")
195                }
196                state if state == GlobalScopeState::Uninitialized as usize => {
197                    if GLOBAL_SCOPE_STATE
198                        .compare_exchange(
199                            GlobalScopeState::Uninitialized as usize,
200                            owner_context,
201                            Ordering::AcqRel,
202                            Ordering::Acquire,
203                        )
204                        .is_ok()
205                    {
206                        let initialization = GlobalInitialization::begin(owner_context);
207                        initialization.publish(Scope::new());
208                        return;
209                    }
210                }
211                _ => core::hint::spin_loop(),
212            }
213        }
214    }
215}
216
217fn current_context_identity() -> usize {
218    let _guard = PreemptGuard::new();
219    // SAFETY: the guard keeps the architecture-selected current context stable
220    // while its header address is acquired. That header stays pinned for the
221    // context lifetime, so the identity survives later migration.
222    let context = unsafe {
223        ax_percpu::with_cpu_pin(|pin| {
224            cpu_local::current_context(pin)
225                .expect("scope-local current context must be valid")
226                .as_ptr() as usize
227        })
228        .expect("scope-local access requires an installed CPU area")
229    };
230    assert!(
231        context > GlobalScopeState::Ready as usize,
232        "scope-local initialization requires a valid current context"
233    );
234    context
235}