Code non sûr et FFI#

Le système de possession (chapitre 5), les références (chapitre 6) et les traits Send/Sync (chapitre 13) forment un filet de sécurité vérifié à la compilation. Cependant, certaines opérations légitimes — interagir avec du matériel, appeler une bibliothèque C, construire une structure de données à base de pointeurs bruts — ne peuvent pas être validées statiquement. Le mot-clé unsafe permet de franchir ponctuellement cette frontière, en transférant au programmeur la responsabilité de maintenir les invariants que le compilateur ne peut plus vérifier.

Les cinq super-pouvoirs de unsafe#

Définition 136 (Bloc unsafe)

Un bloc unsafe est une section de code dans laquelle le programmeur peut effectuer des opérations interdites en Rust sûr. Le mot-clé unsafe ne désactive pas le borrow checker ni les autres vérifications du compilateur : il déverrouille uniquement cinq capacités supplémentaires.

Proposition 40 (Les cinq opérations réservées à unsafe)

À l’intérieur d’un bloc unsafe, et uniquement là, le programmeur peut :

  1. Déréférencer un pointeur brut (*const T ou *mut T).

  2. Appeler une fonction unsafe (y compris une fonction FFI).

  3. Accéder à une variable static mut (en lecture ou en écriture).

  4. Implémenter un trait unsafe (comme Send ou Sync).

  5. Accéder aux champs d’un union.

Toute autre opération Rust reste soumise aux vérifications habituelles, même à l’intérieur d’un bloc unsafe.

Remarque 111

unsafe ne signifie pas que le code est incorrect : il signifie que le compilateur fait confiance au programmeur pour respecter certains invariants. Un bloc unsafe correct est aussi sûr que du Rust ordinaire ; un bloc unsafe incorrect peut provoquer un comportement indéfini (undefined behavior).

Pointeurs bruts#

Définition 137 (Pointeurs bruts)

Un pointeur brut (raw pointer) est une adresse mémoire non soumise aux règles d’emprunt. Il en existe deux variantes :

  • *const T : pointeur brut en lecture seule ;

  • *mut T : pointeur brut mutable.

Contrairement aux références &T et &mut T (chapitre 6), les pointeurs bruts peuvent être nuls, ne garantissent pas la validité de la mémoire pointée, et n’ont pas de durée de vie associée.

La création d’un pointeur brut est une opération sûre. Seul le déréférencement nécessite un bloc unsafe.

Exemple 117 (Création et déréférencement de pointeurs bruts)

Voir le code ci-dessous.

let x = 42;
let ptr_const: *const i32 = &x;       // création sûre
let ptr_mut: *mut i32 = &x as *const i32 as *mut i32;

// Le déréférencement nécessite unsafe
unsafe {
    println!("*ptr_const = {}", *ptr_const);
    println!("*ptr_mut   = {}", *ptr_mut);
}
*ptr_const = 42
*ptr_mut   = 42
()

On peut également créer un pointeur brut à partir d’une adresse arbitraire, bien que le déréférencer soit alors extrêmement dangereux.

let mut valeur = 10;
let ptr: *mut i32 = &mut valeur;

unsafe {
    *ptr += 5;
    println!("Valeur modifiée via pointeur brut : {}", *ptr);
}
Valeur modifiée via pointeur brut : 15
()

Remarque 112

Les pointeurs bruts sont indispensables pour l’interaction avec du code C (FFI), la construction de structures de données à base de pointeurs (listes chaînées, arbres intrusifs), et certaines optimisations de bas niveau. En dehors de ces cas, les références classiques sont toujours préférables.

Fonctions unsafe#

Définition 138 (Fonction unsafe)

Une fonction unsafe est déclarée avec unsafe fn. Son appel constitue un contrat : l’appelant s’engage à respecter les préconditions documentées par la fonction (invariants sur les pointeurs, alignement, initialisation, etc.). L’ensemble du corps d’une fonction unsafe est implicitement un bloc unsafe.

Exemple 118 (Déclaration et appel d’une fonction unsafe)

Voir le code ci-dessous.

/// # Safety
///
/// `ptr` doit pointer vers un `i32` valide et correctement aligné.
unsafe fn lire_pointeur(ptr: *const i32) -> i32 {
    *ptr
}

let val = 7;
let ptr: *const i32 = &val;

// L'appel doit être dans un bloc unsafe
let resultat = unsafe { lire_pointeur(ptr) };
println!("Résultat : {}", resultat);
Résultat : 7

Proposition 41 (Contrat de l’appelant)

