AirCom — « surface Rust sûre par défaut » : note de conception d’incrément
Note de conception. Version 1.0 (2026-08-03). Relevé dans l’arbre le même jour.
Objet. Instruire un incrément d’ADR-001 (AirCom) dont le but tient en une phrase : qu’une application ou un service Air écrit en Rust n’ait pas à écrire d’
unsafepour parler sur AirCom. Ce n’est pas un chantier de transport — le transport fonctionne. C’est un chantier de surface.
1. L’état réel, mesuré
AirCom est nettement plus avancé que sa réputation. Environ 11 200 lignes en trois crates, suivant le motif sans-IO normatif d’ADR-091 :
| Crate | Lignes | Rôle |
|---|---|---|
air-com-schema | 2 555 | Schéma Cap’n Proto v1, code généré committé (ADR-040) |
air-com-proto | 4 640 | Cœur sans-IO pur et fuzzable : framer, codec, handshake, session, invoke, contrôle de flux, ring, registre |
air-com | 4 088 | Pilote I/O — la seule partie qui touche un socket |
Le transport est réel : socket Unix SOCK_SEQPACKET, frontières de message préservées ; le
bootstrap sur std::os::unix::net a disparu. Deux surfaces de première classe partagent le
même cœur — asynchrone sur io_uring (AirCom est le premier gros consommateur d’air-async)
et synchrone sur sockets bloquantes. Façade publique : Connection, Listener,
ObjectRegistry, Registry, Requester, Publisher/Subscriber, ServiceEndpoint. Deux
consommateurs réels : air-notifyd et air-sshd.
Le data plane de capabilities existe et il est sûr. air-com/src/capability.rs expose trois
fonctions publiques, toutes en Rust sûr — seqpacket_pair, send_capability,
receive_capability — au-dessus de SCM_RIGHTS asynchrone sur io_uring, plus de la mémoire
partagée zéro-copie par memfd scellé.
2. Le problème : la face C-ABI est devenue le chemin par défaut
Air expose délibérément deux faces — c’est la doctrine du modèle d’objet C-ABI (ADR-002) et de la documentation polyglotte (ADR-027). La face C n’est pas le problème : elle est ce qui rend les bindings possibles. Le problème est sa primauté.
Côté client — la surface sûre existe et n’est employée nulle part
air-value compte 20 fonctions publiques dont 13 unsafe, et 26 occurrences d’extern "C".
Mais il porte aussi AirValueOwned, un enrobage RAII sûr, avec une surface Rust normale :
#![allow(unused)]
fn main() {
pub fn int(i: i64) -> Self;
pub fn string(s: &str) -> Result<Self, AirError>;
pub fn bytes(b: &[u8]) -> Result<Self, AirError>;
pub fn as_str(&self) -> Option<&str>;
}
Ce type est utilisé nulle part en dehors d’air-value lui-même et d’un fichier d’air-object.
Et les signatures d’air-com ne le connaissent pas :
#![allow(unused)]
fn main() {
pub async fn call_object(&mut self, …, args: &[AirValue]) -> Result<AirValue, Errno>;
pub fn call_object_blocking(&mut self, …, args: &[AirValue]) -> Result<AirValue, Errno>;
}
Conséquence directe : l’exemple phare — celui qu’un développeur copie — écrit
#![allow(unused)]
fn main() {
let status = unsafe { air_value_string(bytes.as_ptr(), bytes.len(), &mut out) };
}
Un développeur qui suit le tutoriel écrit de l’unsafe dans son application. C’est contraire
au Principe 11 par l’esprit, et
frontalement contraire à ADR-149
D2 : unsafe en couche 0 et nulle part ailleurs ; ce qui subsiste au-dessus est une dette, pas
une permission.
Côté serveur — le manque est déjà nommé par le projet
La macro #[air_class] existe et vise exactement ce rôle : déclarer une classe « zéro glue
C ». Mais elle génère aujourd’hui les propriétés observables et la politique de thread, pas
encore les méthodes (AO.4, différé).
air-notifyd le dit lui-même, et c’est la meilleure formulation du besoin qu’on puisse trouver :
La macro
#[air_class]génère aujourd’hui une classe avec ses propriétés observables et sa politique de thread, mais PAS encore ses méthodes. Ce service câble donc sa classe à la main viaAirClassCell::with_members: c’est la forme exacte que#[air_class]émettra une fois la génération de méthodes livrée.
Autrement dit : le service de référence est un tenant-lieu écrit à la main de ce que la macro doit produire. Le périmètre du lot serveur n’est donc pas à inventer — il est déjà décrit, et sa cible est déjà écrite.
L’ampleur réelle
aircom_notifyd_client.rs unsafe = 3 extern "C" = 0
aircom_notifyd_server.rs unsafe = 2 extern "C" = 0
Cinq blocs unsafe en tout. Le chantier est petit rapport à ce qu’il débloque.
3. L’incrément proposé — trois lots
Lot 1 — le client parle AirValueOwned. Les signatures d’invocation d’air-com acceptent et
rendent la forme sûre ; la face AirValue reste, pour les bindings C. Aucun contrat C-ABI n’est
touché : Connection est du Rust de couche 2, pas une surface exportée en C.
Lot 2 — finir AO.4 : la génération de méthodes par #[air_class]. Cible connue : la forme
que air-notifyd câble à la main. Critère de sortie naturel — air-notifyd supprime son
câblage manuel et repasse sous la macro, sans changer son comportement observable.
Lot 3 — réécrire les deux exemples. C’est le vrai livrable : ce que le développeur copie est
ce qu’il croira être la manière de faire. Les deux exécutables aircom_notifyd_server et
aircom_notifyd_client sont, par décision, la référence développeurs d’Air — deux
exécutables séparés discutant sur AirCom.
4. Le critère de sortie
Mécanique, vérifiable, et exactement la propriété voulue :
grep -c unsafesur les deux exemples rend0, et aucunextern "C"n’y apparaît — sans que leur comportement observable change (les deux exécutables continuent de dialoguer, sortie 0).
À quoi s’ajoute le contrôle de doctrine : une application Air de démonstration ne contient pas
d’unsafe, conformément à ADR-149 D2.
5. Ce que cet incrément débloque
Le portail — le mécanisme par lequel un utilisateur désigne un fichier et où l’application reçoit un descripteur plutôt qu’un chemin, sans avoir jamais eu le droit d’ouvrir quoi que ce soit. C’est le prérequis de l’enveloppe par défaut étroite d’ADR-150 (question ouverte n° 1) : sans portail, toute application demandera un accès fichier large, et le modèle d’octroi se dégradera en « tout le monde demande tout ».
Or l’API cliente d’un portail est un appel AirCom qui rend un descripteur. Si l’application
doit écrire de l’unsafe pour l’appeler, le portail est inutilisable par la population qu’il
protège.
6. Deux points à trancher avant de commencer
Additif ou remplaçant ? Les signatures d’air-com peuvent recevoir des méthodes *_owned
en plus des existantes, ou changer directement. Le remplacement est plus propre et
ADR-001 est encore en-cours, donc AirCom n’est pas scellé ;
l’additif est plus prudent mais laisse deux chemins, donc laisse subsister le mauvais. Ma
recommandation : remplacer, précisément parce que le but de l’incrément est qu’il n’y ait
qu’un chemin par défaut.
Le data plane sait-il recevoir un descripteur quelconque ? receive_capability rend
aujourd’hui un Vec<u8> — le contenu d’un segment de mémoire partagée, pas un OwnedFd. Le
chemin est donc prouvé pour les segments memfd, mais un portail doit remettre un descripteur
arbitraire (un fichier ouvert par le sélecteur). C’est un manque distinct, hors du périmètre de
cet incrément, mais qu’il faut inscrire maintenant pour ne pas le redécouvrir au moment de
concevoir le portail.
Licence du document : MPL 2.0 Statut : note de conception. Instruit un incrément d’ADR-001 ; ne décide rien seule.