air-stdio — brique stdio/console bas-niveau
Couche : 1. Statut : implémentée (brique bas-niveau).
Périmètre : roadmap couche 1 (libc + PAL)
§2.D.1 (décision BDFL). Verrou :
ADR-056 D3/D8 (futex maison +
reset post-fork). Conventions :
ADR-021 (EINTR
remonté, conv.2). no_std :
ADR-048 /
ADR-055. Nommage :
ADR-029 (zéro abréviation).
1. Raison d’être
Les deux toits d’Air — la libc (FILE*) et le PAL (std::io::{Stdin, Stdout, Stderr}) — ont besoin du même socle stdio bas-niveau : des handles vers les
descripteurs standard, des E/S brutes, un test « est-ce un terminal ? », et de
quoi sérialiser des séquences d’écritures. L’audit PAL a relevé que « stdio
bas-niveau manque ». air-stdio est cette brique unique et partagée : chaque
toit la réutilise au lieu de la réinventer (et de diverger).
2. Périmètre — le niveau BRUT
Cette crate fournit exactement :
| Élément | Contenu |
|---|---|
| Handles | stdin / stdout / stderr → StandardStream liant 0 / 1 / 2 |
| E/S brutes | StandardStream::read / write — un seul syscall, sans buffering |
| Terminal | StandardStream::is_terminal (via air_terminal::tty::isatty) |
| Verrou par flux | StandardStream::lock → StreamGuard (RAII), futex maison 3 états |
atfork | reset_after_fork — réinitialise les trois verrous dans l’enfant |
HORS PÉRIMÈTRE (libc §4, au-dessus)
Cette brique ne bufferise pas et ne formate pas. Vivent dans le toit :
- le moteur
FILE*bufferisé (buffers ligne/bloc,fflush,setvbuf) ; printf/scanfet tout formatage ;- la boucle sur écriture courte et le retry
EINTR(la brique remonteEINTRtel quel — ADR-021 conv.2 — et rend le compte d’octets d’un seul syscall, potentiellement partiel).
3. Conception : opérations ⟂ liaison
La logique des opérations travaille sur un BorrowedFd quelconque (fonctions
privées raw_read / raw_write / raw_is_terminal) ; la liaison aux
descripteurs 0 / 1 / 2 est isolée dans stdin / stdout / stderr. Cette
séparation rend toute la logique exerçable sur des pipes / ptys en test,
sans jamais toucher le vrai stdout/stderr (aucune pollution de la sortie de
test, aucun root).
StandardStream est une poignée triviale (Copy) : un BorrowedFd<'static>
(le descripteur, ouvert pour toute la vie du processus) + une référence 'static
vers le verrou du flux.
4. E/S brutes
read(&self, buffer: &mut [u8]) -> Result<usize, Errno>— délègue àair_sys_syscall::fs::read.0= fin de fichier.write(&self, buffer: &[u8]) -> Result<usize, Errno>— délègue àair_sys_syscall::fs::write. Le compte rendu peut être inférieur àbuffer.len()(écriture courte).
Aucune boucle, aucune bufferisation, aucun retry : un syscall par appel,
EINTR remonté à l’appelant (le toit écrit la boucle explicite).
5. Verrou par flux + atfork
Chaque flux possède un mot de verrou statique (AtomicU32) : un futex
maison à 3 états (UNLOCKED / LOCKED / CONTENDED, façon Drepper), calqué
sur air-alloc/src/lock.rs. Chemin non
contendu = un compare_exchange + un swap, sans syscall ; le futex n’entre
en jeu que sous contention réelle.
StandardStream::lock prend le verrou et rend un StreamGuard (RAII) qui le
relâche au Drop. Le toit s’en sert pour rendre atomique une séquence
d’écritures (deux write d’un même println! ne s’entrelacent pas). La brique ne
pose jamais le verrou elle-même — read/write sont brutes et non
verrouillées ; la sérialisation est une politique du toit.
reset_after_fork réinitialise les trois verrous à l’état déverrouillé.
Motivation atfork (ADR-056 D8) : après un fork, un verrou tenu par un
autre thread au moment du fork resterait éternellement bloqué dans l’enfant
(ce thread n’y existe pas). Le gardien de fork d’air-runtime l’appellera
dans l’enfant, exactement comme pour air-alloc::reset_after_fork — sens des
dépendances air-runtime → air-stdio, jamais l’inverse.
6. no_std, dépendances, tests
#![no_std], zéro allocation, core seul (couche 1 std-free). Aucune
dépendance externe : seulement les crates sœurs air-sys-syscall (read/write/
futex), air-sys-types (FD/Errno/futex) et air-terminal (isatty). Sous
#[cfg(test)] la crate redevient std (pipes/ptys réels, threads).
Couverture 100 % (lignes + branches, couche 1) : E/S sondées sur des pipes
(succès + EBADF sur mauvais sens d’accès), is_terminal sur PTY réel vs pipe,
verrou sondé en contention déterministe deux threads (technique d’air-alloc,
sans minutage) et reset_after_fork testé dans le parent sans fork réel.
7. Dépendance différée (§2.D.2)
La roadmap §2.D.2 prévoit qu’air-filesystem prenne une dépendance sur
air-stdio (E/S de plus haut niveau adossées aux handles standard). Elle est
différée jusqu’à un consommateur concret : introduire la dépendance sans usage
la rendrait inutilisée (cargo machete).