Rust 🩀

Ce document est une prise de notes personnelle de mes lectures sur le Rust.

Particularités du langage

  1. Runtime

Le runtime de Rust est lĂ©ger car il n’a pas de pas de garbage collector et gĂšre la mĂ©moire grĂące Ă  son concept d’ownership et borrowing. Rust compile directement le code en code machine et n’utilise pas de machine virtuelle pour la gestion des ressources car tout se passe Ă  la compilation (par opposition Ă  Java et sa JVM par exemple).

  1. Pas de pointeurs “classiques”

Dans les langages comme C ou C++, les pointeurs sont des variables qui contiennent l’adresse mĂ©moire d’une autre variable. Cela permet de manipuler directement la mĂ©moire, mais c’est aussi source d’erreurs si un pointeur pointe vers une zone mĂ©moire invalide ou qui a Ă©tĂ© libĂ©rĂ©e (c’est ce qu’on appelle une rĂ©fĂ©rence dangling ou sauvage).

Rust, pour Ă©viter cela, utilise un systĂšme de rĂ©fĂ©rences sĂ©curisĂ©es avec des rĂšgles strictes de propriĂ©tĂ© (ownership), de prĂȘt (borrowing) et de durĂ©e de vie (lifetime). PlutĂŽt que de laisser les programmeurs manipuler directement les adresses mĂ©moire comme en C ou C++, Rust utilise des rĂ©fĂ©rences qui garantissent :

  • qu’une rĂ©fĂ©rence ne peut pas pointer vers de la mĂ©moire libĂ©rĂ©e
  • qu’une rĂ©fĂ©rence mutable ne peut ĂȘtre utilisĂ©e que par un seul emprunteur Ă  la fois, pour Ă©viter les conflits
  • que le compilateur vĂ©rifie toutes ces rĂšgles pour garantir la sĂ©curitĂ© de la mĂ©moire
  1. Pas de valeur “nulle” par dĂ©faut

La valeur nulle (ou NULL) est une notion qui existe dans de nombreux langages pour reprĂ©senter “l’absence de valeur”. Cependant, cette valeur est une grande source de bugs car les programmeurs oublient souvent de vĂ©rifier si une valeur est “nulle” avant de l’utiliser. Si un programme essaie de manipuler une valeur qui est “nulle”, cela peut causer une erreur qui fait planter le programme (on parle de null pointer exception).

Rust a choisi de ne pas avoir de valeur null par dĂ©faut. À la place, il utilise un type spĂ©cial appelĂ© Option pour reprĂ©senter une valeur qui peut ĂȘtre prĂ©sente ou absente.

Enums & Match

Cette dĂ©finition de l’Ă©numĂ©ration AdresseIp indique que chacune des variantes V4 et V6 auront des valeurs associĂ©es de type diffĂ©rents :

    struct Ipv4Addr {}
    
    struct Ipv6Addr {}
    
    enum IpAddr {
        V4(Ipv4Addr),
        V6(Ipv6Addr),
    }

ou bien:

    enum Message {
        Quitter,
        Deplacer { x: i32, y: i32 }, // Structure anonyme
        Ecrire(String), // Tuple
    }

    // Il est possible d'utiliser impl avec un enum:
    impl Message {
        fn appeler(&self) {}
    }
    
    let m = Message::Ecrire(String::from("hello"));
    m.appeler();

Comme la valeur NULL n’existe pas en Rust ce manque est compensĂ© par l’implĂ©mentation de l’enum Option<T>. De cette façon une valeur peut-ĂȘtre soit prĂ©sente soit absente.

   enum Option<T> {
       None,
       Some(T),
   }

   // Exemples
    let un_nombre = Some(5);
    let une_chaine = Some("une chaĂźne");

    let nombre_absent: Option<i32> = None; // Nous spĂ©cifions le type de la variable car Rust ne peut pas infĂ©rer le type lui-mĂȘme.

L’Option<T> permet de garantir que la manipulation d’une variable contient bien une valeur valide. Rust nous oblige a Ă©valuer chaque variante de l’enum pour pouvoir en extraire la valeur.

Un match appliquĂ© Ă  un enum permettra de s’assurer que chaque variante a Ă©tĂ© gĂ©rĂ©e.

    enum PieceUs {
        Penny,
        Nickel,
        Dime,
        Quarter,
    }
    
    fn valeur_en_centimes(piece: PieceUs) -> u8 {
        match piece {
            PieceUs::Penny => 1,
            PieceUs::Nickel => 5,
            PieceUs::Dime => 10,
            PieceUs::Quarter => 25,
        }
    }

Autre exemple avec une valeur générique :

    let jete_de_de = 9;
    match jete_de_de {
        3 => ajouter_chapeau_fantaisie(),
        7 => enleve_chapeau_fantaisie(),
        _ => (),
    }

    fn ajouter_chapeau_fantaisie() {}
    fn enleve_chapeau_fantaisie() {}

