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}