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

Note d’étude — une std Rust sur Air safe, sans libc C (PAL sur la couche 1)

Statut : note d’étude, NON normative. Cadre le nouvel objectif premier — une toolchain Rust dont la std repose uniquement sur Air, par un backend std::sys bindant la couche 1 Rust safe, sans lib C ni bindings C. Elle instruit une décision qui amendera ADR-076 (« option A : std sur la libc C libair_c ») ; les décisions structurantes qui en découleront passeront par un ADR dédié puis, le cas échéant, un RFC amont à la Rust Teams libs. Aucun code n’est engagé par cette note.

Source disséquée (réelle, pas de mémoire) : le rust-src du nightly piné du tree rt/nightly-2026-06-13, library/std/src/sys/.

1. Pourquoi ce pivot

M5 a prouvé que std compile et tourne sur Air, mais par le chemin d’ADR-076 : std réutilise le PAL unix et lie une libc C (libair_c, crates air-libc-*). Or la frontière C (extern "C" / #[no_mangle] / FFI / errno in-band) est unsafe par nature et a même forcé l’escape hatch raw_syscall (ADR-087, futex/gettid appelés en C brut). On a donc retiré de l’unsafe au nom de la doctrine (« l’unsafe vit en couche 0 uniquement », macro-architecture §1) pour le réintroduire par une libc C sous std. Incohérent.

Cible de l’étude : un backend std::sys::pal::air + des modules sys/<domaine>/air.rs qui bindent la couche 1 Rust safe (les Managers de domaine, ADR-077), zéro FFI. libair_c survit — elle reste la libc pour les vrais développeurs C (porter des outils Unix, doctrine « Linux-conforme, pas POSIX ») — mais std ne passe plus par elle.

2. Comment std::sys est structuré aujourd’hui (et pourquoi ça nous arrange)

Le std moderne a vidé le PAL (chantier amont « FIXME(117276) », visible dans sys/mod.rs). Deux niveaux :

  1. Sous-systèmes transverses sys/<domaine>/mod.rs (fs, net, thread, thread_local, time, process, alloc, stdio, random, sync, os_str, args, env, fd, pipe, exit…). Chacun contient un cfg_select! qui route vers un fichier par OS (fs/unix.rs, fs/hermit.rs…) via use <os> as imp;, en enveloppant imp d’une grosse couche générique réutilisable (read_dir, copy, each_addr…).
  2. PAL résiduel sys/pal/<os>/ — ne garde plus que l’amorçage runtime (init, cleanup, abort_internal), le module futex, time (souvent réexporté), stack_overflow, weak (dlsym). sys/pal/mod.rs dispatche par cfg_select!.

Le précédent qui valide le pivot : des backends std d’OS Rust sans libc existent déjà en amonthermit (unikernel Rust), xous (micro-noyau Rust), motor, uefi, zkvm. Le PAL de hermit fait ~171 lignes (2 fichiers) contre ~2000 pour pal/unix. Le mécanisme est donc conçu pour accueillir un backend non-libc.

3. Le patron à copier — et ce qui distingue Air

Structurellement, Air suit le patron hermit : PAL ultra-mince + un fichier sys/<domaine>/air.rs par sous-système, bindant l’interface de l’OS.

Mais tous les backends existants bindent une frontière unsafe — c’est là qu’Air innove :

BackendInterface bindéeNatureunsafe
unixlibc (glibc/musl)C-ABI externepartout à la frontière
hermithermit_abi (extern "C", #[link_name="sys_*"])C-ABI (libc-en-Rust)25 (socket), 17 (fs), 13 (thread)…
xousasm!("ecall") inlineasm bruttout appel noyau
motormoto-rt : façade Rust au-dessus d’une vDSO extern "C"C-ABI cachéeà la frontière
uefir-efi (extern "efiapi")C-ABI firmwareà la frontière
air (cible)Managers couche 1 Rust safe (AirFile, AirTcpSocket, AirThreadBuilder…) → Result typésAPI Rust safe, losslessconfiné en couche 0

Le modèle le plus proche est motor/moto-rt (une façade Rust fn futex_wait(&AtomicU32) -> bool au-dessus d’un mécanisme brut). Air pousse plus loin : la façade elle-même est safe et sans C-ABI intermédiaire — les sys/*/air.rs appellent la couche 1, dont l’unsafe est déjà confiné tout en bas dans air-sys-syscall (couche 0). Air serait le premier backend std safe de bout en bout. C’est précisément l’angle qui justifie un RFC (§7).

⚠️ Invariant de layering (règle absolue). Le PAL est un toit : il s’assoit sur la couche 1, quoi qu’il arrive, et JAMAIS sur la couche 0. La couche 0 n’est pas stable (elle suit le kernel — doctrine de stabilité) : aucun toit ne s’y accroche. Donc tout ce dont le PAL a besoin doit être exposé par le Manager couche 1 adéquat (ADR-077), lequel s’appuie en interne sur la couche 0. Le jeu de cette étude est exactement cela : identifier ce qui manque au PAL et le placer dans le bon Manager couche 1 — pas de raccourci vers la couche 0, jamais.

4. Le contrat exact d’un backend air

4.1 Bras cfg_select! à ajouter

Un bras target_os = "air" (ou target_env = "air") — voir la décision §6.a — à insérer avant le bras unix dans le PAL et chaque sous-système migré :

FichierActionPiège
sys/pal/mod.rstarget_os="air" => mod air avant unix =>sinon target_family="unix" capte Air → pal/unix → libc
sys/alloc/mod.rsmod air (GlobalAlloc)
sys/args/mod.rsmod air (ou réutilise common)
sys/env/mod.rsmod air
sys/fd/mod.rsmod air (FileDesc(OwnedFd))brique de fs/net/pipe/stdio/process
sys/fs/mod.rsmod airle plus gros mais le plus « traduisible »
sys/io/mod.rs + sys/io/error/*mod air (is_terminal, decode_error_kind)
sys/net/connection/{mod,socket/mod,hostname/mod}.rsmod airle plus couplé libc — réécriture complète
sys/paths/mod.rsmod air (getcwd, current_exe, temp_dir…)
sys/pipe/mod.rsmod air
sys/process/mod.rsmod airle plus dur (fork/exec)
sys/random/mod.rsmod air (obligatoire)dispatch par target_os → sinon _ => {} vide, ne compile pas
sys/stdio/mod.rsmod airfacile une fois fd
sys/sync/{mutex,condvar,rwlock,thread_parking}/mod.rsajouter target_os="air" aux bras futex existants (PAS un module)dispatch par target_os → sinon tombe sur pthread = libc
sys/thread/mod.rsmod air (Thread)id() renvoie aujourd’hui libc::pthread_t → id Air
sys/thread_local/mod.rsTLS statique (native) + guard::enable, ou clés osvoir §6.c
sys/exit.rsbras air → exit via AirTaskManager (couche 1 ; 2 endroits : exit et unique_thread_exit)sinon _ => intrinsics::abort() = abort au lieu d’exit (bug silencieux)
sys/personality/mod.rsbras gcc (déjà target_family="unix") → symboles _Unwind_*dépendance de lien, pas un module à écrire

Aucune action (générique, convient déjà) : sys/os_str (→ bytes, OsStr = [u8]), les couches génériques de fs/process/net/connection/sync/once/backtrace, cmath (→ symboles libm externes, fournis par air-libm).

4.2 Le PAL air mince (gabarit = pal/unsupported/common.rs + pal/hermit)

  • pub unsafe fn init(argc, argv, sigpipe) — appelle args::init, protection stack-overflow, SIGPIPE.
  • pub unsafe fn cleanup()
  • pub fn abort_internal() -> !
  • pub fn unsupported<T>() / unsupported_err()
  • pub mod futex : type Futex = Atomic<u32> (+ SmallFutex), futex_wait(&Atomic<u32>, u32, Option<Duration>) -> bool, futex_wake(&Atomic<u32>) -> bool, futex_wake_all(&Atomic<u32>). Binde la couche 1 (le futex exposé par AirTaskManagerair-thread a déjà raw_futex), jamais la couche 0.
  • pub mod time (souvent #[path="../unix/time.rs"], ou un air.rs).
  • point d’entrée runtime (déjà couvert par air-rt-std/air-std-entry du harnais M5).

Le meilleur ratio du projet : ces 3 fonctions futex suffisent — Mutex, Condvar, RwLock, Once, Parker de std sont alors gratuits (impls génériques sync/*/futex.rs, 100 % Rust safe).

5. Cartographie couche 1 → sous-systèmes (la couche 1 couvre déjà tout)

La couche 1 expose partout des APIs AirResult<T> safe, sur couche 0 uniquement, jamais de libc C. Correspondance :

Sous-système stdBrique couche 1État
fsair-filesystem (AirFile, AirFileManager, AirFileMetadata)couvre
fdair-handle (AirHandle, OwnedFd) + air-filesystemcouvre
netair-socket (AirTcpSocket/Listener, AirUdpSocket, AirSocket, AirNameResolver) + air-handlecouvre
threadair-thread (AirThreadBuilder, yield_now, set_thread_name) + air-runtime (JoinHandle, current_tid)couvre
sync / parkingair-thread (raw_futex : futex_wait/futex_wake)couvre (3 wrappers → toute la synchro)
thread_localair-runtime (TCB/TLS ELF natif #[thread_local], run_thread_local_destructors)couvre (voir §6.c pour les clés dynamiques)
timeair-base-core (AirInstant, AirSystemTime)couvre
processair-process (AirCommand, AirProcess, spawn_process, credentials) + air-runtime/forkcouvre
os_str / pathair-base-core (AirOsStr/String, AirPath)couvre
args / envair-runtime/args + air-env (get/vars/set/unset, environ_pointer)couvre
alloc (GlobalAlloc)air-alloc (GlobalAllocator, reset_after_fork)couvre
stdioair-stdio (stdin/stdout/stderr, is_terminal)couvre
randomair-crypto (AirRandom::fill, sur getrandom couche 0)couvre
signaux (SIGPIPE/SIGCHLD, auto-réveil)air-signal (install_handler, AirSignalFd) + air-pollcouvre

Manques (mineurs) à combler côté couche 1, additifs v1.x :

  1. current_exe (/proc/self/exe) — non trouvé ; probable vrai manque pour std::env::current_exe.
  2. DirEntry typéread_directory_entries rend un buffer brut ; le PAL peut parser getdents64 lui-même, ou l’on ajoute un ReadDir/DirEntry ergonomique dans air-filesystem.
  3. park/unpark/park_timeout helper dans air-thread (les primitives futex suffisent, mais éviter de dupliquer).
  4. Options socket dédiées (SO_RCVTIMEO/SO_LINGER/IP_TTL/peek) — faisables via l’API set_socket_option_int générique ou à ajouter.
  5. Clés TLS dynamiques — seul #[thread_local] natif est fourni (suffisant pour le std moderne, voie « native ») ; une API de clés ne serait requise que par la voie os.

Placement Manager (ADR-077) — décision de l’étude. Le PAL binde des Managers, pas des fonctions libres éparses. Le tableau ci-dessus liste les crates ; il faut arrêter, pour chaque besoin du PAL, le Manager qui l’expose. Cas notables :

  • AirTaskManager (fidèle au kernel), PAS AirProcessManager ni AirThreadManager (décision). « Kernel = bible » : le kernel ne connaît que la task (struct task_struct, l’unité ordonnançable) ; ce que POSIX appelle process est un thread group (tasks partageant un tgid ; le leader a pid == tgid), et un thread est une task membre. fork et création de thread sont le même clone3 (seuls les flags CLONE_THREAD/ CLONE_VM/… changent). Le couplage fork → thread unique est intrinsèque (après clone, réinitialiser TID, TLS/TCB, handlers atfork, errno_location, air-alloc::reset_after_fork, air-stdio::reset_after_fork — déjà porté par air-runtime/fork.rs + process_context). ⇒ AirTaskManager modélise AirTask + AirThreadGroup (tgid), médie sur air-process + air-thread + les concerns tâche/fork/TLS d’air-runtime, et produit côté userland les notions POSIX : process = thread group (getpid → tgid), thread = task (gettid → pid de task). Il expose : spawn/join, getpid/gettid, yield, park/unpark, set_name. (Le Manager est une surface de médiation — aucune fusion de crates requise.) Faux ami à éviter : le struct task_group du kernel = regroupement d’ordonnancement CPU / cgroups (CONFIG_CGROUP_SCHED), sans rapport avec le thread group ; il relèverait d’AirSystemManager.
  • futex — à placer consciemment. Ce n’est pas purement « thread » : c’est une primitive de synchronisation sur un mot mémoire, utilisable inter-process (mémoire partagée). Dans le PAL il sert de parking de task (il back Mutex/Condvar/Parker). Reco : l’exposer via AirTaskManager aux côtés de park/unpark (c’est son usage PAL), en documentant sa nature inter-process. (air-thread::raw_futex fournit déjà futex_wait/futex_wake.)
  • Les manques (1)-(5) ci-dessus sont à ajouter dans le Manager de domaine adéquat (additifs couche-1 v1.x) : current_exe/DirEntryAirFileManager ; options socket → AirNetworkManager ; park/unparkAirTaskManager.

Règle du chantier : pour chaque item du contrat PAL (§4), la question n’est jamais « quel syscall » mais « quel Manager couche 1 l’expose, et que faut-il y ajouter ». Zéro accroche couche 0 depuis le PAL.

6. Décisions de conception à trancher (pour l’ADR)

a. La cible : target_os="air" (à la hermit), PAS env=musl+target_family="unix". ADR-076 a délibérément fait ressembler Air à unix (env=musl+vendor=air) pour réutiliser le PAL unix + une libc. Le pivot inverse ce choix : pour un PAL custom, Air doit ne pas être capté par les bras target_family="unix" (sinon il retombe sur pal/unix→libc). hermit est target_os="hermit", hors famille unix. ⇒ Air devrait être target_os="air" (cible JSON custom, comme en phase 1), quitte à ajouter des bras air là où hermit a les siens. (Alternative discutée : rester unix-family et insérer un bras air avant unix partout — plus fragile, plus de patches, risque de capture accidentelle par un bras unix oublié.)

b. Allocateur : air-alloc derrière GlobalAlloc — et la dette unsafe. Le hook GlobalAlloc est unsafe (le trait), mais zéro FFI : air-alloc est un allocateur Rust possédé. C’est la dette GlobalAlloc-safe (macro-architecture §8) — à instruire dans ce chantier (peut-elle devenir safe ? sujet potentiel d’RFC, §7).

c. TLS : voie « native » (statique ELF), pas les clés os. air-runtime supporte déjà #[thread_local] natif ; __tls_get_addr n’est pas requis (TLS statique, acquis M5). Il reste à fournir le hook de fin de thread (guard::enable) qui déclenche les destructeurs. Évite de réimplémenter pthread_key_*.

d. Résorber raw_syscall (ADR-087). raw_syscall n’existait que parce que le PAL unix appelait syscall/futex/gettid en C brut (via la libc). Avec le PAL custom, le PAL binde la couche 1 (jamais la couche 0) : c’est le Manager couche 1 adéquat qui expose futex et gettid (en s’appuyant en interne sur la couche 0). ⇒ l’escape hatch raw_syscall disparaît — le pivot résorbe une dette au lieu d’en créer. futex et gettid sont exposés par AirTaskManager (task + thread group, fidèle au kernel — voir §5, note « placement Manager »).

e. id() du thread : un id Air, pas libc::pthread_t. sys/thread renvoie aujourd’hui un pthread_t ; Air renvoie un Tid/u64. (Attention : bug connu du spawn natif aarch64, cf. suivi — à traiter dans ce chantier.)

7. La frontière de l’RFC — ce qu’on fait seuls vs ce qu’on propose à Rust

Ce qu’on peut faire seuls, tout de suite (downstream) : exactement comme M5 — cible JSON custom + -Z build-std + rust-src patché (bras air ajoutés). C’est éprouvé. Cela n’exige aucun RFC pour prototyper et prouver un std safe sur Air.

Ce qui mérite un RFC amont (pour ne pas forker std::sys éternellement) : l’angle n’est pas « rendre le mécanisme non-libc possible » (hermit le prouve déjà), mais :

  1. L’upstreaming Tier-3 de *-linux-air (cible + mainteneur = projet Air) — planifié (ADR-050) : c’est le véhicule qui rend les bras air légitimes en amont plutôt que patchés localement.
  2. Un point d’extension propre pour qu’un « OS userland Rust safe » branche son backend sans dupliquer les bras cfg_select! de chaque sous-système — la question honnête à poser à la Teams libs : le contrat PAL actuel (pensé pour des frontières C-ABI) peut-il accueillir de première classe un backend safe dont l’unsafe est chez l’OS (couche 0) et non dans std::sys ? Y a-t-il un intérêt à formaliser un trait de backend partagé ?
  3. GlobalAlloc safe (§6.b) — la question la plus « recherche » : un hook d’allocateur global qui n’oblige pas à unsafe impl.

Honnêteté de cadrage : le gros de la valeur RFC est (1) — le reste ((2),(3)) est une conversation à ouvrir avec la Teams libs, pas un préalable au prototype. On avance downstream, on prouve, puis on propose.

8. Prochaines étapes (proposition)

  1. Cette note — revue BDFL (nous y sommes).
  2. ADR de direction — amende ADR-076 : PAL custom safe sur couche 1, target_os="air", libair_c découplée de std, résorption de raw_syscall. Tranche §6.a–e.
  3. Reprofilage de la cible *-linux-air (JSON : target_os="air", plus env=musl) + les bras air dans un rust-src patché (fork de travail hors-arbre rt/, carve-out ADR-030).
  4. Implémentation par sous-système, du plus facile au plus dur, chacun bindant sa brique couche 1 : exit/random/time/os_str/args/envfd/stdiofutex+syncfsthread+thread_localprocessnet. Manques couche 1 (§5) comblés en additifs v1.x au fil de l’eau.
  5. Jalon : hello-std re-tourne sur le PAL safe (2 arches), sans libair_c liée — la preuve que le userland Rust est safe de bout en bout (unsafe confiné couche 0).
  6. RFC amont (Tier-3 + conversation backend safe), une fois la preuve en main.

9. Conclusion

Le pivot est techniquement dégagé : (a) std::sys moderne accueille les backends non-libc (hermit le prouve, PAL ~171 lignes) ; (b) la couche 1 d’Air couvre déjà tous les sous-systèmes en Rust safe, à quelques additifs mineurs près ; (c) le contrat est borné et connu (les bras à ajouter, les ~15 modules air.rs, le module futex qui débloque toute la synchro) ; (d) le pivot résorbe raw_syscall au lieu d’en créer, et confine enfin l’unsafe là où la doctrine l’exige — la couche 0. La singularité d’Air — un backend std safe, non une n-ième frontière C-ABI — est ce qui, à terme, mérite d’être porté à la Rust Teams libs.