NB: Le if / let ne permettra pas de couvrir toutes les variantes de l’enum :

    let une_valeur_u8 = Some(3u8);
    if let Some(max) = une_valeur_u8 {
        println!("Le maximum est réglé sur {}", max);
    }

Packages & Crates

Un package peut contenir une librairie au maximum, et plusieurs binaires. Mais au miniumum il doit contenir une librairie ou un binaire.

monprojet/ <-------------- package
    Cargo.toml <---------- definition du package
    src/
        main.rs <--------- un crate
        lib.rs (et ou) <-- un crate
    
// autre cas
monprojet/
    Cargo.toml <--
    src/
        lib.rs
        bin/
            main1.rs <---- un crate
            main2.rs <---- un autre crate

Un module rassemble des elements Ă  l’intĂ©rieur d’un crate pour donner de la cohĂ©rence Ă  l’orgamisation du projet :

// src/libs.rs
    mod salle_a_manger {
        mod accueil {
            fn ajouter_a_la_liste_attente() {}
            fn installer_a_une_table() {}
        }

        mod service {
            fn prendre_commande() {}
            fn servir_commande() {}
            fn encaisser() {}
        }
    }

Ce qui donne cette arborescence :

crate // lib.rs ou main.rs parent de toute l'arborescence
 └── salle_a_manger
     ├── accueil
     │   ├── ajouter_a_la_liste_attente
     │   └── installer_a_une_table
     └── service
         ├── prendre_commande
         ├── servir_commande
         └── encaisser

Grùce aux modules, nous pouvons regrouper ensemble des définitions qui sont liées et donner un nom à ce lien. Ils servent aussi à gérer la visibilité des fonctionnalités.

Pour indiquer Ă  Rust oĂč trouver un Ă©lĂ©ment dans l’arborescence, il a deux types de chemins :

  • Les chemins absolus –> crate ou le nom d’une crate.
  • Les chemins relatifs –> self, super ou un indicateur Ă  l’intĂ©rieur du module.
// src/lib.rs
    mod salle_a_manger {
        pub mod accueil { // Les modules ne servent pas uniquement à organiser votre code. Ils définissent aussi les limites de visibilité de Rust.
            pub fn ajouter_a_la_liste_attente() {} // Rendre le module public ne rend pas son contenu public
        }
    }

    pub fn manger_au_restaurant() {
        // Chemin absolu
        crate::salle_a_manger::accueil::ajouter_a_la_liste_attente();

        // Chemin relatif
        salle_a_manger::accueil::ajouter_a_la_liste_attente(); // Commencer par un nom signifie que le chemin est relatif.
        // On peut utiliser directement salle_a_manger car il est dĂ©fini au mĂȘme niveau que manger_au_restaurant().
    }

NB: Le module salle_a_manger n’est pas public, mais comme la fonction manger_au_restaurant est dĂ©finie dans le mĂȘme module que salle_a_manger (car manger_au_restaurant et salle_a_manger sont frĂšres), nous pouvons utiliser salle_a_manger Ă  partir de manger_au_restaurant

La visibilité en Rust fait en sorte que tous les éléments (fonctions, méthodes, structures, énumérations, modules et constantes) sont privés par défaut. Les éléments dans un module parent ne peuvent pas utiliser les éléments privés dans les modules enfants, mais les éléments dans les modules enfants peuvent utiliser les éléments dans les modules parents.

Super

Nous pouvons aussi crĂ©er des chemins relatifs qui commencent Ă  partir du module parent en utilisant super au dĂ©but du chemin. C’est comme dĂ©buter un chemin dans un systĂšme de fichiers avec la syntaxe ...

Pub

Nous pouvons aussi utiliser pub pour dĂ©clarer des structures et des Ă©numĂ©rations publiquement, mais il y a d’autres points Ă  prendre en compte. Si nous utilisons pub avant la dĂ©finition d’une structure, nous rendons la structure publique, mais les champs de la structure restent privĂ©s. Par contre, si nous rendons publique une Ă©numĂ©ration, toutes ses variantes seront publiques.

Use

Le mot-clef use permet de dĂ©finir une importation et rend accessible dans le fichier oĂč il est appelĂ© tous les objets publics prĂ©sents dans le chemin spĂ©cifiĂ©.

// lib/src.rs
    mod salle_a_manger {
        pub mod accueil {
            pub fn ajouter_a_la_liste_attente() {}
        }
    }

    use (crate::)salle_a_manger::accueil::ajouter_a_la_liste_attente;

    pub fn manger_au_restaurant() {
        ajouter_a_la_liste_attente();
        ajouter_a_la_liste_attente();
        ajouter_a_la_liste_attente();
    }

