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

ADR-158 — Versionnement des symboles des bibliothèques Air : instruction et décision

Statut : Accepté (2026-08-14, BDFL) — voie A, mise en œuvre DIFFÉRÉE sous condition nommée. Instruit l’outil air-symver annoncé par ADR-012 et par macro-architecture-fr.md, et jamais écrit.

Catégorie : Stabilité contractuelle. Décide comment Air tient sa promesse d’ABI sur dix ans au niveau des symboles, et non plus seulement des déclarations.

Contexte

ADR-012 promet une ABI garantie dix ans et nomme trois outils mainteneurs — air-abi-check, air-symver, air-deprecation-tracker — « à intégrer dès la phase 0 ». Aucun des trois n’a jamais été écrit ; le constat a été fait le 2026-08-14, à l’occasion de la règle NO GO inscrite en tête de CLAUDE.md.

Le premier a reçu un début de réponse le même jour : cargo xtask check-abi fige la surface des déclarations des quatre headers C committés (392 au total) et refuse qu’une déclaration disparaisse ou change de signature. C’est réel, et c’est borné : le gate lit des headers, pas des bibliothèques.

Ce que la mesure montre, et que les déclarations ne disaient pas

Confrontation des headers aux symboles réellement exportés par les cdylib (nm -D --defined-only, build de développement) :

BibliothèqueDéclarésExportésÉcart
libair_base_c.so6666aucun
libair_value_c.so2020aucun
libair_object_c.so274720 exportés sans déclaration
libair_c.so27929611 _Unwind_*, 7 symboles Air non déclarés, + air_main

Trois faits en découlent, de natures différentes :

  1. libair_object_c.so expose vingt symboles que son header ne déclare pas — l’API air_value_*, ré-exportée du fait de la liaison statique de la rlib sous-jacente. Un consommateur C peut s’y lier ; aucun contrat ne l’en empêche, et aucun ne l’en protège. C’est une surface livrée que rien ne garde.
  2. Les dix-huit _Unwind_* sont attendus : le dérouleur d’exceptions accompagne toute cdylib. Ils devront être exclus sciemment, par une liste nommée — jamais par un filtre qui devinerait.
  3. air_main est déclaré et non exporté, et c’est correct. Le header documente ici un contrat en sens inverse : c’est le programme lié qui fournit air_main au runtime. Un contrôle au niveau des symboles qui l’ignorerait crierait à tort — la vérification a été faite avant d’appeler cela un défaut.

Ces trois faits ne sont pas les mêmes que ceux qu’un versionnement de symboles adresse ; ils disent seulement que la question se pose au niveau des symboles, et qu’on ne peut pas y répondre en lisant des headers.

Ce que le versionnement de symboles apporterait

Sur ELF/GNU, un version script attache une version à chaque symbole exporté (air_uuid_new_v7@@AIR_1.0). Deux propriétés en découlent, et une seule compte vraiment :

  • La coexistence. Deux définitions d’un même nom peuvent vivre dans la même bibliothèque, sous deux versions. Un binaire lié il y a huit ans continue d’appeler la sienne, un binaire neuf appelle la nouvelle. C’est la seule technique connue qui tienne une promesse de dix ans sans figer le code : sans elle, corriger la signature d’une fonction impose de recompiler tout ce qui l’appelle.
  • La fermeture. local: *; rend privé tout ce qui n’est pas explicitement exporté. Cela supprimerait d’un trait les vingt symboles non déclarés de libair_object_c.so — la surface livrée deviendrait, par construction, celle du header.

L’état actuel, mesuré : les .so portent bien une section .gnu.version, mais uniquement pour les symboles importés de la glibc. Aucun symbole exporté par Air ne porte de version. Rien n’est amorcé.

Décision demandée

Trois voies, exclusives.

Voie A — Adopter le versionnement de symboles

air-symver (sous-commande cargo xtask check-symver) génère le version script depuis xtask/abi-reference.toml, l’applique au lien, et vérifie après coup que la bibliothèque produite porte bien les versions attendues.

  • Ce qu’on gagne : la coexistence, donc la possibilité de corriger sans rompre ; et la fermeture de la surface exportée, qui règle mécaniquement les vingt symboles non déclarés.
  • Ce qu’on paie : une dépendance au comportement du lien. À valider sur la cible linux-air avant de s’engager — c’est notre propre chaîne, et notre propre libc est elle-même une bibliothèque partagée. Le risque n’est pas théorique : un mécanisme de versionnement qui ne fonctionnerait qu’en gnu serait une garantie qui s’évapore exactement là où Air livre.
  • Ce qu’il faut trancher en plus : le schéma des noms de version (AIR_1.0 par bibliothèque, ou une version commune à tout Air) et le moment du premier gel.

