III. Aller plus loin
2. La compilation conditionnelle
Si vous souhaitez qu'une partie de votre code soit compilée mais seulement dans certaines conditions, par exemple sur un système d'exploitation en particulier, il est possible de le faire avec la compilation conditionnelle.
Par-exemple pour savoir sur quelle système le programme est compilé, on va écrire en C :
#ifdef linux
#define SYSTEM "linux"
#elif _WIN32
#define SYSTEM "windows"
#endif
void show_system() {
printf("%s", SYSTEM);
}
Et en Rust on va écrire :
Run#[cfg(target_os = "linux")]
const SYSTEM: &str = "linux";
#[cfg(target_os = "windows")]
const SYSTEM: &str = "windows";
fn show_system() {
println!("{}", SYSTEM);
}
C'est donc avec l'attribut cfg
que la compilation conditionnelle est gérée.
Ajouter des conditions dans l'attribut cfg
Avec le code précédent, si on compile sur un autre système que Windows ou Linux, la compilation va échouer car SYSTEM
ne sera pas défini. Pour contourner le problème, il nous suffit de rajouter une autre déclaration de SYSTEM
dans le cas où le système n'est ni Linux ni Windows :
Run#[cfg(not(any(target_os = "linux", target_os = "windows")))]
const SYSTEM: &str = "inconnu";
cfg
peut donc prendre des conditions qui peuvent être imbriquées. Dans l'exemple ci-dessus, nous avons utilisé not
et any
. Il existe une troisième condition : all
. Expliquons ce que chacun de ces attributs fait :
all
renverratrue
tant qu'aucun de ses arguments ne renvoiefalse
.any
renverratrue
tant qu'au moins un de ses arguments renvoietrue
.not
inverse la condition. C'est un équivalent de!
. Il ne prend qu'un seul argument.
Donc pour résumer :
Run#[cfg(all())] // true
#[cfg(any())] // false
Arguments de cfg
Jusqu'à présent, nous n'avons vu que target_os
, cependant il en existe bien d'autres :
target_arch
: Correspond à l'architecture du CPU. Par-exemplex86_64
,arm
,aarch64
...target_family
: Une "famille" de système d'exploitations commewindows
,unix
ouwasm
.target_endian
: Correspond à l'endianness du CPU. Peut prendre comme valeurbig
ousmall
.target_pointer_width
: Correspond à la taille d'un pointeur. Ce doit être une puissance de 2. Par-exemple16
,32
,64
...feature
: Les features dans Rust sont déclarées dans le fichierCargo.toml
comme déjà évoqué dans le chapitre sur "Cargo" justement. Elles permettent de rendre certaines fonctionnalités optionnelles pour pouvoir par-exemple compiler plus rapidement, générer un binaire plus petit, etc.
Il existe aussi des cas sans valeur associée :
test
: Quand on compile notre programme avec--test
pour lancer les tests unitaires. On revient sur les tests unitaires un peu plus loin dans ce livre.doc
: Quand on est en train de générer la documentation pour notre crate. Cela peut être utile dans certains cas pour unifier l'API visible dans la documentation.doctest
: On on lance les tests de la documentation.
Il y a encore beaucoup d'autres valeurs possible. Une liste plus exhaustive est disponible dans la référence.
L'attribut cfg_attr
Imaginons que vous ne vouliez générer les implémentations du trait Debug
via derive
uniquement lorsque la feature debug
est activée. On pourrait écrire :
Run#[cfg(feature = "debug")]
#[derive(Debug)]
pub struct Struct;
#[cfg(not(feature = "debug"))]
pub struct Struct;
Cependant ce n'est pas très pratique, surtout si on doit dupliquer beaucoup de code. C'est là que cfg_attr
devient utile. Plutôt que de dupliquer ce code, on peut écrire :
Run#[cfg_attr(feature = "debug", derive(Debug))]
pub struct Struct;
Le premier argument de cfg_attr
est la condition de compilation. Le deuxième est l'attribut que l'on souhaite générer si la condition du premier argument est satisfaite.
Donc si vous voulez utiliser un attribut mais seulement dans certaines conditions, utilisez cfg_attr
.
La macro cfg!
Voici le dernier cas pour la compilation conditionnelle : la macro cfg!
. Reprenons notre premier exemple :
Runfn show_system() {
if cfg!(target_os = "linux") {
println!("linux");
} else if cfg!(target_os = "windows") {
println!("windows");
} else {
println!("inconnu");
}
}
Comme la condition dans cfg!
sera remplacée par true
ou false
au moment de la compilation quand la macro sera étendue ("expanded" en anglais), si on compile sur Linux, le code ressemblera à ça :
Runfn show_system() {
if true {
println!("linux");
} else if false {
println!("windows");
} else {
println!("inconnu");
}
}
Et comme le compilateur voit au moment de la compilation que les conditions des branches sont déjà resolues, il va simplement les supprimer. Ce qui va donner :
Runfn show_system() {
println!("linux");
}
Vous savez maintenant comment gérer la compilation conditionnelle en Rust.