Rust non sûr (unsafe Rust) et FFI
Rust tire sa force de ses garanties de sécurité strictes vérifiées à la compilation. Cependant, l’ordinateur sous-jacent et les systèmes d’exploitation ne partagent pas ces contraintes. Pour interagir directement avec le matériel, écrire des structures de données ultra-optimisées ou communiquer avec d’autres langages, Rust fournit une trappe de décompression : le mot-clé unsafe.
Les « super-pouvoirs » d’unsafe Rust
Le mot-clé unsafe ne désactive pas le vérificateur de cycle de vie (borrow checker) et ne supprime pas les vérifications de types. Il donne simplement accès à cinq « super-pouvoirs » spécifiques que le compilateur ne peut pas garantir comme sûrs :
- Déréférencer des pointeurs nus (raw pointers).
- Appeler une fonction ou une fonction externe non sûre (
unsafe). - Implémenter un trait non sûr (comme
SendouSyncmanuellement). - Modifier un état statique mutable.
- Accéder aux champs d’une
union.
Pointeurs nus (*const T et *mut T)
Contrairement aux références (&T et &mut T), les pointeurs nus peuvent ignorer les règles d’ownership : ils peuvent être nuls, être clonés librement, et pointer vers de la mémoire partagée en lecture/écriture simultanée. Leur création est sûre, mais leur déréférencement doit se faire dans un bloc unsafe.
fn main() {
let mut num = 42;
// Création légale et sûre de pointeurs nus
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
// Le déréférencement nécessite obligatoirement un bloc unsafe
unsafe {
println!("r1 pointe vers : {}", *r1);
*r2 = 1337;
println!("r2 a modifié la valeur à : {}", *r2);
}
}
Le contrat de confiance : abstraction sûre
Le code unsafe ne doit pas fuiter dans l’API publique de nos modules. La philosophie de Rust est d’envelopper (encapsulate) le code potentiellement dangereux derrière une interface publique totalement sûre (safe abstraction).
C’est ainsi que sont développés les types de la bibliothèque standard comme Vec, String ou Rc.
// Exemple : Création d'une fonction de découpe de slice (similaire à la std)
fn decouper_en_deux_mut(slice: &mut [i32], indice: usize) -> (&mut [i32], &mut [i32]) {
let longueur = slice.len();
let pointeur_nu = slice.as_mut_ptr();
assert!(indice <= longueur);
// Le borrow checker interdirait de retourner deux références mutables
// issues du même slice d'origine. On passe par les pointeurs nus.
unsafe {
(
std::slice::from_raw_parts_mut(pointeur_nu, indice),
std::slice::from_raw_parts_mut(pointeur_nu.add(indice), longueur - indice),
)
}
}
Interopérabilité avec le C avec FFI (Foreign Function Interface)
La FFI permet à Rust d’appeler du code écrit en C, et inversement, de compiler une bibliothèque Rust pour qu’elle soit consommable par du C, du C++, ou des langages de plus haut niveau (Python, Node.js via des bindings).
Appeler du code C depuis Rust
Pour appeler une fonction C, Rust a besoin de déclarer la signature de la fonction externe dans un bloc extern "C". Comme Rust ne peut pas vérifier la validité du code écrit en C, l’appel de ces fonctions est intrinsèquement unsafe.
// Liaison avec la bibliothèque standard C (abs sur Linux/macOS)
unsafe extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
// Rust fait confiance à la signature déclarée
let resultat = abs(-25);
println!("Valeur absolue depuis le C : {}", resultat);
}
}
Exposer du code Rust au C
Pour qu’une fonction Rust soit appelable depuis le C, elle doit respecter deux conditions :
#[no_mangle]: Indique au compilateur de ne pas modifier le nom de la fonction dans le binaire (le mangling permet normalement de gérer les namespaces et surcharges en Rust).extern "C": Force la fonction à utiliser l’ABI (Application Binary Interface) standard du C pour le passage des arguments.
#[unsafe(no_mangle)]
pub extern "C" fn additionner_en_rust(a: i32, b: i32) -> i32 {
a + b
}
Outils de vérification : le comportement indéfini
Écrire du code unsafe expose au comportement indéfini (UB = Undefined Behavior).
Rust dispose d’outils pour valider la correction du code non sûr :
- Miri : Un interpréteur de code intermédiaire (MIR) pour Rust. Il peut exécuter vos suites de tests et détecter instantanément les violations d’alignement, les accès mémoire hors-limites, les fuites de mémoire ou les violations des règles d’aliasing des références.
cargo diehardetcargo-valgrindpermettent de coupler les tests avec des outils d’analyse dynamique de la mémoire.
# Tester son code avec l'interpréteur Miri
rustup component add miri
cargo miri test