Introduction

Rust est un langage de programmation impératif fortement typé avec une sémantique d'utilisation sûre de la mémoire. Le compilateur Rust génère du code natif.

Le développement de Rust a été influencé par de nombreux autres langages, mais certains aspects de ce dernier sont originaux et spécifiques, notamment le modèle de possession des références à la mémoire, rendant le langage particulièrement adapté au développement de code système sûr et performant.

Ce modèle d'utilisation de la mémoire combine la puissance d'un langage possédant un mécanisme de ramasse-miettes avec la performance d'une gestion manuelle de la mémoire.

Histoire de Rust

  • Exposé de Steve Klabnik
  • Objectif de Rust: concevoir un langage pour la programmation système compilé, concurrent et sûr.
  • Utilisé par Mozilla pour le moteur de rendu web Servo, dont une partie du code est partagé avec le navigateur web Firefox
  • 2006-2016: v1.0. Développement ouvert (initié par Graydon Hoare)
  • Many older languages better than the new ones. We keep forgetting already-learned lessons.
  • Technologies from the past come to save the future from itself.
  • Premier code "Rust" montré à la communauté (syntaxe obsolète):
fn main() {
    log "Hello, world!";
}

fn max(int x, int y) -> int {
    if (x > y) {
        ret x;
    } else {
        ret y;
    }
}
  • Ancien mécanisme pour les objets:
obj counter(int i) {
    fn incr() {
        i += 1;
    }
    fn get() -> int {
        ret i;
    }
}

fn main() {
    auto c = counter(10);
    c.incr();
    log c.get();
}
  • Ancien mécanisme de généricité:
fn swap[T](tup(T,T) pair) -> tup(T,T) {
    ret tup(pair._1, pair._0);
}

fn main() {
    auto str_pair = tup("hi", "there");
    auto int_pair = tup(10, 12);
    str_pair = swap[str](str_pair);
    int_pair = swap[int](int_pair);
}
  • Dans la conception: importance de la sémantique par rapport à la syntaxe
  • D'OCaml à Rust dans LLVM

Ressources utilisées pour ce cours

Toutes les ressources utilisées sont gratuites et libre d'accès, et leurs codes sources sont disponibles sur Github.

Manuels

  • The Rust Programming Language: également appelé "The Rust Book", un livre d'introduction à Rust
  • Rust by Example: une collection d'exemples exécutables pour illustrer différents concepts de Rust et de ses bibliothèques
  • Cookin' with Rust: également appelé the "The Rust Cookbook", des exemples de code pour effectuer des tâches de programmation courantes, utilisant des crates courants (les crates sont des paquetages développés par des tiers)
  • The Rust Reference: la référence du langage, décrivant chaque construction. Ce n'est pas un guide d'intruction mais il est utile pour comprendre précisément certains aspects du langage.

Outils

Ce cours a été fait avec:

  • mdBook qui permet de créer des documentations
  • The Rust Playground qui permet de compiler et d'exécuter du code Rust en ligne et qui possède également une API qui permet de compiler et d'exécuter les exemples du cours

Première étape: installation de Rust et "Hello, world!"

  • Utiliser la page d'installation de Rust pour l'installer sur votre machine.
  • Créer, compiler et exécuter le programme hello.rs suivant:
fn main() {
    println!("Hello, world");
}

Deuxième étape: Rustlings

  • Récupérer et installer les Rustlings (des exercices cours pour apprendre à lire et à écrire du code en Rust)
git clone -b 5.6.1 --depth 1 https://github.com/rust-lang/rustlings
cd rustlings
cargo install --force --path .
  • Continuer les Rustlings en vous servant des manuels ci-dessus
  • À la fin de votre session de travaux pratiques, générez un patch (git diff > p)

Troisième étape: outil d'indexation (baf-core)

baf-core index <repertoire>

Expressions

Expression: combinaison de valeurs (calcul arithmétique, booleen, etc.) retournant (exprimant) une valeur.

Exemple d'expressions arithmétiques, booléennes et litérales:

fn main() {
    // Integer addition
    println!("1 + 2 = {}", 1u32 + 2);

    // Integer subtraction
    println!("1 - 2 = {}", 1i32 - 2);

    // Short-circuiting boolean logic
    println!("true AND false is {}", true && false);
    println!("true OR false is {}", true || false);
    println!("NOT true is {}", !true);

    // Bitwise operations
    println!("0011 AND 0101 is {:04b}", 0b0011u32 & 0b0101);
    println!("0011 OR 0101 is {:04b}", 0b0011u32 | 0b0101);
    println!("0011 XOR 0101 is {:04b}", 0b0011u32 ^ 0b0101);
    println!("1 << 5 is {}", 1u32 << 5);
    println!("0x80 >> 2 is 0x{:x}", 0x80u32 >> 2);

    // Use underscores to improve readability!
    println!("One million is written as {}", 1_000_000u32);
}

À la différence de langages comme C/C++, en Rust, les blocs peuvent "renvoyer" une expression, ce qui le rapproche d'un langage fonctionnel, comme dans l'exemple ci-dessous:

fn main() {
    let y = 8;
    let x = 6 + { let x = 7; if y > 5 { 5 * x } else { y + x } };
    println!("{x} and {y}");
}

ou encore ici:

fn main() {
    let x = 5u32;

    let y = {
        let x_squared = x * x;
        let x_cube = x_squared * x;

        // This expression will be assigned to `y`
        x_cube + x_squared + x
    };

    let z = {
        // The semicolon suppresses this expression and `()` is assigned to `z`
        2 * x;
    };

    println!("x is {:?}", x);
    println!("y is {:?}", y);
    println!("z is {:?}", z);
}

La valeur () correspond à une valeur vide et est appelée unit.

Variables et types de base

Lier une variable

En Rust, on ne déclare pas à proprement parler les variables, mais on les associe à une valeur avec le mot clef let. La ligne suivante crée une variable a qui va être liée à la valeur 3:

fn main() {
let a = 3;
println!("La valeur de a est {a}.");
}

Le type de a va être inféré par rapport à la valeur du litéral, ici un entier signé sur 32 bits (i32). On aurait pu également donner le type de manière explicite à la variable de la manière suivante:

fn main() {
let a:i64 = 3;
println!("La valeur de a est {a}.");
}

En Rust, une instruction let n'est pas une expression (à la différence de C/C++ ou l'affectation renvoie une valeur).

Types de données scalaires

Rust a les types scalaires classiques entiers, flottants, booléens et caractères.

Entiers (integers)

Les entiers sont les suivants:

LengthSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

Les entiers litéraux peuvent être définis avec les syntaxes suivantes:

Number literalsExample
Decimal98_222
Hex0xff
Octal0o77
Binary0b1111_0000
Byte (u8 only)b'A'

Un entier litéral sera par défaut en 32 bits (i32).

Flottants

Rust propose des flottants suivant le standard IEEE-754: simple précision f32 et double précision f64. Un litéral de type flottant comme 2.0 sera par défaut en double précision.

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

Booléens

Le type booléen (bool) supporte les deux valeurs littérales true et false.

fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
}

Caractères

Les caractères en Rust sont sur quatre octets et contiennent des valeurs Unicode scalaires.

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // with explicit type annotation
    let heart_eyed_cat = '😻';
}

Opérations

fn main() {
    // addition
    let sum = 5 + 10;

    // subtraction
    let difference = 95.5 - 4.3;

    // multiplication
    let product = 4 * 30;

    // division
    let quotient = 56.7 / 32.2;
    let truncated = -5 / 3; // Results is -1
    println!("Truncated = {truncated}");
    
    // remainder
    let remainder = 43 % 5;
}

Synthèse

fn print_type_of<T>(_: &T) {
    println!("{}", std::any::type_name::<T>())
}

fn main() {
    // Variables can be type annotated.
    let logical: bool = true;

    let a_float: f64 = 1.0;  // Regular annotation
    let an_integer   = 5i32; // Suffix annotation

    // Or a default will be used.
    let default_float   = 3.0; // `f64`
    let default_integer = 7;   // `i32`

    let a = 'a';
    let alpha = 'α';
    let inf = '∞';
    let z: char = 'ℤ';
    let heart_eyed_cat = '😻';

    // A type can also be inferred from context 
    let mut inferred_type = 12; // Type i64 is inferred from another line
    inferred_type = 4294967296i64;
    print_type_of(&inferred_type);
}