Dans une portĂ©e, utiliser un use s’apparente Ă  crĂ©er un lien symbolique dans le systĂšme de fichier.

NB: l’import de deux types ayant le mĂȘme nom dans la mĂȘme portĂ©e nĂ©cessite d’utiliser leurs modules parents.

    use std::fmt;
    use std::io;

    fn fonction1() -> fmt::Result {}

    fn fonction2() -> io::Result<()> {}

Une autre alternative si deux types ont le mĂȘme nom est d’utiliser as.

    use std::fmt::Result;
    use std::io::Result as IoResult;

    fn fonction1() -> Result {}

    fn fonction2() -> IoResult<()> {}

Il est possible de réexporter un élément privée en public:

//src/lib.rs
    mod salle_a_manger {
        pub mod accueil {
            pub fn ajouter_a_la_liste_attente() {}
        }
    }

    pub use crate::salle_a_manger::accueil;

    pub fn manger_au_restaurant() {
        accueil::ajouter_a_la_liste_attente();
        accueil::ajouter_a_la_liste_attente();
        accueil::ajouter_a_la_liste_attente();
    }

GrĂące Ă  pub use, le code externe peut maintenant appeler la fonction ajouter_a_la_liste_attente en utilisant accueil::ajouter_a_la_liste_attente. Si nous n’avions pas utilisĂ© pub use, la fonction manger_au_restaurant aurait pu appeler accueil::ajouter_a_la_liste_attente dans sa portĂ©e, mais le code externe n’aurait pas pu profiter de ce nouveau chemin.

Exemple d’imbrication d’importations:

    use std::io;
    use std::io::Write;

    // =

    use std::io::{self, Write};

    // Pour importer tous les éléments:

    use std::collections::*; // NB: cette méthode est déconseillée sauf dans le cas des tests.

Collections

Une collection est un regroupement de plusieurs Ă©lĂ©ments. La taille d’une collection n’est pas connue Ă  la dĂ©claration ce qui veut dire que les donnĂ©es seront stockĂ©es dans la heap car Rust ne peut pas dĂ©terminer sa taille Ă  la compilation car elle dĂ©pendra de l’exĂ©cution du programme. Parmi les collections les plus usitĂ©es nous retrouvons:

  • Vec, liste de variables du mĂȘme type.
  • String, collection de char.
  • HashMap qui permet d’associer une variable Ă  une clef.

Vec

Un vecteur peut stocker n’importe quel type mais la liste doit contenir des Ă©lĂ©ments de mĂȘme type.

    let mut v: Vec<i32> = Vec::new(); // Initialisation avec annotation de type
    v.push(5);

NĂ©anmoins, Rust peut infĂ©rer le type lui-mĂȘme selon les instructions qui vont suivre l’initialisation. Par exemple:

    let v = vec![1, 2, 3];

NB: La mémoire sera libérée lorsque le vecteur sortira de la portée.

Il existe deux façons de désigner une valeur enregistrée dans un vecteur : via les indices ou en utilisant la méthode get.

    let v = vec![1, 2, 3, 4, 5];

    // Cette méthode peut paniquer
    let troisieme: &i32 = &v[2]; // Renvoie une référence
    println!("Le troisiÚme élément est {}", troisieme);

    match v.get(2) { // Renvoie une Option<&T>
        Some(troisieme) => println!("Le troisiÚme élément est {}", troisieme),
        None => println!("Il n'y a pas de troisiÚme élément."),
    }

Lorsque le programme obtient une rĂ©fĂ©rence valide, le vĂ©rificateur d’emprunt va faire appliquer les rĂšgles de possession et d’emprunt pour s’assurer que cette rĂ©fĂ©rence ainsi que toutes les autres rĂ©fĂ©rences au contenu de ce vecteur restent valides. On ne peut pas avoir des rĂ©fĂ©rences mutables et immuables dans la mĂȘme portĂ©e.

    let mut v = vec![1, 2, 3, 4, 5];

    let premier = &v[0]; // <-- erreur de compilation

    v.push(6); // <------------ erreur de compilation

    println!("Le premier élément est : {}", premier);

Pour ajouter un Ă©lĂ©ment dans un vecteur, Rust va utilisĂ© l’espace contigue dans la mĂ©moire. Si il n’y a pas assez de place il devra rĂ©allouer l’intĂ©gralitĂ© du vecteur dans un autre espace mĂ©moire. Dans ce cas, le pointeur du premier Ă©lĂ©ment du vecter fera rĂ©fĂ©rence Ă  un emplacement mĂ©moire dĂ©sallouĂ©.

Il est possible d’utiliser un Ă©num, cela permet de stocker des Ă©lĂ©ments de type diffĂ©rents dans un mĂȘme vecteur.

String

HashMap


🔗