swiftui-native 0.0.1

SwiftUI from pure Rust — no Swift bridge, no shim, only swift-runtime calls
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
//! Minimal Objective-C runtime bridge.
//!
//! AppKit and UIKit classes are reached through `objc_msgSend` rather than the Swift
//! calling convention — that is their native ABI, so nothing is lost.
//!
//! Every send goes through a correctly-typed, *non-variadic* view of
//! `objc_msgSend`. On arm64 variadic arguments are passed on the stack, so
//! calling a variadic declaration with real arguments delivers them where the
//! receiver never looks and the callee reads uninitialised registers instead.

use core::ffi::{c_char, c_void};

unsafe extern "C" {
    fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
    fn objc_msgSend(receiver: *mut c_void, sel: *const c_void, ...) -> *mut c_void;
    fn sel_registerName(name: *const c_char) -> *const c_void;

    fn objc_allocateClassPair(
        superclass: *mut c_void,
        name: *const c_char,
        extra_bytes: usize,
    ) -> *mut c_void;
    fn objc_registerClassPair(class: *mut c_void);
    fn objc_getClass(name: *const c_char) -> *mut c_void;
    fn objc_getProtocol(name: *const c_char) -> *mut c_void;
    fn class_addMethod(
        class: *mut c_void,
        name: *const c_void,
        imp: *const c_void,
        types: *const c_char,
    ) -> bool;
    fn class_addProtocol(class: *mut c_void, protocol: *mut c_void) -> bool;
}

const RTLD_DEFAULT: *mut c_void = -2isize as *mut c_void;

#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct NSPoint {
    pub x: f64,
    pub y: f64,
}

#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct NSSize {
    pub width: f64,
    pub height: f64,
}

#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct NSRect {
    pub origin: NSPoint,
    pub size: NSSize,
}

impl NSRect {
    pub fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
        Self { origin: NSPoint { x, y }, size: NSSize { width, height } }
    }
}

/// An autorelease pool, drained when it goes out of scope.
///
/// Objective-C's convention is that a method returning an object it does not
/// own hands back an *autoreleased* one — alive until the enclosing pool
/// drains. Cocoa's own event loop provides that pool; a Rust program has none,
/// so without this every temporary AppKit or UIKit object accumulates for the
/// life of the process. It is not a slow leak: capturing a few hundred frames
/// costs tens of megabytes.
///
/// Every entry point here that talks to AppKit or UIKit holds one for the
/// duration of the call. Code that loops over such calls wants its own around
/// the loop as well, so peak memory tracks one iteration rather than all of
/// them.
///
/// ```ignore
/// for frame in 0..600 {
///     let _pool = objc::AutoreleasePool::new();
///     capture(frame);
/// }
/// ```
pub struct AutoreleasePool {
    context: *mut c_void,
}

impl AutoreleasePool {
    pub fn new() -> Self {
        unsafe extern "C" {
            fn objc_autoreleasePoolPush() -> *mut c_void;
        }
        Self { context: unsafe { objc_autoreleasePoolPush() } }
    }
}

impl Default for AutoreleasePool {
    fn default() -> Self {
        Self::new()
    }
}

impl Drop for AutoreleasePool {
    fn drop(&mut self) {
        unsafe extern "C" {
            fn objc_autoreleasePoolPop(context: *mut c_void);
        }
        unsafe { objc_autoreleasePoolPop(self.context) };
    }
}

/// The address of `objc_msgSend`, for building a typed view of it.
///
/// Every call site needs its own signature — see the module note on why a
/// variadic declaration will not do — so they transmute this rather than
/// calling `objc_msgSend` directly.
pub fn msg_send_addr() -> *const c_void {
    objc_msgSend as *const c_void
}

/// Register (or look up) a selector.
pub fn sel(name: &core::ffi::CStr) -> *const c_void {
    unsafe { sel_registerName(name.as_ptr().cast()) }
}

/// Look up an Objective-C class by its `OBJC_CLASS_$_Name` symbol.
pub fn class(symbol: &core::ffi::CStr) -> *mut c_void {
    let c = unsafe { dlsym(RTLD_DEFAULT, symbol.as_ptr()) };
    assert!(!c.is_null(), "Objective-C class not found: {symbol:?}");
    c
}

macro_rules! send {
    ($name:ident, ($($arg:ident: $ty:ty),*) -> $ret:ty) => {
        /// Send a message with a specific, non-variadic signature.
                /// # Safety
        /// `recv` must be a live Objective-C object that responds to `s`, and the
        /// declared argument and return types must match that selector's. Nothing
        /// here checks either.
pub unsafe fn $name(recv: *mut c_void, s: *const c_void $(, $arg: $ty)*) -> $ret {
            type F = unsafe extern "C" fn(*mut c_void, *const c_void $(, $ty)*) -> $ret;
            let f: F = unsafe { core::mem::transmute(objc_msgSend as *const c_void) };
            unsafe { f(recv, s $(, $arg)*) }
        }
    };
}

