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

ADR-021 — Conventions transverses de la couche 0

Statut : Accepté. Document fondateur de la phase 0.

Catégorie : Méthode (conventions de design).

Contexte

Au fil de la spécification des différentes familles de la couche 0 (process, fs, mem, signal, time, net, ipc, security, system, et io_uring), plusieurs conventions transverses ont émergé. Ces conventions ne sont pas spécifiques à une famille ; elles s’appliquent uniformément à toute l’API de la couche 0.

Plutôt que les redocumenter dans chaque spec de famille, cet ADR les consigne au niveau du projet. Toute future addition à la couche 0 (ou aux couches qui suivent les mêmes conventions) doit s’y conformer.

Conventions

Convention 1 : Option<T> pour remplacer les sentinelles kernel

Les API kernel utilisent fréquemment des valeurs spéciales pour signifier “défaut”, “self”, ou “aucun”. Par exemple :

  • 0 dans setpgid(0, ...) signifie “le processus courant”.
  • 0 dans kill(pid, 0) signifie “tester l’existence sans envoyer de signal”.
  • nullptr dans diverses structures kernel signifie “champ absent”.

Le wrapper Air remplace ces sentinelles par Option<T> typé :

#![allow(unused)]
fn main() {
// Kernel : setpgid(0, 0)
// Air : setpgid(None, None)
pub fn setpgid(pid: Option<Pid>, pgid: Option<Pid>) -> Result<(), Errno>;

// Kernel : kill(pid, 0)
// Air : kill(pid, None)
pub fn kill(pid: Pid, signal: Option<Signal>) -> Result<(), Errno>;
}

Justification. L’Option<T> est explicitement typé, lisible, et empêche la confusion entre “PID 0” (qui n’existe pas) et “PID courant” (sentinelle). Application du Principe d’ingénierie 7 (verbosité au service de la clarté).

Convention 2 : EINTR remonté à l’appelant, jamais retried automatiquement

Le syscall EINTR indique qu’une opération bloquante a été interrompue par un signal avant complétion. La convention historique est ambiguë : certaines bibliothèques retentent automatiquement, d’autres remontent à l’appelant.

Air remonte systématiquement EINTR à l’appelant en couche 0. Pas de retry automatique.

#![allow(unused)]
fn main() {
// Si l'appelant veut retry, il le fait explicitement
loop {
    match fs::read(fd, &mut buf) {
        Ok(n) => return Ok(n),
        Err(Errno::EINTR) => continue,
        Err(e) => return Err(e),
    }
}
}

Justification. L’appelant a parfois besoin de savoir qu’un signal est arrivé pour réagir (par exemple, vérifier un drapeau de shutdown). Un retry automatique masquerait cette information. Pour les patterns où le retry est désiré, des helpers en couche 1 fourniront la boucle classique.

Convention 3 : Refus des wrappers génériques pour syscalls multiplexés

Plusieurs syscalls Linux sont multiplexés : un seul nom de syscall couvre 60+ opérations distinctes selon le premier argument. Exemples : prctl, fcntl, ioctl.

Air refuse d’exposer ces syscalls via un wrapper générique. À la place, chaque opération est exposée comme une fonction dédiée typée :

#![allow(unused)]
fn main() {
// PAS d'exposition générique comme :
// pub unsafe fn prctl(op: i32, arg2: u64, ...) -> Result<i32, Errno>;

// MAIS des fonctions individuelles typées :
pub fn set_no_new_privs() -> Result<(), Errno>;
pub fn get_no_new_privs() -> Result<bool, Errno>;
pub fn set_thread_name(name: &CStr) -> Result<(), Errno>;
pub fn get_thread_name() -> Result<CString, Errno>;
// ... etc
}

Justification. Les wrappers génériques prctl(op, arg2, arg3, ...) sont impossibles à utiliser safely : les types des arguments dépendent de l’opération, certaines opérations retournent des valeurs encodées différemment, certaines demandent des préconditions spécifiques. Exposer une API typée par opération concentre la complexité au point de définition (côté Air) plutôt qu’au point d’usage (côté appelant). Le coût en volume de code est compensé par la clarté et la sûreté.

Convention 4 : Pas d’allocation heap dans le happy path sauf nécessité documentée

La couche 0 vise à être performante et utilisable depuis tous les contextes, y compris des contextes contraints (pas de heap disponible, allocator custom, etc.).

Convention : pas d’allocation heap dans le happy path des fonctions wrapper sauf si :

  1. L’opération kernel demande intrinsèquement un buffer dynamique (par exemple, getdents64 qui lit un nombre variable d’entrées).
  2. La sémantique exige un type owned (par exemple, gethostname qui retourne une CString).

Dans ces cas, l’allocation est explicite et documentée.

Cas typique : les fonctions qui retournent un Result<Errno> n’allouent pas, même en cas d’erreur. L’Errno lui-même est NonZeroI32, sans allocation.

Justification. Performances prévisibles. Compatibilité future avec des allocateurs custom ou des contextes no_std. Respect du Principe d’ingénierie 5 (optimiser après mesure, mais sans dégrader gratuitement).

Convention 5 : Infallible pour les fonctions qui ne retournent jamais en succès

Certaines opérations Unix ne retournent jamais en cas de succès. Exemples :

  • execve : remplace l’image du processus, ne retourne que si échec.
  • exit_group : termine le processus.

Pour exit_group, le type de retour est ! (never type) :

#![allow(unused)]
fn main() {
pub fn exit_group(status: i32) -> !;
}

Pour execve, qui peut échouer, le type est Result<Infallible, Errno> :

#![allow(unused)]
fn main() {
pub fn execve(
    path: &CStr,
    argv: &[&CStr],
    envp: &[&CStr],
) -> Result<Infallible, Errno>;
}

Justification. Le Result<Infallible, Errno> est l’idiome Rust pour exprimer “soit ça retourne une erreur, soit ça ne retourne pas”. L’utilisateur peut écrire :

#![allow(unused)]
fn main() {
let _: Infallible = execve(path, &argv, &envp)?;
unreachable!("execve should not return on success");
}

Le type signale clairement que le succès est impossible dans le retour normal de la fonction.

Application

Ces 5 conventions s’appliquent à toute la couche 0, déjà spécifiée et future. Elles s’étendent par cohérence aux couches supérieures quand les patterns équivalents apparaissent.

Tout PR qui introduit une nouvelle fonction wrapper doit respecter ces conventions. Le reviewer vérifie en particulier :

  • Sentinelles kernel converties en Option<T> ?
  • EINTR remonté tel quel (ou retry justifié) ?
  • Pas d’API multiplexée générique ajoutée ?
  • Pas d’allocation heap injustifiée ?
  • Infallible ou ! utilisé quand approprié ?

Conséquences

Conséquence positive principale : cohérence transversale de l’API Air. Un développeur qui connaît une famille reconnaît immédiatement les patterns dans une autre famille.

Conséquence à accepter : volume de code accru par rapport à des wrappers génériques. Compensé par la clarté et la sûreté.

Statut futur

ADR immuable dans ses 5 conventions. Des conventions additionnelles peuvent émerger au fil du développement et seraient consignées dans un ADR successeur (ADR-021-bis) plutôt qu’amender celui-ci.


Licence du document : MPL 2.0 Statut : Document fondateur immuable.