Lorsqu’une fonction est marquée unsafe, la responsabilité de la correction est partagée :

  • L”auteur de la fonction documente les invariants que l’appelant doit respecter (section # Safety dans la documentation).

  • L”appelant garantit, en écrivant le bloc unsafe, que ces invariants sont satisfaits.

Si l’appelant viole le contrat, le comportement est indéfini.

Remarque 113

La convention dans l’écosystème Rust est de documenter systématiquement les fonctions unsafe avec une section # Safety décrivant chaque précondition. Cette documentation fait partie intégrante de l’API.

unsafe impl — implémenter un trait unsafe#

Définition 139 (Trait unsafe)

Un trait unsafe est un trait dont l’implémentation exige des garanties que le compilateur ne peut pas vérifier. Les exemples canoniques sont Send et Sync (chapitre 13) : le compilateur les implémente automatiquement pour la plupart des types, mais certains types nécessitent une implémentation manuelle.

Exemple 119 (Implémentation manuelle de Send et Sync)

Considérons un type encapsulant un pointeur brut. Le compilateur ne peut pas déduire automatiquement s’il est sûr de l’envoyer entre threads.

struct MonPointeur {
    ptr: *mut i32,
}

// Le compilateur n'implémente pas Send/Sync pour les types
// contenant des pointeurs bruts. On affirme manuellement que
// notre type est sûr à envoyer entre threads.
//
// # Safety
// Le pointeur encapsulé pointe toujours vers une allocation valide
// et n'est jamais partagé entre threads sans synchronisation.
unsafe impl Send for MonPointeur {}
unsafe impl Sync for MonPointeur {}

fn est_send<T: Send>() {}
fn est_sync<T: Sync>() {}

est_send::<MonPointeur>();
est_sync::<MonPointeur>();
println!("MonPointeur est Send + Sync");
MonPointeur est Send + Sync

Remarque 114

Implémenter Send ou Sync de manière incorrecte peut provoquer des data races, qui sont un comportement indéfini en Rust. Il ne faut le faire que lorsqu’on peut prouver que les invariants de concurrence sont respectés par la conception du type.

extern "C" et FFI#

Définition 140 (FFI (Foreign Function Interface))

L”interface de fonctions étrangères (FFI) permet à Rust d’appeler des fonctions écrites dans d’autres langages (principalement le C) et, réciproquement, d’exposer des fonctions Rust au monde extérieur. La FFI repose sur le bloc extern et la convention d’appel "C".

Appeler du C depuis Rust#

Pour appeler une fonction C, on déclare sa signature dans un bloc extern "C". Chaque appel est implicitement unsafe, car le compilateur Rust ne peut pas vérifier les invariants du code C.

Exemple 120 (Appel d’une fonction de la libc)

Voir le code ci-dessous.

unsafe extern "C" {
    fn abs(input: i32) -> i32;
}

let x = -42;
let resultat = unsafe { abs(x) };
println!("abs({}) = {}", x, resultat);
abs(-42) = 42

Exposer du Rust au C#

Pour rendre une fonction Rust appelable depuis le C, on utilise extern "C" dans la déclaration et #[no_mangle] pour empêcher le compilateur d’altérer le nom du symbole.

Exemple 121 (Fonction Rust exportée pour le C)

Voir le code ci-dessous.

/// Fonction appelable depuis du code C.
#[no_mangle]
pub extern "C" fn addition(a: i32, b: i32) -> i32 {
    a + b
}

#[repr(C)] — compatibilité de disposition mémoire#

Définition 141 (repr(C))

L’attribut #[repr(C)] impose au compilateur d’organiser les champs d’une structure selon les règles de disposition mémoire du C (ordre de déclaration, règles d’alignement standard). Sans cet attribut, Rust se réserve le droit de réordonner les champs pour optimiser la taille.

#[repr(C)]
#[derive(Debug)]
struct Point {
    x: f64,
    y: f64,
}

let p = Point { x: 1.0, y: 2.0 };
println!("{:?} — taille : {} octets", p, std::mem::size_of::<Point>());
Point { x: 1.0, y: 2.0 } — taille : 16 octets

Remarque 115

#[repr(C)] est indispensable pour tout type échangé avec du code C via la FFI. Sans lui, la disposition mémoire choisie par Rust pourrait ne pas correspondre à celle attendue par le code C, provoquant des lectures corrompues.

Accéder aux champs d’un union#

Définition 142 (Union)

Un union en Rust est analogue à un union en C : tous les champs partagent le même espace mémoire. La lecture d’un champ d’un union est unsafe car le compilateur ne peut pas savoir quel champ a été écrit en dernier.

#[repr(C)]
union Nombre {
    entier: i32,
    flottant: f32,
}

let n = Nombre { entier: 42 };

unsafe {
    println!("Interprété comme entier   : {}", n.entier);
    println!("Interprété comme flottant : {}", n.flottant);
}
Interprété comme entier   : 42
Interprété comme flottant : 0.000000000000000000000000000000000000000000059
()

Variables static mut#

Définition 143 (Variable statique mutable)

Une variable static mut est une variable globale mutable. Tout accès (en lecture ou en écriture) nécessite un bloc unsafe, car le compilateur ne peut pas garantir l’absence de data races entre threads accédant simultanément à la variable.

{
static mut COMPTEUR: i32 = 0;

fn incrementer() {
    unsafe {
        COMPTEUR += 1;
    }
}

incrementer();
incrementer();
incrementer();
unsafe {
    println!("COMPTEUR = {}", *std::ptr::addr_of!(COMPTEUR));
}
}
COMPTEUR = 3
()

Remarque 116

L’utilisation de static mut est fortement déconseillée dans la plupart des cas. On lui préfère les alternatives thread-safe : AtomicI32, Mutex<T>, ou le patron OnceLock. Ces types offrent les mêmes fonctionnalités sans nécessiter de code unsafe.

Abstractions sûres sur du code unsafe#

Définition 144 (Abstraction sûre)

Une abstraction sûre consiste à encapsuler du code unsafe dans une API publique sûre, dont les invariants garantissent que le code unsafe interne ne peut jamais produire de comportement indéfini, quel que soit l’usage fait par l’appelant. C’est le patron fondamental de la bibliothèque standard.

La fonction split_at_mut de la bibliothèque standard illustre parfaitement ce principe. Elle retourne deux tranches mutables disjointes d’un même vecteur — ce que le borrow checker interdit normalement, car il ne peut pas prouver statiquement que les tranches ne se chevauchent pas.

Exemple 122 (Encapsuler du code unsafe dans une API sûre)

Voir le code ci-dessous.

{
use std::slice;

fn decouper_en_deux(tranche: &mut [i32], milieu: usize) -> (&mut [i32], &mut [i32]) {
    let longueur = tranche.len();
    let ptr = tranche.as_mut_ptr();

    assert!(milieu <= longueur, "indice hors limites");

    unsafe {
        (
            slice::from_raw_parts_mut(ptr, milieu),
            slice::from_raw_parts_mut(ptr.add(milieu), longueur - milieu),
        )
    }
}

let mut donnees = vec![1, 2, 3, 4, 5, 6];
let (gauche, droite) = decouper_en_deux(&mut donnees, 3);

gauche[0] = 10;
droite[0] = 40;

println!("Gauche : {:?}", gauche);
println!("Droite : {:?}", droite);
}
Gauche : [10, 2, 3]
Droite : [40, 5, 6]
()

Proposition 42 (Principe d’encapsulation du code unsafe)

La bibliothèque standard de Rust applique systématiquement le principe suivant : le code unsafe est confiné à l”intérieur des modules, et l’API publique est entièrement sûre. Les invariants du module garantissent que le code unsafe ne peut pas être mal utilisé depuis l’extérieur. Ce patron permet de limiter la surface d’audit aux seuls blocs unsafe, plutôt qu’à l’ensemble du programme.

Remarque 117

La bonne pratique consiste à minimiser la quantité de code unsafe et à l’isoler dans des fonctions ou modules dédiés. Chaque bloc unsafe doit être accompagné d’un commentaire // SAFETY: ... expliquant pourquoi les invariants sont respectés. Cela facilite l’audit et la maintenance.

Résumé#

Le mot-clé unsafe est un outil de précision, pas une échappatoire générale. Il permet cinq opérations spécifiques — déréférencer un pointeur brut, appeler une fonction unsafe, accéder à une static mut, implémenter un trait unsafe, lire un champ d’union — et transfère au programmeur la responsabilité des invariants correspondants.

Le patron central de Rust est celui de l”abstraction sûre : confiner le code unsafe à l’intérieur de modules bien audités, et exposer une API publique entièrement sûre. C’est ainsi que la bibliothèque standard fournit Vec, String, HashMap et bien d’autres types qui reposent sur du code unsafe sans jamais exposer l’utilisateur au risque de comportement indéfini.

En pratique, la grande majorité du code Rust applicatif ne nécessite aucun unsafe. Lorsqu’il est indispensable — FFI, optimisations de bas niveau, structures de données avancées — il doit être minimal, documenté et encapsulé.