send!(send, () -> *mut c_void);
send!(send_id, (a: *mut c_void) -> *mut c_void);
send!(send_bool, (a: bool) -> *mut c_void);
send!(send_i64, (a: i64) -> *mut c_void);
send!(send_u64, (a: u64) -> *mut c_void);
send!(send_f64, (a: f64) -> *mut c_void);
send!(send_rect, (a: NSRect) -> *mut c_void);
send!(send_rect_id, (a: NSRect, b: *mut c_void) -> *mut c_void);
send!(send_u64_id, (a: u64, b: *mut c_void) -> *mut c_void);
send!(send_id_bool, (a: *mut c_void, b: bool) -> bool);
send!(send_ret_size, () -> NSSize);
send!(send_ret_rect, () -> NSRect);
send!(send_ret_u64, () -> u64);
send!(send_ret_f64, () -> f64);
send!(send_ret_ptr, () -> *const u8);
send!(send_bool_id, (a: bool, b: *mut c_void) -> *mut c_void);
send!(send_i64_id, (a: i64, b: *mut c_void) -> *mut c_void);
send!(send_f64_id, (a: f64, b: *mut c_void) -> *mut c_void);
send!(send_id_id, (a: *mut c_void, b: *mut c_void) -> *mut c_void);
send!(send_id_ret_bool, (a: *mut c_void) -> bool);
send!(send_id_ret_i64, (a: *mut c_void) -> i64);
send!(send_id_ret_f64, (a: *mut c_void) -> f64);
send!(send_ret_bool, () -> bool);

/// Does `receiver` implement `selector`?
///
/// Used to pick between an API and its newer replacement at runtime rather than
/// at build time.
///
/// # Safety
/// `receiver` must be a live object or class.
pub unsafe fn responds(receiver: *mut c_void, selector: *const c_void) -> bool {
    unsafe { send_id(receiver, sel(c"respondsToSelector:"), selector as *mut c_void) as usize & 1 == 1 }
}

/// Create an `NSString` from a Rust `&str`.
/// # Safety
/// Foundation must be loaded. The returned object is `+1`.
pub unsafe fn nsstring(s: &str) -> *mut c_void {
    let alloc = unsafe { send(class(c"OBJC_CLASS_$_NSString"), sel(c"alloc")) };
    type InitBytes =
        unsafe extern "C" fn(*mut c_void, *const c_void, *const u8, u64, u64) -> *mut c_void;
    let f: InitBytes = unsafe { core::mem::transmute(objc_msgSend as *const c_void) };
    // 4 = NSUTF8StringEncoding
    let out = unsafe {
        f(alloc, sel(c"initWithBytes:length:encoding:"), s.as_ptr(), s.len() as u64, 4)
    };
    assert!(!out.is_null(), "NSString creation failed for {s:?}");
    out
}

/// `[NSApplication sharedApplication]`, pinned to the light appearance so that
/// rendering does not depend on the host's system setting.
#[cfg(target_os = "macos")]
/// # Safety
/// AppKit must be loaded and this must run on the main thread, as every
/// `NSApplication` call must.
pub unsafe fn shared_app() -> *mut c_void {
    let app = unsafe { send(class(c"OBJC_CLASS_$_NSApplication"), sel(c"sharedApplication")) };
    unsafe {
        let name = nsstring("NSAppearanceNameAqua");
        let appearance = send_id(
            class(c"OBJC_CLASS_$_NSAppearance"),
            sel(c"appearanceNamed:"),
            name,
        );
        if !appearance.is_null() {
            send_id(app, sel(c"setAppearance:"), appearance);
        }
    }
    app
}

/// `[UIApplication sharedApplication]`.
///
/// Null unless the process actually went through `UIApplicationMain` — which a
/// plain binary run under `simctl spawn` has not. Rendering does not need it;
/// showing a window does.
///
/// # Safety
/// Calling into UIKit off the main thread is not supported.
#[cfg(not(target_os = "macos"))]
pub unsafe fn shared_app() -> *mut c_void {
    unsafe { send(class(c"OBJC_CLASS_$_UIApplication"), sel(c"sharedApplication")) }
}

// ═══════════════════════════════════════════════════════════════════════════
// Defining classes at runtime
// ═══════════════════════════════════════════════════════════════════════════
//
// The Objective-C counterpart of what [`crate::state`] does on the Swift side:
// build the type, give it its methods, register it. UIKit will not start an
// application without a delegate *class* to name, and writing one in Objective-C
// would mean shipping a source file — so it is synthesised here instead.

/// A class being built. Add methods, then [`register`](ClassBuilder::register).
pub struct ClassBuilder {
    class: *mut c_void,
}

impl ClassBuilder {
    /// Begin a new class, or return `None` if the name is already taken.
    ///
    /// # Safety
    /// `superclass` must name a registered class.
    pub unsafe fn new(superclass: &core::ffi::CStr, name: &core::ffi::CStr) -> Option<Self> {
        unsafe {
            let sup = objc_getClass(superclass.as_ptr().cast());
            assert!(!sup.is_null(), "superclass not found: {superclass:?}");
            let class = objc_allocateClassPair(sup, name.as_ptr(), 0);
            if class.is_null() {
                return None;
            }
            Some(Self { class })
        }
    }

