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 à ungoto
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
PATH
s), 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
etstderr
, 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.