Immutabilité et Ombrage

Immutabilité par défaut

Une variable est immutable par défaut, c'est-à-dire qu'une fois liée à une valeur, cette dernière ne peut pas être changée:

fn main() {
    let x = 5;
    println!("The value of x is: {x}");
    //x = 6; // Ce code est invalide
    //println!("The value of x is: {x}");
}

Ainsi, pour qu'il soit valide, ce code doit être réécrit ainsi (noter le mot-clef mut):

fn main() {
    let mut x = 5;
    println!("The value of x is: {x}");
    x = 6; // Ce code est valide
    println!("The value of x is: {x}");
}

Ombrage

L'ombrage (shadowing) consiste à lier une nouvelle variable avec le même nom afin de modifier sa valeur:

fn main() {
    let x = 5;
    println!("The value of x is: {x}");
    let x = x + 1;
    println!("The value of x is: {x}");
}

La portée (scope) d'une variable liée est limitée à son bloc de définition et aux blocs enfants, à partir de l'endroit où elle est déclarée:

fn main() {
    let x = 5;  // x_1

    let x = x + 1; // x_2 = x_1 + 1

    {
        let x = x * 2; // x_3 = x_2 * 2
        println!("The value of x in the inner scope is: {x}"); // x_3
    }

    println!("The value of x is: {x}"); // x_2
}

On peut changer le type d'une variable:

fn main() {
    let spaces = "   ";
    let spaces = spaces.len();
    println!("{} spaces", spaces);
}

On ne peut pas changer le type d'une variable mutable:

fn main() {
    let mut spaces = "   ";
    // spaces = spaces.len(); // faux !
    println!("{} spaces", spaces);
}

Constantes

Une constante est déclarée avec le mot-clef const et doit être typée. La valeur d'une constante doit être calculable au moment de la compilation du code. La portée d'une constante est identique à celle d'une variable.

const SECONDS_IN_ONE_HOUR: u32 = 60 * 60;
const THREE_HOURS_IN_SECONDS: u32 = SECONDS_IN_ONE_HOUR * 3;

fn main() {
    println!("There are {} seconds in 3 hours.", THREE_HOURS_IN_SECONDS);
}

Fonctions

Une fonction en Rust est définie avec le mot clef fn, doit être nommée, peut prendre un certain nombre de paramètres qui doivent être typés, et peut éventuellement retourner une valeur qui doit également être typée.

fn number_of_seconds_in_hours(hours: i32) -> i32 {
    hours * 60 * 60
}

fn main() {
    println!("Seconds in 3 hours: {}", number_of_seconds_in_hours(3));
}

Une fonction non typée renvoie "unit" ().

Instructions et expressions

Le corps d'une fonction en Rust peut être composé d'instructions et d'expressions:

  • une instruction effectue une action et ne retourne pas de valeur
  • une expression est évaluée et retourne une valeur

Ainsi, let x = 6 est une instruction et ne renvoie pas de valeur, alors que x + 3 est une expression et renvoie une valeur (le résultat de l'addition de x avec 3).

Une fonction renvoie la valeur de la dernière expression évaluée sur son flot de contrôle:

fn truc_idiot(x: i32) -> i32 {
    let y = 3;
    x + y
}

fn truc_idiot_bis(x: i32) {
    let y = 3;
    x + y; // return ()
}

fn truc_idiot_ter(x: i32) {
    let y = 3;
    x + y;
    ()  // return () too
}

fn main() {
    println!("{}", truc_idiot(1));
    truc_idiot_bis(2);
    truc_idiot_ter(3);
}

Flot de contrôle

if

La construction syntaxique if est une expression permettant d'exécuter une branche de code selon une certaine condition:

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

La construction if étant une expression, elle peut-être notamment être utilisée dans une instruction let:

fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

    println!("The value of number is: {number}");
}

Boucles

Plusieurs constructions de boucles peuvent être utilisées en Rust.

loop

L'expression loop permet de créer des boucles infinie:

fn main() {
    loop {
        println!("again!");
    }
}

Une boucle peut être interrompue avec le mot-clef break qui permet de renvoyer une valeur éventuelle:

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {result}");
}

