//! Objective-C runtime FFI bindings and Rust abstractions. //! //! Provides safe(ish) wrappers around the Objective-C runtime for message //! dispatch, class lookup, selector registration, and dynamic class creation. //! //! # Safety //! //! This module contains `unsafe` code for FFI with `libobjc.dylib`. //! The `platform` crate is one of the few crates where `unsafe` is permitted. use std::ffi::CStr; use std::fmt; use std::os::raw::{c_char, c_void}; use std::ptr::NonNull; // --------------------------------------------------------------------------- // Raw FFI bindings to libobjc.dylib // --------------------------------------------------------------------------- // Opaque types matching the ObjC runtime's internal structures. // These are public because they appear in the signatures of public methods // (e.g. `as_ptr`), but they are zero-sized and cannot be constructed. #[repr(C)] pub struct ObjcClass { _private: [u8; 0], } #[repr(C)] pub struct ObjcObject { _private: [u8; 0], } #[repr(C)] pub struct ObjcSelector { _private: [u8; 0], } /// An Objective-C method implementation function pointer. /// /// The actual signature varies per method, but the runtime treats all IMPs /// as `extern "C" fn()` for registration purposes. pub type Imp = unsafe extern "C" fn(); #[link(name = "objc", kind = "dylib")] extern "C" { fn objc_getClass(name: *const c_char) -> *mut ObjcClass; fn sel_registerName(name: *const c_char) -> *mut ObjcSelector; fn objc_allocateClassPair( superclass: *mut ObjcClass, name: *const c_char, extra_bytes: usize, ) -> *mut ObjcClass; fn objc_registerClassPair(cls: *mut ObjcClass); fn class_addMethod( cls: *mut ObjcClass, sel: *mut ObjcSelector, imp: Imp, types: *const c_char, ) -> bool; fn class_getName(cls: *mut ObjcClass) -> *const c_char; fn class_addIvar( cls: *mut ObjcClass, name: *const c_char, size: usize, alignment: u8, types: *const c_char, ) -> bool; fn object_setInstanceVariable( obj: *mut ObjcObject, name: *const c_char, value: *mut c_void, ) -> *mut c_void; // returns Ivar (opaque, we don't use it) fn object_getInstanceVariable( obj: *mut ObjcObject, name: *const c_char, out_value: *mut *mut c_void, ) -> *mut c_void; // returns Ivar // objc_msgSend has a variadic ABI. On AArch64 the calling convention is // the standard C calling convention (arguments in x0..x7, return in x0). // We declare it with no extra arguments; the msg_send! macro transmutes // the function pointer to the appropriate concrete signature at each call // site. fn objc_msgSend(); } // --------------------------------------------------------------------------- // Sel — a registered selector // --------------------------------------------------------------------------- /// A registered Objective-C selector. /// /// Selectors are interned strings that identify a method name. Two selectors /// with the same name are pointer-equal. #[derive(Clone, Copy, PartialEq, Eq, Hash)] #[repr(transparent)] pub struct Sel(NonNull); impl Sel { /// Register (or look up) a selector by name. /// /// `name` must be a nul-terminated C string (use `c"foo:"` literals or /// the `sel!` helper). /// /// # Panics /// /// Panics if the runtime returns null (should never happen for valid names). pub fn register(name: &CStr) -> Sel { // SAFETY: sel_registerName is thread-safe and always returns a valid // pointer for well-formed selector names. let ptr = unsafe { sel_registerName(name.as_ptr()) }; Sel(NonNull::new(ptr).expect("sel_registerName returned null")) } /// Return the raw pointer to the selector. #[inline] pub fn as_ptr(self) -> *mut ObjcSelector { self.0.as_ptr() } } impl fmt::Debug for Sel { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // We can't easily get the selector name without sel_getName, // so just print the pointer. write!(f, "Sel({:?})", self.0) } } // SAFETY: Selectors are globally interned pointers; they're safe to send // across threads. unsafe impl Send for Sel {} unsafe impl Sync for Sel {} // --------------------------------------------------------------------------- // Class — a reference to an Obj-C class // --------------------------------------------------------------------------- /// A reference to an Objective-C class. #[derive(Clone, Copy, PartialEq, Eq, Hash)] #[repr(transparent)] pub struct Class(NonNull); impl Class { /// Look up a class by name. /// /// Returns `None` if no class with that name is registered. pub fn get(name: &CStr) -> Option { // SAFETY: objc_getClass is thread-safe; it returns null for unknown // class names. let ptr = unsafe { objc_getClass(name.as_ptr()) }; NonNull::new(ptr).map(Class) } /// Allocate a new class pair (a new class and its metaclass). /// /// `superclass` is the parent class; `name` is the new class name; /// `extra_bytes` is usually 0. /// /// Returns `None` if a class with `name` already exists. /// /// You must call [`Class::register`] on the returned class after adding /// methods and ivars. pub fn allocate(superclass: Class, name: &CStr, extra_bytes: usize) -> Option { // SAFETY: objc_allocateClassPair returns null if the name is already // taken. The superclass pointer is valid because it came from a Class. let ptr = unsafe { objc_allocateClassPair(superclass.as_ptr(), name.as_ptr(), extra_bytes) }; NonNull::new(ptr).map(Class) } /// Register a class that was previously created with [`Class::allocate`]. /// /// After this call the class is usable and no further methods/ivars can be /// added. pub fn register(self) { // SAFETY: the pointer is valid and the class has not been registered // yet (caller invariant). unsafe { objc_registerClassPair(self.as_ptr()) } } /// Add a method to a class that has not yet been registered. /// /// `sel` is the method selector, `imp` is the implementation function /// pointer, and `types` is the ObjC type encoding string. /// /// Returns `true` if the method was added, `false` if a method with that /// selector already existed. pub fn add_method(self, sel: Sel, imp: Imp, types: &CStr) -> bool { // SAFETY: the class pointer is valid and not yet registered. unsafe { class_addMethod(self.as_ptr(), sel.as_ptr(), imp, types.as_ptr()) } } /// Add an instance variable to a class that has not yet been registered. /// /// `name` is the ivar name, `size` is the size in bytes, `alignment` is /// the log2 alignment (e.g., 3 for 8-byte alignment), and `types` is the /// ObjC type encoding string. /// /// Returns `true` if the ivar was added. pub fn add_ivar(self, name: &CStr, size: usize, alignment: u8, types: &CStr) -> bool { // SAFETY: the class pointer is valid and not yet registered. unsafe { class_addIvar( self.as_ptr(), name.as_ptr(), size, alignment, types.as_ptr(), ) } } /// Get the name of this class. pub fn name(self) -> &'static str { // SAFETY: class_getName always returns a valid C string for a valid // class pointer. let c_str = unsafe { CStr::from_ptr(class_getName(self.as_ptr())) }; c_str.to_str().unwrap_or("") } /// Return the raw pointer to the class. #[inline] pub fn as_ptr(self) -> *mut ObjcClass { self.0.as_ptr() } } impl fmt::Debug for Class { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Class(\"{}\")", self.name()) } } // SAFETY: ObjC classes are globally registered and immutable after // registration; safe to share across threads. unsafe impl Send for Class {} unsafe impl Sync for Class {} // --------------------------------------------------------------------------- // Id — a non-null pointer to an Obj-C object instance // --------------------------------------------------------------------------- /// A non-null pointer to an Objective-C object. /// /// This is an *unowned* (raw) reference — no automatic retain/release. /// The caller is responsible for memory management. #[derive(Clone, Copy, PartialEq, Eq, Hash)] #[repr(transparent)] pub struct Id(NonNull); impl Id { /// Create an `Id` from a raw non-null pointer. /// /// # Safety /// /// The pointer must point to a valid Objective-C object. #[inline] pub unsafe fn from_ptr(ptr: NonNull) -> Id { Id(ptr) } /// Try to create an `Id` from a raw pointer that might be null. /// /// # Safety /// /// If non-null, the pointer must point to a valid Objective-C object. #[inline] pub unsafe fn from_raw(ptr: *mut ObjcObject) -> Option { NonNull::new(ptr).map(Id) } /// Return the raw pointer. #[inline] pub fn as_ptr(self) -> *mut ObjcObject { self.0.as_ptr() } /// Cast to `*mut T` for use as a function argument. /// /// # Safety /// /// The caller must ensure the object is actually of type `T`. #[inline] pub unsafe fn cast(self) -> *mut T { self.0.as_ptr().cast() } /// Set an instance variable by name. /// /// # Safety /// /// The object must have an ivar with the given `name` of pointer type. /// The caller is responsible for the validity and lifetime of `value`. pub unsafe fn set_ivar(self, name: &CStr, value: *mut c_void) { object_setInstanceVariable(self.as_ptr(), name.as_ptr(), value); } /// Get an instance variable by name. /// /// # Safety /// /// The object must have an ivar with the given `name` of pointer type. pub unsafe fn get_ivar(self, name: &CStr) -> *mut c_void { let mut out: *mut c_void = std::ptr::null_mut(); object_getInstanceVariable(self.as_ptr(), name.as_ptr(), &mut out); out } } impl fmt::Debug for Id { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Id({:?})", self.0) } } // SAFETY: ObjC objects can be sent across threads (whether this is safe // depends on the object, but at the pointer level it's fine). unsafe impl Send for Id {} unsafe impl Sync for Id {} // --------------------------------------------------------------------------- // msg_send! macro // --------------------------------------------------------------------------- /// Return the raw `objc_msgSend` function pointer. /// /// The caller must transmute this to the correct function signature for the /// message being sent. The `msg_send!` macro does this automatically. #[inline] pub fn msg_send_fn() -> unsafe extern "C" fn() { objc_msgSend } /// Send an Objective-C message. /// /// # Syntax /// /// ```ignore /// // No arguments, returns Id: /// let obj: Id = msg_send![class.as_ptr(), alloc]; /// /// // With arguments, returns Id: /// let obj: Id = msg_send![obj.as_ptr(), initWithFrame: rect]; /// /// // No return value (returns ()): /// msg_send![obj.as_ptr(), release]; /// ``` /// /// The first argument must be a `*mut T` (the receiver). The return type is /// inferred from context. /// /// # Safety /// /// This is inherently unsafe: wrong selector, wrong argument types, or wrong /// return type will cause undefined behavior. #[macro_export] macro_rules! msg_send { // No arguments: msg_send![receiver, selector] [$receiver:expr, $sel:ident] => {{ let sel_name = concat!(stringify!($sel), "\0"); let sel = $crate::objc::Sel::register(unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(sel_name.as_bytes()) }); let func: unsafe extern "C" fn(*mut std::os::raw::c_void, *mut std::os::raw::c_void) -> _ = unsafe { std::mem::transmute($crate::objc::msg_send_fn()) }; let receiver = $receiver as *mut std::os::raw::c_void; unsafe { func(receiver, sel.as_ptr() as *mut std::os::raw::c_void) } }}; // One argument: msg_send![receiver, selector: arg] [$receiver:expr, $sel:ident : $arg:expr] => {{ let sel_name = concat!(stringify!($sel), ":\0"); let sel = $crate::objc::Sel::register(unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(sel_name.as_bytes()) }); let func: unsafe extern "C" fn( *mut std::os::raw::c_void, *mut std::os::raw::c_void, _, ) -> _ = unsafe { std::mem::transmute($crate::objc::msg_send_fn()) }; let receiver = $receiver as *mut std::os::raw::c_void; let arg = $arg; unsafe { func( receiver, sel.as_ptr() as *mut std::os::raw::c_void, arg, ) } }}; // Two arguments: msg_send![receiver, sel1: arg1, sel2: arg2] [$receiver:expr, $sel1:ident : $arg1:expr, $sel2:ident : $arg2:expr] => {{ let sel_name = concat!(stringify!($sel1), ":", stringify!($sel2), ":\0"); let sel = $crate::objc::Sel::register(unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(sel_name.as_bytes()) }); let func: unsafe extern "C" fn( *mut std::os::raw::c_void, *mut std::os::raw::c_void, _, _, ) -> _ = unsafe { std::mem::transmute($crate::objc::msg_send_fn()) }; let receiver = $receiver as *mut std::os::raw::c_void; let arg1 = $arg1; let arg2 = $arg2; unsafe { func( receiver, sel.as_ptr() as *mut std::os::raw::c_void, arg1, arg2, ) } }}; // Three arguments: msg_send![receiver, sel1: arg1, sel2: arg2, sel3: arg3] [$receiver:expr, $sel1:ident : $arg1:expr, $sel2:ident : $arg2:expr, $sel3:ident : $arg3:expr] => {{ let sel_name = concat!( stringify!($sel1), ":", stringify!($sel2), ":", stringify!($sel3), ":\0" ); let sel = $crate::objc::Sel::register(unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(sel_name.as_bytes()) }); let func: unsafe extern "C" fn( *mut std::os::raw::c_void, *mut std::os::raw::c_void, _, _, _, ) -> _ = unsafe { std::mem::transmute($crate::objc::msg_send_fn()) }; let receiver = $receiver as *mut std::os::raw::c_void; let arg1 = $arg1; let arg2 = $arg2; let arg3 = $arg3; unsafe { func( receiver, sel.as_ptr() as *mut std::os::raw::c_void, arg1, arg2, arg3, ) } }}; // Four arguments [$receiver:expr, $sel1:ident : $arg1:expr, $sel2:ident : $arg2:expr, $sel3:ident : $arg3:expr, $sel4:ident : $arg4:expr] => {{ let sel_name = concat!( stringify!($sel1), ":", stringify!($sel2), ":", stringify!($sel3), ":", stringify!($sel4), ":\0" ); let sel = $crate::objc::Sel::register(unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(sel_name.as_bytes()) }); let func: unsafe extern "C" fn( *mut std::os::raw::c_void, *mut std::os::raw::c_void, _, _, _, _, ) -> _ = unsafe { std::mem::transmute($crate::objc::msg_send_fn()) }; let receiver = $receiver as *mut std::os::raw::c_void; let arg1 = $arg1; let arg2 = $arg2; let arg3 = $arg3; let arg4 = $arg4; unsafe { func( receiver, sel.as_ptr() as *mut std::os::raw::c_void, arg1, arg2, arg3, arg4, ) } }}; } // --------------------------------------------------------------------------- // Convenience: sel! macro // --------------------------------------------------------------------------- /// Register a selector from a string literal. /// /// ```ignore /// let sel = sel!("init"); /// let sel = sel!("initWithFrame:"); /// ``` #[macro_export] macro_rules! sel { ($name:literal) => {{ $crate::objc::Sel::register(unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(concat!($name, "\0").as_bytes()) }) }}; } /// Look up a class by name (string literal). /// /// Returns `Option`. /// /// ```ignore /// let cls = class!("NSObject").unwrap(); /// ``` #[macro_export] macro_rules! class { ($name:literal) => {{ $crate::objc::Class::get(unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(concat!($name, "\0").as_bytes()) }) }}; } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; #[test] fn get_nsobject_class() { let cls = Class::get(c"NSObject").expect("NSObject class should exist"); assert_eq!(cls.name(), "NSObject"); } #[test] fn get_nonexistent_class_returns_none() { let cls = Class::get(c"ThisClassDoesNotExist12345"); assert!(cls.is_none()); } #[test] fn register_selector() { let sel1 = Sel::register(c"init"); let sel2 = Sel::register(c"init"); // Same selector name should return the same pointer. assert_eq!(sel1, sel2); } #[test] fn different_selectors_differ() { let sel1 = Sel::register(c"init"); let sel2 = Sel::register(c"alloc"); assert_ne!(sel1, sel2); } #[test] fn alloc_init_nsobject() { let cls = Class::get(c"NSObject").unwrap(); // alloc let obj: *mut std::os::raw::c_void = msg_send![cls.as_ptr(), alloc]; assert!(!obj.is_null()); // init let obj: *mut std::os::raw::c_void = msg_send![obj, init]; assert!(!obj.is_null()); // release let _: *mut std::os::raw::c_void = msg_send![obj, release]; } #[test] fn class_macro_works() { let cls = class!("NSObject").expect("NSObject should exist"); assert_eq!(cls.name(), "NSObject"); } #[test] fn sel_macro_works() { let sel = sel!("init"); let sel2 = Sel::register(c"init"); assert_eq!(sel, sel2); } #[test] fn allocate_and_register_custom_class() { let superclass = Class::get(c"NSObject").unwrap(); // Use a unique class name to avoid conflicts with other tests. let new_cls = Class::allocate(superclass, c"WeTestCustomClass", 0) .expect("should allocate new class"); new_cls.register(); // Verify we can look it up. let found = Class::get(c"WeTestCustomClass").expect("custom class should be registered"); assert_eq!(found.name(), "WeTestCustomClass"); } #[test] fn add_method_to_custom_class() { let superclass = Class::get(c"NSObject").unwrap(); let new_cls = Class::allocate(superclass, c"WeTestMethodClass", 0) .expect("should allocate new class"); // Define a simple method that returns an integer. extern "C" fn my_method( _this: *mut std::os::raw::c_void, _sel: *mut std::os::raw::c_void, ) -> i64 { 42 } let sel = Sel::register(c"myMethod"); // Type encoding: returns long long (q), takes id (@) and SEL (:) let added = new_cls.add_method( sel, unsafe { std::mem::transmute::<*const (), Imp>(my_method as *const ()) }, c"q@:", ); assert!(added); new_cls.register(); // Allocate an instance and call our method. let cls = Class::get(c"WeTestMethodClass").unwrap(); let obj: *mut std::os::raw::c_void = msg_send![cls.as_ptr(), alloc]; let obj: *mut std::os::raw::c_void = msg_send![obj, init]; let result: i64 = msg_send![obj, myMethod]; assert_eq!(result, 42); let _: *mut std::os::raw::c_void = msg_send![obj, release]; } #[test] fn msg_send_with_argument() { let cls = Class::get(c"NSObject").unwrap(); let obj: *mut std::os::raw::c_void = msg_send![cls.as_ptr(), alloc]; let obj: *mut std::os::raw::c_void = msg_send![obj, init]; // respondsToSelector: takes a SEL, returns BOOL (i8 on AArch64). let sel = Sel::register(c"init"); let responds: bool = msg_send![obj, respondsToSelector: sel.as_ptr()]; assert!(responds); let _: *mut std::os::raw::c_void = msg_send![obj, release]; } #[test] fn class_debug_format() { let cls = Class::get(c"NSObject").unwrap(); let debug = format!("{:?}", cls); assert!(debug.contains("NSObject")); } #[test] fn sel_debug_format() { let sel = Sel::register(c"init"); let debug = format!("{:?}", sel); assert!(debug.contains("Sel(")); } }