ADR-045 — Modèle d’erreurs de l’ABI C : AirStatus in-band, sans errno, panic = abort
Statut : Accepté (2026-06-26, validé par le BDFL). Companion d’ADR-019
(modèle d’erreurs hybride), d’ADR-027 (doc ABI C) et
d’ADR-012 (stabilité ABI 10 ans). Réalise la doctrine
libc (vision « libc Rust Air » + « kernel = bible, pas de globale libc ») au niveau de
la frontière C. Cadrage détaillé : docs/draft/abi-c-t1-design-fr.md.
Catégorie : Architecture (ABI C — contrat, transverse à toute la surface C-ABI).
Contexte
La première passe ABI C (crate air-base-capi → libair-base.so, ADR-027
addendum C) doit décider comment les erreurs traversent la frontière C — décision
contractuelle (ABI 10 ans) qui fera précédent pour toute la future libc Air.
POSIX/glibc utilisent errno : un canal hors-bande, thread-local aujourd’hui
mais implicite (posé en effet de bord, absent de la signature, oublié, écrasé par un
appel intermédiaire, non composable). La doctrine Air le refuse : pas de globale
inventée par la libc, erreurs in-band, fidèles au kernel (qui rend -errno
dans le registre de retour — c’est la libc qui ajoute la couche errno par-dessus).
Par ailleurs, aucune panic Rust ne doit traverser l’ABI (ADR-027 §B.2) ; et Air
ne provoque aucune panic (Principes 2/3) — une panic = un bug.
Décision
-
AirStatusrendu in-band : enum#[repr(C)]retourné comme valeur de retour de chaquepub extern "C",AIR_STATUS_OK = 0. Sur-ensemble d’AirErrorKind(miroir 1..=13) + codes propres à la frontière (AIR_STATUS_NULL_ARGUMENT = 100,AIR_STATUS_BUFFER_TOO_SMALL = 101). Discriminants explicites et committés (stabilité ABI, ADR-012). -
Aucun
errno, aucune globale, aucun canal hors-bande. Les*out-params ne sont écrits que surAIR_STATUS_OK. Validation amont des pointeurs : tout pointeur requisNULL→AIR_STATUS_NULL_ARGUMENT, jamais de déréférencement (Principe 4). -
Message humain par chaîne statique :
air_status_message(AirStatus) -> *const c_charrend une chaîneconststatique (+0, jamais libérée), façonstrerrormais sans état global ni locale. Le message dynamique d’AirError(contexte riche) n’est pas transporté en T1 (différé ; à réinstruire si un besoin réel apparaît). -
panic = "abort"(fail-fast) : aucune panic n’est voulue (panic = bug Air) → pas decatch_unwind, pas de statutAIR_PANIC. Un bug tue le process au point de faute, bruyamment, sans état douteux qui continue. Mémoire-sûr : même sousunwind, une panic atteignant une frontièreextern "C"abort (Rust ≥ 1.81), jamais d’UB. La cdylib release est buildéepanic = "abort"; Cargo build les cibles de test enunwindautomatiquement (libtest /should_panic/ proptest intacts).
Conséquences
- + Fidèle au kernel (in-band), zéro globale libc, erreurs visibles dans la signature → transparence (« dit ce qu’elle fait »). Convention d’erreur unique pour toute la future libc.
- + ABI simple en T1 : aucun objet erreur à allouer/libérer (ownership trivial).
- +
panic = abortsimplifie l’ABI (pas d’AIR_PANIC) et reste honnête (un bug est un bug). - − Du code C attendant
errnone porte pas tel quel → adaptation requise (ou shimerrnoopt-in ultérieur — différé, réintroduirait le smell). - − Le message dynamique (contexte riche d’
AirError) est reporté.
Alternatives écartées
errno-style (globale / TLS) : rejeté — canal hors-bande implicite qui masque l’erreur ; viole « pas de globale libc » et « dit ce qu’elle fait ». (errnoest déjà thread-local dans toute libc à threads : le vrai défaut n’est pas la course mais le caractère hors-bande.)- Objet
AirErroropaque (air_error_message/air_error_free) dès T1 : rejeté pour T1 — introduit propriété mémoire + allocation + fonction de libération prématurément. Réinstruire si le message dynamique devient nécessaire. catch_unwind→AIR_PANIC: rejeté — convertit un bug Air en statut « récupérable » que l’appelant peut ignorer, sur un état possiblement douteux ;abortest plus honnête et simplifie l’ABI.