Environnement
Notre tour des fonctionnalités de programmation système de Rust va se terminer
vers le module std::env
.
Comme vous pouvez le deviner, il permet notamment de manipuler des variables
d’environnement. Mais on y trouve aussi d’autres spécificités des systèmes
d’exploitation sur lesquelles un programme Rust est susceptible de s’exécuter,
qui n’ont pas trouvé leur place ailleurs : arguments d’un exécutable,
répertoire de travail, etc.
Variables d’environnement
En C++, le seul utilitaire standard disponible pour interroger l’environnement
est std::getenv()
,
une fonction qui prend un nom de variable d’environnement en paramètre et
retourne un pointeur vers la valeur de la variable en sortie. Si la variable
n’a pas de valeur, le pointeur retourné est nul.
Cette conception a plusieurs conséquences :
- On n’a pas accès à la liste complète des variables d’environnement. Il faut
utiliser pour ça des utilitaires spécifiques à chaque OS, comme la variable
globale
environ
et le paramètre optionelenvp
sur les systèmes POSIX. - On ne peut pas modifier une variable d’environnement de façon portable. Il
faut utiliser pour ça des utilitaires spécifiques à chaque OS, comme la
fonction
setenv()
de POSIX. - L’une des utilisations les plus courantes des variables d’environnement est
la famille des variables
PATH
. Mais la bibliothèque standard C++ ne fournit aucun utilitaire pour les manipuler d’une façon indépendante du système d’exploitation. - La fonction
getenv()
ne peut pas être utilisée de façon sécurisée dans un programme multi-thread, car rien n’empêche un autre thread d’appeler des fonctions commesetenv()
en parallèle, et la conception degetenv()
ne permet pas de synchroniser implicitement les accès aux variables d’environnement au niveau de l’implémentation de la bibliothèque standard.
En comparaison, l’API fournie par Rust pour manipuler des variables d’environnement est à la fois plus complète et plus sécurisée :
- On peut lister les variables d’environnement avec
vars()
, lire une variable unique avecvar()
, modifier une variable avecset_var()
, et supprimer une variable avecremove_var()
. - Par défaut, les fonctions ci-dessus supposent que les variables
d’environnement contiennent du texte Unicode valide, tentent de le décoder en
UTF-8, et retournent une erreur si ça échoue. On peut accéder aux données
brutes des variables d’environnement avec des variantes des fonctions de
lecture ayant un suffixe
_os
, commevars_os()
. - Les fonctions
split_paths()
etjoin_paths()
permettent de manipuler une variable d’environnementPATH
de façon indépendante du système d’exploitation. - Les accès aux variables d’environnement par la bibliothèque standard Rust sont synchronisés entre threads. Le comportement indéfini observé en C++ n’est malheureusement pas complètement éliminé, mais n’est possible que si les threads utilisent directement les fonctions de l’OS sans passer par la bibliothèque standard Rust, ce qui réduit le risque d’accident.
Voici un exemple de manipulation de variables d’environnements en Rust :
#![allow(unused)] fn main() { println!("Mes variables d'environnement :"); for (cle, valeur) in std::env::vars() { print!("- {cle} : "); if cle.ends_with("PATH") { let chemins = std::env::split_paths(&valeur).collect::<Vec<_>>(); println!("{chemins:#?}"); } else { println!("{valeur:?}"); } } }
Arguments et chemin d’exécutable
En C++, la manière normale d’accéder aux arguments du programme est d’utiliser
les arguments spéciaux argc
et argv
de la fonction main()
, qui contiennent
respectivement le nombre d’arguments et un pointeur vers un char**
contenant
les valeurs des arguments. L’implémentation de la bibliothèque standard C
peut mettre un pointeur nul en premier argument, si elle ne le fait pas elle
est censée y mettre le nom du programme.
En résumé…
- On doit manipuler un tableau C, avec le risque habituel de se tromper sur l’itération et de lire en-dehors des bornes du tableau.
- On doit gérer la possibilité qu’il y ait des pointeurs nuls dans la liste des arguments.
- Seul l’exécutable peut avoir accès à la liste des arguments. Une bibliothèque
ne peut y avoir accès que si on lui transmet
argc
etargv
ou utilise des extensions spécifiques à un OS. - La liste des arguments est modifiable, et on ne peut pas en dépendre dans du
code opérant sous contraintes de sécurité. Par exemple, si un programme
setuid veut exécuter récursivement une copie de lui-même, il ne peut pas
utiliser
argv[0]
pour savoir comment il s’appelle, car c’est une donnée sous le contrôle de l’attaquant qui appelle le programme. - Plus généralement, le premier argument est difficile à interpréter car…
- Il peut contenir un chemin absolu ou relatif.
- Si le programme a été invoqué via un symlink, il peut contenir le nom du symlink ou le nom du programme (donc celui de la cible du symlink).
- Si le programme est renommé, il ne correspondra plus au réel chemin vers le programme sur le système de fichiers.
La bibliothèque standard Rust ne peut pas corriger l’ensemble de ces problèmes,
car certains se situent au niveau du contrat d’interface de base entre la libc
et les programmes. Mais elle en corrige autant que possible :
- On itère sur les arguments via l’itérateur haut niveau
args()
ou sa variante bas niveauargs_os()
, la nuance étant identique à celle entrevars()
etvars_os()
. - Les pointeurs nuls sont gérés par l’implémentation de la bibliothèque
standard, le programme Rust ne voit que des
String
(éventuellement vides). - Les arguments sont accessibles en tous points du programme et ne peuvent pas être modifiés via la bibliothèque standard.
- Les risques liés à l’interprétation du premier argument sont documentés, et
une fonction dédiée
current_exe()
est disponible pour tenter de déterminer le chemin absolu vers l’exécutable du programme (même si on ne peut toujours pas en dépendre dans du code opérant sous contraintes de sécurité).
Voici un exemple d’utilisation :
#![allow(unused)] fn main() { print!("Commande utilisée, selon la libc : "); for arg in std::env::args() { print!("{arg} "); } println!(); println!("Chemin d'exécutable, selon la libc : {:?}", std::env::current_exe()); }
Répertoires spéciaux
Jusqu’en C++17, il n’y avait pas de manière standard de lire et modifier le répertoire de travail du processus actif en C++, ni de connaître le chemin du répertoire standard des fichiers temporaires.
Dès sa première version stable en 2015, Rust a résolu ces problèmes avec les
fonctions
current_dir()
,
set_current_dir()
et
temp_dir()
de
std::env
. En 2017, C++ a rattrapé ce retard en intégrant des fonctions
similaires à sa nouvelle API std::filesystem
.
#![allow(unused)] fn main() { println!("Répertoire de travail : {:?}", std::env::current_dir()); println!("Répertoire temporaire : {:?}", std::env::temp_dir()); }
Au moment de la sortie de Rust v1, ça avait aussi semblé être une bonne idée de
fournir l’emplacement du répertoire de travail de l’utilisateur avec
home_dir()
. Mais un
examen plus approfondi survenu après la stabilisation de Rust a révélé que la
méthode utilisée pour obtenir cet emplacement était moins fiable que prévue
sous Windows (notamment en présence de couches d’émulation Unix comme MinGW et
Cygwin), et qu’il n’existait pas de méthode fiable sur cet OS.
La fonction home_dir()
est donc aujourd’hui dépréciée et ne devrait plus être
utilisée. A la place, il est recommandé de lui préférer des bibliothèques
dédiées comme dirs
.
Constantes diverses
Le module std::env
contient enfin un sous-module std::env::consts
, qui
contient quelques constantes spécifiques à la plate-forme cible :
ARCH
,FAMILY
etOS
identifient la plate-forme cible en suivant la même syntaxe que les options de configurationcfg!()
(voir le chapitre dédié pour plus d’informations). Cela permet de gérer ces options de configuration à l’exécution plutôt qu’à la compilation.EXE_SUFFIX
etEXE_EXTENSION
indiquent la manière standard de terminer un nom d’exécutable sur l’OS cible, avec ou sans le.
d’extension.- De même,
DLL_PREFIX
,DLL_SUFFIX
etDLL_EXTENSION
indiquent la manière standard de commencer et terminer un nom de bibliothèque partagée sur l’OS cible.
#![allow(unused)] fn main() { println!("Compilé pour les CPUs {} et l'OS {} (famille {})", std::env::consts::ARCH, std::env::consts::OS, std::env::consts::FAMILY); println!("Exemple de nom d'exécutable : echo{}", std::env::consts::EXE_SUFFIX); println!("Exemple de nom de bibliothèque : {}SDL{}", std::env::consts::DLL_PREFIX, std::env::consts::DLL_SUFFIX); }