    /// Add a method.
    ///
    /// `types` is an Objective-C type encoding: return type, then `@:` for the
    /// implicit receiver and selector, then one code per argument. `B@:@@` is
    /// "returns BOOL, takes two objects".
    ///
    /// # Safety
    /// `imp`'s signature must match `types`, receiver and selector included.
    pub unsafe fn method(
        self,
        selector: &core::ffi::CStr,
        imp: *const c_void,
        types: &core::ffi::CStr,
    ) -> Self {
        unsafe {
            let ok = class_addMethod(self.class, sel(selector), imp, types.as_ptr());
            assert!(ok, "could not add {selector:?}");
        }
        self
    }

    /// Declare conformance to a protocol.
    ///
    /// # Safety
    /// The class must actually implement the protocol's required methods.
    pub unsafe fn conforms(self, protocol: &core::ffi::CStr) -> Self {
        unsafe {
            let p = objc_getProtocol(protocol.as_ptr());
            if !p.is_null() {
                class_addProtocol(self.class, p);
            }
        }
        self
    }

    /// Register the class with the runtime and return it.
    pub fn register(self) -> *mut c_void {
        unsafe { objc_registerClassPair(self.class) };
        self.class
    }
}

/// Look up a registered class by name, or null.
pub fn class_named(name: &core::ffi::CStr) -> *mut c_void {
    unsafe { objc_getClass(name.as_ptr().cast()) }
}

/// An Objective-C block with no captures.
///
/// A block is an object whose first word is an `isa` like any other; what makes
/// it callable is the `invoke` pointer at offset 16, which is passed the block
/// itself as its first argument. With nothing captured there is nothing to copy
/// or release, so the block can be a plain `static` tagged `BLOCK_IS_GLOBAL` —
/// `_NSConcreteGlobalBlock` is exactly the isa for that.
#[repr(C)]
pub struct GlobalBlock {
    isa: *const c_void,
    flags: i32,
    reserved: i32,
    invoke: *const c_void,
    descriptor: *const BlockDescriptor,
}

/// The size-and-copy record every block points at.
#[repr(C)]
pub struct BlockDescriptor {
    reserved: u64,
    size: u64,
}

// `Sync` because nothing here is ever written after construction.
unsafe impl Sync for GlobalBlock {}
unsafe impl Sync for BlockDescriptor {}

/// `BLOCK_IS_GLOBAL`.
const BLOCK_IS_GLOBAL: i32 = 1 << 28;

impl GlobalBlock {
    /// Wrap `invoke` as a block. It is called with the block as its first
    /// argument, then whatever the block's signature declares.
    ///
    /// # Safety
    /// `invoke` must match the signature the receiver expects, including the
    /// leading block pointer.
    pub unsafe fn new(invoke: *const c_void, descriptor: &'static BlockDescriptor) -> Self {
        let isa = unsafe { dlsym(RTLD_DEFAULT, c"_NSConcreteGlobalBlock".as_ptr()) };
        assert!(!isa.is_null(), "_NSConcreteGlobalBlock");
        GlobalBlock { isa, flags: BLOCK_IS_GLOBAL, reserved: 0, invoke, descriptor }
    }
}

impl BlockDescriptor {
    /// The descriptor for a block with no captures.
    pub const fn empty() -> Self {
        BlockDescriptor { reserved: 0, size: core::mem::size_of::<GlobalBlock>() as u64 }
    }
}

/// A block carrying one pointer.
///
/// [`GlobalBlock`] captures nothing, which is all a block needs when the
/// receiver only ever calls it — but a *completion* handler has to reach
/// something, and the something differs per call. One captured word is enough
/// for that: it is where the answer gets written.
///
/// `_NSConcreteStackBlock` with no copy/dispose helpers, so a receiver that
/// copies it gets a `memcpy` and the capture comes along by value. That is only
/// sound while the caller outlives the call, which is why this is used with a
/// wait and not handed to anything that keeps it.
#[repr(C)]
pub struct CapturingBlock {
    isa: *const c_void,
    flags: i32,
    reserved: i32,
    invoke: *const c_void,
    descriptor: *const BlockDescriptor,
    /// What the invoke function reads, at a fixed offset from the block.
    pub capture: *mut c_void,
}

impl CapturingBlock {
    /// Wrap `invoke` as a block carrying `capture`.
    ///
    /// # Safety
    /// `invoke` must match the signature the receiver expects, taking the block
    /// itself first; and the block must outlive every call to it.
    pub unsafe fn new(invoke: *const c_void, capture: *mut c_void) -> Self {
        static DESCRIPTOR: BlockDescriptor = BlockDescriptor {
            reserved: 0,
            size: core::mem::size_of::<CapturingBlock>() as u64,
        };
        let isa = unsafe { dlsym(RTLD_DEFAULT, c"_NSConcreteStackBlock".as_ptr()) };
        assert!(!isa.is_null(), "_NSConcreteStackBlock");
        CapturingBlock {
            isa,
            flags: 0,
            reserved: 0,
            invoke,
            descriptor: &DESCRIPTOR,
            capture,
        }
    }
}