Skip to main content

swiftui_native/
observable.rs

1//! `@Observable` from Rust — a model SwiftUI re-renders on.
2//!
3//! The last thing on SwiftUI's surface this crate could not build, and the
4//! reason it looked impossible was wrong. The obvious blockers are not blockers:
5//! `Observation.Observable` is a **marker protocol** with zero requirements, and
6//! `ObservationRegistrar` and `withObservationTracking` are fully exported. What
7//! stood in the way was in the middle — an `@Observable` model is a *class*, and
8//! this crate mints structs.
9//!
10//! It does not have to be a *Swift* class. `Observable` requires `AnyObject`, and
11//! an `NSObject` subclass is one; [`crate::objc::ClassBuilder`] mints those. The
12//! only piece missing was a conformance record whose type reference is a class
13//! rather than a nominal descriptor — `IndirectObjCClass`, one of the four kinds
14//! the format already has, which [`crate::conformance::Conformer`] now emits.
15//!
16//! ```no_run
17//! # use swiftui_native::observable;
18//! let model = observable::model(0);
19//! let watched = model.clone();
20//! // Whatever the block reads is what the handler is told about.
21//! observable::track(move || { watched.get(); }, || println!("it changed"));
22//! model.set(1);
23//! ```
24//!
25//! # What an `@Observable` property actually is
26//!
27//! Three calls around ordinary storage, which the macro writes for you:
28//!
29//! ```text
30//! get { registrar.access(self, keyPath: \.value); return storage }
31//! set { registrar.willSet(self, keyPath: \.value)
32//!       storage = newValue
33//!       registrar.didSet(self, keyPath: \.value) }
34//! ```
35//!
36//! [`Property::get`] and [`Property::set`] are that, with the storage on the Rust
37//! side because a class minted through the Objective-C runtime has no ivars.
38//!
39//! # Several properties
40//!
41//! A model has as many named properties as it is asked for, each its own type:
42//!
43//! ```no_run
44//! # use swiftui_native::observable;
45//! let model = observable::model(0);
46//! let name = model.property("name", "Ada".to_string());
47//! let ratio = model.property("ratio", 0.5f64);
48//! ```
49//!
50//! What makes them independent is that the key path is an *identity* — what
51//! `access` records and `didSet` matches against — so each property needs one of
52//! its own, and a view that read only `name` is not rebuilt when `ratio` changes.
53//! Key paths are per *name*, not per model: `\.name` is one key path in Swift too,
54//! and the registrar keys its observations by `(subject, keyPath)`, so two models
55//! with a `name` property still change independently.
56
57use crate::abi;
58use crate::conformance::{synthesize_for, Conformance, Conformer, Protocol};
59use crate::keypath;
60use crate::objc;
61use crate::resolve;
62use crate::views::ViewHandle;
63use core::ffi::c_void;
64use std::sync::{Arc, Mutex, OnceLock};
65
66/// The minted class and its `Observable` conformance.
67struct Kind {
68    class: usize,
69    metadata: usize,
70    observable_wt: usize,
71}
72
73// The three are immortal runtime objects; what makes this shareable is that
74// nothing here is ever written after `OnceLock` publishes it.
75unsafe impl Send for Kind {}
76unsafe impl Sync for Kind {}
77
78fn kind() -> &'static Kind {
79    static K: OnceLock<Kind> = OnceLock::new();
80    K.get_or_init(|| unsafe {
81        let class = objc::ClassBuilder::new(c"NSObject", c"RustObservable")
82            .expect("the class name `RustObservable` is already taken")
83            .register();
84        let metadata = swift_runtime_sys::MetadataIntrospection::swift_getObjCClassMetadata(class);
85        assert!(!metadata.is_null(), "RustObservable metadata");
86
87        // `Observable` has no requirements at all, so the witness table is the
88        // conformance descriptor and nothing else. Registering it is the whole
89        // point: SwiftUI and the registrar both find it by lookup.
90        let protocol = Protocol::from_symbol(c"$s11Observation10ObservableMp");
91        assert_eq!(
92            protocol.requirement_count(),
93            0,
94            "Observable used to be a marker protocol with nothing to implement",
95        );
96        let conformance: &'static Conformance = Box::leak(Box::new(synthesize_for(
97            &protocol,
98            Conformer::ObjCClass(class),
99            &[],
100        )));
101
102        Kind {
103            class: class as usize,
104            metadata: metadata as usize,
105            observable_wt: conformance.witness_table() as usize,
106        }
107    })
108}
109
110/// One property *name*'s key path, and what reading through it produces.
111struct Named {
112    name: String,
113    /// Which `T` the name was first declared with. A second model declaring the
114    /// same name with a different type would silently share a key path whose
115    /// value type is the first one's, so it is refused instead.
116    value_type: core::any::TypeId,
117    key_path: u64,
118}
119
120fn names() -> &'static Mutex<Vec<Named>> {
121    static N: OnceLock<Mutex<Vec<Named>>> = OnceLock::new();
122    N.get_or_init(|| Mutex::new(Vec::new()))
123}
124
125/// Writes a `+1` copy of a property's declared value into a buffer.
126type WriteInitial = Box<dyn Fn(*mut u8) + Send + Sync>;
127
128/// What a read through slot `i`'s key path writes: a `+1` copy of the value the
129/// property was declared with.
130///
131/// Indexed by key path slot, so [`identity_get`] — which knows only its slot —
132/// can produce a valid value of whatever type the property holds.
133fn initials() -> &'static Mutex<Vec<Option<WriteInitial>>> {
134    static I: OnceLock<Mutex<Vec<Option<WriteInitial>>>> = OnceLock::new();
135    I.get_or_init(|| Mutex::new((0..keypath::VALUE_SLOTS).map(|_| None).collect()))
136}
137
138/// The key path for a property called `name` holding a `T`, made once per name.
139///
140/// The root is named `So14RustObservableC`: an imported Objective-C class, which
141/// the runtime resolves by asking the Objective-C runtime for the class by name,
142/// and which therefore reaches a class minted at run time.
143/// A mismatched type panics *after* the registry is unlocked: a panic while
144/// holding it poisons a `static` mutex, which turns one caller's mistake into
145/// every later model's failure — and in a test binary, into every other test's.
146fn key_path_for<T: crate::views::KeyValue>(name: &str, initial: &T) -> u64 {
147    let mut names = names().lock().expect("observable property names");
148    if let Some(n) = names.iter().find(|n| n.name == name) {
149        let (key_path, declared) = (n.key_path, n.value_type);
150        drop(names);
151        assert_eq!(
152            declared,
153            core::any::TypeId::of::<T>(),
154            "the observable property `{name}` was already declared with a different \
155             type; one name is one key path, so one name is one type",
156        );
157        return key_path;
158    }
159
160    let slot = keypath::next_value_slot();
161    let initial = initial.clone();
162    initials().lock().expect("observable property initials")[slot] =
163        Some(Box::new(move |out| unsafe { initial.write(out) }));
164    unsafe {
165        let (get, set, get_target, set_target) = keypath::value_thunks(slot);
166        *get_target = identity_get as *const c_void;
167        *set_target = identity_set as *const c_void;
168        let key_path = keypath::build_writable_computed(
169            keypath::value_slot(slot),
170            b"So14RustObservableC",
171            T::mangled().as_bytes(),
172            get,
173            set,
174        );
175        swift_runtime_sys::RuntimeRaw::swift_retain(key_path as *mut c_void);
176        names.push(Named {
177            name: name.to_string(),
178            value_type: core::any::TypeId::of::<T>(),
179            key_path,
180        });
181        key_path
182    }
183}
184
185/// The key path's getter, which nothing in this crate reads through.
186///
187/// A key path has to have one, and this one exists to be *compared*: `access`,
188/// `willSet` and `didSet` use the key path as a dictionary key and never call it.
189/// So what it writes is the value the property was declared with — which is a
190/// valid `T`, and the only value available to a function that knows the property
191/// but not which object the read is for. Live values come from [`Property::get`],
192/// whose storage is per object.
193///
194/// Writing a zeroed word regardless of type is what this did while a model held
195/// one `Int`, and it stopped being safe the moment a property could hold a
196/// `String`: all-zero is not a valid `String`, and destroying one is a crash.
197///
198/// # Safety
199/// Reached through [`keypath::value_thunks`].
200unsafe extern "C" fn identity_get(out: *mut u8, _root: *const c_void, slot: u64) {
201    let initials = initials().lock().expect("observable property initials");
202    if let Some(write) = initials[slot as usize].as_ref() {
203        write(out);
204    }
205}
206
207/// The key path's setter, which nothing reads through either — a write here would
208/// have to find the object's storage, and the object is not what a computed
209/// component's root reliably is.
210///
211/// # Safety
212/// As [`identity_get`].
213unsafe extern "C" fn identity_set(_value: *const u8, _root: *mut c_void, _slot: u64) {}
214
215/// One property's storage, with its type forgotten so one map can hold them all.
216type ErasedStorage = Arc<dyn core::any::Any + Send + Sync>;
217
218/// An observable model: a class instance, a registrar, and its named properties.
219///
220/// Cloning shares all three, as a class reference does in Swift — two handles to
221/// one model, not two models.
222#[derive(Clone)]
223pub struct Model {
224    object: usize,
225    /// `ObservationRegistrar`, one per model, held by address because it is
226    /// resilient.
227    registrar: Arc<Mutex<Vec<u8>>>,
228    /// The properties declared on this model, by name. Type-erased so one map
229    /// can hold storage of every type; [`Model::property`] casts it back and
230    /// refuses a name reused with a different type.
231    properties: Arc<Mutex<Vec<(String, ErasedStorage)>>>,
232}
233
234// The object is immortal for the model's lifetime and the rest is behind locks.
235unsafe impl Send for Model {}
236unsafe impl Sync for Model {}
237
238/// A new observable model with no properties yet.
239///
240/// What [`crate::observable!`] builds on: the class instance and the registrar,
241/// with [`Model::property`] called once per declared property. [`model`] is this
242/// plus one `Int` property, which is the short form for a model that holds one
243/// number.
244pub fn object() -> Model {
245    model_of(None)
246}
247
248/// A new observable model, with one `Int` property holding `initial`.
249///
250/// That property is what [`Model::get`] and [`Model::set`] mean; anything else
251/// the model should hold is [`Model::property`], named and of whatever type — or
252/// [`crate::observable!`], which declares them all at once.
253pub fn model(initial: i64) -> Model {
254    model_of(Some(initial))
255}
256
257/// The two constructors: a bare object, or one with the default `Int` property.
258fn model_of(initial: Option<i64>) -> Model {
259    let kind = kind();
260    unsafe {
261        let object = objc::send(
262            objc::send(kind.class as *mut c_void, objc::sel(c"alloc")),
263            objc::sel(c"init"),
264        );
265        assert!(!object.is_null(), "RustObservable init");
266
267        let meta = abi::metadata0(resolve::observation_registrar_ma());
268        assert!(!meta.is_null(), "ObservationRegistrar metadata");
269        let mut registrar = vec![0u8; abi::type_bytes(meta)];
270        core::arch::asm!("blr {f}", f = in(reg) resolve::observation_registrar_init(),
271            in("x8") registrar.as_mut_ptr(),
272            lateout("x0") _, lateout("x1") _, lateout("x2") _, lateout("x3") _,
273            lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
274            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
275            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
276            lateout("x17") _, lateout("lr") _, lateout("x20") _, lateout("x21") _,
277            clobber_abi("C"),
278        );
279
280        let model = Model {
281            object: object as usize,
282            registrar: Arc::new(Mutex::new(registrar)),
283            properties: Arc::new(Mutex::new(Vec::new())),
284        };
285        // `Model::get`/`set`/`peek` are this property, so when it is asked for it
286        // exists from the start and `model(0)` reads as it always did. A model
287        // built by `observable!` has its own names and never wants it, and
288        // declaring it anyway would spend a key path slot on nothing.
289        if let Some(initial) = initial {
290            let _ = model.property(DEFAULT_PROPERTY, initial);
291        }
292        model
293    }
294}
295
296/// The property [`Model::get`] and [`Model::set`] mean.
297///
298/// A model made by [`model`] has one `Int` property, and the two-line form that
299/// reads and writes it stays the shortest thing that works. Naming it here rather
300/// than in both methods is what keeps them from drifting apart.
301const DEFAULT_PROPERTY: &str = "value";
302
303impl Model {
304    /// The object itself, for handing to SwiftUI.
305    ///
306    /// What `.environment(_ object:)` and `focusedSceneValue(_ object:)` take:
307    /// an `AnyObject` that conforms to `Observable`, which this is.
308    pub fn as_object(&self) -> *mut c_void {
309        self.object as *mut c_void
310    }
311
312    /// The model's Swift metadata and its `Observable` conformance.
313    pub fn conformance(&self) -> (*const c_void, *const c_void) {
314        let k = kind();
315        (k.metadata as *const c_void, k.observable_wt as *const c_void)
316    }
317
318    /// A named property of this model, declared with `initial` if it is new.
319    ///
320    /// Calling this twice with the same name returns two handles to *one*
321    /// property — the second `initial` is ignored, as a stored property's
322    /// initialiser is after the object exists. That is what lets a view and the
323    /// code that updates it each ask for the property they mean without either
324    /// having to own it.
325    ///
326    /// # Panics
327    /// If `name` was declared with a different type, here or on another model: a
328    /// property name is one key path, and a key path has one value type.
329    pub fn property<T: crate::views::KeyValue>(
330        &self,
331        name: &str,
332        initial: impl crate::views::IntoKeyValue<T>,
333    ) -> Property<T> {
334        let initial = initial.into_key_value();
335        let key_path = key_path_for(name, &initial);
336        let mut properties = self.properties.lock().expect("model properties");
337        let existing = properties.iter().find(|(n, _)| n == name).map(|(_, s)| s.clone());
338        let storage = match existing {
339            Some(erased) => {
340                // Unlocked before the cast, for the reason `key_path_for` is.
341                drop(properties);
342                erased.downcast::<Mutex<T>>().unwrap_or_else(|_| {
343                    panic!("the property `{name}` on this model does not hold this type")
344                })
345            }
346            None => {
347                let storage = Arc::new(Mutex::new(initial));
348                properties.push((name.to_string(), storage.clone()));
349                storage
350            }
351        };
352        Property {
353            object: self.object,
354            registrar: self.registrar.clone(),
355            key_path,
356            value: storage,
357        }
358    }
359
360    /// The default `Int` property — see [`Property::get`].
361    pub fn get(&self) -> i64 {
362        self.property(DEFAULT_PROPERTY, 0).get()
363    }
364
365    /// The default `Int` property — see [`Property::set`].
366    pub fn set(&self, value: i64) {
367        self.property(DEFAULT_PROPERTY, 0).set(value);
368    }
369
370    /// The default `Int` property — see [`Property::peek`].
371    pub fn peek(&self) -> i64 {
372        self.property(DEFAULT_PROPERTY, 0).peek()
373    }
374}
375
376/// One named property of a [`Model`]: storage, and the key path that names it.
377///
378/// Cloning shares the storage, so two handles to `"count"` are two views of one
379/// property.
380#[derive(Clone)]
381pub struct Property<T> {
382    object: usize,
383    registrar: Arc<Mutex<Vec<u8>>>,
384    key_path: u64,
385    value: Arc<Mutex<T>>,
386}
387
388impl<T: Clone> Property<T> {
389    /// Read the value, telling the registrar that it was read.
390    ///
391    /// The `access` half of what an `@Observable` property's getter does. Inside
392    /// a [`track`] block — or inside SwiftUI's own tracking, which is how a view
393    /// body works — this is what subscribes the reader to the next change *of this
394    /// property*: a body that read only this one is not re-run when another
395    /// changes, because the registrar recorded this key path and no other.
396    pub fn get(&self) -> T {
397        unsafe { self.registrar_call(resolve::registrar_access()) };
398        self.value.lock().expect("property value").clone()
399    }
400
401    /// Write the value, telling the registrar before and after.
402    ///
403    /// The `willSet` / `didSet` half. `didSet` is what fires every handler that
404    /// read this property since it last changed.
405    pub fn set(&self, value: T) {
406        unsafe {
407            self.registrar_call(resolve::registrar_will_set());
408            *self.value.lock().expect("property value") = value;
409            self.registrar_call(resolve::registrar_did_set());
410        }
411    }
412
413    /// Read without telling anyone, for assertions that must not subscribe.
414    pub fn peek(&self) -> T {
415        self.value.lock().expect("property value").clone()
416    }
417
418    /// One of `access`, `willSet` and `didSet`, which take the same arguments.
419    ///
420    /// # Safety
421    /// `f` must be one of those three.
422    unsafe fn registrar_call(&self, f: *const c_void) {
423        let k = kind();
424        // `Subject` is `@in`, so what is passed is a pointer *to* the class
425        // reference rather than the reference.
426        let mut subject = self.object;
427        let mut registrar = self.registrar.lock().expect("registrar");
428        unsafe {
429            core::arch::asm!("blr {f}", f = in(reg) f,
430                in("x0") &mut subject as *mut usize,
431                in("x1") self.key_path,
432                in("x2") k.observable_wt,
433                in("x20") registrar.as_mut_ptr(),
434                lateout("x3") _, lateout("x4") _, lateout("x5") _, lateout("x6") _,
435                lateout("x7") _, lateout("x8") _,
436                lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
437                lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
438                lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
439            );
440        }
441    }
442}
443
444// ── withObservationTracking ──
445
446/// What the `apply` closure runs.
447struct Reading<'a> {
448    read: &'a mut dyn FnMut(),
449}
450
451/// The `() -> T` block, with `T == ()` so there is nothing to write out.
452///
453/// # Safety
454/// Reached through [`abi::swift_closure_thunk`]; `payload` must be a [`Reading`].
455unsafe extern "C" fn run_reading(_out: *mut u8, payload: *mut c_void) {
456    let r = unsafe { &mut *(payload as *mut Reading<'_>) };
457    (r.read)();
458}
459
460// `onChange` is an *autoclosure*: what Swift passes is a closure returning the
461// handler, so the thunk hands back two words — the handler's function and its
462// context. A thick function is returned in x0/x1, and the closure's own context
463// arrives in x20, so the one shuffle is x20 into the first argument register.
464core::arch::global_asm!(
465    ".p2align 2",
466    ".globl _rswift_obs_onchange_thunk",
467    "_rswift_obs_onchange_thunk:",
468    "mov x0, x20",
469    "b _rswift_obs_onchange",
470);
471
472unsafe extern "C" {
473    #[link_name = "rswift_obs_onchange_thunk"]
474    fn onchange_thunk();
475}
476
477/// A thick Swift function: its code and its context.
478#[repr(C)]
479struct Thick {
480    function: u64,
481    context: u64,
482}
483
484/// The handler the autoclosure returns, kept where the thunk can find it.
485#[repr(C)]
486struct OnChange {
487    isa: *const c_void,
488    refcount: u64,
489    handler: Thick,
490}
491
492/// Branch target of the autoclosure thunk: hand back the handler.
493///
494/// # Safety
495/// `ctx` must be an [`OnChange`] this module leaked.
496#[unsafe(no_mangle)]
497unsafe extern "C" fn rswift_obs_onchange(ctx: *mut c_void) -> Thick {
498    let c = unsafe { &*(ctx as *const OnChange) };
499    Thick { function: c.handler.function, context: c.handler.context }
500}
501
502/// Run `read`, remembering every observable property it touches, and call
503/// `on_change` once when any of them next changes.
504///
505/// `withObservationTracking`, which is what SwiftUI itself uses to decide that a
506/// view needs rebuilding. **Once**: the handler fires for the first change and
507/// then the subscription is over, which is Swift's behaviour too — a view
508/// re-reads on the next render and so re-subscribes.
509pub fn track(read: impl FnMut(), on_change: impl Fn() + Send + Sync + 'static) {
510    let mut read = read;
511    let mut reading = Reading { read: &mut read };
512    unsafe {
513        let mut ctx = abi::ClosureCtx::new(
514            run_reading,
515            &mut reading as *mut Reading<'_> as *mut c_void,
516        );
517        let handler = crate::views::action_words(on_change);
518        let onchange = Box::leak(Box::new(OnChange {
519            isa: abi::immortal_isa(),
520            refcount: abi::IMMORTAL_REFCOUNT,
521            handler: Thick { function: handler[0], context: handler[1] },
522        })) as *mut OnChange;
523
524        // `T` is `()`: the result slot is never written, but the runtime is
525        // given somewhere real to point at rather than a null.
526        let unit = abi::resolve_type_by_mangled_name(b"yt");
527        assert!(!unit.is_null(), "() metadata");
528        let mut out = [0u8; 8];
529        core::arch::asm!("blr {f}", f = in(reg) resolve::with_observation_tracking(),
530            in("x0") abi::swift_closure_thunk as *const c_void,
531            in("x1") &mut ctx as *mut abi::ClosureCtx,
532            in("x2") onchange_thunk as *const c_void,
533            in("x3") onchange,
534            in("x4") unit,
535            in("x8") out.as_mut_ptr(),
536            lateout("x5") _, lateout("x6") _, lateout("x7") _,
537            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
538            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
539            lateout("x17") _, lateout("lr") _, lateout("x20") _, lateout("x21") _,
540            clobber_abi("C"),
541        );
542    }
543}
544
545// ── Handing the model to SwiftUI ──
546//
547// The two modifiers that take an `Observable` *object* rather than a key path.
548// Both were unreachable until there was an object to pass: the crate had the
549// symbols and a synthesised `Observable` conformance, but the conformance was
550// for a borrowed `Foundation.JSONDecoder` — a stand-in that proved the binding
551// worked and was of no use to an app.
552
553/// `.environment(model)` — put the model where every view below can read it.
554///
555/// The `Observable` overload. A body that reads a property of the model through
556/// [`Model::get`] while SwiftUI is tracking it is re-run when that property
557/// changes; one that reads a *different* property is not.
558pub fn environment_model(view: &ViewHandle, model: &Model) -> ViewHandle {
559    unsafe { publish(view, model, resolve::environment_observable_fn(), resolve::environment_observable_oret()) }
560}
561
562/// `.focusedValue(model)` — publish the model for the commands above it.
563///
564/// The object form of what [`crate::views::focused_scene_value`] does with a key
565/// path, and the one Apple now recommends: a key is a type, and an `@Observable`
566/// object already is one.
567pub fn focused_value_model(view: &ViewHandle, model: &Model) -> ViewHandle {
568    unsafe { publish(view, model, resolve::focused_value_fn(), resolve::focused_value_oret()) }
569}
570
571/// `.environment(_:)` and `.focusedValue(_:)` take the same arguments.
572///
573/// `T?` where `T` is a class is one word and `nil` is a null one, so the object
574/// goes in a register rather than by address.
575///
576/// # Safety
577/// `f`/`oret` must be a matching `Observable`-object modifier and descriptor.
578unsafe fn publish(
579    view: &ViewHandle,
580    model: &Model,
581    f: *const c_void,
582    oret: *const c_void,
583) -> ViewHandle {
584    let (meta, wt) = model.conformance();
585    unsafe {
586        crate::views::generic1_modifier_ints(
587            f,
588            oret,
589            view,
590            meta,
591            wt,
592            &[model.as_object() as u64],
593            [0.0; 4],
594        )
595    }
596}