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-160 — La frontière variadique : écrire printf en Rust, et ne pas bâtir d’échafaudage C

Statut : Accepté (2026-08-14, BDFL). Décide comment Air fournit ses points d’entrée C-variadiques (printf, open, fcntl…), et scelle le premier #![feature(…)] du code d’Air.

Catégorie : Toolchain + libc. Touche ADR-025 (builds reproductibles, toolchain épinglée) et la politique « zéro surface C ».

Contexte

Ce que Rust sait, et ce qu’il ne sait pas — mesuré

Rust sait déclarer et appeler une fonction C-variadique depuis toujours. Ce qu’il ne sait pas sur le canal stable, c’est en définir une : recevoir le ... et parcourir la liste d’arguments. Constaté sur la toolchain du projet :

error[E0658]: C-variadic functions are unstable
  = note: see issue #44930

Or une libc doit en définir : printf(fmt, ...), open(path, flags, ...), fcntl(fd, cmd, ...) sont parmi les fonctions les plus appelées de POSIX.

Le contournement actuel, et son coût

Deux shims en C pur portent la frontière variadique — printf_shim.c (208 lignes) et fileio_shim.c (86 lignes). Ils reçoivent le ..., l’aplatissent, et appellent le vrai moteur Rust. Ils vivent dans air-libc-c, crate HOST-ONLY : rien de tout cela n’est livré. Leur raison d’être est de permettre à un programme C d’exercer notre libc dans les tests de conformité.

Le prix de cette couture, mesuré :

CoûtDétail
7 symboles exportés non déclarésair_fmt_* (3) et air_libc_* (4) : le shim C les appelle par leur nom, donc ils portent #[no_mangle], donc rustc les met dans .dynsym
Deux scripts de version committéscsrc/printf_exports.list et csrc/fileio_exports.list re-globalisent les symboles du shim que rustc localise
Des --undefined de rétentionbuild.rs retient 15 symboles du --gc-sections
Un compilateur C au buildinvoqué via cc/$CC en direct — la crate cc étant bannie
294 lignes hors couverturecargo llvm-cov n’instrumente que le Rust ; couvrable-vide ne connaît que le Rust. Ces lignes sont exercées (tests/hello_world.rs compile un vrai programme C, le lie et l’exécute) mais jamais mesurées : on ignore lesquelles le sont

Le fait qui change la question

Le projet est déjà sur nightly pour tout ce qui est livré. xtask épingle nightly-2026-07-11 — la cible custom *-linux-air et -Z build-std l’exigent. rust-toolchain.toml épingle stable 1.96 pour l’hôte seulement.

Et c_variadic fonctionne sur ce nightly épinglé : mesuré, la fonction se définit, le symbole sort exporté. L’API est VaList::next_arg::<T: VaArgSafe>().

Et le fait qui la rend urgente

Aucun code Rust ne définit printf : mesuré. Le seul int printf du dépôt est dans le shim C, qui est host-only. Autrement dit, côté cible, les points d’entrée variadiques n’existent pas. air-libc-capi/src/lib.rs:206 l’écrit d’ailleurs au futur : le câblage variadique « s’appuiera » dessus.

Il n’y a donc pas de statu quo à préserver : il faut créer cette frontière côté cible. Deux manières seulement — compiler les shims C pour linux-air (donc livrer du C, et avec lui les symboles non déclarés), ou l’écrire en Rust.

Décision

D1 — Les points d’entrée variadiques de *-linux-air sont écrits en Rust

Directement, sans passer par du C. On ne construit pas un échafaudage qu’il faudra démonter : c’est la forme la moins chère de dette, celle qu’on n’a pas contractée.

Conséquence assumée et centrale : ce sera le premier #![feature(…)] du code d’Air. Aujourd’hui le dépôt n’en compte aucun — le nightly n’est exigé que par des drapeaux de build, jamais par les sources. Après D1, les sources de la frontière variadique ne compileront plus sur stable.

C’est un vrai seuil, et il est franchi en connaissance de cause : l’alternative n’est pas « rester sur stable », c’est livrer du C.

D2 — La cdylib hôte est bâtie avec le même nightly épinglé, et les shims C sont retirés

air-libc-c passe du stable de l’hôte au nightly-2026-07-11 déjà épinglé. Les 294 lignes de C disparaissent, et avec elles : les 7 symboles non déclarés, les deux scripts de version, les --undefined de rétention, et le besoin d’un compilateur C au build.

La couverture cesse d’avoir un angle mort : ce qui était du C non instrumenté devient du Rust, que llvm-cov mesure et que couvrable-vide gouverne comme le reste de la couche 1.

Le cliquet de check-abi annoncera lui-même la fin de la dette : le jour où les 7 symboles ne seront plus exportés, il signalera que les entrées consignées sont périmées. La dette enregistre sa propre résolution — aucune mémoire n’est sollicitée.

D3 — Un gate de sonde : cargo xtask check-toolchain

Air dépend d’un nightly. Rien n’inventorie pourquoi. Le gate tient ce registre : pour chaque dépendance instable — -Z build-std, cible JSON custom, c_variadic —, il sonde si elle est devenue stable, et le dit.

Sa valeur n’est pas d’échouer, mais d’annoncer. Le jour où la dernière se stabilisera, Air pourra quitter le nightly ; sans ce registre, personne ne saurait dire quand ce jour arrive. C’est le même patron que check-symver : un gate qui aujourd’hui ne trouve rien, écrit qu’il n’a rien trouvé, et fera du bruit le jour où le monde change.

Il borne aussi : les #![feature(…)] autorisés sont ceux du registre, et là où le registre les autorise. Un feature gate qui apparaîtrait ailleurs que sur la frontière variadique serait un écart — sans quoi D1 ouvrirait la porte à l’instable partout.

Conséquences

  • À mesurer avant d’adopter, sur les DEUX arches. L’ABI variadique diffère entre x86_64 et aarch64 — la va_list d’ARM64 est une structure, non un pointeur. Cet ADR ne déclare pas la chose conforme : il exige qu’elle soit prouvée sur les deux, comme l’a été le bug d’assembleur x19/x21 que la vérification deux-arches avait attrapé.
  • check-c-surface gagne du sens. Il bannit aujourd’hui les crates *-sys ; après D2, le dépôt n’aura plus aucun C livré ni compilé, et la politique « zéro surface C » cessera d’avoir une exception nommée.
  • Le tableau de bord des tests de conformité C est inchangé. Un programme C compile toujours contre le header committé et se lie à libair_c.so : c’est la fabrication de cette bibliothèque qui change, pas son contrat. check-abi le vérifiera — les déclarations ne bougent pas.
  • ADR-025 reste tenu. Le nightly est épinglé, comme il l’est déjà ; le déterminisme des builds ne dépend pas du canal mais de son épinglage.

Alternatives rejetées

  • Compiler les shims C pour linux-air. C’est la seule autre façon d’avoir des variadiques on-target. Rejetée : elle livrerait du C, et avec lui les symboles non déclarés et un angle mort de couverture — sur la brique la plus fondamentale, et à contre-courant de la politique zéro C.
  • Attendre la stabilisation de c_variadic. Rejetée pour D1 : côté cible il n’y a rien à préserver, donc attendre signifierait construire l’échafaudage C entre-temps, pour le démonter ensuite. On paierait deux fois.
  • Garder les shims C côté hôte, écrire du Rust côté cible. Rejetée : deux implémentations du même point d’entrée, l’une testée par les conformités C, l’autre livrée. C’est exactement le doublon que le projet refuse — et le pire, celui où la version testée n’est pas la version livrée.

Licence du document : MPL 2.0