Skip to main content

mtl_sys/
runtime.rs

1//! Objective-C runtime bindings.
2//!
3//! Provides low-level access to the Objective-C runtime for selector
4//! registration and class lookup.
5
6use std::ffi::{CString, c_char, c_void};
7use std::sync::OnceLock;
8
9/// Objective-C selector.
10///
11/// A selector is a unique identifier for a method name. Selectors are
12/// registered with the runtime and can be compared by pointer equality.
13#[repr(transparent)]
14#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
15pub struct Sel(pub(crate) *const c_void);
16
17// SAFETY: Selectors are immutable identifiers managed by the ObjC runtime
18unsafe impl Send for Sel {}
19unsafe impl Sync for Sel {}
20
21/// Objective-C class.
22///
23/// A class is a blueprint for creating objects. Classes are looked up
24/// by name from the runtime.
25#[repr(transparent)]
26#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
27pub struct Class(pub(crate) *const c_void);
28
29// SAFETY: Class pointers are immutable after registration
30unsafe impl Send for Class {}
31unsafe impl Sync for Class {}
32
33// Link against libobjc
34#[link(name = "objc")]
35unsafe extern "C" {
36    fn sel_registerName(name: *const c_char) -> Sel;
37    fn objc_lookUpClass(name: *const c_char) -> *const c_void;
38    fn objc_getProtocol(name: *const c_char) -> *const c_void;
39    fn class_getName(cls: *const c_void) -> *const c_char;
40    fn class_getInstanceMethod(cls: *const c_void, sel: Sel) -> *const c_void;
41    fn class_getClassMethod(cls: *const c_void, sel: Sel) -> *const c_void;
42    fn protocol_getMethodDescription(
43        proto: *const c_void,
44        sel: Sel,
45        is_required: bool,
46        is_instance: bool,
47    ) -> MethodDescription;
48}
49
50/// Objective-C method description (for protocol methods).
51#[repr(C)]
52#[derive(Copy, Clone, Debug)]
53pub struct MethodDescription {
54    /// The selector for this method, or null if not found.
55    pub sel: Sel,
56    /// The type encoding string, or null if not found.
57    pub types: *const c_char,
58}
59
60impl Sel {
61    /// Register a selector with the given name.
62    ///
63    /// The selector is cached by the runtime, so subsequent calls with
64    /// the same name will return the same selector.
65    #[inline]
66    pub fn register(name: &str) -> Self {
67        let c_name = CString::new(name).expect("selector name contains null byte");
68        unsafe { sel_registerName(c_name.as_ptr()) }
69    }
70
71    /// Register a selector from a null-terminated C string.
72    ///
73    /// # Safety
74    ///
75    /// The pointer must be a valid null-terminated C string.
76    #[inline]
77    pub unsafe fn register_cstr(name: *const c_char) -> Self {
78        unsafe { sel_registerName(name) }
79    }
80
81    /// Returns `true` if this selector is null.
82    #[inline]
83    pub fn is_null(&self) -> bool {
84        self.0.is_null()
85    }
86
87    /// Returns the raw pointer value.
88    #[inline]
89    pub fn as_ptr(&self) -> *const c_void {
90        self.0
91    }
92}
93
94impl Class {
95    /// Look up a class by name.
96    ///
97    /// Returns `None` if the class is not registered with the runtime.
98    #[inline]
99    pub fn get(name: &str) -> Option<Self> {
100        let c_name = CString::new(name).expect("class name contains null byte");
101        let ptr = unsafe { objc_lookUpClass(c_name.as_ptr()) };
102        if ptr.is_null() { None } else { Some(Self(ptr)) }
103    }
104
105    /// Look up a class from a null-terminated C string.
106    ///
107    /// # Safety
108    ///
109    /// The pointer must be a valid null-terminated C string.
110    #[inline]
111    pub unsafe fn get_cstr(name: *const c_char) -> Option<Self> {
112        let ptr = unsafe { objc_lookUpClass(name) };
113        if ptr.is_null() { None } else { Some(Self(ptr)) }
114    }
115
116    /// Returns the raw pointer value.
117    #[inline]
118    pub fn as_ptr(&self) -> *const c_void {
119        self.0
120    }
121
122    /// Returns `true` if this class pointer is null.
123    #[inline]
124    pub fn is_null(&self) -> bool {
125        self.0.is_null()
126    }
127
128    /// Check if instances of this class respond to the given selector.
129    ///
130    /// Returns `true` if the class has an instance method for the selector.
131    #[inline]
132    pub fn instances_respond_to(&self, sel: Sel) -> bool {
133        let method = unsafe { class_getInstanceMethod(self.0, sel) };
134        !method.is_null()
135    }
136
137    /// Check if this class responds to the given selector (class method).
138    ///
139    /// Returns `true` if the class has a class method for the selector.
140    #[inline]
141    pub fn responds_to(&self, sel: Sel) -> bool {
142        let method = unsafe { class_getClassMethod(self.0, sel) };
143        !method.is_null()
144    }
145
146    /// Get the name of this class.
147    ///
148    /// # Safety
149    ///
150    /// The class pointer must be valid.
151    pub unsafe fn name(&self) -> &'static str {
152        let name_ptr = unsafe { class_getName(self.0) };
153        if name_ptr.is_null() {
154            "<unknown>"
155        } else {
156            unsafe { std::ffi::CStr::from_ptr(name_ptr) }
157                .to_str()
158                .unwrap_or("<invalid utf8>")
159        }
160    }
161}
162
163/// Objective-C protocol.
164///
165/// A protocol defines a set of methods that a class can implement.
166#[repr(transparent)]
167#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
168pub struct Protocol(pub(crate) *const c_void);
169
170// SAFETY: Protocol pointers are immutable after registration
171unsafe impl Send for Protocol {}
172unsafe impl Sync for Protocol {}
173
174impl Protocol {
175    /// Look up a protocol by name.
176    ///
177    /// Returns `None` if the protocol is not registered with the runtime.
178    #[inline]
179    pub fn get(name: &str) -> Option<Self> {
180        let c_name = CString::new(name).expect("protocol name contains null byte");
181        let ptr = unsafe { objc_getProtocol(c_name.as_ptr()) };
182        if ptr.is_null() { None } else { Some(Self(ptr)) }
183    }
184
185    /// Check if this protocol declares an instance method with the given selector.
186    ///
187    /// Checks both required and optional methods.
188    #[inline]
189    pub fn has_instance_method(&self, sel: Sel) -> bool {
190        // Check required methods first
191        let desc = unsafe { protocol_getMethodDescription(self.0, sel, true, true) };
192        if !desc.sel.is_null() {
193            return true;
194        }
195        // Check optional methods
196        let desc = unsafe { protocol_getMethodDescription(self.0, sel, false, true) };
197        !desc.sel.is_null()
198    }
199
200    /// Check if this protocol declares a class method with the given selector.
201    ///
202    /// Checks both required and optional methods.
203    #[inline]
204    pub fn has_class_method(&self, sel: Sel) -> bool {
205        // Check required methods first
206        let desc = unsafe { protocol_getMethodDescription(self.0, sel, true, false) };
207        if !desc.sel.is_null() {
208            return true;
209        }
210        // Check optional methods
211        let desc = unsafe { protocol_getMethodDescription(self.0, sel, false, false) };
212        !desc.sel.is_null()
213    }
214
215    /// Returns `true` if this protocol pointer is null.
216    #[inline]
217    pub fn is_null(&self) -> bool {
218        self.0.is_null()
219    }
220
221    /// Returns the raw pointer value.
222    #[inline]
223    pub fn as_ptr(&self) -> *const c_void {
224        self.0
225    }
226}
227
228/// Look up a protocol by name.
229///
230/// Returns the protocol pointer or null if not found.
231#[inline]
232pub fn get_protocol(name: &str) -> *const c_void {
233    let c_name = CString::new(name).expect("protocol name contains null byte");
234    unsafe { objc_getProtocol(c_name.as_ptr()) }
235}
236
237/// Helper type for caching selectors.
238#[derive(Default)]
239pub struct CachedSel {
240    inner: OnceLock<Sel>,
241}
242
243impl CachedSel {
244    /// Create a new uninitialized cached selector.
245    pub const fn new() -> Self {
246        Self {
247            inner: OnceLock::new(),
248        }
249    }
250
251    /// Get the selector, initializing it if needed.
252    #[inline]
253    pub fn get(&self, name: &str) -> Sel {
254        *self.inner.get_or_init(|| Sel::register(name))
255    }
256}
257
258/// Helper type for caching classes.
259#[derive(Default)]
260pub struct CachedClass {
261    inner: OnceLock<Class>,
262}
263
264impl CachedClass {
265    /// Create a new uninitialized cached class.
266    pub const fn new() -> Self {
267        Self {
268            inner: OnceLock::new(),
269        }
270    }
271
272    /// Get the class, initializing it if needed.
273    ///
274    /// # Panics
275    ///
276    /// Panics if the class is not found.
277    #[inline]
278    pub fn get(&self, name: &str) -> Class {
279        *self
280            .inner
281            .get_or_init(|| Class::get(name).unwrap_or_else(|| panic!("class {} not found", name)))
282    }
283
284    /// Get the class if available, initializing it if needed.
285    #[inline]
286    pub fn try_get(&self, name: &str) -> Option<Class> {
287        // First check if already initialized
288        if let Some(cls) = self.inner.get() {
289            return Some(*cls);
290        }
291        // Try to get and cache the class
292        let cls = Class::get(name)?;
293        Some(*self.inner.get_or_init(|| cls))
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn test_selector_registration() {
303        let sel1 = Sel::register("init");
304        let sel2 = Sel::register("init");
305        assert_eq!(sel1, sel2);
306        assert!(!sel1.is_null());
307    }
308
309    #[test]
310    fn test_class_lookup() {
311        let cls = Class::get("NSObject");
312        assert!(cls.is_some());
313        assert!(!cls.unwrap().is_null());
314    }
315
316    #[test]
317    fn test_class_not_found() {
318        let cls = Class::get("NonExistentClass12345");
319        assert!(cls.is_none());
320    }
321}