Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 nom air-runtime est désormais le runtime async, couche 1). Bibliothèque C : libair-object.so, zone air-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 AirValue RETOURNÉ par une fonction Air est +1 (possédé) : le receveur doit air_value_release(v) (qui relâche l’objet contenu) ;
  • tout AirValue passé en ARGUMENT est +0 (emprunté) : le callee n’en devient pas propriétaire ; s’il le garde, il retain.

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èle AirString).
  • MainThreadOnly : thread principal uniquement (modèle AirView ; 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)

  1. Refcount = AtomicU32 (compact ; en-tête 24 o ; saturation/overflow réglés à la marge de l’impl, cohérent slab io_uring).
  2. 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).
  3. Refcount strict v1, GC cyclique DIFFÉRÉ (cycles évités par discipline, modèle ARC).
  4. 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).
  5. Exception 80 % proc-macro : syn/quote/proc-macro2 (pour air-object-macros) acceptés → à nommer dans docs/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és AirString, spec séparée). Macro air-object-macros = syn/quote/proc-macro2 (exception 80 % à acter — outillage de macro, comme tout l’écosystème proc-macro Rust). À tracer dans DEPENDENCIES.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)

  1. 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.
  2. Universalité : air_object_* suffit aux bindings — zéro glue par classe.
  3. Politique de thread déclarée obligatoire (ne compile pas sinon).
  4. Refcount atomique + AirHandle (Drop=release/Clone=retain).
  5. 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.