L'ownership en Rust !
Tags: rust, ownership, memory, allocation
Published: 2023-11-15 11:20:25

## Introduction

On m'a souvent demandé :

> "Parmi toutes les fonctionnalités de Rust, si tu devais en retenir une seule qui fait sa force, ce serait quoi ?"

Eh ben ce serait ça.

Aujourd'hui, je vais vous parler de ce concept clé du langage, et pas des moindres : ☄ **L'Ownership** ☄. Je vais ainsi vous montrer pourquoi Rust est un langage _sûr_ pour la mémoire.

À celles et ceux qui veulent un résumé rapide pour découvrir le langage : [Rust : ce langage natif safe et puissant](https://www.younup.fr/blog/rust-ce-langage-natif-safe-et-puissant)

## Principes fondamentaux de l'ownership

L'allocation mémoire dans un programme, c'est le nerf de la guerre. Une mauvaise gestion des allocations peut entraîner une surcharge de la RAM, des pointeurs zombies 🧟‍♂️,... bref, des trucs vraiment pas cool et qu'on a malheureusement trop rencontré avec des langages comme le C++.

Le but de l'Ownership, c'est justement de **maintenir la mémoire propre** : minimiser les duplications de données, éviter les comportements imprévisibles avec les opérations de lectures/écritures, et nettoyer rapidement les données inutilisées.

Alors, comment ça marche ?

Concrètement, toutes les allocations suivent les règles fondamentales suivantes :

- Chaque allocation mémoire a un **unique propriétaire**
- Seul le propriétaire (ou l'emprunteur dans certains cas) a les droits de modification
- S'il n'y a plus de propriétaire, la mémoire est libérée

Ces règles sont appliquées sur l'ensemble de notre code, à chaque compilation ! Bien évidemment si une seule règle est enfreinte..., ça ne compile pas !

Notez que le concept de _lifetime_ participe aussi activement à la protection mémoire, mais c'est un sujet à part entière que je ne traiterai pas dans cet article.

## Sémantique de mouvement

Etant donné que les allocations n'ont pas toutes le même coût, le comportement du symbol `=` va être différent selon le type de variable.

Par exemple, sur l'affectation d'un entier, que l'on peut considérer comme une variable "simple" :

```rust
    let x = 5; // allocation à taille fixe
    let y = x; // l'affectation d'un entier déclenche une copie par défaut

    println!("x = {}", x); //OK
```

Jusque là, rien de bien nouveau. Mais si l'on essaye de faire la même chose avec un String, que l'on peut considérer comme une variable "complexe" :

```rust
    let s1 = String::from("hello"); // allocation à taille dynamique, s1 est propriétaire
    let s2 = s1; // s2 prend désormais l'ownership

    println!("{}, world!", s1); // erreur de compilation : s1 has moved!!
```

> "Quoi ? quoi ? 😲"

Féliciations, vous venez de rencontrer **la sémantique de mouvement** en Rust ! 😀
Par défaut en Rust, les variables complexes (allocation dynamique, taille variable, structures,...) ne sont pas copiées. Il y a simplement un transfert de propriété !

Attention, cela n'a rien à voir avec les références implicites d'objets comme en Java ! Ici il n'y a pas de référence.

Autre exemple avec une fonction :

```rust
fn main() {
    let s1 = String::from("hello"); // allocation à taille variable, s1 est propriétaire
    my_func(s1); // s1 est transféré dans le scope my_func !

    println!("{}, world!", s1); // erreur de compilation : s1 has moved!!
}

fn my_func(s: String){
    println!("{}, world!", s); // s est le propriétaire de "hello"
}
```

La bonne pratique ici est donc de passer la valeur par référence immutable :

```rust
fn main() {
    let s1 = String::from("hello");
    my_func(&s1); // s1 est passé par référence immutable dans le scope my_func

    println!("{}, world!", s1); // OK : s1 est toujours propriétaire
}

fn my_func(s: &String){
    println!("{}, world!", s); // s est une référence immutable
}
```

> **Remarque** : pour les structures légères, on peut aussi implémenter automatiquement les traits [Clone](https://doc.rust-lang.org/std/clone/trait.Clone.html) et [Copy](https://doc.rust-lang.org/std/marker/trait.Copy.html). Mais cela risque de dupliquer inutilement les données, ce qu'on veut justement éviter ! À utiliser consciencieusement donc.

Avec cette référence immutable, on vient d'aborder une autre notion : **l'emprunt (borrowing)**.

## Les emprunts (borrowing)

En toute franchise, il est difficile de prévoir la réaction du compilateur et de son redoutable **borrow checker** sans une bonne expérience, mais on pourrait résumer les règles de borrowing comme suit :

- S'il n'y a pas de référence mutable, il peut y avoir autant de références immutables que l'on souhaite sur une même variable.
- Il ne peut y avoir **qu'une seule référence mutable à la fois** sur une même variable => et c'est ce concept qui nous protège de bien des erreurs en écriture concurrentielle ! Et directement à la compilation ! ✌

C'est là que les choses peuvent se compliquer un peu... mais c'est pour notre bien !

Observons quelques exemples :

```rust
    let mut s = String::from("hello");

    let r1 = &s; // référence immutable 1 : OK
    let r2 = &s; // référence immutable 2 : OK

    println!("{} and {}", r1, r2); // affiche "hello and hello"
    // variables r1 et r2 ne sont plus utilisées après ce point

    let r3 = &mut s; // 1 emprunt par référence mutable : no problemo
    *r3 += " world"; // modification de la String d'origine par déréférencement

    println!("{}", r3); // OK : affiche "hello world"
```

Mais si on modifiait juste la fin de cet exemple :

```rust
    let mut s = String::from("hello");
    // [...]

    let r3 = &mut s; // 1 emprunt par référence mutable : no problemo
    *r3 += " world";

    println!("{}", s); // erreur de compilation : "cannot borrow `s` as immutable because it is also borrowed as mutable"

    println!("{}", r3); // OK : affiche "hello world"
```

Impossible d'emprunter et printer `s` car il est potentiellement en cours de modification par `r3` ! Et cela est vrai seulement car `r3` est utilisé juste après dans le `print`.

Si on change alors simplement l'ordre des `print`s :

```rust
    let mut s = String::from("hello");
    // [...]

    let r3 = &mut s;
    *r3 += " world";
    println!("{}", r3); // OK : affiche "hello world"

    println!("{}", s); // OK : r3 ne peut plus être utilisé ici
```

Bingo ! le compilateur est rassuré 😎

## Conclusion

On a vu comment étaient transmises les variables d'un contexte à un autre, et des exemples simples pour comprendre la puissance des vérifications d'ownership.

Au final, **Rust révèle les erreurs potentielles à la compilation plutôt que nous laisser subir les conséquences à l'exécution**, ce qui constitue une de ses plus grandes forces. Dites vous bien que si votre programme compile ~~enfin~~, c'est que votre code est sûr !

On pourrait passer des journées entières à tester d'autres cas, mais c'est justement l'expérience pratique qui permet de découvrir les différentes problématiques au fil du temps. Je vous invite donc à expérimenter par vous même le borrow checker sur le [Rust Playground](https://play.rust-lang.org) 😉

> "...et sinon, qu'en est-il des accès concurrents dans du multithreading ?"

C'est un autre sujet très intéressant que nous aborderons sans doute une prochaine fois, et vous verrez qu'avec des outils comme les `Mutex<T>` et `Arc<T>`, on peut aussi venir efficacement à bout de ces problématiques ! Affaire à suivre...