Acheter version papier

Rust

Fork me on GitHub

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 renverra true tant qu'aucun de ses arguments ne renvoie false.
  • any renverra true tant qu'au moins un de ses arguments renvoie true.
  • 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-exemple x86_64, arm, aarch64...
  • target_family : Une "famille" de système d'exploitations comme windows, unix ou wasm.
  • target_endian : Correspond à l'endianness du CPU. Peut prendre comme valeur big ou small.
  • target_pointer_width : Correspond à la taille d'un pointeur. Ce doit être une puissance de 2. Par-exemple 16, 32, 64...
  • feature : Les features dans Rust sont déclarées dans le fichier Cargo.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.