Voie B — Refuser le versionnement, vérifier la cohérence de version

air-symver se borne à déduire du diff ABI le changement de version exigé — ajout ⇒ mineure, rupture ⇒ majeure et changement de SONAME — et à échouer si le numéro porté ne suit pas.

  • Ce qu’on gagne : aucune dépendance au linker, la vérification marche partout, tout de suite.
  • Ce qu’on paie : aucune coexistence. Toute rupture impose une majeure, et tous les consommateurs recompilent. Sur dix ans, cela revient à ne jamais rompre — ce qui est tenable, mais doit être dit, car c’est alors le code qui est figé, pas seulement l’ABI.
  • Les vingt symboles non déclarés de libair_object_c.so restent à traiter séparément.

Voie C — Différer, et le dire

On ne fait ni l’un ni l’autre maintenant ; ADR-012 et macro-architecture-fr.md sont corrigés pour dire que air-symver est spécifié et non livré, et l’ADR reste ouvert.

  • Ce qu’on gagne : rien n’est engagé à tort, et les textes cessent de mentir.
  • Ce qu’on paie : la promesse de dix ans reste tenue par la seule discipline de ne pas rompre, sans filet au niveau des symboles.

La validation exigée, et ce qu’elle a rendu

La décision conditionnait la voie A à une mesure préalable. Elle a été faite, et elle a rendu autre chose que ce qu’on attendait — c’est précisément pourquoi elle était exigée.

Fait 1 — Air ne livre aucune bibliothèque partagée

Les quatre crates *-c portent en toutes lettres « HOST-ONLY : jamais buildée pour *-linux-air » (ADR-102/ADR-029), et aucune .so n’est empaquetée : les livrables sont des exécutables static-PIE. Les .so existent pour être exercées par les tests de conformité C, sur l’hôte.

La mesure « sur linux-air » ne peut donc pas se faire, faute d’objet à mesurer. Ce n’est pas un échec de la voie A : c’est un changement de ce qu’elle signifie aujourd’hui.

Fait 2 — rustc possède le script de version

Le lien honore bien un script (AIR_1.0 apparaît dans .gnu.version_d). Mais la trace du lien montre que rustc passe déjà le sien :

-Wl,--version-script=target/debug/deps/rustcXXXXXX/list

GNU ld fait l’union des global: de tous les scripts. Conséquence mesurée sur libair_object_c.so : notre script ne peut ni retirer un symbole de la surface — même en le nommant explicitement sous local: (les 19 air_value_* restent exportés) — ni lui attacher une version : les symboles retombent sur BASE, sans version.

-Wl,--exclude-libs,ALL masque bien les 19, et aussi les 27 air_object_* : cette crate est un mince ré-export dont les propres symboles viennent eux aussi d’une archive. Trop brutal.

Fait 2 bis — sept écarts que cette mesure avait masqués (correction du 2026-08-14)

La ligne libair_c.so ci-dessus a d’abord été écrite « 18 _Unwind_* + air_main ». C’est faux : le dérouleur n’en fournit que onze. Les sept autres sont des symboles d’Air que air_c.h ne déclare pas :

air_fmt_arg_kinds   air_fmt_render_buf   air_fmt_render_stream
air_libc_fcntl      air_libc_open        air_libc_openat      air_libc_stdio_flush_all

Ils ont été trouvés par le niveau symboles de check-abi, écrit dans la foulée de cet ADR. L’approximation initiale — avoir rangé tout l’écart sous « dérouleur » sans le compter — les avait rendus invisibles. C’est exactement le motif que la règle NO GO vise, produit ici par l’instruction même qui la sert.

Conséquence : ces sept-là sont à trancher au même titre que les vingt de libair_object_c.so — entrer au header, ou cesser d’être exportés. Ils sont pour l’heure consignés au cliquet de xtask/abi-reference.toml : tolérés parce qu’inventoriés, un nouveau serait refusé.

Fait 3 — .symver fonctionne, et la coexistence est prouvée

