Skip to main content

apple_objc_sys/
lib.rs

1#![allow(unsafe_op_in_unsafe_fn)]
2//! Minimal ObjC runtime FFI for Apple framework crates.
3//!
4//! Provides the raw ObjC runtime symbols (`objc_msgSend`, `objc_getClass`,
5//! `sel_registerName`) plus ergonomic helper macros so framework crates can
6//! call ObjC methods directly from Rust — no `.m` files, no `cc` build step.
7//!
8//! # Example
9//!
10//! ```ignore
11//! use apple_objc_sys::*;
12//!
13//! unsafe {
14//!     let cls = class!(b"NSProcessInfo");
15//!     let info: Id = msg_send![cls, processInfo];
16//!     let count: isize = msg_send![info, processorCount];
17//! }
18//! ```
19//!
20//! ## License
21//!
22//! GPL-3.0 — Copyright © 2025 [Eugene Hauptmann](https://github.com/eugenehp)
23
24#![allow(non_camel_case_types)]
25
26use core::ffi::c_void;
27
28// ── Types ───────────────────────────────────────────────────────────────────
29
30/// Opaque ObjC object pointer.
31pub type Id = *mut c_void;
32/// Opaque ObjC class pointer.
33pub type Class = *const c_void;
34/// Opaque ObjC selector pointer.
35pub type Sel = *const c_void;
36/// NULL object.
37pub const NIL: Id = core::ptr::null_mut();
38
39// ── ObjC runtime ────────────────────────────────────────────────────────────
40
41unsafe extern "C" {
42    pub fn objc_getClass(name: *const u8) -> Class;
43    pub fn sel_registerName(name: *const u8) -> Sel;
44    pub fn objc_msgSend(receiver: Id, sel: Sel, ...) -> Id;
45    /// `objc_msgSend` variant for stret on x86_64 (structs > 16 bytes).
46    #[cfg(target_arch = "x86_64")]
47    pub fn objc_msgSend_stret(stret: *mut c_void, receiver: Id, sel: Sel, ...);
48}
49
50// ── CoreFoundation helpers ──────────────────────────────────────────────────
51
52pub type CFTypeRef = *const c_void;
53pub type CFStringRef = *const c_void;
54pub type CFDataRef = *const c_void;
55pub type CFDictionaryRef = *const c_void;
56pub type CFAllocatorRef = *const c_void;
57pub type CFIndex = isize;
58
59pub const K_CF_STRING_ENCODING_UTF8: u32 = 0x08000100;
60pub const K_CF_ALLOCATOR_DEFAULT: CFAllocatorRef = core::ptr::null();
61
62unsafe extern "C" {
63    pub fn CFRelease(cf: CFTypeRef);
64    pub fn CFRetain(cf: CFTypeRef) -> CFTypeRef;
65    pub fn CFStringCreateWithBytes(
66        alloc: CFAllocatorRef, bytes: *const u8, num_bytes: CFIndex,
67        encoding: u32, is_external: bool,
68    ) -> CFStringRef;
69    pub fn CFStringGetLength(s: CFStringRef) -> CFIndex;
70    pub fn CFStringGetCStringPtr(s: CFStringRef, encoding: u32) -> *const u8;
71    pub fn CFStringGetCString(
72        s: CFStringRef, buffer: *mut u8, buffer_size: CFIndex, encoding: u32,
73    ) -> bool;
74    pub fn CFDataCreate(alloc: CFAllocatorRef, bytes: *const u8, length: CFIndex) -> CFDataRef;
75    pub fn CFDataGetLength(data: CFDataRef) -> CFIndex;
76    pub fn CFDataGetBytePtr(data: CFDataRef) -> *const u8;
77    pub fn CFDictionaryCreate(
78        alloc: CFAllocatorRef,
79        keys: *const CFTypeRef, values: *const CFTypeRef, num_values: CFIndex,
80        key_cbs: *const c_void, value_cbs: *const c_void,
81    ) -> CFDictionaryRef;
82
83    pub static kCFTypeDictionaryKeyCallBacks: c_void;
84    pub static kCFTypeDictionaryValueCallBacks: c_void;
85    pub static kCFBooleanTrue: CFTypeRef;
86    pub static kCFBooleanFalse: CFTypeRef;
87}
88
89// ── dlsym ───────────────────────────────────────────────────────────────────
90
91unsafe extern "C" {
92    fn dlsym(handle: *mut c_void, symbol: *const u8) -> *mut c_void;
93}
94
95/// Look up a global symbol by name via `dlsym(RTLD_DEFAULT, name)`.
96#[inline]
97pub unsafe fn dlsym_global(name: &[u8]) -> *const c_void {
98    dlsym((-2isize) as *mut c_void, name.as_ptr()) as *const c_void
99}
100
101/// Read a global `NSString*` / `CFStringRef` constant by symbol name.
102/// Many Apple framework constants (e.g. `kSecClass`) are pointers to
103/// `CFStringRef` — this dereferences the pointer.
104#[inline]
105pub unsafe fn global_string_const(name: &[u8]) -> CFStringRef {
106    let ptr = dlsym_global(name);
107    if ptr.is_null() { return core::ptr::null(); }
108    *(ptr as *const CFStringRef)
109}
110
111// ── Helper: NSString ↔ Rust ─────────────────────────────────────────────────
112
113/// Create an autoreleased `NSString` from a Rust `&str`.
114///
115/// Uses `CFStringCreateWithBytes` which returns a toll-free-bridged
116/// `NSString*`. Caller must manage the refcount (or let autorelease handle it).
117#[inline]
118pub unsafe fn nsstring(s: &str) -> Id {
119    CFStringCreateWithBytes(
120        K_CF_ALLOCATOR_DEFAULT,
121        s.as_ptr(), s.len() as CFIndex,
122        K_CF_STRING_ENCODING_UTF8, false,
123    ) as Id
124}
125
126/// Read an `NSString*` (toll-free bridged `CFStringRef`) into a Rust `String`.
127pub unsafe fn nsstring_to_string(s: Id) -> Option<String> {
128    if s.is_null() { return None; }
129    let cfstr = s as CFStringRef;
130    // Fast path: direct C pointer
131    let cptr = CFStringGetCStringPtr(cfstr, K_CF_STRING_ENCODING_UTF8);
132    if !cptr.is_null() {
133        let cstr = core::ffi::CStr::from_ptr(cptr as *const core::ffi::c_char);
134        return Some(cstr.to_string_lossy().into_owned());
135    }
136    // Slow path: copy into buffer
137    let len = CFStringGetLength(cfstr);
138    let buf_size = len * 4 + 1; // worst case UTF-8
139    let mut buf = vec![0u8; buf_size as usize];
140    if CFStringGetCString(cfstr, buf.as_mut_ptr(), buf_size, K_CF_STRING_ENCODING_UTF8) {
141        let cstr = core::ffi::CStr::from_ptr(buf.as_ptr() as *const core::ffi::c_char);
142        Some(cstr.to_string_lossy().into_owned())
143    } else {
144        None
145    }
146}
147
148/// Write an `NSString*` into a `(buf, len)` pair, returning bytes written or -1.
149pub unsafe fn nsstring_to_buf(s: Id, buf: *mut u8, buf_len: usize) -> isize {
150    if s.is_null() { return -1; }
151    match nsstring_to_string(s) {
152        Some(string) => {
153            let bytes = string.as_bytes();
154            let n = bytes.len().min(buf_len);
155            core::ptr::copy_nonoverlapping(bytes.as_ptr(), buf, n);
156            n as isize
157        }
158        None => -1,
159    }
160}
161
162/// Build a `CFDictionaryRef` from parallel key/value slices.
163#[inline]
164pub unsafe fn cfdict(keys: &[CFTypeRef], vals: &[CFTypeRef]) -> CFDictionaryRef {
165    CFDictionaryCreate(
166        K_CF_ALLOCATOR_DEFAULT,
167        keys.as_ptr(), vals.as_ptr(), keys.len() as CFIndex,
168        &kCFTypeDictionaryKeyCallBacks as *const _ as *const c_void,
169        &kCFTypeDictionaryValueCallBacks as *const _ as *const c_void,
170    )
171}
172
173// ── Macros ──────────────────────────────────────────────────────────────────
174
175/// Look up an ObjC class by name (null-terminated byte string).
176///
177/// ```ignore
178/// let cls = class!(b"NSString\0");
179/// ```
180#[macro_export]
181macro_rules! class {
182    ($name:expr) => {
183        $crate::objc_getClass($name.as_ptr())
184    };
185}
186
187/// Register / look up an ObjC selector (null-terminated byte string).
188///
189/// ```ignore
190/// let sel = sel!(b"init\0");
191/// ```
192#[macro_export]
193macro_rules! sel {
194    ($name:expr) => {
195        $crate::sel_registerName($name.as_ptr())
196    };
197}
198
199/// Send an ObjC message. Returns `Id` by default.
200///
201/// ```ignore
202/// // No args
203/// let obj: Id = msg_send![receiver, init];
204/// // With args
205/// let s: Id = msg_send![cls, stringWithUTF8String: ptr];
206/// ```
207#[macro_export]
208macro_rules! msg_send {
209    // No arguments → returns Id
210    [$obj:expr, $sel:ident] => {{
211        let f: unsafe extern "C" fn($crate::Id, $crate::Sel) -> $crate::Id =
212            core::mem::transmute($crate::objc_msgSend as *const ());
213        f($obj as $crate::Id, $crate::sel!(concat!(stringify!($sel), "\0").as_bytes()))
214    }};
215
216    // With arguments → returns Id
217    [$obj:expr, $($sel:ident : $arg:expr),+ $(,)?] => {{
218        // Build selector string at compile time
219        let sel_name = concat!($(stringify!($sel), ":",)+ "\0");
220        let sel = $crate::sel_registerName(sel_name.as_ptr());
221        // We transmute to the right variadic-free signature
222        msg_send!(@call $obj, sel, $($arg),+)
223    }};
224
225    // Internal: call with 1 arg
226    (@call $obj:expr, $sel:expr, $a1:expr) => {{
227        let f: unsafe extern "C" fn($crate::Id, $crate::Sel, _) -> $crate::Id =
228            core::mem::transmute($crate::objc_msgSend as *const ());
229        f($obj as $crate::Id, $sel, $a1)
230    }};
231    // 2 args
232    (@call $obj:expr, $sel:expr, $a1:expr, $a2:expr) => {{
233        let f: unsafe extern "C" fn($crate::Id, $crate::Sel, _, _) -> $crate::Id =
234            core::mem::transmute($crate::objc_msgSend as *const ());
235        f($obj as $crate::Id, $sel, $a1, $a2)
236    }};
237    // 3 args
238    (@call $obj:expr, $sel:expr, $a1:expr, $a2:expr, $a3:expr) => {{
239        let f: unsafe extern "C" fn($crate::Id, $crate::Sel, _, _, _) -> $crate::Id =
240            core::mem::transmute($crate::objc_msgSend as *const ());
241        f($obj as $crate::Id, $sel, $a1, $a2, $a3)
242    }};
243    // 4 args
244    (@call $obj:expr, $sel:expr, $a1:expr, $a2:expr, $a3:expr, $a4:expr) => {{
245        let f: unsafe extern "C" fn($crate::Id, $crate::Sel, _, _, _, _) -> $crate::Id =
246            core::mem::transmute($crate::objc_msgSend as *const ());
247        f($obj as $crate::Id, $sel, $a1, $a2, $a3, $a4)
248    }};
249    // 5 args
250    (@call $obj:expr, $sel:expr, $a1:expr, $a2:expr, $a3:expr, $a4:expr, $a5:expr) => {{
251        let f: unsafe extern "C" fn($crate::Id, $crate::Sel, _, _, _, _, _) -> $crate::Id =
252            core::mem::transmute($crate::objc_msgSend as *const ());
253        f($obj as $crate::Id, $sel, $a1, $a2, $a3, $a4, $a5)
254    }};
255    // 6 args
256    (@call $obj:expr, $sel:expr, $a1:expr, $a2:expr, $a3:expr, $a4:expr, $a5:expr, $a6:expr) => {{
257        let f: unsafe extern "C" fn($crate::Id, $crate::Sel, _, _, _, _, _, _) -> $crate::Id =
258            core::mem::transmute($crate::objc_msgSend as *const ());
259        f($obj as $crate::Id, $sel, $a1, $a2, $a3, $a4, $a5, $a6)
260    }};
261}
262
263/// Send an ObjC message that returns a typed non-Id value (bool, isize, f64, etc.).
264///
265/// ```ignore
266/// let count: isize = msg_send_t![info, processorCount];
267/// let ok: bool = msg_send_t![mgr, fileExistsAtPath: path];
268/// ```
269#[macro_export]
270macro_rules! msg_send_t {
271    // Return type $t, no args
272    [$t:ty; $obj:expr, $sel:ident] => {{
273        let f: unsafe extern "C" fn($crate::Id, $crate::Sel) -> $t =
274            core::mem::transmute($crate::objc_msgSend as *const ());
275        f($obj as $crate::Id, $crate::sel!(concat!(stringify!($sel), "\0").as_bytes()))
276    }};
277    // Return type $t, with args
278    [$t:ty; $obj:expr, $($sel:ident : $arg:expr),+ $(,)?] => {{
279        let sel_name = concat!($(stringify!($sel), ":",)+ "\0");
280        let sel = $crate::sel_registerName(sel_name.as_ptr());
281        msg_send_t!(@call $t; $obj, sel, $($arg),+)
282    }};
283
284    (@call $t:ty; $obj:expr, $sel:expr, $a1:expr) => {{
285        let f: unsafe extern "C" fn($crate::Id, $crate::Sel, _) -> $t =
286            core::mem::transmute($crate::objc_msgSend as *const ());
287        f($obj as $crate::Id, $sel, $a1)
288    }};
289    (@call $t:ty; $obj:expr, $sel:expr, $a1:expr, $a2:expr) => {{
290        let f: unsafe extern "C" fn($crate::Id, $crate::Sel, _, _) -> $t =
291            core::mem::transmute($crate::objc_msgSend as *const ());
292        f($obj as $crate::Id, $sel, $a1, $a2)
293    }};
294    (@call $t:ty; $obj:expr, $sel:expr, $a1:expr, $a2:expr, $a3:expr) => {{
295        let f: unsafe extern "C" fn($crate::Id, $crate::Sel, _, _, _) -> $t =
296            core::mem::transmute($crate::objc_msgSend as *const ());
297        f($obj as $crate::Id, $sel, $a1, $a2, $a3)
298    }};
299    (@call $t:ty; $obj:expr, $sel:expr, $a1:expr, $a2:expr, $a3:expr, $a4:expr) => {{
300        let f: unsafe extern "C" fn($crate::Id, $crate::Sel, _, _, _, _) -> $t =
301            core::mem::transmute($crate::objc_msgSend as *const ());
302        f($obj as $crate::Id, $sel, $a1, $a2, $a3, $a4)
303    }};
304}
305
306/// Void-returning ObjC message send.
307#[macro_export]
308macro_rules! msg_send_void {
309    [$obj:expr, $sel:ident] => {{
310        let f: unsafe extern "C" fn($crate::Id, $crate::Sel) =
311            core::mem::transmute($crate::objc_msgSend as *const ());
312        f($obj as $crate::Id, $crate::sel!(concat!(stringify!($sel), "\0").as_bytes()))
313    }};
314    [$obj:expr, $($sel:ident : $arg:expr),+ $(,)?] => {{
315        let sel_name = concat!($(stringify!($sel), ":",)+ "\0");
316        let sel = $crate::sel_registerName(sel_name.as_ptr());
317        msg_send_void!(@call $obj, sel, $($arg),+)
318    }};
319
320    (@call $obj:expr, $sel:expr, $a1:expr) => {{
321        let f: unsafe extern "C" fn($crate::Id, $crate::Sel, _) =
322            core::mem::transmute($crate::objc_msgSend as *const ());
323        f($obj as $crate::Id, $sel, $a1)
324    }};
325    (@call $obj:expr, $sel:expr, $a1:expr, $a2:expr) => {{
326        let f: unsafe extern "C" fn($crate::Id, $crate::Sel, _, _) =
327            core::mem::transmute($crate::objc_msgSend as *const ());
328        f($obj as $crate::Id, $sel, $a1, $a2)
329    }};
330    (@call $obj:expr, $sel:expr, $a1:expr, $a2:expr, $a3:expr) => {{
331        let f: unsafe extern "C" fn($crate::Id, $crate::Sel, _, _, _) =
332            core::mem::transmute($crate::objc_msgSend as *const ());
333        f($obj as $crate::Id, $sel, $a1, $a2, $a3)
334    }};
335}