Processus

Les fonctionnalités du module std::process de la bibliothèque standard Rust permettent de…

  • Contrôler le processus associé au programme en cours d’exécution.
  • Exécuter d’autres programmes en contrôlant leur exécution et en échangeant des données avec eux via leurs entrées/sorties standard.

Si l’on compare aux fonctionnalités standard C++ héritées du C, les possibilités de contrôle du processus actif sont comparables, mais la gestion des processus enfants est beaucoup plus complète et se rapproche plus du module subprocess de la bibliothèque standard Python.

Contrôle du processus actif

Pour arrêter le processus actif, on trouve d’abord les fonctions exit() et abort(). Elles fonctionnent comme leur homologues C/++, mais leurs effets pervers sont mieux documentés.

Contrairement à C/++, Rust n’expose pas de nuances plus fines d’exit() comme quick_exit(), _Exit() et atexit(), car l’expérience du C++ montre que ces missions sont généralement mieux assurées en privilégiant une gestion normale des erreurs qui se propage jusqu’à main(), et en réservant l’utilisation de exit() aux situations exceptionnelles.

Le module std::process définit également le trait Termination, qui représente l’ensemble des types pouvant être retournés en résultat de la fonction main().

L’idée générale est qu’on peut soit retourner directement un code de statut comme en C/++, soit retourner un type qui peut être converti vers deux codes de statut standard ExitCode::SUCCESS et ExitCode::FAILURE. La conversion associée aux types erreur affiche la description Debug de l’erreur sur stderr avant d’arrêter le programme :

use std::fmt::{Debug, Formatter, self};

// Type erreur minimal compatible avec Termination
// (Un type erreur plus complet implémenterait aussi Error)
struct BadMood;
//
impl Debug for BadMood {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        writeln!(f, "Sorry, not in the mood to do this today")
    }
}

// Démonstration de l'effet sur la sortie de main()
fn main() -> Result<(), BadMood> {
    Err(BadMood)
}

Un ajout mineur par rapport à ce qui est disponible en C/++ est la fonction id(), qui permet de récupérer le PID du programme en cours d’exécution.

Et une limitation mineure par rapport au C/++ est que les fonctionnalités de gestion des signaux Unix et de setjmp/longjmp de la bibliothèque standard C ne sont pas exposées :

  • Il est difficile d’exposer la gestion de signaux Unix sans risque de comportement indéfini pour l’utilisateur, et ils n’ont pas leur place dans une bibliothèque standard ayant vocation à être portable vers tous les OS, donc ce travail est sous-traité à des bibliothèques tierces.
  • setjmp/longjmp, qui est plus ou moins équivalent à un goto sans restriction, n’est pas du tout supporté en Rust. C’est un comportement indéfini d’appeler ces fonctions, et le compilateur peut mieux optimiser le code en présumant que ça n’arrivera pas.

Exécution de programmes

Le seul outil fourni par la bibliothèque standard C++ pour exécuter d’autres programmes est system(), qui prend en paramètre une commande, fait exécuter cette commande par le shell, et retourne le code de statut résultant. Il y a de très nombreux problèmes avec cette logique :

  • On ne sait pas quel shell est utilisé, et donc comment la commande va être interprétée (par exemple, les shells Windows sont très différents des shells Unix, eux-même assez divers).
  • Il faut faire attention au contenu des variables d’environnement (notamment les PATHs), l’interprétation de la commande par le shell peut en dépendre de façon indésirable.
  • Ce fonctionnement basé sur la génération de code shell, dont l’interprétation est sensible à l’environnement d’exécution, est difficile à sécuriser face à des utilisateurs malveillants.
  • On ne peut pas échanger avec le programme en cours d’exécution via stdin, stdout et stderr, ce qui est nécessaire pour de nombreux outils en ligne de commande.
  • On est forcé d’attendre la fin du programme, c’est compliqué de l’interrompre en cours de route si il prend trop de temps ou l’utilisateur a changé d’avis.

Pour toutes ces raisons, il est préférable de prendre exemple sur Python et ne pas exposer le shell, mais plutôt la fonctionnalité plus bas niveau du système d’exploitation : créer un processus qui exécute un certain binaire avec certains arguments, dans un certain environnement. Puis suivre l’exécution du programme, échanger via ses entrées/sorties standard, le tuer si il faut…

C’est donc le modèle qu’a repris Rust, en adaptant la conception d’API à ses besoins :

#![allow(unused)]
fn main() {
use std::process::Command;

// Commande équivalente à "ls / /usr", exécutée de façon synchrone et avec
// héritage de l'environnement parent pour simplifier cet exemple.
//
// Notez qu'il faut séparer les arguments nous-même : puisque nous ne passons
// pas par le shell, nous devons faire ce travail à sa place.
//
let sortie = Command::new("ls")
                     .args(["/", "/usr"])
                     .output()
                     .expect("sortie");

// On s'attend à ce que cette exécution réussisse et ne produise rien sur stderr
assert!(sortie.status.success());
assert!(sortie.stderr.is_empty());

// La sortie stdout est une séquence d'octets. Pour la traiter comme une chaîne,
// nous devons spécifier comment les octets non UTF-8 seront traités.
println!("Octets bruts : {:02x?}", sortie.stdout);
let stdout = String::from_utf8_lossy(&sortie.stdout[..]);
println!("\n--- Interprétation textuelle ---\n{stdout}--------------------------------");
}

Si vous voulez en savoir plus, le point d’entrée est la création d’un objet Command, qui sert à paramétrer un processus avant exécution. Le style d’API utilisé s’appelle builder pattern, c’est une des façons usuelles de gérer des paramètres optionnels en Rust.