La technique qu’emploie la glibc contourne l’obstacle : la version s’attache au niveau de l’objet, par des directives .symver, notre script définissant les nœuds de version.

#![allow(unused)]
fn main() {
core::arch::global_asm!(
    ".symver air_exemple_v1, air_exemple@AIR_1.0",
    ".symver air_exemple_v2, air_exemple@@AIR_1.1",
);
}

Résultat mesuré — deux versions du même nom dans une seule bibliothèque :

air_exemple@AIR_1.0     T
air_exemple@@AIR_1.1    T   (défaut)

Et la propriété entière, éprouvée par deux consommateurs C liés à la même bibliothèque :

ConsommateurLié àRend
neufair_exemple@AIR_1.12
ancien, épingléair_exemple@AIR_1.01

C’est la promesse de dix ans, constatée : le binaire d’hier obtient toujours son implémentation, tandis que le code évolue.

Réserve honnête : les symboles porteurs (air_exemple_v1, air_exemple_v2) restent eux aussi exportés, rustc les listant. La mise en œuvre devra les nommer de façon à ce qu’ils ne fassent pas partie du contrat — ou accepter qu’ils en fassent partie, ce qui serait un choix, pas un oubli.

Recommandation

Voie A retenue, mise en œuvre DIFFÉRÉE, et bordée. Le raisonnement tient en trois pas.

  1. Le versionnement achète une chose : la coexistence. Elle ne protège personne tant qu’aucune .so n’est livrée, et Air n’en livre aucune. Le construire aujourd’hui, ce serait le construire sur des artefacts hôtes qui n’existent que pour faire tourner des tests.
  2. Mais différer naïvement est dangereux : le versionnement doit exister dès la première .so publiée. Attacher des versions à des symboles qui n’en portaient pas est soi-même une rupture de liaison. Une première bibliothèque livrée sans versions rendrait la voie A inatteignable sans casser ce qu’elle protège.
  3. Donc : prouver la technique (fait — §Fait 3), border le report par un gate qui refuse toute .so empaquetée sans versions de symboles, et mettre en œuvre au premier .so réellement livré sur linux-air.

La condition de reprise est ainsi nommée et outillée, non confiée à la mémoire.

Conséquences

  • Un gate d’empaquetage refuse toute bibliothèque partagée livrée dont les symboles ne portent pas de version. C’est lui qui rend le report sûr : le mécanisme peut attendre, la garantie non.
  • Les vingt symboles non déclarés de libair_object_c.so cessent d’être exportés (décision BDFL du 2026-08-14). libair_value_c.so les exporte déjà correctement, 20 sur 20 ; les déclarer une seconde fois engagerait les mêmes symboles par deux bibliothèques pendant dix ans, avec deux définitions à tenir cohérentes et des doublons pour qui lie les deux. Suppose que libair_object_c.so se lie à libair_value_c.so au lieu d’embarquer sa rlib — chantier de structure qui revient sur le découpage host-only d’ADR-102/029.
  • Quelle que soit la voie, check-abi gagne un niveau symboles : la comparaison des symboles exportés à la référence, avec une liste nommée d’exclusions (_Unwind_*) et une liste nommée des symboles fournis par le consommateur (air_main). Ce n’est pas conditionné à la décision ci-dessus, et c’est ce qui aurait vu les vingt symboles.
  • Les vingt symboles non déclarés de libair_object_c.so doivent être tranchés : soit ils entrent au header (et deviennent contractuels), soit ils cessent d’être exportés. Les laisser exportés-mais-non-déclarés est le seul état qui ne se défend pas.
  • La règle des zones (air-stable / air-internal / air-experimental) d’ADR-012 est le prérequis de tout ceci et n’existe pas non plus dans le code. Elle est traitée à part.

Alternatives rejetées

  • Déduire les versions automatiquement du numéro de version des crates. Rejeté : la version d’une crate suit son code, pas son contrat ; deux crates peuvent monter de version sans qu’aucun symbole ne bouge, et l’inverse est tout aussi vrai.
  • N’exporter qu’un symbole unique de dispatch (une table de fonctions passée au consommateur), qui supprimerait la question. Rejeté : ce n’est plus une ABI C, c’est un protocole ; les consommateurs C attendus (harnais, portages) ne le consommeraient pas.

Licence du document : MPL 2.0