Une boucle peut-être identifiée par un label avec la syntaxe 'label pour déterminer sur quelle boucle le break doit agir (ce dernier peut éventuellement également retourner une valeur):

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;

        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {count}");
}

while

La construction while permet d'exécuter du code tant qu'une condition est vraie:

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{number}!");

        number -= 1;
    }

    println!("LIFTOFF!!!");
}

for

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("the value is: {element}");
    }
}
fn main() {
    for number in (1..4).rev() {
        println!("{number}!");
    }
    println!("LIFTOFF!!!");
}

Tuples

A tuple is a collection of values of different types. Tuples are constructed using parentheses (), and each tuple itself is a value with type signature (T1, T2, ...), where T1, T2 are the types of its members. Functions can use tuples to return multiple values, as tuples can hold any number of values.

// Tuples can be used as function arguments and as return values.
fn reverse(pair: (i32, bool)) -> (bool, i32) {
    // `let` can be used to bind the members of a tuple to variables.
    let (int_param, bool_param) = pair;

    (bool_param, int_param)
}

// The following struct is for the activity.
#[derive(Debug)]
struct Matrix(f32, f32, f32, f32);

fn main() {
    // A tuple with a bunch of different types.
    let long_tuple = (1u8, 2u16, 3u32, 4u64,
                      -1i8, -2i16, -3i32, -4i64,
                      0.1f32, 0.2f64,
                      'a', true);

    // Values can be extracted from the tuple using tuple indexing.
    println!("Long tuple first value: {}", long_tuple.0);
    println!("Long tuple second value: {}", long_tuple.1);

    // Tuples can be tuple members.
    let tuple_of_tuples = ((1u8, 2u16, 2u32), (4u64, -1i8), -2i16);

    // Tuples are printable.
    println!("tuple of tuples: {:?}", tuple_of_tuples);

    // But long Tuples (more than 12 elements) cannot be printed.
    //let too_long_tuple = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13);
    //println!("Too long tuple: {:?}", too_long_tuple);
    // TODO ^ Uncomment the above 2 lines to see the compiler error

    let pair = (1, true);
    println!("Pair is {:?}", pair);

    println!("Uhe reversed pair is {:?}", reverse(pair));

    // To create one element tuples, the comma is required to tell them apart
    // from a literal surrounded by parentheses.
    println!("One element tuple: {:?}", (5u32,));
    println!("Just an integer: {:?}", (5u32));

    // Tuples can be destructured to create bindings.
    let tuple = (1, "hello", 4.5, true);

    let (a, b, c, d) = tuple;
    println!("{:?}, {:?}, {:?}, {:?}", a, b, c, d);

    let matrix = Matrix(1.1, 1.2, 2.1, 2.2);
    println!("{:?}", matrix);
}

Tableaux

fn main() {
    let a = [1, 2, 3, 4, 5];
}
#![allow(unused)]
fn main() {
    let months = ["January", "February", "March", "April", "May", "June", "July",
                  "August", "September", "October", "November", "December"];
}
#![allow(unused)]
fn main() {
    let a: [i32; 5] = [1, 2, 3, 4, 5];
}
#![allow(unused)]
fn main() {
    let a = [3; 5];
}

Possession

Mécanisme de possession (ownership)

En Rust, la mémoire est gérée avec un mécanisme de possession (ownership) qui est vérifié au moment de la compilation. Les règles de l'ownership sont les suivantes:

  • chaque valeur en Rust a un propriétaire (owner)
  • il ne peut y avoir qu'un seul propriétaire de cette valeur à un instant t
  • quand le propriétaire est hors de portée, la valeur est détruite

Pour illustrer ce concept, nous utiliserons le type String qui est stocké sur le tas (heap). Nous avons déjà utilisé des chaînes de caractères mais ces dernières, de type &str, ont un contenu immutable:

fn print_type_of<T>(_: &T) {
    println!("{}", std::any::type_name::<T>())
}

fn main() {
    let mut x = "hello";
    x = "world";
    print_type_of(&x);
    println!("{}", x);
}

La fonction from de String permet de créer une chaîne de caractère dont on va pouvoir modifier le contenu:

fn print_type_of<T>(_: &T) {
    println!("{}", std::any::type_name::<T>())
}

fn main() {
    let mut s = String::from("hello");

    s.push_str(", world!"); // push_str() appends a literal to a String

    println!("{}", s); // This will print `hello, world!`

    print_type_of(&s);
}

Examinons la relation entre une variable s1 et la mémoire dans le cas de la déclaration suivante:

#![allow(unused)]
fn main() {
let s1 = String::from("hello");
}

Dans le cas d'un type String, la mémoire utilisée est de deux types: une structure, stockée sur la pile, contenant ta taille maximale de la chaîne de caractère, le nombre de caractères actuels, ainsi qu'un pointeur vers une zone mémoire sur le tas (heap) contenant la chaîne elle-même:

Two tables: the first table contains the representation of s1 on the
stack, consisting of its length (5), capacity (5), and a pointer to the first
value in the second table. The second table contains the representation of the
string data on the heap, byte by byte.

Si l'on écrit le code suivant, regardons ce qui se passe au niveau de la mémoire:

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

En mémoire, la structure contenant les informations sur la chaîne est dupliquée, à la différence de la chaîne elle-même se trouvant sur le tas:

Three tables: tables s1 and s2 representing those strings on the
stack, respectively, and both pointing to the same string data on the heap.

Si l'on fait un parallèle avec d'autres langages comme C++, on parlera de shallow copy: une partie de la mémoire est dupliquée, à la différence d'une deep copy.

En Rust, la mémoire avec laquelle la variable est liée est automatiquement détruire lorsque la variable devient hors de portée: Rust appelle la méthode drop qui rend (au système d'exploitation) la mémoire allouée sur le tas. Sur le schéma ci-dessus, la question de pose de savoir qui de s1 et s2 doit posséder la zone mémoire et doit donc être responsable de sa destruction, afin d'éviter les double free.

Pour résoudre ce problème, Rust considère qu'après la ligne:

#![allow(unused)]
fn main() {
let s2 = s1;
}

la variable s1 n'est plus valable. En effet, pour toutes les données possédant une méthode drop, une affectation correspond à un déplacement (move) plutôt qu'à une copie (copy): l'emplacement mémoire de départ est donc invalidé.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    // s1 n'est plus valide
    println!("{}", s1);
    println!("{}", s2);
}

Le schéma de la mémoire après l'affectation de s1 à s2 est donc le suivant:

Three tables: tables s1 and s2 representing those strings on the
stack, respectively, and both pointing to the same string data on the heap.
Table s1 is grayed out be-cause s1 is no longer valid; only s2 can be used to
access the heap data.

Rust, par défaut, ne fera jamais de copie profonde des données stockées sur le tas. Cette copie doit être faite de manière explicite avec clone():

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

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

Le code ci-dessus correspondant à l'organisation de la mémoire suivante:

Four tables: two tables representing the stack data for s1 and s2,
and each points to its own copy of string data on the heap.

Implémentation des traits Copy et Drop

Revenons au code exemple que nous avions vu précédemment:

fn main() {
    let x = 5;
    let y = x;

    println!("x = {}, y = {}", x, y);
}

Ici, il n'est pas nécessaire d'utiliser clone() car pour les types de données de base (comme les entiers), la taille est connue au moment de la compilation. Ces types de données donnent donc lieu à une allocation statique effectuée sur la pile, les notions de copie profonde et de shallow copy ont ici le même comportement: les données, dans le cas d'une affectation sont bien copiées.

Ces types de données implémentent le trait Copy, régissant le comportement de l'affectation. Les données de type String implémentent quant à elles le trait Drop: ces données impliquent une affectation de type déplacement plutôt qu'une copie: elles seront donc régies par le mécanisme de possession. Rust ne permet pas qu'un type implémentant Drop implémente le type Copy.

Parmi les types implémentant le trait Copy, on trouve:

  • Tous les types entiers, tel que u32
  • Le type booléen bool
  • Tous les types flottants, comme f64
  • Le type caractère char
  • Les tuples, seulement si tous leurs membres implémentent Copy. Par exemple, (i32, i32) implémente Copy, mais pas (i32, String)

Possession et fonctions

