Spec couche 2 — air-object (modèle d’objet C-ABI)
Spécification technique — Version 1.0 (décisions validées BDFL 2026-06-25). Couche 2 « Modèle d’objet, IPC, services fondamentaux ».
Position et méthode
air-object est la clé de voûte de la couche 2 : le modèle d’objet C-ABI d’Air
(ADR-002). Il donne à toute classe « de surface »
une identité runtime, des propriétés observables, l’introspection, et la
bindabilité polyglotte sans glue par classe. C’est ce dont air-event,
air-aircom, les collections, air-string, et les couches 3-5 dépendent.
Nommage (ADR-039) : le modèle d’objet s’appelle
air-object(le nomair-runtimeest désormais le runtime async, couche 1). Bibliothèque C :libair-object.so, zoneair-stable(ABI 10 ans — ADR-012).
Frontière explicite (ADR-002, Principe 7) : un type qui dérive #[air_class] rejoint
le monde C-ABI (refcount atomique, classe = vtable + métadonnées, propriétés observables,
introspection). Tout le reste — algorithmes, parseurs, structures privées — reste
Rust pur (zéro surcoût). Pont entre les deux mondes par conversion explicite.
Crates (ADR-002 / ADR-039)
air-object/ — runtime d'objets : AirObject, AirClass, API C-ABI air_object_*
air-object-macros/ — macro procédurale #[air_class] (génère layout C, métadonnées, exports C)
air-value/ — AirValue (union typée) + AirStatus
Section 1 — AirObject et AirClass
AirObject : structure C opaque, en-tête fixe + payload variable.
AirObject (en-tête 24 octets sur 64-bit)
├── class : *const AirClass — pointeur vers la classe
├── refcount : AtomicU32 (ou U64) — comptage de référence atomique
└── flags : u32 — frozen, observed, traits accessibilité…
payload (taille variable selon la classe) — dont les propriétés observables
AirClass est elle-même un AirObject (méta-classe). Elle porte :
- la vtable (pointeurs vers les méthodes) ;
- les métadonnées d’introspection : nom, parente, liste de propriétés/méthodes ;
- les traits accessibilité : rôle par défaut, attributs universels (ADR-017).
Section 2 — API C-ABI universelle
AirObject* air_object_alloc(const AirClass*);
void air_object_retain(AirObject*);
void air_object_release(AirObject*);
const AirClass* air_object_class(const AirObject*);
int air_object_is_kind_of(const AirObject*, const AirClass*);
AirValue air_object_get_property(const AirObject*, const char* name);
int air_object_set_property(AirObject*, const char* name, AirValue);
typedef void (*AirObserverFn)(AirObject* obj, const char* prop, AirValue old, AirValue new_, void* ctx);
AirObservation* air_object_observe(AirObject*, const char* prop, AirObserverFn, void* ctx);
void air_object_unobserve(AirObservation*);
AirValue air_object_send_message(AirObject*, const char* selector, const AirValue* args, size_t n);
C’est l’universalité : un binding Python/Ruby appelle uniquement ces fonctions — aucun code spécifique par classe (le modèle qui fait marcher PyObjC sans glue, ADR-002).
Section 3 — AirValue et AirStatus
// Union typée (≈ NSObject/id macOS ou GValue GObject).
typedef enum { AIR_NULL, AIR_INT, AIR_FLOAT, AIR_BOOL, AIR_STRING, AIR_BYTES, AIR_OBJECT } AirValueKind;
typedef struct { AirValueKind kind; union { int64_t i; double f; bool b; /* … */ AirObject* obj; }; } AirValue;
Règle de propriété UNIFORME (validée, ≠ Create/Get d’Apple) : quand un AirValue
contient un AirObject* —
- tout
AirValueRETOURNÉ par une fonction Air est +1 (possédé) : le receveur doitair_value_release(v)(qui relâche l’objet contenu) ; - tout
AirValuepassé en ARGUMENT est +0 (emprunté) : le callee n’en devient pas propriétaire ; s’il le garde, ilretain.
Soit « retours possédés / arguments empruntés » — une seule règle, sans la mémoire
par-nom de CoreFoundation. Côté Rust, AirValue/AirHandle l’automatisent (Drop =
release) ; seul un consommateur C brut la gère à la main. AirStatus (code + message,
cohérent ADR-019/AirError) porte les erreurs à travers l’ABI (jamais de panic :
catch_unwind à la frontière).
Section 4 — Définition des classes en Rust : #[air_class]
#![allow(unused)]
fn main() {
#[air_class(thread = MainThreadOnly)] // politique de thread OBLIGATOIRE (cf. §6)
pub struct AirButton {
#[observable] pub label: AirString,
#[observable] pub enabled: bool,
#[accessibility(role = "button")] _a11y: AccessibilityMarker,
on_click: Option<AirCallback<()>>,
}
}
La macro génère : layout C opaque (ABI stable), AirClass enregistrée au runtime,
implémentations standard (retain/release/get/set property…), fonctions C exportées,
annotations accessibilité consommées par air.wm.accessibility (ADR-017).
AirHandle<T> : smart pointer Rust à sémantique Arc interopérant avec le refcount
AirObject : Drop = release, Clone = retain. Le Rust de couche 2+ manipule des
AirHandle<T> typés ; le C voit des AirObject*.
Section 5 — Propriétés observables
Fondement du data binding, des frameworks réactifs (air-ui), de l’accessibilité
(lecteur d’écran), du débogage (inspecteur). #[observable] rend un champ observable :
get/set/observe. Le set notifie les observateurs (ancienne/nouvelle valeur).
AirObservable<T> (wrapper) et le AirNotificationCenter intra-processus
(≠ AirCom inter-processus) sont fournis.
Section 6 — Mémoire et threads (politique OBLIGATOIRE)
Refcount atomique ⇒ un objet peut être détenu par plusieurs threads, mais son contenu n’est pas thread-safe par défaut. Toute classe déclare sa politique (sinon ne compile pas — sécurité par construction, ADR-002) :
Immutable: créée une fois, jamais modifiée (thread-safe trivial — modèleAirString).MainThreadOnly: thread principal uniquement (modèleAirView; vérifié en debug, coût release à mesurer).ThreadSafe: accès concurrent autorisé (l’implémentation protège ses invariants).
Section 7 — Introspection et bindings
const AirClass* air_class_list_all(size_t* n);
AirPropertyInfo air_class_properties(const AirClass*, size_t* n);
const AirClass* air_class_parent(const AirClass*);
char* air_object_describe(const AirObject*); // debug
Activée en production → permet à air-screenreader (ADR-017) de fonctionner sur
n’importe quelle app sans plugin. Bindings : Pattern A générique (Python/Ruby :
uniquement air_object_*, zéro code par classe) ; Pattern B typé (Swift via
@dynamicMemberLookup, généré depuis les métadonnées).
ABI C et stabilité
libair-object.so, symboles air_object_*/air_class_*/air_value_*, zone
air-stable (versioned symbols GNU, ABI garantie 10 ans — ADR-012). Tests de
conformité ABI en CI (air-abi-check).
Décisions ratifiées (BDFL 2026-06-25)
- Refcount =
AtomicU32(compact ; en-tête 24 o ; saturation/overflow réglés à la marge de l’impl, cohérent slab io_uring). MainThreadOnly: enforcement debug-only (assert en debug, zéro coût en release — comme le « Main Thread Checker » d’UIKit ; remesuré si besoin, Principe 5).- Refcount strict v1, GC cyclique DIFFÉRÉ (cycles évités par discipline, modèle ARC).
AirValue: union taguée à layout fixe + règle de propriété uniforme « +1 retours / +0 arguments » (§3) ; représentation figée avant l’impl (ABI 10 ans).- Exception 80 % proc-macro :
syn/quote/proc-macro2(pourair-object-macros) acceptés → à nommer dansdocs/EXCEPTIONS.md(build-time only, non livré dans l’artefact, sans alternative crédible, universel/audité). Fixe la politique proc-macro d’Air (vaut pour toute future crate*-macros).
Détail laissé à l’implémentation (non bloquant) : format exact de #[air_class].
Périmètre v1 / différé
- v1 :
AirObject/AirClass, API C-ABI universelle,AirValue,#[air_class]+AirHandle, propriétés observables, politiques de thread, introspection, ABI stable. - Différé : GC cyclique, bindings polyglottes générés (phase bindings :
cbindgen/uniffià évaluer), collections observables (air-collections, spec séparée).
Dépendances
air-base-lib(erreurs/temps),air-string(consommée par les propriétésAirString, spec séparée). Macroair-object-macros=syn/quote/proc-macro2(exception 80 % à acter — outillage de macro, comme tout l’écosystème proc-macro Rust). À tracer dansDEPENDENCIES.md+EXCEPTIONS.md.
Stratégie de tests
- Couverture très élevée (couche 2) ; tests de conformité ABI C (compilation C +
appels à
libair-object.so) ; tests depuis C/Swift/Python (universalité sans glue) ; multi-thread (refcount atomique, races, politiques de thread) ; observation (notification ancienne/nouvelle valeur, ré-entrance) ; introspection (lister classes/propriétés). Property-based sur le refcount (retain/release équilibrés).
Décisions de fond (ADR-002)
- Modèle d’objet C-ABI hybride asymétrique : C-ABI pour la surface observable/bindable (collections, strings, vues, services) ; Rust pur pour le reste.
- Universalité :
air_object_*suffit aux bindings — zéro glue par classe. - Politique de thread déclarée obligatoire (ne compile pas sinon).
- Refcount atomique +
AirHandle(Drop=release/Clone=retain). - Introspection en production (accessibilité/outils sur toute app).
Licence du document : MPL 2.0
Statut : v1.0 — décisions validées (2026-06-25). Fondation couche 2 (consommée par
air-event/air-aircom/collections). Implémentation à suivre ; l’entrée EXCEPTIONS.md
(proc-macro) sera créée au câblage d’air-object-macros. Bases : ADR-002 / ADR-012 /
ADR-017 / ADR-039 + macro-architecture §4 Partie A.