La gestion de mémoire de Rust

Par mlemay, 15 février, 2025

Allocation de la mémoire en programmation

Dans les langages de bas niveau comme C ou C++ les développeurs peuvent allouer ou libérer la mémoire manuellement. Cela permet une gestion plus efficace de la mémoire. Cependant, les développeurs ont tendance libérer la mémoire trop tôt, créant ainsi une variable invalide ou à la libérer plusieurs fois par erreur ce qui crée des bugs.

Dans les langages de haut niveau comme Python ou Javascript, un ramasse-miettes surveille et nettoie la mémoire qui n'est plus utilisée. Il n’y a généralement pas de problèmes de mémoire, mais le temps d'exécution est beaucoup plus long.

Rust essaie d’avoir le meilleur des deux mondes. Le langage utilise une allocation et libération de la mémoire automatique pour empêcher les erreurs de mémoire. Par contre, il remplace le ramasse-miettes par une série de règles (possession et emprunt) vérifié par le « Borrow Cheker » une partie du compilateur. Comme le compilateur se charge de la vérification avant l’exécution du code, le temps d’exécution ne souffre pas.

Possession

Les 3 règles de la possession

1- Pour chaque valeur Rust, il y a une variable qui est son propriétaire

2- Chaque valeur a un seul propriétaire à la fois

3- Une fois que le propriétaire sort de la portée, la valeur est supprimée

Voici des exemples inspiré du Rust Book pour mieux comprendre la possession.

Ce code ne compile pas.

fn main() {
    {                    
        let s1 = String::from("hello");	    // s1 est propriétaire du String "hello"
        let s2 = s1;                        // s2 devient propriétaire du String "hello"
        println!("{}, world!", s1);        //opération illégale car s1 n’est plus propriétaire du String "hello"
    }                    
}

On peut corriger le code ci-dessus en utilisant la variable s2 pour l'affichage.

fn main() {
    {                    
        let s1 = String::from("hello");	    // s1 est propriétaire du String "hello"
        let s2 = s1;                        // s2 devient propriétaire du String "hello"
        println!("{}, world!", s2);        // s2 est  propriétaire du String "hello" tout va bien
    }                    
}

On peut aussi utiliser la méthode Rust clone() pour créer une copie de s1 avec sa propre mémoire allouée.

fn main() {
    {                    
        let s1 = String::from("hello");	    // s1 est propriétaire du String "hello"
        let s2 = s1.clone( );               // s2 est propriétaire d'un nouveau String "hello"
        println!("{}, world!", s1);         //  s1 est toujours  propriétaire du String "hello"  tout va bien
        println!("{}, world!", s2);        // s2 est  propriétaire du nouveau String "hello" tout va bien
    }                    
}

La porté

La portée d'une variable en Rust est délimitée par des blocs de code {}, des fonctions ou des autres structures de contrôle. Voici un exemple pour vous aider à visualiser la portée.

fn main() {
    {                             // aucune mémoire alloué
        let s = "hello world"; 	  // début de la portée, s est en vigueur, la mémoire est alloué 
        println!("{s}");          // s est en vigueur
    }                            // fin de la portée, la mémoire est libérée
}

L’emprunt

La possession peut poser des problèmes si on veut utilise la valeur plus qu’une fois. Par exemple si on veut appeler deux fois une fonction d’affichage. Dans ces cas, on peut utiliser l’emprunt. L’emprunt permet de créer une référence à une variable sans en prendre possession. On utilise le symbole « & » pour emprunter.

Les 2 règles de l’emprunt

1- Il peut y avoir en même temps, soit une unique référence mutable, ou plusieurs références immutables.

2- Les références doivent toujours être en vigueur, soit dans leur portée

Ce code ne compile pas.

fn main() {
    {      			
        let s1 = "hello world"; 	 
        print_string(s1);	//en appellant la function print_string, on libère la mémoire alloué à s1
        print_string(s1);	// le code ne peut pas compilé car la valeur de s1 a été libérée
    }
}
fn print_string (s : String){	// début de la portée, s est en vigueur, la mémoire est alloué
	println!("{s}");	 // s est en vigueur
}				// fin de la portée, la mémoire est libérée

On ajoute l’emprunt.

fn main() {
    {      			
        let s1 = "hello world"; 	 
        print_string(&s1);	//comme c’est un emprunt, s1 est toujours en vigueur
        print_string(&s1);	//s1 est toujours en vigueur, le code peut compiler
    }
}
fn print_string (s : &String){	// début de la portée, s est en vigueur, la mémoire est alloué
	println!("{s}");	 // s est en vigueur
}				// fin de la portée, la mémoire est libérée

Borrow Checker

Le désavantage de Rust, c’est que le Borrow Checker doit vérifier que toutes les règles sont suivies avant de compiler le projet. C’est normal d’avoir beaucoup d’erreurs de compilation. Une expression souvent utilisée par les développeurs Rust est : ‘’Fighting the borrow checker’’.

Bien que le compilateur soit très strict, ces messages d’erreurs sont habituellement très utiles. Il affiche clairement la ligne où est le problème et ce qui a conduit au problème. Il offre parfois des pistes de solutions et une commande pour en apprendre plus sur le problème.

Si on reprend le premier exemple.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;

    println!("{}, world!", s1);
}

Voici la réponse du compilateur Rust.

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:28
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 | 
5 |     println!("{}, world!", s1);
  |                            ^^ value borrowed here after move

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` due to previous error

Sources

Étiquettes

Commentaires1

lcirpaci

il y a 3 mois

Wow,

j'apprécie énormement ton article qui simplifie se concept de propriété.

Tu as débuter avec une comparaison aux C, C++ sur l'allocation manuel. Peut être que sa aurais était bien d'avoir un parallèle avec la gestion manuelle en rust grace aux box et pointeur pour simuler l'avantage.