Le passage d'un paramètre à une fonction utilise le même mécanisme que l'affectation. Dans l'exemple ci-dessous, le fait de passer s en paramètre à la fonction takes_ownership transfère la propriété de s au paramètre some_string de la fonction. Lorsque l'on sort de la fonction, comme some_string est hors de portée, la mémoire associée est rendue. Pour une variable entière le comportement est différent puisque le passage au paramètre some_integer de la fonction makes_copy se fait en effectuant une copie.

fn main() {
    let s = String::from("hello");  // s comes into scope

    takes_ownership(s);             // s's value moves into the function...
                                    // ... and so is no longer valid here

    let x = 5;                      // x comes into scope

    makes_copy(x);                  // x would move into the function,
                                    // but i32 is Copy, so it's okay to still
                                    // use x afterward

} // Here, x goes out of scope, then s. But because s's value was moved, nothing
  // special happens.

fn takes_ownership(some_string: String) { // some_string comes into scope
    println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
  // memory is freed.

fn makes_copy(some_integer: i32) { // some_integer comes into scope
    println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.

Il est tout à fait possible de transférer la possession à une fonction et de la récupérer ensuite. Il suffit de retourner les données:

fn main() {
    let s1 = gives_ownership();         // gives_ownership moves its return
                                        // value into s1

    let s2 = String::from("hello");     // s2 comes into scope

    let s3 = takes_and_gives_back(s2);  // s2 is moved into
                                        // takes_and_gives_back, which also
                                        // moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
  // happens. s1 goes out of scope and is dropped.

fn gives_ownership() -> String {             // gives_ownership will move its
                                             // return value into the function
                                             // that calls it

    let some_string = String::from("yours"); // some_string comes into scope

    some_string                              // some_string is returned and
                                             // moves out to the calling
                                             // function
}

// This function takes a String and returns one
fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
                                                      // scope

    a_string  // a_string is returned and moves out to the calling function
}

Nous avons donc trois possibilités: une fonction peut prendre la possession de données, la donner, ou la prendre puis la rendre. Notons un dernier mécanisme: une fonction peut tout à fait retourner un tuple, permettant ainsi de retourner plusieurs valeurs.

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

    let (s2, len) = calculate_length(s1);

    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() returns the length of a String

    (s, length)
}

Références et emprunts

Emprunt d'une possession via une référence

Les mécanismes de transfert de possession présentés précédemment sont fastidieux à utiliser. Rust utilise un mécanisme d'emprunt (borrowing) simplifiant l'utilisation de données en mémoire, par l'intermédiaire d'un mécanisme de référence à une zone mémoire.

Examinons par exemple ce code qui retourne la longueur d'une chaîne stockée dans un type String:

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

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

La notation &s1 indique que l'on emprunte la zone mémoire par l'intermédiaire d'une référence. Le type de s dans la fonction calculate_length est &String qui indique une référence vers une zone mémoire de type String. L'organisation de cette zone mémoire est la suivante:

Three tables: the table for s contains only a pointer to the table
for s1. The table for s1 contains the stack data for s1 and points to the
string data on the heap.

Lorsque l'exécution de la fonction se termine, s est détruite, ainsi que sa mémoire associée, mais pas s1 qui avait été empruntée.

Mutabilité des références

Par défaut, une référence est immutable, même si la zone mémoire est mutable. Ainsi, le code en commentaire ci-dessous est incorrect:

fn main() {
    let mut s = String::from("hello");

    // change(&s);  // invalide

    s.push_str("!");
    println!("{}", s);
}

// Cette fonction est invalide
// fn change(some_string: &String) {
//    some_string.push_str(", world");
//}

Pour permettre à s d'être modifiée lors de l'emprunt, il faut explicitement définir la référence comme étant mutable avec le mot-clef mut, dans la définition du type du paramètre et lors du passage du paramètre:

fn main() {
    let mut s = String::from("hello");

    change(&mut s);

    s.push_str("!");
    println!("{}", s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

Il n'est pas possible de créer une référence mutable à partir d'une variable qui ne l'est pas: si s n'était pas mutable, il ne serait pas possible de créer une référence &mut s.

Références multiples

Il est possible de créer plusieurs références en même temps vers une zone mémoire, donc de l'emprunter simultanément, avec néanmoins une restriction importante: il ne doit pas y avoir de référence mutable en même temps qu'une autre référence mutable ou immutable. Une référence mutable verrouille donc l'emprunt. Ce mécanisme de verrou est vérifié au moment de la compilation.

Ainsi, le code ci-dessous est correct, mais la partie en commentaire ne l'est pas:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem

    println!("{}, {}", r1, r2);

    //let r3 = &mut s; // BIG PROBLEM
    //println!("{}, {}, and {}", r1, r2, r3);
}

Cet exemple nous permet d'introduire la notion de durée de vie (liveness) d'une variable, légèrement différente de la notion de portée. Considérons le code suivant:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem

    println!("{}, {}", r1, r2);

    let r3 = &mut s; // no problem
    println!("{}", r3);

    //let r3 = &mut s; // BIG PROBLEM
    //println!("{}", r3);
    //println!("{}, {}", r1, r2);
}

Ainsi, bien que la portée de r1 et r2 soit celle du bloc de la fonction main(), la durée de vie des variables r1 et r2 s'arrête après le println!: comme ces variable ne sont plus utilisées par la suite, Rust considère qu'elle ne sont plus vivantes.

Dangling Reference

Rust ne permet pas de créer des références vers de la mémoire qui peut potentiellement être détruite avant que la référence ne le soit. Ainsi, le code ci-dessous est faux:

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}

