Skip to main content

objc2/__macro_helpers/
cache.rs

1use core::ffi::{c_char, c_void, CStr};
2use core::ptr;
3use core::str;
4use core::sync::atomic::{AtomicPtr, Ordering};
5
6use crate::ffi;
7use crate::runtime::{AnyClass, Sel};
8
9/// Allows storing a [`Sel`] in a static and lazily loading it.
10#[derive(Debug)]
11pub struct CachedSel {
12    ptr: AtomicPtr<c_void>,
13}
14
15impl CachedSel {
16    /// Constructs a new [`CachedSel`].
17    #[allow(clippy::new_without_default)]
18    pub const fn new() -> Self {
19        Self {
20            ptr: AtomicPtr::new(ptr::null_mut()),
21        }
22    }
23
24    // Mark as cold since this should only ever be called once (or maybe twice
25    // if running on multiple threads).
26    #[cold]
27    unsafe fn fetch(&self, name: *const c_char) -> Sel {
28        // The panic inside `Sel::register_unchecked` is unfortunate, but
29        // strict correctness is more important than speed
30
31        // SAFETY: Input is a non-null, NUL-terminated C-string pointer.
32        //
33        // We know this, because we construct it in `sel!` ourselves
34        let sel = unsafe { Sel::register_unchecked(name) };
35        self.ptr.store(sel.as_ptr() as *mut _, Ordering::Relaxed);
36        sel
37    }
38
39    /// Returns the cached selector. If no selector is yet cached, registers
40    /// one with the given name and stores it.
41    #[inline]
42    pub unsafe fn get(&self, name: &str) -> Sel {
43        // `Relaxed` should be fine since `sel_registerName` is thread-safe.
44        let ptr = self.ptr.load(Ordering::Relaxed);
45        if let Some(sel) = unsafe { Sel::from_ptr(ptr) } {
46            sel
47        } else {
48            // SAFETY: Checked by caller
49            unsafe { self.fetch(name.as_ptr().cast()) }
50        }
51    }
52}
53
54/// Allows storing a [`AnyClass`] reference in a static and lazily loading it.
55#[derive(Debug)]
56pub struct CachedClass {
57    ptr: AtomicPtr<AnyClass>,
58}
59
60impl CachedClass {
61    /// Constructs a new [`CachedClass`].
62    #[allow(clippy::new_without_default)]
63    pub const fn new() -> CachedClass {
64        CachedClass {
65            ptr: AtomicPtr::new(ptr::null_mut()),
66        }
67    }
68
69    // Mark as cold since this should only ever be called once (or maybe twice
70    // if running on multiple threads).
71    #[cold]
72    #[track_caller]
73    unsafe fn fetch(&self, name: *const c_char) -> &'static AnyClass {
74        let ptr: *const AnyClass = unsafe { ffi::objc_getClass(name) }.cast();
75        self.ptr.store(ptr as *mut AnyClass, Ordering::Relaxed);
76        if let Some(cls) = unsafe { ptr.as_ref() } {
77            cls
78        } else {
79            // Recover the name from the pointer. We do it like this so that
80            // we don't have to pass the length of the class to this method,
81            // improving binary size.
82            let name = unsafe { CStr::from_ptr(name) };
83            let name = str::from_utf8(name.to_bytes()).unwrap();
84            panic!("class {name} could not be found")
85        }
86    }
87
88    /// Returns the cached class. If no class is yet cached, gets one with
89    /// the given name and stores it.
90    #[inline]
91    #[track_caller]
92    pub unsafe fn get(&self, name: &str) -> &'static AnyClass {
93        // `Relaxed` should be fine since `objc_getClass` is thread-safe.
94        let ptr = self.ptr.load(Ordering::Relaxed);
95        if let Some(cls) = unsafe { ptr.as_ref() } {
96            cls
97        } else {
98            // SAFETY: Checked by caller
99            unsafe { self.fetch(name.as_ptr().cast()) }
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    #[test]
107    #[should_panic = "class NonExistentClass could not be found"]
108    #[cfg(not(feature = "unstable-static-class"))]
109    fn test_not_found() {
110        let _ = crate::class!(NonExistentClass);
111    }
112}