Skip to main content

swiftui_native/
objc.rs

1//! Minimal Objective-C runtime bridge.
2//!
3//! AppKit and UIKit classes are reached through `objc_msgSend` rather than the Swift
4//! calling convention — that is their native ABI, so nothing is lost.
5//!
6//! Every send goes through a correctly-typed, *non-variadic* view of
7//! `objc_msgSend`. On arm64 variadic arguments are passed on the stack, so
8//! calling a variadic declaration with real arguments delivers them where the
9//! receiver never looks and the callee reads uninitialised registers instead.
10
11use core::ffi::{c_char, c_void};
12
13unsafe extern "C" {
14    fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
15    fn objc_msgSend(receiver: *mut c_void, sel: *const c_void, ...) -> *mut c_void;
16    fn sel_registerName(name: *const c_char) -> *const c_void;
17
18    fn objc_allocateClassPair(
19        superclass: *mut c_void,
20        name: *const c_char,
21        extra_bytes: usize,
22    ) -> *mut c_void;
23    fn objc_registerClassPair(class: *mut c_void);
24    fn objc_getClass(name: *const c_char) -> *mut c_void;
25    fn objc_getProtocol(name: *const c_char) -> *mut c_void;
26    fn class_addMethod(
27        class: *mut c_void,
28        name: *const c_void,
29        imp: *const c_void,
30        types: *const c_char,
31    ) -> bool;
32    fn class_addProtocol(class: *mut c_void, protocol: *mut c_void) -> bool;
33}
34
35const RTLD_DEFAULT: *mut c_void = -2isize as *mut c_void;
36
37#[repr(C)]
38#[derive(Clone, Copy, Debug, PartialEq)]
39pub struct NSPoint {
40    pub x: f64,
41    pub y: f64,
42}
43
44#[repr(C)]
45#[derive(Clone, Copy, Debug, PartialEq)]
46pub struct NSSize {
47    pub width: f64,
48    pub height: f64,
49}
50
51#[repr(C)]
52#[derive(Clone, Copy, Debug, PartialEq)]
53pub struct NSRect {
54    pub origin: NSPoint,
55    pub size: NSSize,
56}
57
58impl NSRect {
59    pub fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
60        Self { origin: NSPoint { x, y }, size: NSSize { width, height } }
61    }
62}
63
64/// An autorelease pool, drained when it goes out of scope.
65///
66/// Objective-C's convention is that a method returning an object it does not
67/// own hands back an *autoreleased* one — alive until the enclosing pool
68/// drains. Cocoa's own event loop provides that pool; a Rust program has none,
69/// so without this every temporary AppKit or UIKit object accumulates for the
70/// life of the process. It is not a slow leak: capturing a few hundred frames
71/// costs tens of megabytes.
72///
73/// Every entry point here that talks to AppKit or UIKit holds one for the
74/// duration of the call. Code that loops over such calls wants its own around
75/// the loop as well, so peak memory tracks one iteration rather than all of
76/// them.
77///
78/// ```ignore
79/// for frame in 0..600 {
80///     let _pool = objc::AutoreleasePool::new();
81///     capture(frame);
82/// }
83/// ```
84pub struct AutoreleasePool {
85    context: *mut c_void,
86}
87
88impl AutoreleasePool {
89    pub fn new() -> Self {
90        unsafe extern "C" {
91            fn objc_autoreleasePoolPush() -> *mut c_void;
92        }
93        Self { context: unsafe { objc_autoreleasePoolPush() } }
94    }
95}
96
97impl Default for AutoreleasePool {
98    fn default() -> Self {
99        Self::new()
100    }
101}
102
103impl Drop for AutoreleasePool {
104    fn drop(&mut self) {
105        unsafe extern "C" {
106            fn objc_autoreleasePoolPop(context: *mut c_void);
107        }
108        unsafe { objc_autoreleasePoolPop(self.context) };
109    }
110}
111
112/// The address of `objc_msgSend`, for building a typed view of it.
113///
114/// Every call site needs its own signature — see the module note on why a
115/// variadic declaration will not do — so they transmute this rather than
116/// calling `objc_msgSend` directly.
117pub fn msg_send_addr() -> *const c_void {
118    objc_msgSend as *const c_void
119}
120
121/// Register (or look up) a selector.
122pub fn sel(name: &core::ffi::CStr) -> *const c_void {
123    unsafe { sel_registerName(name.as_ptr().cast()) }
124}
125
126/// Look up an Objective-C class by its `OBJC_CLASS_$_Name` symbol.
127pub fn class(symbol: &core::ffi::CStr) -> *mut c_void {
128    let c = unsafe { dlsym(RTLD_DEFAULT, symbol.as_ptr()) };
129    assert!(!c.is_null(), "Objective-C class not found: {symbol:?}");
130    c
131}
132
133macro_rules! send {
134    ($name:ident, ($($arg:ident: $ty:ty),*) -> $ret:ty) => {
135        /// Send a message with a specific, non-variadic signature.
136                /// # Safety
137        /// `recv` must be a live Objective-C object that responds to `s`, and the
138        /// declared argument and return types must match that selector's. Nothing
139        /// here checks either.
140pub unsafe fn $name(recv: *mut c_void, s: *const c_void $(, $arg: $ty)*) -> $ret {
141            type F = unsafe extern "C" fn(*mut c_void, *const c_void $(, $ty)*) -> $ret;
142            let f: F = unsafe { core::mem::transmute(objc_msgSend as *const c_void) };
143            unsafe { f(recv, s $(, $arg)*) }
144        }
145    };
146}
147
148send!(send, () -> *mut c_void);
149send!(send_id, (a: *mut c_void) -> *mut c_void);
150send!(send_bool, (a: bool) -> *mut c_void);
151send!(send_i64, (a: i64) -> *mut c_void);
152send!(send_u64, (a: u64) -> *mut c_void);
153send!(send_f64, (a: f64) -> *mut c_void);
154send!(send_rect, (a: NSRect) -> *mut c_void);
155send!(send_rect_id, (a: NSRect, b: *mut c_void) -> *mut c_void);
156send!(send_u64_id, (a: u64, b: *mut c_void) -> *mut c_void);
157send!(send_id_bool, (a: *mut c_void, b: bool) -> bool);
158send!(send_ret_size, () -> NSSize);
159send!(send_ret_rect, () -> NSRect);
160send!(send_ret_u64, () -> u64);
161send!(send_ret_f64, () -> f64);
162send!(send_ret_ptr, () -> *const u8);
163send!(send_bool_id, (a: bool, b: *mut c_void) -> *mut c_void);
164send!(send_i64_id, (a: i64, b: *mut c_void) -> *mut c_void);
165send!(send_f64_id, (a: f64, b: *mut c_void) -> *mut c_void);
166send!(send_id_id, (a: *mut c_void, b: *mut c_void) -> *mut c_void);
167send!(send_id_ret_bool, (a: *mut c_void) -> bool);
168send!(send_id_ret_i64, (a: *mut c_void) -> i64);
169send!(send_id_ret_f64, (a: *mut c_void) -> f64);
170send!(send_ret_bool, () -> bool);
171
172/// Does `receiver` implement `selector`?
173///
174/// Used to pick between an API and its newer replacement at runtime rather than
175/// at build time.
176///
177/// # Safety
178/// `receiver` must be a live object or class.
179pub unsafe fn responds(receiver: *mut c_void, selector: *const c_void) -> bool {
180    unsafe { send_id(receiver, sel(c"respondsToSelector:"), selector as *mut c_void) as usize & 1 == 1 }
181}
182
183/// Create an `NSString` from a Rust `&str`.
184/// # Safety
185/// Foundation must be loaded. The returned object is `+1`.
186pub unsafe fn nsstring(s: &str) -> *mut c_void {
187    let alloc = unsafe { send(class(c"OBJC_CLASS_$_NSString"), sel(c"alloc")) };
188    type InitBytes =
189        unsafe extern "C" fn(*mut c_void, *const c_void, *const u8, u64, u64) -> *mut c_void;
190    let f: InitBytes = unsafe { core::mem::transmute(objc_msgSend as *const c_void) };
191    // 4 = NSUTF8StringEncoding
192    let out = unsafe {
193        f(alloc, sel(c"initWithBytes:length:encoding:"), s.as_ptr(), s.len() as u64, 4)
194    };
195    assert!(!out.is_null(), "NSString creation failed for {s:?}");
196    out
197}
198
199/// `[NSApplication sharedApplication]`, pinned to the light appearance so that
200/// rendering does not depend on the host's system setting.
201#[cfg(target_os = "macos")]
202/// # Safety
203/// AppKit must be loaded and this must run on the main thread, as every
204/// `NSApplication` call must.
205pub unsafe fn shared_app() -> *mut c_void {
206    let app = unsafe { send(class(c"OBJC_CLASS_$_NSApplication"), sel(c"sharedApplication")) };
207    unsafe {
208        let name = nsstring("NSAppearanceNameAqua");
209        let appearance = send_id(
210            class(c"OBJC_CLASS_$_NSAppearance"),
211            sel(c"appearanceNamed:"),
212            name,
213        );
214        if !appearance.is_null() {
215            send_id(app, sel(c"setAppearance:"), appearance);
216        }
217    }
218    app
219}
220
221/// `[UIApplication sharedApplication]`.
222///
223/// Null unless the process actually went through `UIApplicationMain` — which a
224/// plain binary run under `simctl spawn` has not. Rendering does not need it;
225/// showing a window does.
226///
227/// # Safety
228/// Calling into UIKit off the main thread is not supported.
229#[cfg(not(target_os = "macos"))]
230pub unsafe fn shared_app() -> *mut c_void {
231    unsafe { send(class(c"OBJC_CLASS_$_UIApplication"), sel(c"sharedApplication")) }
232}
233
234// ═══════════════════════════════════════════════════════════════════════════
235// Defining classes at runtime
236// ═══════════════════════════════════════════════════════════════════════════
237//
238// The Objective-C counterpart of what [`crate::state`] does on the Swift side:
239// build the type, give it its methods, register it. UIKit will not start an
240// application without a delegate *class* to name, and writing one in Objective-C
241// would mean shipping a source file — so it is synthesised here instead.
242
243/// A class being built. Add methods, then [`register`](ClassBuilder::register).
244pub struct ClassBuilder {
245    class: *mut c_void,
246}
247
248impl ClassBuilder {
249    /// Begin a new class, or return `None` if the name is already taken.
250    ///
251    /// # Safety
252    /// `superclass` must name a registered class.
253    pub unsafe fn new(superclass: &core::ffi::CStr, name: &core::ffi::CStr) -> Option<Self> {
254        unsafe {
255            let sup = objc_getClass(superclass.as_ptr().cast());
256            assert!(!sup.is_null(), "superclass not found: {superclass:?}");
257            let class = objc_allocateClassPair(sup, name.as_ptr(), 0);
258            if class.is_null() {
259                return None;
260            }
261            Some(Self { class })
262        }
263    }
264
265    /// Add a method.
266    ///
267    /// `types` is an Objective-C type encoding: return type, then `@:` for the
268    /// implicit receiver and selector, then one code per argument. `B@:@@` is
269    /// "returns BOOL, takes two objects".
270    ///
271    /// # Safety
272    /// `imp`'s signature must match `types`, receiver and selector included.
273    pub unsafe fn method(
274        self,
275        selector: &core::ffi::CStr,
276        imp: *const c_void,
277        types: &core::ffi::CStr,
278    ) -> Self {
279        unsafe {
280            let ok = class_addMethod(self.class, sel(selector), imp, types.as_ptr());
281            assert!(ok, "could not add {selector:?}");
282        }
283        self
284    }
285
286    /// Declare conformance to a protocol.
287    ///
288    /// # Safety
289    /// The class must actually implement the protocol's required methods.
290    pub unsafe fn conforms(self, protocol: &core::ffi::CStr) -> Self {
291        unsafe {
292            let p = objc_getProtocol(protocol.as_ptr());
293            if !p.is_null() {
294                class_addProtocol(self.class, p);
295            }
296        }
297        self
298    }
299
300    /// Register the class with the runtime and return it.
301    pub fn register(self) -> *mut c_void {
302        unsafe { objc_registerClassPair(self.class) };
303        self.class
304    }
305}
306
307/// Look up a registered class by name, or null.
308pub fn class_named(name: &core::ffi::CStr) -> *mut c_void {
309    unsafe { objc_getClass(name.as_ptr().cast()) }
310}
311
312/// An Objective-C block with no captures.
313///
314/// A block is an object whose first word is an `isa` like any other; what makes
315/// it callable is the `invoke` pointer at offset 16, which is passed the block
316/// itself as its first argument. With nothing captured there is nothing to copy
317/// or release, so the block can be a plain `static` tagged `BLOCK_IS_GLOBAL` —
318/// `_NSConcreteGlobalBlock` is exactly the isa for that.
319#[repr(C)]
320pub struct GlobalBlock {
321    isa: *const c_void,
322    flags: i32,
323    reserved: i32,
324    invoke: *const c_void,
325    descriptor: *const BlockDescriptor,
326}
327
328/// The size-and-copy record every block points at.
329#[repr(C)]
330pub struct BlockDescriptor {
331    reserved: u64,
332    size: u64,
333}
334
335// `Sync` because nothing here is ever written after construction.
336unsafe impl Sync for GlobalBlock {}
337unsafe impl Sync for BlockDescriptor {}
338
339/// `BLOCK_IS_GLOBAL`.
340const BLOCK_IS_GLOBAL: i32 = 1 << 28;
341
342impl GlobalBlock {
343    /// Wrap `invoke` as a block. It is called with the block as its first
344    /// argument, then whatever the block's signature declares.
345    ///
346    /// # Safety
347    /// `invoke` must match the signature the receiver expects, including the
348    /// leading block pointer.
349    pub unsafe fn new(invoke: *const c_void, descriptor: &'static BlockDescriptor) -> Self {
350        let isa = unsafe { dlsym(RTLD_DEFAULT, c"_NSConcreteGlobalBlock".as_ptr()) };
351        assert!(!isa.is_null(), "_NSConcreteGlobalBlock");
352        GlobalBlock { isa, flags: BLOCK_IS_GLOBAL, reserved: 0, invoke, descriptor }
353    }
354}
355
356impl BlockDescriptor {
357    /// The descriptor for a block with no captures.
358    pub const fn empty() -> Self {
359        BlockDescriptor { reserved: 0, size: core::mem::size_of::<GlobalBlock>() as u64 }
360    }
361}
362
363/// A block carrying one pointer.
364///
365/// [`GlobalBlock`] captures nothing, which is all a block needs when the
366/// receiver only ever calls it — but a *completion* handler has to reach
367/// something, and the something differs per call. One captured word is enough
368/// for that: it is where the answer gets written.
369///
370/// `_NSConcreteStackBlock` with no copy/dispose helpers, so a receiver that
371/// copies it gets a `memcpy` and the capture comes along by value. That is only
372/// sound while the caller outlives the call, which is why this is used with a
373/// wait and not handed to anything that keeps it.
374#[repr(C)]
375pub struct CapturingBlock {
376    isa: *const c_void,
377    flags: i32,
378    reserved: i32,
379    invoke: *const c_void,
380    descriptor: *const BlockDescriptor,
381    /// What the invoke function reads, at a fixed offset from the block.
382    pub capture: *mut c_void,
383}
384
385impl CapturingBlock {
386    /// Wrap `invoke` as a block carrying `capture`.
387    ///
388    /// # Safety
389    /// `invoke` must match the signature the receiver expects, taking the block
390    /// itself first; and the block must outlive every call to it.
391    pub unsafe fn new(invoke: *const c_void, capture: *mut c_void) -> Self {
392        static DESCRIPTOR: BlockDescriptor = BlockDescriptor {
393            reserved: 0,
394            size: core::mem::size_of::<CapturingBlock>() as u64,
395        };
396        let isa = unsafe { dlsym(RTLD_DEFAULT, c"_NSConcreteStackBlock".as_ptr()) };
397        assert!(!isa.is_null(), "_NSConcreteStackBlock");
398        CapturingBlock {
399            isa,
400            flags: 0,
401            reserved: 0,
402            invoke,
403            descriptor: &DESCRIPTOR,
404            capture,
405        }
406    }
407}