Généralités sur le langage
Nommage
La convention de nommage employée par la bibliothèque standard est de facto le standard pour le nommage des éléments des programmes écrits en Rust. Un effort a été fait pour formaliser ces conventions de nommage, d'abord dans la RFC 430, puis dans le document des Rust API Guidelines.
La règle de base consiste à utiliser :
UpperCamelCase
pour les types, traits et valeurs d'énumérations ;snake_case
pour les fonctions, méthodes, macros, variables et modules ;SCREAMING_SNAKE_CASE
pour les variables statiques et les constantes ;'lowercase
pour les durées de vie (lifetimes).
Les Rust API Guidelines recommandent également des conventions de nommage plus précises pour certaines constructions particulières :
- (C-CONV) pour les méthodes de conversion (
as_
,to_
,into_
) ; - (C-GETTER) pour les accesseurs ;
- (C-ITER) pour les méthodes produisant des itérateurs ;
- (C-ITER-TY) pour les types itérateur ;
- (C-FEATURE) pour les noms de features ;
- (C-WORD-ORDER) pour la cohérence sur l'ordre des mots.
Règle LANG-NAMING
Le développement d'une application sécurisée doit suivre les conventions de nommage décrites dans les Rust API Guidelines.
Code unsafe
L'utilisation conjointe du système de types et du système d'ownership vise à
apporter un haut niveau de sûreté quant à la gestion de la mémoire dans les
programmes écrits en Rust. Le langage permet alors d'éviter les débordements
mémoire, la construction de pointeurs nuls ou invalides, et les problèmes
d'accès concurrents à la mémoire.
Pour effectuer des actions considérées risquées comme des appels système, des
conversions de types ou la manipulation directe de pointeurs mémoire, le
langage fournit le mot-clé unsafe
.
Règle LANG-UNSAFE
Pour un développement sécurisé, les blocs
unsafe
doivent être évités. Ci-dessous, nous listons les seuls cas pour lesquels des blocsunsafe
peuvent être utilisés, à la condition que leur usage soit justifié :
L'interfaçage entre Rust et d'autres langages (FFI) permet la déclaration de fonctions dont l'implantation est faite en C, en utilisant le préfixe
extern "C"
. Pour utiliser une telle fonction, le mot-cléunsafe
est requis. Un wrapper "sûr" doit être défini pour que le code C soit finalement appelé de façon souple et sûre.Pour la programmation des systèmes embarqués, on accède souvent aux registres et à d'autres ressources au travers d'adresses mémoire fixées Dans ce cas, des blocs
unsafe
sont nécessaires afin de pouvoir initialiser et déréférencer des pointeurs en Rust pour ces adresses. Afin de minimiser le nombre de déclarationsunsafe
pour permettre au développeur de facilement identifier les accès critiques, une abstraction adaptée (structure de données ou module) doit être mise en place.Une fonction peut être marquée globalement comme non sûre (en préfixant sa déclaration par le mot-clé
unsafe
) lorsqu'elle exhibe inévitablement des comportements non sûrs en fonction de ses arguments. Par exemple, cela arrive lorsqu'une fonction doit déréférencer un pointeur passé en argument.À l'exception de l'un ou plusieurs de ces cas
#![forbid(unsafe_code)]
doit apparaître dans à la racine de la crate (typiquementmain.rs
oulib.rs
) afin de générer des erreurs de compilation dans le cas ou le mot-cléunsafe
est utilisé dans le projet.
Dépassement d'entiers
Bien que des vérifications soient effectuées par Rust en ce qui concerne les potentiels dépassements d'entiers, des précautions doivent être prises lors de l'exécution d'opérations arithmétiques sur les entiers.
En particulier, il doit être noté que les profils de compilation debug et
release produisent des variations de comportements quant à la gestion des
dépassements d'entiers. Dans la configuration debug, un dépassement provoque
la terminaison du programme (panic
), tandis que dans la configuration
release la valeur calculée est silencieusement tronquée en fonction de la
valeur maximum qui peut être stockée pour le type considéré.
Ce comportement peut être rendu explicite en utilisant le type générique
Wrapping
, ou les opérations sur les entiers overflowing_<op>
et
wrapping_<op>
(la partie <op>
étant remplacée par le type de calcul :
add
, mul
, sub
, shr
, etc.).
use std::num::Wrapping; use std::panic; fn main() { let x: u8 = 242; let result = panic::catch_unwind(|| { println!("{}", x + 50); // panique en mode debug, affiche 36 en mode release. }); if result.is_err() { println!("panic"); } println!("{}", x.overflowing_add(50).0); // affiche toujours 36. println!("{}", x.wrapping_add(50)); // affiche toujours 36. println!("{}", Wrapping(x) + Wrapping(50)); // affiche toujours 36. // panique toujours : let (res, c) = x.overflowing_add(50); let result = panic::catch_unwind(|| { if c { panic!("custom error"); } else { println!("{}", res); } }); if result.is_err() { println!("panic"); } }
Règle LANG-ARITH
Lorsqu'une opération arithmétique peut produire un dépassement d'entier, les fonctions spécialisées
overflowing_<op>
,wrapping_<op>
ou le typeWrapping
doivent être utilisés.
Gestion des erreurs
Le type Result
est la façon privilégiée en Rust pour décrire le type de retour
des fonctions dont le traitement peut échouer. Un objet Result
doit être
testé et jamais ignoré.
Recommandation LANG-ERRWRAP
Une crate peut implanter son propre type
Error
qui peut contenir toutes les erreurs possibles. Des précautions supplémentaires doivent être prises : ce type doit être exception-safe (RFC 1236) et implémenter les traitsError + Send + Sync + 'static
ainsi queDisplay
.
Recommandation LANG-ERRDO
L'opérateur
?
doit être utilisé pour améliorer la lisibilité du code. La macrotry!
ne doit pas être utilisée.
Des crates tierces peuvent être utilisées pour faciliter la gestion d'erreurs. La plupart (failure, snafu, thiserror) proposent la création de types d'erreurs personnalisées qui implémentent les traits nécessaires et permettent l'encapsulation d'autres erreurs.
Une autre approche (notamment proposée dans anyhow) consiste à envelopper automatiquement les erreurs dans un seul type d'erreur universel. Une telle approche ne devrait pas être utilisée dans des bibliothèques ou des systèmes complexes parce qu'elle ne permet pas de fournir de contexte sur les erreurs ainsi initialement enveloppées, contrairement à la première approche.
Panics
La gestion explicite des erreurs (Result
) doit être préférée à la place de
l'utilisation de la macro panic
. La cause de l'erreur doit être rendue
disponible, et les erreurs trop génériques doivent être évitées.
Les crates fournissant des bibliothèques ne doivent pas utiliser de fonctions
ou d'instructions qui peuvent échouer en engendrant un panic
.
Des motifs courants de code qui provoquent des panic
sont :
- une utilisation de
unwrap
ou deexpect
; - une utilisation de
assert
; - un accès non vérifié à un tableau ;
- un dépassement d'entier (en mode debug) ;
- une division par zéro ;
- l'utilisation de
format!
pour le formatage d'une chaîne de caractères.
Règle LANG-NOPANIC
Les fonctions et instructions qui peuvent causer des
panic
à l'exécution ne doivent pas être utilisées.
Règle LANG-ARRINDEXING
L'indice d'accès à un tableau doit être testé, ou la méthode
get
doit être utilisée pour récupérer uneOption
.
FFI et panic
s
Lorsque du code Rust est appelé depuis du code écrit dans un autre langage (par exemple, du code C), le code Rust doit être écrit de sorte à ne jamais pouvoir paniquer. Dérouler (unwinding) depuis le code Rust vers le code étranger résulte en un comportement indéfini.
Règle LANG-FFIPANIC
Le code Rust appelé depuis une FFI doit soit être assuré de ne pas paniquer, soit utiliser
catch_unwind
ou le modulestd::panic
pour s'assurer qu'il ne va pas abandonner un traitement puis que l'exécution retourne dans le langage appelant dans un état instable.
Il est porté à l'attention du développeur que catch_unwind
ne va traiter que
les cas de panic
, et va préserver les abandons de processus causés par
d'autres raisons.