Et quel est le compromis?
La possibilité de choisir des fonctionnalités de compilation dans Rust peut améliorer les performances, la taille, la maintenabilité, la sécurité et la portabilité de votre code.
Vous trouverez ci-dessous quelques arguments expliquant pourquoi vous devriez utiliser de manière proactive les fonctionnalités lorsque vous consommez des dépendances et les proposer aux autres utilisateurs de la bibliothèque.
L’utilisation d’indicateurs de fonctionnalité dans Rust peut améliorer les performances du code résultant. En n’incluant que le code nécessaire à une application spécifique, vous pouvez éviter la surcharge de code inutilisé ou inutile.
Bien qu’il existe des optimisations du compilateur pour supprimer le code mort, cela peut néanmoins aboutir à des programmes plus rapides et plus efficaces (et faciliter la vie du compilateur).
La taille globale du binaire résultant est influencée par les dépendances que vous incluez et la façon dont vous les utilisez.
La sélection des fonctionnalités peut aider le binaire résultant à être plus petit, ce qui profite aux applications qui doivent être distribuées ou déployées dans des environnements à ressources limitées.
J’ai récemment eu une dépendance en amont de rupture, où j’ai eu la chance d’avoir ce code de rupture en amont sous un indicateur de fonctionnalité – pour une fonctionnalité que je n’utilisais pas.
En attendant la mise à jour de la bibliothèque en amont, j’ai supprimé la fonctionnalité de mon projet local, qui fonctionnait à nouveau correctement. Cela signifie que vous pouvez améliorer la maintenabilité du code Rust en permettant aux développeurs d’inclure ou d’exclure des fonctionnalités spécifiques de manière sélective.
Statistiquement parlant, plus vous dépendez de code, plus le risque de problème de sécurité est élevé. Dépendre uniquement des fonctionnalités dont vous avez besoin pour réduire les risques d’un problème de sécurité est une réflexion sur la sécurité dès la conception, et une caisse qui s’offre « en morceaux » contribue à cela.
Il existe également des moyens de sélectionner différentes implémentations de la même fonctionnalité en fonction de votre degré de confort avec la sécurité d’une implémentation. Par exemple, vous préférerez peut-être une implémentation TLS native de Rust à une implémentation basée sur C car Rust est un langage sûr, et certains caisses comme Reqwest proposent une sélection de backends TLS.
En tant que langage compilé, un aspect important des feature flags est d’améliorer la portabilité de votre code.
Vous pouvez inclure ou exclure de manière sélective des fonctionnalités spécifiques pour rendre votre code plus portable sur différentes plateformes et environnements.
C et C++ ont toujours été les archétypes du code portable compilé déployé sur de nombreuses plates-formes et architectures de processeur.
C++ n’a pas de fonctionnalité intégrée directement équivalente à la possibilité de sélectionner les fonctionnalités de compilation dans Rust. Cependant, C++ a un certain nombre de directives de préprocesseur qui peut être utilisé pour inclure ou exclure certains codes au moment de la compilation de manière sélective.
Cela peut offrir certains des mêmes avantages que les drapeaux de fonctionnalités dans Rust. Pourtant, c’est compliqué et difficile à découvrir – à la fois en tant que programmeur cherchant à intégrer une base de code existante et en tant que consommateur cherchant à activer ou désactiver des fonctionnalités.
Pour activer un indicateur de fonctionnalité spécifique pour une caisse spécifique, vous pouvez utiliser le default-features = false
et features
attributs dans la caisse Cargo.toml
dossier.
Par exemple:
[dependencies]
my-crate = { default-features = false, features = ["my-feature"] }
Pour activer un indicateur de fonctionnalité pour un morceau de code spécifique, vous pouvez utiliser le #[cfg(feature = "my-feature")]
attribut. Par exemple:
#[cfg(feature = "my-feature")]
fn my_function() {
// Code that is only included when the "my-feature" flag is enabled
}
Pour activer un indicateur de fonctionnalité pour un module spécifique, vous pouvez utiliser le #[cfg(feature = "my-feature")]
attribut sur le mod
déclaration. Par exemple:
#[cfg(feature = "my-feature")]
mod my_module {
// Code that is only included when the "my-feature" flag is enabled
}
Pour activer un indicateur de fonctionnalité pour une structure ou une énumération spécifique avec derive
vous pouvez utiliser le #[cfg_attr(feature = "my-feature", derive(...))]
attribut. Par exemple:
#[cfg_attr(feature = "my-feature", derive(Debug, PartialEq))]
struct MyStruct {
// Fields and methods that are only included when the "my-feature" flag is enabled
}
Voici comment activer ou désactiver la prise en charge d’une plate-forme spécifique :
#[cfg(target_os = "linux")]
mod linux_specific_code {
// Linux-specific code goes here...
}
Et comment activer ou désactiver une implémentation spécifique d’un trait :
#[cfg(feature = "special_case")]
impl MyTrait for MyType {
// Implementation of trait for special case goes here...
}
Comment activer ou désactiver un scénario de test spécifique :
#[cfg(feature = "expensive_tests")]
#[test]
fn test_expensive_computation() {
// Test that performs expensive computation goes here...
}
Voici le code pour activer ou désactiver un benchmark spécifique :
#[cfg(feature = "long_benchmarks")]
#[bench]
fn bench_long_running_operation(b: &mut Bencher) {
// Benchmark for a long-running operation goes here...
}
Pour activer une fonctionnalité uniquement lorsque plusieurs indicateurs sont définis, vous pouvez utiliser la #[cfg(all(feature1, feature2, ...))]
attribut. Par exemple, pour activer un my_function()
seulement lorsque les deux my_feature1
et my_feature2
les drapeaux sont définis :
#[cfg(all(feature = "my_feature1", feature = "my_feature2"))]
fn my_function() {
// code for my_function
}
Pour activer une fonction uniquement lorsque l’un des multiples indicateurs est défini, vous pouvez utiliser la #[cfg(any(feature1, feature2, ...))]
attribut. Par exemple, pour activer un my_function()
quand soit le my_feature1
ou my_feature2
l’indicateur est défini :
#[cfg(any(feature = "my_feature1", feature = "my_feature2"))]
fn my_function() {
// code for my_function
}
Même module, mais pointez vers un chemin différent pour la mise en œuvre, puis extrayez une fonction à exposer à partir de ce module avec pub use
.
//! Signal monitor
#[cfg(unix)]
#[path = "unix.rs"]
mod imp;
#[cfg(windows)]
#[path = "windows.rs"]
mod imp;
#[cfg(not(any(windows, unix)))]
#[path = "other.rs"]
mod imp;
pub use self::imp::create_signal_monitor;
Voir https://github.com/shadowsocks/shadowsocks-rust/blob/master/src/monitor/mod.rs
Lorsque différents composants ont la même implémentation : vous pouvez tout proposer sous le soleil sans aucun inconvénient car seules les fonctionnalités sélectionnées sont compilées.
Le compromis est que vous avez maintenant une matrice de test plus grande, qui grandit de manière combinatoire avec chaque nouvelle alternative.
Dans cet exemple, la bibliothèque vous permet de choisir n’importe quel répartiteur auquel vous pouvez penser car les répartiteurs ont une interface bien définie et ne nécessitent aucun travail de votre part pour échanger :
//! Memory allocator
#[cfg(feature = "jemalloc")]
#[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
#[cfg(feature = "tcmalloc")]
#[global_allocator]
static ALLOC: tcmalloc::TCMalloc = tcmalloc::TCMalloc;
#[cfg(feature = "mimalloc")]
#[global_allocator]
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
#[cfg(feature = "snmalloc")]
#[global_allocator]
static ALLOC: snmalloc_rs::SnMalloc = snmalloc_rs::SnMalloc;
#[cfg(feature = "rpmalloc")]
#[global_allocator]
static ALLOC: rpmalloc::RpMalloc = rpmalloc::RpMalloc
Dans cet exemple, vous voyez comment laisser vos utilisateurs « ajouter » les fonctionnalités dont ils ont besoin, où vous pouvez choisir jusqu’où vous voulez aller :
//! Service launchers
pub mod genkey;
#[cfg(feature = "local")]
pub mod local;
#[cfg(feature = "manager")]
pub mod manager;
#[cfg(feature = "server")]
pub mod server;
Dans l’exemple ci-dessous, vous pouvez utiliser des blocs pour couvrir « artificiellement » des morceaux de code entiers sous une fonctionnalité :
#[cfg(feature = "local-tunnel")]
{
app = app.arg(
Arg::new("FORWARD_ADDR")
.short('f')
.long("forward-addr")
.num_args(1)
.action(ArgAction::Set)
.requires("LOCAL_ADDR")
.value_parser(vparser::parse_address)
.required_if_eq("PROTOCOL", "tunnel")
.help("Forwarding data directly to this address (for tunnel)"),
);
}
Dans cet exemple, nous incorporons des implémentations vides, car pourquoi payer le prix d’un appel de fonction si son corps renvoie toujours une valeur simpliste et vide ? (Ok(()
).
#[cfg(all(not(windows), not(unix)))]
#[inline]
fn set_common_sockopt_after_connect_sys(_: &tokio::net::TcpStream, _: &ConnectOpts) -> io::Result<()> {
Ok(())
}
Si les fonctionnalités sont si puissantes et éliminent de nombreuses méthodes primitives de compilation de code conditionnelle de C/C++, pourquoi ne pas les utiliser partout et toujours ? Voici quelques éléments que vous devriez considérer.
- Utiliser trop de fonctionnalités est une réalité. Dans le cas imaginaire et extrême, imaginez que vous aviez une fonctionnalité sur chaque module et fonction. Cela obligerait vos consommateurs à résoudre un casse-tête très difficile pour comprendre comment composer votre bibliothèque à partir de ses caractéristiques discrètes. C’est le danger des fonctionnalités. Vous voulez être modeste avec le nombre de fonctionnalités que vous proposez pour réduire une charge cognitive et pour que ces fonctionnalités soient des choses que les gens souhaitent supprimer ou ajouter.
- Les tests sont un autre gros problème avec les fonctionnalités. Vous ne savez jamais quelle combinaison de fonctionnalités vos utilisateurs sélectionneront, et chaque combinaison sélectionne un ensemble de code différent – et ces morceaux de code doivent interagir en douceur à la fois dans la compilation (compilation réussie). En logique (sans introduire de bugs), vous devez tester une combinaison de toutes les fonctionnalités avec toutes les autres fonctionnalités et créer un ensemble de fonctionnalités !
- Vous pouvez automatiser cela avec
xtaskops::powerset
– voir plus ici: https://github.com/jondot/xtaskops.