alors que celui-ci est correct:

fn main() {
    let s1 = no_dangle();
}


fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

Synthèse

  • À tout instant, nous pouvons avoir soit une référence mutable, soit un nombre arbitraire de références immutables
  • les références doivent toujours être valides

Tranches

Les tranches (slices) sont des références spéciales permettant d'emprunter une séquence d'éléments d'une collection plutôt que la collection complète. Une collection est une suite d'éléments: une chaîne de caractères, un vecteur, un tableau, etc.

Tranche de chaîne de caractère

Prenons l'exemple d'une fonction devant retourner le premier mot d'une chaîne de caractères constituée de mots séparés par des espaces:

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s); // word will get the value 5

    s.clear(); // this empties the String, making it equal to ""   
    // word still has the value 5 here, but there's no more string that
    // we could meaningfully use the value 5 with. word is now totally invalid!

    println!("End index of first word: {}", word);
}

Bien que correcte, cette implémentation n'est pas pertinente. En effet, même si word contient toujours 5 après l'appel à s.clear(), cet index n'a plus de sens.

Afin d'implémenter cette fonction de manière plus pertinente, il est plus judicieux d'utiliser un mécanisme de tranche:

fn main() {
    let s = String::from("hello world");

    let hello = &s[0..5];
    let world = &s[6..11];

    println!("{}:{}", hello, world);
}

La représentation en mémoire est la suivante:

Three tables: a table representing the stack data of s, which points
to the byte at index 0 in a table of the string data "hello world" on
the heap. The third table rep-resents the stack data of the slice world, which
has a length value of 5 and points to byte 6 of the heap data table.

Une tranche de chaîne est dénotée en Rust par &src, ce qui nous permet de réécrire la fonction précédente en:

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s); // word will get the value 5

    // s.clear(); // Incorrect !

    println!("first word: {}", word);
}

L'appel s.clear() a été mis en commentaire car il est incorrect. En effet, nous avons créé une référence immutable à s lors de l'appel à first_word, transmise sous la forme d'une tranche à word. Nous ne pouvons donc pas changer s par l'intermédiaire d'une référence mutable.

Les littéraux de type chaîne de caractère sont des tranches de type &str:

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let hs = "hello world";
    let my_string = String::from(hs);

    // `first_word` works on slices of `String`s, whether partial or whole
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` also works on references to `String`s, which are equivalent
    // to whole slices of `String`s
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` works on slices of string literals, whether partial or whole
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}

Autres tranches

fn main() {
    let a = [1, 2, 3, 4, 5];

    let slice = &a[1..3];

    assert_eq!(slice, &[2, 3]);
}

Réalisations en Rust

Quelques exemples de réalisations en Rust.

Frameworks web

Base de données

ORM

Clients

Formats de données texte

Sérialisation / Désérialisation

XML