Système de types
Traits de la bibliothèque standard
Trait Drop
: le destructeur
Les types implémentent le trait std::ops::Drop
dans le but d'effectuer
certaines opérations lorsque la mémoire associée à une valeur est réclamée.
Drop
est l'équivalent Rust d'un destructeur en C++ ou un finaliseur en Java.
Drop
agit récursivement, depuis la valeur externe vers les valeurs imbriquées.
Lorsqu'une valeur sort du scope (ou est explicitement relâchée avec
std::mem::drop
), elle est relâchée en deux étapes. La première étape a lieu
uniquement si le type de la valeur en question implémente le trait Drop
et
consiste en l'appel de la méthode drop
. La seconde étape consiste en la
répétition de processus de drop récursivement sur tous les champs que contient
la valeur. Il est à noter que l'implémentation de Drop
est
responsable uniquement de la valeur extérieure.
Tout d'abord, l'implémentation de Drop
ne doit pas être systématique. Elle est
nécessaire uniquement lorsque le type requiert un traitement logique à la
destruction. Drop
est typiquement utilisé dans le cas du relâchement des
ressources externes (connexions réseau, fichier, etc.) ou de ressources mémoire
complexes (smart pointers comme les Box
ou les Rc
par exemple). Au
final, il est probable que l'implémentation du trait Drop
contienne des blocs
unsafe
ainsi que d'autres opérations critiques du point de vue de la sécurité.
Recommandation LANG-DROP
Dans un développement sécurisé en Rust, l'implémentation du trait
std::ops::Drop
doit être justifiée, documentée et examinée par des pairs.
Ensuite, le système de types de Rust assure seulement la sûreté mémoire et,
du point de vue du typage, des drop
s peuvent tout à fait être manqués.
Plusieurs situations peuvent mener à manquer des drop
s, comme :
- un cycle dans la référence (par exemple avec
Rc
ouArc
) ; - un appel explicite à
std::mem::forget
(oucore::mem::forget
) (voir paragraphe à propos deforget
et des fuites de mémoire ; - un
panic
dans undrop
; - un arrêt du programme (et un
panic
lorsqueabort-on-panic
est activé).
Les drop
s manqués peuvent mener à l'exposition de données sensibles ou bien
encore à l'épuisement de ressources limitées et par là même à des problèmes
d'indisponibilité.
Règle LANG-DROP-NO-PANIC
Dans un développement sécurisé en Rust, l'implémentation du trait
std::ops::Drop
ne doit pas causer depanic
.
En plus des panic
s, les drop
s contenant du code critique doivent être
protégés.
Règle LANG-DROP-NO-CYCLE
Les valeurs dont le type implémente
Drop
ne doivent pas être incluses, directement ou indirectement, dans un cycle de références à compteurs.
Recommandation LANG-DROP-SEC
Certaines opérations liées à la sécurité d'une application à la fin d'un traitement (comme l'effacement de secrets cryptographiques par exemple) ne doivent pas reposer uniquement sur l'implémentation du trait
Drop
.
Les traits Send
et Sync
Les traits Send
et Sync
(définis dans std::marker
ou core::marker
) sont
des marqueurs utilisés pour assurer la sûreté des accès concurrents en Rust.
Lorsqu'ils sont correctement implémentés, ils permettent au compilateur Rust de
garantir l'absence de problèmes d'accès concurrents. Leurs sémantiques sont
définies comme suit :
- Un type est
Send
s’il est sûr d'envoyer (move) des valeurs de ce type vers un autre fil d'exécution. - Un type est
Sync
s’il est sûr de partager des valeurs de ce type par une référence immutable avec un autre fil d'exécution.
Ces deux traits sont des traits unsafe, c'est-à-dire que le compilateur Rust ne vérifie d'aucune manière que leur implémentation est correcte. Le danger est réel : une implémentation incorrecte peut mener à un comportement indéfini.
Heureusement, dans la plupart des cas, il n'est pas nécessaire de fournir une
implémentation. En Rust, la quasi-totalité des types primitifs implémente
Send
et Sync
, et dans la majorité des cas, Rust fournit de manière
automatique une implémentation pour les types composés. Quelques exceptions
notables sont :
- les pointeurs
raw
, qui n'implémentent niSend
, niSync
, puisqu'ils n'offrent aucune garantie quant à la sûreté ; - les références
UnsafeCell
, qui n'implémentent pasSync
(et par extensions, les référencesCell
etRefCell
non plus), puisqu'elles autorisent la mutabilité des valeurs contenues (interior mutability) ; - les références
Rc
, qui n'implémentent niSend
, niSync
, puisque les compteurs de références seraient partagés de manière désynchronisée.
L'implémentation automatique de Send
(respectivement Sync
) a lieu pour les
types composés (structures ou énumérations) lorsque tous les champs contenus
implémentent Send
(respectivement Sync
). Une fonctionnalité notable, mais
instable, de Rust (depuis 1.37.0) permet d'empêcher cette implémentation
automatique en annotant explicitement le type considéré avec une
négation d'implementation :
#![feature(option_builtin_traits)]
struct SpecialType(u8);
impl !Send for SpecialType {}
impl !Sync for SpecialType {}
L'implémentation négative de Send
ou Sync
est également utilisée dans la
bibliothèque standard pour les exceptions, et est automatiquement implémentée
lorsque cela est approprié. En résultat, la documentation générée est toujours
explicite : un type implémente soit Send
(respectivement Sync
), soit
!Send
(respectivement !Sync
).
En guise d'alternative stable à l'implémentation négative, il est possible
d'utiliser un champ typé par un type fantôme (PhantomData
) :
use std::marker::PhantomData;
struct SpecialType(u8, PhantomData<*const ()>);
Recommandation LANG-SYNC-TRAITS
Dans un développement sécurisé en Rust, l'implémentation manuelle des traits
Send
etSync
doit être évitée, et, si nécessaire, doit être justifiée, documentée et révisée par des pairs.
Les traits de comparaison : PartialEq
, Eq
, PartialOrd
, Ord
Les comparaisons (==
, !=
, <
, <=
, >
, >=
) en Rust reposent sur quatre
traits de la bibliothèque standard disponibles dans std::cmp
(ou core::cmp
pour une compilation avec no_std
) :
PartialEq<Rhs>
qui définit la relation d'équivalence partielle entre objets de typesSelf
etRhs
;PartialOrd<Rhs>
qui définit la relation d'ordre partiel entre les objets de typesSelf
etRhs
;Eq
qui définit la relation d'équivalence totale entre les objets du même type. Il s'agit d'un trait de marquage qui requiert le traitPartialEq<Self>
;Ord
qui définit la relation d'ordre total entre les objets du même type. Le traitPartialOrd<Self>
est alors requis.
Comme stipulé dans la documentation de la bibliothèque standard, Rust présuppose de nombreux invariants lors de l'implémentation de ces traits :
-
Pour
PartialEq
:-
Cohérence interne :
a.ne(b)
est équivalent à!a.eq(b)
, c.-à-d.,ne
est le strict inverse deeq
. Cela correspond précisément à l'implémentation par défaut dene
. -
Symétrie :
a.eq(b)
etb.eq(a)
sont équivalents. Du point de vue du développeur, cela signifie que :PartialEq<B>
est implémenté pour le typeA
(notéA: PartialEq<B>
).PartialEq<A>
est implémenté pour le typeB
(notéB: PartialEq<A>
).- Les deux implémentations sont cohérentes l'une avec l'autre.
-
Transitivité :
a.eq(b)
etb.eq(c)
impliquenta.eq(c)
. Cela signifie que :A: PartialEq<B>
.B: PartialEq<C>
.A: PartialEq<C>
.- Les trois implémentations sont cohérentes les unes avec les autres (ainsi qu'avec leurs implémentations symétriques).
-
-
Pour
Eq
:-
PartialEq<Self>
est implémenté. -
Réflexivité :
a.eq(a)
. Cela signifie quePartialEq<Self>
est implémenté (Eq
ne fournit aucune méthode).
-
-
Pour
PartialOrd
:-
Consistance de la relation d'égalité :
a.eq(b)
est équivalent àa.partial_cmp(b) == Some(std::ordering::Eq)
. -
Consistence interne :
a.lt(b)
ssia.partial_cmp(b) == Some(std::ordering::Less)
.a.gt(b)
ssia.partial_cmp(b) == Some(std::ordering::Greater)
.a.le(b)
ssia.lt(b) || a.eq(b)
.a.ge(b)
ssia.gt(b) || a.eq(b)
.
Il faut noter qu'en définissant seulement
partial_cmp
, la consistance interne est garantie par les implémentations par défaut delt
,le
,gt
, andge
. -
Antisymétrie :
a.lt(b)
(respectivementa.gt(b)
) impliqueb.gt(a)
(respectivementb.lt(b)
). Du point de vue du développeur, cela signifie que :A: PartialOrd<B>
.B: PartialOrd<A>
.- Les deux implémentations sont cohérentes l'une avec l'autre.
-
Transitivité :
a.lt(b)
etb.lt(c)
impliquenta.lt(c)
(également avecgt
,le
etge
). Cela signifie que :A: PartialOrd<B>
.B: PartialOrd<C>
.A: PartialOrd<C>
.- Les trois implémentations sont cohérentes les unes avec les autres (et avec leurs implémentations symétriques).
-
-
Pour
Ord
:-
PartialOrd<Self>
-
Totalité :
a.partial_cmp(b) != None
est toujours vrai. En d'autres mots, exactement une assertion parmia.eq(b)
,a.lt(b)
eta.gt(b)
est vraie. -
Cohérence avec
PartialOrd<Self>
:Some(a.cmp(b)) == a.partial_cmp(b)
.
-
Le compilateur ne vérifie aucun de ces prérequis, à l'exception des
vérifications sur les types. Toutefois, les comparaisons sont des éléments
importants puisqu'elles jouent un rôle tant dans les propriétés de vivacité
des systèmes critiques comme des ordonnanceurs ou des répartiteurs de charge
que dans les algorithmes optimisés qui peuvent éventuellement utiliser des
blocs unsafe
. Dans le premier cas d'usage, une mauvaise relation d'ordre
peut causer des problèmes de disponibilité comme des interblocages. Dans le
second cas, cela peut mener à des problèmes classiques de sécurité liés à des
violations de propriétés de sûreté mémoire. C'est là encore un atout que de
limiter au possible l'utilisation des blocs unsafe
.
Règle LANG-CMP-INV
Dans un développement sécurisé en Rust, l'implémentation des traits de comparaison standards doit respecter les invariants décrits dans la documentation.
Recommandation LANG-CMP-DEFAULTS
Dans un développement sécurisé en Rust, l'implémentation des traits de comparaison standard ne doit être effectuée que par l'implémentation des méthodes ne fournissant pas d'implémentation par défaut, dans le but de réduire le risque de violation des invariants associés auxdits traits.
Il existe un lint Clippy qui permet de vérifier que PartialEq::ne
n'est pas
défini lors d'une implémentation du trait PartialEq
.
Rust propose une façon de fournir automatiquement des implémentations par défaut
pour les traits de comparaison, au travers de l'attribut #[derive(...)]
:
- La dérivation de
PartialEq
implémentePartialEq<Self>
avec une égalité structurelle à condition que chacun des types des données membres implémentePartialEq<Self>
. - La dérivation de
Eq
implémente le trait de marquageEq
à condition que chacun des types des données membres implémenteEq
. - La dérivation de
PartialOrd
implémentePartialOrd<Self>
comme un ordre lexicographique à condition que chacun des types des données membres implémentePartialOrd
. - La dérivation de
Ord
implémenteOrd
comme un ordre lexicographique à condition que chacun des types des données membres implémenteOrd
.
Par exemple, le court extrait de code suivant montre comment comparer deux
valeurs de type T1
facilement. Toutes les assertions sont vraies.
#[derive(PartialEq, Eq, PartialOrd, Ord)] struct T1 { a: u8, b: u8 } fn main() { assert!(&T1 { a: 0, b: 0 } == Box::new(T1 { a: 0, b: 0 }).as_ref()); assert!(T1 { a: 1, b: 0 } > T1 { a: 0, b: 0 }); assert!(T1 { a: 1, b: 1 } > T1 { a: 1, b: 0 }); println!("tous les tests sont validés."); }
Attention
La dérivation des traits de comparaison pour les types composites dépend de l'ordre de déclaration des champs et non de leur nom.
D'abord, cela implique que changer l'ordre des champs modifie l'ordre des valeurs. Par exemple, en considérant le type suivant :
#[derive(PartialEq, Eq, PartialOrd, Ord)] struct T2{ b: u8, a: u8 };
on a
T1 {a: 1, b: 0} > T1 {a: 0, b: 1}
maisT2 {a: 1, b: 0} < T2 {a: 0, b: 1}
.Ensuite, si une comparaison sous-jacente provoque un
panic
, l'ordre peut changer le résultat à cause de l'utilisation d'un opérateur logique court- circuitant dans l'implémentation automatique.Pour les énumérations, les comparaisons dérivées dépendent d'abord de l'ordre des variants, puis de l'ordre des champs.
En dépit de ces avertissements sur les ordres dérivés, les comparaisons dérivées automatiquement sont bien moins sujettes à erreurs que des implémentations manuelles, et rendent le code plus court et plus simple à maintenir.
Recommandation LANG-CMP-DERIVE
Dans un développement sécurisé en Rust, l'implémentation des traits de comparaison standard doit être automatiquement dérivée à l'aide de
#[derive(...)]
lorsque l'égalité structurelle et l'ordre lexicographique sont nécessaires. Toute implémentation manuelle d'un trait de comparaison standard doit être justifiée et documentée.