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ût | Détail |
|---|---|
| 7 symboles exportés non déclarés | air_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és | csrc/printf_exports.list et csrc/fileio_exports.list re-globalisent les symboles du shim que rustc localise |
Des --undefined de rétention | build.rs retient 15 symboles du --gc-sections |
| Un compilateur C au build | invoqué via cc/$CC en direct — la crate cc étant bannie |
| 294 lignes hors couverture | cargo 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_64etaarch64— lava_listd’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’assembleurx19/x21que la vérification deux-arches avait attrapé. check-c-surfacegagne 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-abile 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