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

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’unsafe pour 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 :

CrateLignesRôle
air-com-schema2 555Schéma Cap’n Proto v1, code généré committé (ADR-040)
air-com-proto4 640Cœur sans-IO pur et fuzzable : framer, codec, handshake, session, invoke, contrôle de flux, ring, registre
air-com4 088Pilote 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ûrseqpacket_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 via AirClassCell::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 unsafe sur les deux exemples rend 0, et aucun extern "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.