SESSION

Programming Rust

Edition 2021

Installation de RUST

rustup, rustc & cargo

Installation de Rust

cd $HOME
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

o installer le programme curl

o Choisir l'installation par défaut

La toolchain stable sera installée, 2 dossiers caches seront crées:

~/ .cargo

~/ .rustup

o Maintenant, ajouter les programmes rust au $PATH. Le compilateur RUST, les outils et les programme tiers seront désormais accessibles.

echo "source $HOME/.cargo/env" >> .bashrc
exec bash

Rustup permet de gérer les toolchains et d'obtenir aussi de la documentation sur Rust, qui est très complète.

Rustup

rustup toolchain install nightly

2 toolchains principales

o stable

o nightly: fonctionnalités plus avancées mais qui peuvent changer, c'est a dire que le code doit être parfois modifié pour compiler après une mise a jour. Il arrive que des bugs soient présents mais c'est en général très rare.

 

o Installation de la toolchain nightly

o Il existe aussi une toolchain nommée beta

o Chaque toolchain existe pour différentes architectures

username@mordak-pc:~$ rustup show
Default host: x86_64-unknown-linux-gnu
rustup home:  /home/username/.rustup

installed toolchains
--------------------

stable-x86_64-unknown-linux-gnu (default)
nightly-x86_64-unknown-linux-gnu

active toolchain
----------------

stable-x86_64-unknown-linux-gnu (default)
rustc 1.62.0 (a8314ef7d 2022-06-27)
username@mordak-pc:~/Documents/x-exchange$ cat rust-toolchain 
[toolchain]
channel = "nightly-2022-06-14"
# https://rust-lang.github.io/rustup-components-history/
rustup update
rustup target add arm-linux-androideab

rustup target add arm-linux-androideab

o Un projet peut avoir une toolchain spécifique différente de celles installées sur la session utilisateur.

o Accéder à la liste des versions nightly et leurs fonctionnalités

Et cela pour toutes les architectures supportées (Target)

rustup doc

o Accéder à la documentation de Rust (Toolchain & target par défaut)

o En savoir plus sur Rustup

Rustc est le compilateur Rust, l’équivalent de gcc pour le C. Dans la pratique, on ne l'utilise jamais directement, l'on préfère utiliser cargo.

Rustc

username@mordak-pc:~$ echo "fn main() { println!(\"Hello World\"); }" > main.rs 
username@mordak-pc:~$ rustc main.rs 
username@mordak-pc:~$ ./main 
Hello World
username@mordak-pc:~$

Cargo est au Rust ce que make est au C mais en bien mieux, il compile le programme et gère les dépendances. A la place du fichier Makefile, il utilise un fichier Cargo.toml, un fichier Cargo.lock est écrit par le cargo, ce dernier précise les versions de chaque dépendance.

Cargo

username@mordak-pc:/Documents/x-exchange$ cat Cargo.lock
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3

[[package]]
name = "addr2line"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
dependencies = [
 "gimli",
]

[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"

...

Cargo.toml

username@mordak-pc:/Documents/x-exchange$ cat Cargo.toml 
[package]
name = "x-exchange"
version = "0.1.0"
authors = ["mordak & vcombey"]
edition = "2021"

[[bin]]
name = "x-server"
path = "x-server/main.rs"

[[bin]]
name = "ohlc-register"
path = "ohlc-register/main.rs"

[[bin]]
name = "client"
path = "client/main.rs"

[dependencies]
clap = "2.23.0"
lazy_static = "1.4.0"
unixcli = "0.1.3"
rustyline = "6.0.0"
chrono = "0.4.10"
log = "0.4.8"
colored = "2.0.0"
rust_decimal = { version = "1.25.0", features = ["serde-float"] }
lettre = "0.9.3"
regex = "1.4.1"
serde = { version = "1.0.117", features = ["derive"] }
serde_json = "1.0"
dotenv = "0.15.0"
dotenv_codegen = "0.15.0"
aes = "0.6.0"
block-modes = "0.7.0"
hex-literal = "0.3.1"

[features]
fake-token = ["kraken-rust-api/fake-token"]

[dependencies.reqwest]
version = "0.10.8"
features = ["blocking"]

[dependencies.kraken-rust-api]
path = "dependencies/kraken-rust-api"

[dependencies.ta]
path = "dependencies/ta-rs"

[build-dependencies]

[workspace]
members = [
    "dependencies/kraken-rust-api","dependencies/ta-rs",
]

Les dépendances de [dependencies] se trouvent sur https://crates.io/

Premier programme

La magie de Cargo

On ne peut faire plus simple...

username@mordak-pc:~$ cargo new premier_programme
username@mordak-pc:~$ cd premier_programme/
username@mordak-pc:~/premier_programme$ ls -lR
.:
total 8
-rw-r--r-- 1 username username  186 Jul  2 00:30 Cargo.toml
drwxr-xr-x 2 username username 4096 Jul  2 00:30 src

./src:
total 4
-rw-r--r-- 1 username username 45 Jul  2 00:30 main.rs

Il suffit d'utiliser la commande cargo new avec en argument le nom du programme.

Cargo a écrit tous les fichiers nécessaires au projet.

Il n'y a pas de fichier Cargo.lock car pour l'instant, nous n'utilisons aucune dépendance.

On ne peut faire plus simple...

username@mordak-pc:~/premier_programme$ cat Cargo.toml 
[package]
name = "premier_programme"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
fn main() {
    println!("Hello, world!");
}

Le fichier main.rs du dossier /src contient déjà son Hello World.

Quand au fichier Cargo.toml, il contient le minimum.

On ne peut faire plus simple...

username@mordak-pc:~/premier_programme$ cargo build
   Compiling premier_programme v0.1.0 (/home/username/premier_programme)
    Finished dev [unoptimized + debuginfo] target(s) in 0.56s

o Compiler le programme:

o Exécuter le programme:

username@mordak-pc:~/premier_programme$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/premier_programme`
Hello, world!

Notez que Cargo run compile puis exécute, il n'y a pas besoin de build explicitement si c'est seulement pour l’exécution.

o Notez aussi que le 'profile' de compilation utilise ici (par défaut) est celui de debug, pour les versions de production, l'on utilisera le profil release.

username@mordak-pc:~/premier_programme$ cargo run --release
    Finished release [optimized] target(s) in 0.00s
     Running `target/release/premier_programme`
Hello, world!

On ne peut faire plus simple...

username@mordak-pc:~/premier_programme$ cargo doc --open

o Enfin, il est possible de générer la propre documentation de son programme:

La documentation sera ouverte dans le navigateur Web par défaut, cargo doc tout court la généré et --open l'affiche directement.

On ne peut faire plus simple...

o Maintenant, commentons notre code:

/// La fonction principale de mon programme
fn main() {
    // Write to stdout
    println!("Hello, world!");
}

On ne peut faire plus simple...

Comme nous pouvons l'observer, les commentaires composs de 3 slashs ont été écrits dans la documentation du programme.  A contrario, ceux avec deux slashs ne serviront qu'à préciser quelque chose qui restera uniquement dans le code.

fn main() {
    // Write to stdout
    println!("Hello, world!");
    /*
    Ce est
    un commentaire
    multiligne
    */
}

o Il existe aussi evidement des commentaires multi-lignes.

o D'autres types de commentaire existent, dont un qui est souvent utilisé  //!

Ce lien donne tous les exemples.

o Il existe aussi evidement des commentaires multi-lignes.

o Il existe aussi évidement des commentaires multi-lignes.

On ne peut faire plus simple...

Enfin, il existe un autre outil de cargo qui est cargo-fmt, il permet de formater le code selon  soit la norme Rust par défaut, soit une norme que nous avons définie dans le fichier rustfmt.toml. Notez que Rust par défaut utilise des tabulations de 4 espaces !

mordak@mordak-pc:~/Documents/KFS/rust_kernel$ cat rustfmt.toml
max_width = 120

Il suffit d'utiliser la commande cargo-fmt pour lancer le formatage du code.

mordak@mordak-pc:~/premier_programme$ cat -e src/main.rs 
   //! La fonction principale de mon programme$
fn main()    {$
		    // Write to stdout$
    		println!("Hello, world!");                                        $
}$
mordak@mordak-pc:~/premier_programme$ cargo-fmt
mordak@mordak-pc:~/premier_programme$ cat -e src/main.rs 
//! La fonction principale de mon programme$
fn main() {$
    // Write to stdout$
    println!("Hello, world!");$
}$

Les dépendances

Utilisation d'une crate

La crate colored

Une crate est un binaire ou une bibliothèque, elles sont répertoriées sur le site https://crates.io. Il y a toutes les contributions de la communauté Rust, chacun peut créer des crates et les y diffuser. Il n'est cependant pas nécessaire de diffuser une de ses propres crate pour pouvoir l'utiliser, on se contentera dans ce cas de la mettre en dépendance directement dans le code source du programme., nous reviendrons plus tard sur la notion de workspace.  Ici pour l'exemple, nous allons utiliser la crate colored de crate.io afin que notre Hello World puisse prendre quelques couleurs.

[dependencies]
colored = "2.0.0"

o Première chose à faire, modifier le fichier Cargo.toml, l'on rajoute la dépendance et sa version dans la catégorie [dependencies]:

La crate colored

Elle sera téléchargée lors de notre prochain cargo build

mordak@mordak-pc:~/premier_programme$ cargo build
    Updating crates.io index
   Compiling libc v0.2.126
   Compiling lazy_static v1.4.0
   Compiling atty v0.2.14
   Compiling colored v2.0.0
   Compiling premier_programme v0.1.0 (/home/mordak/premier_programme)
    Finished dev [unoptimized + debuginfo] target(s) in 16.05s

Cargo s'est aussi chargé de lui-même de télécharger les dépendances de la crate.

La crate colored

Faisons maintenant un tour dans la documentation. cargo doc --open

Cargo s'est aussi charge de lui meme de telecharger les dependances de la crate

Text

La documentation de la crate colored est désormais accessible.

La crate colored

Grace à la documentation, on apprend facilement comment utiliser la crate.

La crate colored

use colored::Colorize;

/// La fonction principale de mon programme
fn main() {
    // Write to stdout
    println!("{}", "Hello, world!".cyan());
}

Ici, l'exemple le plus proche de ce que nous voulons est ceci:

Si l'on reporte ces modifications dans notre code, cela donnerait ceci

Exécutons

Les tests unitaires

cargo test ou comment s'assurer du bon fonctionnement de son programme

Les tests unitaires

fn fibo(n: u32) -> u32 {
    n
}

fn main() {
}

#[cfg(test)]
mod test {
    use crate::fibo;

    #[test]
    fn check_fibonacci() {
        assert_eq!(fibo(1), 1)
    }
}

o D'abord nous allons créer un nouveau programme nomme fibonacci.

mordak@mordak-pc:~$ cargo new fibonacci
     Created binary (application) `fibonacci` package

o Ensuite, modifier le fichier main.rs comme tel

Les tests unitaires

o La commande cargo test donne ceci:

mordak@mordak-pc:~/Documents/fibonacci$ cargo test
   Compiling fibonacci v0.1.0 (/home/mordak/Documents/fibonacci)
    Finished test [unoptimized + debuginfo] target(s) in 0.27s
     Running unittests src/main.rs (target/debug/deps/fibonacci-d72ba84568f68472)

running 1 test
test test::check_fibonacci ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Les tests unitaires

fn fibo(n: u32) -> u32 {
    n
}

o u32 est un type primitif encode sur 32 bits et positif.

 

o En rust, lorsque l'on déclare une variable, on déclare d'abord l'identifiant puis ensuite le type, d’où le n: u32 !

 

o La flèche -> indique le retour de la fonction suivi du type

 

o Il est souvent inutile d'utiliser le mot clef return, bien qu'il existe.

Les tests unitaires

#[cfg(test)]
mod test {
    use crate::fibo;

    #[test]
    fn check_fibonacci() {
        assert_eq!(fibo(1), 1)
    }
}

o #[cfg(test)] ne s’exécutera qu'à l'invocation de la commande cargo test.

 

o On doit créer un sous-module pour les tests.

 

o Ici, crate dans crate::fibo fait référence au projet lui-même, et la fonction fibo en est a la racine. on aurait pu utiliser super à la place étant donné la hiérarchie du module. Le sous-module n'a pas accès directement a la fonction fibo, d’où l'importation.

Les tests unitaires

#[cfg(test)]
mod test {
    use crate::fibo;

    #[test]
    fn check_fibonacci() {
        assert_eq!(fibo(1), 1)
    }
}

o assert_eq! est une macro Rust de la STD,  elle fonctionne comme une assertion en C, eq signifie équivalent et vérifie si l'expression de droite et de gauche sont égales.

 

o En outre, il existe aussi assert_ne! qui fait le contraire.

Les tests unitaires

    #[test]
    fn check_fibonacci() {
        assert_eq!(fibo(1), 2)
    }

o Si l'assertion est au contraire fausse le programme va 'paniquer', il stop et explique pourquoi.

Les types de base

Et plusieurs notions de Rust

Les types numériques

NB: Les types f80 et f128 n'existent pas en Rust pour des raisons de portage de code entre les différentes architectures.

    let a: u32 = 42;
    let b: i64 = -127;
    let c: isize = -128;
    let pi: f32 = std::f32::consts::PI;
    let pi: f64 = std::f64::consts::PI;
unsigned signed float representation
u8 i8 8 bits
u16 i16 16 bits
u32 i32 f32 32 bits
u64 i64 f64 64 bits
u128 i128 128 bits
usize isize cpu register size

Les booléens

Aucune réelle surprise dans les conditions.

    let b: bool = false;
    let c: bool = true;
    let b: bool = 0;
    let c: bool = 1;
    let mut b: bool = false;
    if !b {
        println!("B is false");
    }
    b = true;
    if b {
        println!("B is true");
    }
    let c: bool = true;
    if b & c {
        println!("B and C are true")
    }

Les caractères

    let c: char = 't';
    println!("len: {} - {}", c.len_utf8(), c);
    let c: char = 'ф';
    println!("len: {} - {}", c.len_utf8(), c);
    let c: char = '錆';
    println!("len: {} - {}", c.len_utf8(), c);
len: 1 - t
len: 2 - ф
len: 3 - 錆

format UTF8 pour les caractères en Rust

Les couples (Tuples)

    fn mul(n: (u32, f64), e: u32) -> (u32, f64) {
        (n.0 * e, n.1 * e as f64)
    }
    let g = mul((42, std::f64::consts::PI), 2);    
    dbg!(g);
[src/main.rs:60] g = (
    84,
    6.283185307179586,
)

Un couple est une collection de différents types, on utilisera une parenthèse.

NB: L’accès aux éléments d'un tuple avec le .n peut être source de beaucoup d'erreurs surtout si les types contenus sont identiques, certains programmeurs évitent au maximum le recours aux tuples.

La macro dbg! permet d'afficher les champs d'une variable facilement.

Type ou l’équivalent de Typedef en C

    type Tuple = (u32, f64);
    fn mul2(n: Tuple, e: u32) -> Tuple {
        (n.0 * e, n.1 * e as f64)
    }
    let g = mul2((42, std::f64::consts::PI), 2);    
    dbg!(g);

Tout comme en C, il est possible de nommer ses propres alias de type.

    let g = mul2(Tuple(42, std::f64::consts::PI), 2);    
    dbg!(g);

Cependant, les alias de type ne sont pas des constructeurs.

NB: Généralement le compilateur Rust devine bien ce que l'on a tenté de faire !

Introduction a l’inférence de type

    // Les types ne sont pas declares ici
    let mut a = (42, 1.25);
    fn mul3(n: (u32, f64), e: u32) -> (u32, f64) {
        (n.0 * e, n.1 * e as f64) 
    }
    a = mul3(a, 2);
    dbg!(a);

Il ne sert à rien de déclarer tous les types a Rust car le compilateur tentera de découvrir par lui-même quels sont les types utilisés. C'est assez trivial quand on appelle une fonction qui prend un type donne en argument par exemple.

Le compilateur devine que mon tuple a pour type (u32, f64).

Introduction a l’inférence de type

Cependant, si je rajoute cette fonction à mon code:

    fn mul4(n: (u32, f32), e: u32) -> (u32, f32) {
        (n.0 * e, n.1 * e as f32) 
    }
    a = mul4(a, 2);
    dbg!(a);

Le compilateur ne comprendra plus si mon type est  (u32, f64) ou bien (u32, f32)...

La mutabilité

Certaines variables ont été déclarées avec le mot clef mut et d'autres non. Il est en fait obligatoire de signaler à Rust si une variable peut changer de valeur en cours de route. La raison ?

Aider le programmeur à bien comprendre ce qu'il fait et lui éviter bon nombre d'erreurs.

La mutabilité

    let a: u32 = 42;
    a += 1;
    let mut a: u32 = 42;
    dbg!(a);
    return;

La variables globales

static PORT1: u32 = 8080;
const PORT2: u32 = 8443;

fn dump_ports() {
    println!("Port1: {}", PORT1)
    println!("Port2: {}", PORT2)
}
Port1: 8080
Port2: 8443

Il existe deux types de variables globales, les constantes et les mutables, Rust les traitera de façon très différente.

o Constantes:

Elles peuvent se déclarer via deux mots clef, soit static soit const, sans entrer dans les détails, static représente un endroit en mémoire et const non.

La variables globales

static mut PORT: u32 = 8080;

fn change_port() {
    PORT = 8443;
}
Port1: 8080
Port2: 8443

o Mutables:

On les déclare via les mots clefs static mut.

Ça ne compile pas...

Pourquoi le compilateur demande d’écrire un bloc unsafe ?

La variables globales

static mut PORT: u32 = 8080;

fn change_port() {
    unsafe {
        PORT = 8443;
    }
}

La raison est décrite dans l'erreur:

note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior

Plusieurs threads peuvent accéder en même temps à cette variable et provoquer des comportement indéfinis. Même si le programme ne possède qu'un seul thread, le compilateur ne laissera pas le code compiler sans le mot clef unsafe.

Rust est un langage dit concurrent, son compilateur veille à ce qu'il n'y ait pas de problème entre les ressources utilisées par plusieurs threads.

Ici, marquer unsafe permet au programmeur de vite comprendre qu'il peut y avoir un problème ici si son programme bug.  Un Mutex réglerait le problème.

Le trait Copy

let a = true;
let b = a;
dbg!(a);
dbg!(b);
let a = (std::f32::INFINITY, std::f32::INFINITY);
let b = a;
dbg!(a);
dbg!(b);

Notons aussi que tous ces types de base implémentent le trait Copy. C'est à dire que les valeurs peuvent être aisément dupliquées entres plusieurs variables.

Rechercher std::marker::Copy dans la documentation

Les traits Copy et Clone

let banane: String = "Banane".into();
let deuxieme_banane = banane;
dbg!(banane);
dbg!(deuxieme_banane);

Si au contraire, l'on tente d'utiliser un type plus complexe comme String par exemple. Ce type est dynamiquement alloué en mémoire (sur le tas) et n’implémente pas le trait Copy. Une conséquence fâcheuse à cela serait de gaspiller de la mémoire sans que le programmeur ne s'en rendre vraiment compte.

A la seconde ligne, la variable banane n'a pas été copiée, c'est un move, cette première variable ne possède plus l'objet banane, qui en fait, passe sur la variable deuxieme_banane. Il n'y a pas eu de copie.

Les traits Copy et Clone

let banane: String = "Banane".into();
let deuxieme_banane = banane.clone();
dbg!(banane);
dbg!(deuxieme_banane);

Notons qu'il n'y aurait pas eu de problème dans le code si on n'avait plus tenté de lire la variable banane d'origine. Si l'on veut forcer la copie, quitte à prendre le risque de gaspiller un peu de mémoire, l'on doit explicitement utiliser le trait Clone. Le type String implémente Clone.

Rechercher  std::clone::Clone dans le documentation

Le trait Clone nécessite la méthode clone()

Le trait Clone

#[derive(Clone)]
struct Remote {
    ipv4: (u8, u8, u8, u8),
    port: u32,
    server_name: String,
}
let r1 = Remote {
    ipv4: (192, 168, 41, 1),
    port: 8080,
    server_name: "xp6".into(),
};
let r2 = r1.clone()

La méthode clone() est dans la partie nommée Required Methods. Pourquoi required ? Cela veut dire que tout objet qui veut implémenter le trait Clone doit définir la méthode clone(). Dans de très rares cas, le programmeur doit implémenter ce trait manuellement bien que normalement, il puisse utiliser la directive #[derive(Clone)] pour ses propres structures.

Sans le derive, la structure Remote ne serait pas clonable. Et le derive ne fonctionne que si tous les sous types de la structures implémentent Clone.

Le trait Clone

Enfin TOUT ce qui est implémente Copy implémente aussi Clone. Il est aussi possible de cloner explicitement les types de base mais ça ne sert a rien.

* L’implémentation du trait Clone est nécessaire pour le trait Copy.

On a fait le tour des types de base en Rust, on ne pouvait pas éviter d'aborder certains concepts du langage car le maniement de ces types mêmes simples demande de comprendre un peu ces concepts.

If else et les boucles

Conditions et répétitions

if et else

fn main() {
    let n: u32 = 11 - 3;
    if n == 8 {
        println!("8");    
    } else if (n > 8) {
        println!(">8: {}", n)
    } else {
        println!("<8: {}", n)
    }
    let a = true;
    if n == 8 && a == true {
        println!("8 et a: true");    
    }
}

Rien de bien nouveau, si ce n'est que les parenthèses sont inutiles en Rust.

if et else

fn main() {
    let n: u32 = 11 - 3;
    if n == 8 {
        println!("8");    
    } else if n > 8 {
        println!(">8: {}", n)
    } else println!("<8: {}", n);
    let a = true;
    if n == 8 && a == true {
        println!("8 et a: true");    
    }
}

Et que les brackets sont indispensables. Ici, oublie à la ligne 7.

la boucle loop

fn main() {
    let mut count = 0u32;

    println!("Let's count until infinity!");
    // Infinite loop
    loop {
        count += 1;
        if count == 3 {
            println!("three");
            // Skip the rest of this iteration
            continue;
        }
        println!("{}", count);
        if count == 5 {
            println!("OK, that's enough");
            // Exit this loop
            break;
        }
    }
}

Simple boucle infinie,  les directives de contrôle continue et break fonctionnent comme dans d'autres langages. loop, c'est toujours mieux que while(1) { ... } du C.

la boucle while

fn main() {
    // A counter variable
    let mut n = 1;

    // Loop while `n` is less than 101
    while n < 101 {
        if n % 15 == 0 {
            println!("fizzbuzz");
        } else if n % 3 == 0 {
            println!("fizz");
        } else if n % 5 == 0 {
            println!("buzz");
        } else {
            println!("{}", n);
        }

        // Increment counter
        n += 1;
    }
}

Rien de bien nouveau non plus, l'on enlève juste les parenthèses qui ne servent a rien en Rust et le programme se comporte comme il le ferait en C....

la boucle for

#include <stdio.h>
int main(void) {
    for (int i = 0; i < 10; i++) {
        printf("%i\n", i);
    }
    return 0;
}

La boucle For est beaucoup plus intéressante puisqu'elle prend en paramètre un iterateur (aspect fonctionnel de Rust). Iterator est un autre trait de Rust, et le type Range l’implémente.

fn main() {
    for i in 0..10 {
        println!("{}", i);
    }
}

o code C équivalent

la boucle for

#include <stdio.h>
int main(void) {
    for (int i = 0; i < 10; i += 2) {
        printf("%i\n", i);
    }
    return 0;
}

Un Itérateur peut utiliser la méthode step_by(self,  step: usize) -> StepBy<Self> qui produit une structure StepBy qui implémente aussi le trait Iterator, cela produit donc un autre itérateur.

fn main() {
    for i in (0..10).step_by(2) {
        println!("{}", i);
    }
}

o code C équivalent:

Consulter la documentation sur std::ops::Range, le trait Iterator et sa méthode step_by ainsi que la structure std::iter::StepBy

la boucle for

Ici l'on un itérateur sur un Array, le type Array implémente le trait IntoItertor

fn main() {
    let fizzbuzz = ["Fizz", "Buzz", "FizzBuzz"];
    // Creation d'un iterateur sur l'array
    let iter = fizzbuzz.iter();
    // On utilise une refence ici pour ne pas 'move'
    // par erreur l'iterateur, un iterateur n'est pas Copy
    dbg!(&iter);
    for word in iter {
        println!("{}", word);
    }
}
[src/main.rs:32] &iter = Iter(
    [
        "Fizz",
        "Buzz",
        "FizzBuzz",
    ],
)
Fizz
Buzz
FizzBuzz

La gestion des erreurs

Ok() or Err()

Qu'est-ce que Result ?

En Rust, beaucoup de fonctions peuvent renvoyer un Result, c'est à dire une enum de deux valeurs, soit Ok(valeur attendue) ou bien Err(type d'erreur).

fn main() {
    // On declare une chaine de caractere
    // Elle ne contient que des chiffres
    let a = "1637";
    let res = a.parse::<u32>();
    // eq: let res: Result<u32, _> = a.parse(); cf turbofish
    dbg!(number);

    // Ici, une chaine comppsee de chiffre et de lettre
    let b = "16cents trente-sept";
    let res = b.parse::<u32>();
    dbg!(res);
}

Exécutons le code suivant:

La fonction parse::<u32>() sur une chaîne de caractère tente de convertir en u32.

Les deux cas de Result

Sortie du programme:

pub fn parse<F>(&self) -> Result<F, <F as FromStr>::Err>
where
    F: FromStr, 
[src/main.rs:35] res = Ok(
    1637,
)
[src/main.rs:40] res = Err(
    ParseIntError {
        kind: InvalidDigit,
    },
)

Le prototype de la fonction parse.

o Elle prend un generic F , ici u32 renseigne grâce au turbofish

o Ce generic F doit implémenter le trait FromStr (where)

o Elle retourne un Result<F, type associe d'erreur>

Définition de Result

Text

T et E sont deux types génériques.

if let et Result

fn main() {
    // Ici, une chaine comppsee de chiffre et de lettre
    let b = "16cents trente-sept";
    let result = b.parse::<u32>();
    dbg!(&result);

    if let Ok(number) = result {
        println!("result: {}", number);
    }
    if let Err(err) = result {
        println!("error: {}", err);
    }
}

Il est possible de connaître la valeur d'un résultat grâce à if let, comme on peut le faire avec n'importe quelle enum. Notez qu'il existe un outil bien puissant pour la gestion des énumérations qui se nomme le pattern matching, on abordera ça plus tard.

Gestion fine des erreurs

Que doit-on faire face a une erreur ?

Il existe en Rust grosso modo deux façons de gérer les erreurs.

fn main() {
    let b = "16cents trente-sept";
    let result = b.parse::<u32>();
    if let Err(error) = result {
        println!("Sorry, but an error has occured: {}", error);
        return;
    }
}

o Soit on la gère proprement en expliquant bien ce qui s'est passé, l'on tente de la rattraper si possible ou on quitte calmement le programme.

La méthode brute

fn main() {
    // Ici, une chaine comppsee de chiffre et de lettre
    let b = "16cents trente-sept";
    let number = b.parse::<u32>().unwrap();
    dbg!(number);
}

o Soit l'on fait panic! le programme, il cesse brutalement en laissant une trace.

Le programme s’arrêtera subitement.

thread 'main' panicked at 'called `Result::unwrap()` on an `Err`
value: ParseIntError { kind: InvalidDigit }', src/main.rs:83:35
note: run with `RUST_BACKTRACE=1` environment variable to display
a backtrace

unwrap, expect et surtout panic!

Voici la définition de la méthode unwrap() pour std::result::Result:

NB: Le choix du bon comportement a adopter en cas d'erreur est a la discrétions du programmeur selon le type d'erreur a gérer.

pub fn expect(self, msg: &str) -> T
where
    E: Debug, 

Il existe aussi une méthode nommée expect() qui a le même comportement mais qui permet d'afficher tout de même un message custom sur la sortie d'erreur.

Les options

Some() or None

Définition de Option

Les Option ressemblent énormément aux Result, ce sont eux aussi des énumérations, a la différence qu'ils ne prennent qu'un seul type de générique pour le variant Some(T). Dans le second cas, sa valeur est None.

Itérateur et Option

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

	let mut iter = a.iter();

	// A call to next() returns the next value...
	assert_eq!(Some(&1), iter.next());
	assert_eq!(Some(&2), iter.next());
	assert_eq!(Some(&3), iter.next());

	// ... and then None once it's over.
	assert_eq!(None, iter.next());
}

Le trait std::iter::Iterator définit sa méthode next() avec le prototype suivant. Tant qu'il y aura une donnée a exploiter, il retourne Some(quelque chose). Itérer retourne toujours une Option. L’itérateur est dit consumé lorsqu'il retourne None.

if let et Option

fn main() {
    let a: Option<u32> = Some(11);
    if let Some(value) = a {
        println!("{}", value)
    }
    if let None = a {
        println!("None");
    }
}

if let fonctionne aussi avec les Option. De manière plus générale, if let fonctionne avec toutes les énumerations.

Option et gestion d’erreur

unwrap et expect sont aussi implémentés pour les Option.

Le pattern matching

filtrage par motif

Pattern matching sur un Result

fn main() {
    let b = "16cents trente-sept";
    let result = b.parse::<u32>();
    match result {
        Ok(number) => println!("{}", number),
        Err(error) => {
            eprintln!("error: {}", error);
            panic!("Je veux que le programme panic!");
        }
    }
}

Commençons par un exemple:

error: invalid digit found in string
thread 'main' panicked at 'Je veux que le programme panic!',
src/main.rs:88:13
note: run with `RUST_BACKTRACE=1` environment variable to
display a backtrace

Sortie du programme:

Alors le Pattern matching, un alias du switch case ?

fn main() {
    let b = "16cents trente-sept";
    let result = b.parse::<u32>();
    match result {
        Ok(number) => println!("{}", number),
    }
}

A première vue, nous sommes sur un équivalent du switch case en C/C++, cependant la pattern matching est bien plus puissant. Déjà il peut travailler avec n'importe quel type de donnée, ensuite il peut déstructurer les tuples, les structs etc... et enfin, le compilateur contrôle que TOUTES les possibilité de valeur ont bien été prises en charge. Dans la pratique, l'on va donc éviter de mettre un defaut: a la fin, justement, pour permettre au compilateur de faire ses vérifications.

Par exemple...

... ne compile pas !

Ai-je bien géré tous les cas ?

fn main() {
    let b = "16cents trente-sept";
    let result = b.parse::<u32>();
    match result {
        Ok(number) => println!("{}", number),
        _default => {},
    }
}

Le compilateur est assez bavard la dessus:

Il nous indique clairement ce que nous devrions écrire dans le code.

Alors, oui, on peut faire un cas de défaut, comme ça, avec l'underscore...

... mais ce serait la pire chose a faire, autant unwrap() direct ou arrêter de coder.

Autres exemples de Pattern matching

fn main() {
    enum Target {
        X86(u32),
        Ia32(u32),
        Mips,
    }
    use Target::*;
    let my_target = Ia32(25);
    match my_target {
        X86(freq) => println!("X86 at frequency {} mhz", freq),
        Ia32(_) => println!("Ia32 but we dont worried about frequency"), 
        Mips => println!("Just an other Mips again"), 
    }
}

Avec une simple enum:

fn main() {
    let tuple: (u8, u8, u8, u8, u8) = (0, 1, 1, 2, 3);
    // destructured tupple
    match tuple {
        (0..=5, .., v5) => println!("v5 = {}", v5),
        (6..=u8::MAX, .., v4, v5) => println!("v4 = {} v5 = {}", v4, v5),
    }
}

Destructuration d'un tuple:

Autres exemples de Pattern matching

fn main() {    
    let v: u32 = 142;
    match v {
        i if i < 100_u32 => println!("Below than 100: {}", i),
        i if i >= 100_u32 => println!("Above or equal than 100: {}", i),
        _ => panic!("WOOT ?"),
    }
}

Une petite limite au compilateur tout de même avec des conditions:

Ici, le compilateur se plaint si on ne met pas de cas par défaut, pourtant la plage couverte des valeurs de v est complète. Le cas défaut doit donc (en théorie) ne jamais arriver. Je n'ai jamais trop compris pourquoi.... Si quelqu'un trouve ça.

Sans le défaut, la sortie du compilateur est ainsi:

Command line args

Recuperer les arguments

Command line args

o Remplacons le main du programme maintenant

o Consulter la doc sur le module std::env

o La fonction args semble faire le travail.

Command line args

fn main() {
    use std::env;

    // Prints each argument on a separate line
    for argument in env::args() {
        println!("argument: {}", argument);
    }
}

o Qu'est ce que la structure Args ?

Il y a parmi les traits implémentés le trait Iterator, il devrait être donc possible de parcourir les arguments avec une boucle For.

La documentation de la fonction std::env::args donnait déjà l'exemple.

Command line args

o Le trait ExactSizeIterator étant aussi implémente, il permet de connaître a l'avance la len de Args. (Bien que l'on pourrait d'abord collecter les arguments puis ensuite demander la len du Vecteur obtenu).

fn main() {
    use std::env;

    let args = env::args();
    dbg!(args.len());
}

Command line args

use std::env;

fn main() {
    let arguments: Vec<String> = env::args().collect();
    dbg!(&arguments);
    if arguments.len() != 2 {
        eprintln!("Usage: {} POSITIF_NUMBER", arguments[0]);
        return;
    }
    let argument = arguments[1].parse::<u32>().unwrap();
    println!("le nombre est {}", argument);
}

Récupération d'un entier positif passe en argument:

Trait Iterator -> Possibilité de récupérer les éléments dans une collection

Command line args

mordak@mordak-pc:~/Documents/fibonacci$ cargo run 99933
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/fibonacci 99933`
[src/main.rs:8] &args = [
    "target/debug/fibonacci",
    "99933",
]
le nombre est 99933

o Entrée correct

o Entrée incorrect

mordak@mordak-pc:~/Documents/fibonacci$ cargo run 99933ggg
   Compiling fibonacci v0.1.0 (/home/mordak/Documents/fibonacci)
    Finished dev [unoptimized + debuginfo] target(s) in 0.26s
     Running `target/debug/fibonacci 99933ggg`
[src/main.rs:8] &args = [
    "target/debug/fibonacci",
    "99933ggg",
]
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }', src/main.rs:13:43
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Fibonacci

Suite de Fibonacci

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 ...

Suite de Fibonacci

// recursive function
unsigned int fibo(unsigned int n) {
    if (n == 0) {
        return 0;
    } else if (n == 1) {
        return 1;
    } else {
        return fibo(n - 1) + fibo(n - 2);
    }
}

Code de la fonction de Fibonacci récursive en C

- Écrire le code en Rust.

- Gérer les erreurs éventuelles.

- Le programme prend en entrée un entier positif et affiche le résultat de la Fibonacci de cet entier.

- Utiliser le filtrage par motif (pattern matching) le plus possible.

- Utiliser les itérateurs le plus possible.

- Pour le début de la séquence [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144], faire un test unitaire.

use std::env;

fn fibo(n: u32) -> u32 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibo(n - 1) + fibo(n - 2),
    }
}

fn main() {
    let arguments: Vec<String> = env::args().collect();
    if arguments.len() != 2 {
        eprintln!("Usage: {} POSITIF_NUMBER", arguments[0]);
        return;
    }
    match arguments[1].parse::<u32>() {
        Ok(number) => println!("Fibonacci of {} -> {}", number, fibo(number)),
        Err(err) => eprintln!("Bad number format: {}", err),
    }
}

#[cfg(test)]
mod test {
    use super::fibo;

    #[test]
    fn check_fibonacci() {
        let sequence = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144];
        for (i, result) in sequence.into_iter().enumerate() {
            assert_eq!(result, fibo(i as u32));
        }
    }
}

Création d'une crate

Librairie

Cargo new pour librairie

mkdir dependencies
cd dependencies
cargo new --lib ma-lib

On va créer une crate qui sera utilisée comme une librairie. Elle sera dans un sous-dossier dependencies (le choix du nom de ce dernier revient au programmeur).

Mettons-nous a la racine du projet.

Références mutables

Si l'on regarde dans dependencies/src, il y a non plus un fichier main.rs mais un fichier lib.rs

dependencies/:
total 4
drwxr-xr-x 3 mordak mordak 4096 Jul  5 03:24 ma-lib

dependencies/ma-lib:
total 12
-rw-r--r-- 1 mordak mordak  153 Jul  5 03:22 Cargo.lock
-rw-r--r-- 1 mordak mordak  178 Jul  5 03:17 Cargo.toml
drwxr-xr-x 2 mordak mordak 4096 Jul  5 03:17 src

dependencies/ma-lib:
total 4
-rw-r--r-- 1 mordak mordak 216 Jul  5 03:17 lib.rs

Cette indication suffit a Rust de savoir qu'il s'agit d'une librairie. Le fichier Cargo.toml ne diffère guère de celui d'un binaire

Cargo new pour librairie

[dependencies.ma-lib]
path = "dependencies/ma-lib"
pub fn add(left: usize, right: usize) -> usize {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

Afin de pouvoir se servir de la librairie, le fichier Cargo.toml de l’exécutable doit être modifie comme tel.

Plutôt qu'un main() Hello World, Rust a produit une fonction add dans lib.rs.

A ce point donne, le programme principal compile et s’exécute déjà avec la librairie. La commande cargo test fonctionne aussi.

Notons bien que la fonction est publique pub, sans cela elle serait inaccessible depuis le programme exécutable.

Cargo new pour librairie

[src/main.rs:4] add(42, 42) = 84
use interface::add;

Afin de pouvoir utiliser cette fonction add depuis le programme exécutable, il est nécessaire d'utiliser la directive use lib_name::function_name. Notons que remplacer function_name par un wildcard fonctionnerait aussi.

Utilisons la fonction:

fn main() {
    dbg!(add(42, 42));
}

Ce n'est pas plus complique que ça. Cependant il faut bien veiller a certains points

 

o Toujours décider de ce qui doit être publique ou pas, donner a l’exécutable un accès total a tous les sous-items de la librairie n'est généralement pas une bonne idée.

Cargo new pour librairie

main.rs

use ma-lib::operations::test;

o Bien gérer les modules. si par exemple dans ma lib, j'ai une fonction sub qui se trouve dans le fichier operations.rs enfant de lib.rs, il faudrait soit:

- que je déclare le mod operations comme public dans ma lib et que j'y accède ainsi depuis l’exécutable use ma-lib::operations::sub

- Soit je laisse le module privé mais je dois metrre dans lib.rs pub use operations::sub.

lib.rs

pub mod operations;
use ma-lib::sub;

main.rs

lib.rs

mod operations;
pub use uperations::sub;
use ma-lib::{add, sub};

On peut aussi mettre une liste dans le use du main.

Ses propres types

Enumerations et Structures

Les énumérations

fn main() {    
    #[derive(Debug)]
    enum Error {
        FileNotFound,
        BadFormat,
        Unexpected,
    }
    let my_error = Error::Unexpected;
    dbg!(&my_error);
    use Error::*;
    match my_error {
        FileNotFound => {}
        BadFormat => {}
        Unexpected => {}
    }
}

A ce point du cours, les énumérations ne sont plus guère difficiles a saisir.

A partir de maintenant, l'on va systématiquement dériver Debug sur nos propres types afin de pouvoir utiliser la macro dbg!.

use sert a importer les items de l'enum Error. Sans lui, il aurait fallu dans le match des expressions telles Error::FileNotFound.

Les énumérations

fn main() {
    #[derive(Debug)]
    #[repr(u32)]
    enum Error {
        FileNotFound = 0,
        BadFormat,
        Unexpected,
    }
    let my_error = Error::FileNotFound;
    if my_error as u32 == 0 {
        println!("C'est bien FileNotFound");
    }
}

On peut aussi forcer la représentation des valeurs d'une énumération par un type primitif tel u32 via la directive #[repr(u32)],  ceci peut avoir son importance si l'on veut interfacer le code Rust avec du C par exemple. cf. FFI

NB: Le cast le plus simple se fait avec le mot clef as, il existe bien d'autres façons plus complexes et plus safe de caster.

Les énumérations

fn main() {
    #[derive(Debug)]
    enum Voiture<T> {
        Lada(T),
        Wolkswagen(T),
        Peugeot(T),
    }
    let v1 = Voiture::Lada(100_u32);
    let mut v2 = Voiture::Wolkswagen(1000_usize);
    v2 = Voiture::Lada(4200_usize);
    dbg!(v2);
}

Enfin, comme pour Result ou Option, une énumeration peut prendre un type générique:

Cependant, on ne peut pas faite n'importe quoi non plus !

    use Voiture::{Lada, Peugeot};
    let mut v3 = Peugeot(12_u8);
    v3 = Lada(42_u32);
    dbg!(v3);

Dans ce cas précis, le compilateur se plaindra car la variable v3 représente d'abord une enum Voiture<T> ou T est un u8, puis ensuite, on tente de lui assigner un nouveau type qui est un u32.

Les structures: Définition

fn main() {
    #[derive(Debug, Clone)]
    struct Remote {
        ipv4: (u8, u8, u8, u8),
        port: u32,
        name: String,
    }
}

Déclarer une structure n'est pas chose difficile. chacun des champs est déclaré suivi de son type.

NB: On dérive ici le trait Clone en plus du trait Debug, tous les sous-types étant clonables aussi, on pourra ainsi cloner la structure entière. Néanmoins, il est impossible de dériver Copy puisque le sous-type String n'est pas Copy.

Contrairement a d'autres langages il n'est pas possible de déclarer une ou plusieurs valeurs par défaut du genre: port: u32 = 0, ou port: u32: 0, Il existe cependant un trait nommé Default qui peut être implémenté puis utilisé.

Les structures: Assignation

#[derive(Debug, Clone)]
struct Remote {
    ipv4: (u8, u8, u8, u8),
    port: u32,
    name: String,
}

#[derive(Debug, Clone)]
struct Vector {
    i: i32,
    j: i32,
}

// Ici, les identifiants du prototype de la fonction sont les memes
// que ceux de la structure.
fn get_vector(i: i32, j: i32) -> Vector {
    Vector {
        i,
        j,
    }
}

fn main() {
    let r = Remote{
        ipv4: (192, 168, 12, 4),
        port: 3212,
        name: String::from("vbx"),
    };
    dbg!(r);
    let v = get_vector(1, -1);
    dbg!(v);
}

Exemples d'assignation de structures:

Les structures: Implémentation de méthode

    impl Remote {
        fn new(ipv4: (u8, u8, u8, u8), port: u32, name: &str) -> Self {
            Self {
                ipv4,
                port,
                name: name.into(),
            }
        }
        fn get_addr(&self) -> String {
            format!(
                "{}.{}.{}.{}:{}",
                self.ipv4.0, self.ipv4.1, self.ipv4.2, self.ipv4.3, self.port
            )
        }
        fn change_port(&mut self, new_port: u32) {
            self.port = new_port;
        }
    }

Il est possible et très fréquent d’implémenter des méthodes sur un type de structure, comme un constructeur, des modificateurs etc...

NB: Si la structure est définie dans un autre module, le mot clef pub devra être utilise et précéder fn. Par défaut, tous les champs et méthodes sont privées.

- Self avec S majuscule fait référence au type même de la structure.

- self avec s minuscule fait référence a la variable de type structure.

- L'on passe aux méthodes une référence mutable ou non de la structure. (&self)

Les structures: Implémentation d'un trait

    impl Drop for Remote {
        fn drop(&mut self) {
            println!("Droping remote")
        }
    }

    fn drop_remote(remote: Remote) {}
    let mut s = Remote::new((0, 0, 0, 0), 443, "Space");
    dbg!(&s);
    s.change_port(80);
    dbg!(&s);
    drop_remote(s);
    println!("after call");

Le trait Drop est invoqué quand l'item est détruit que ce soit de la pile ou du tas. Il n'y a pas de fuite mémoire en Rust (sauf explicitement voulue), l'objet String contenu dans la structure sera détruit en même temps qu'elle. (tout juste avant)

NB: La documentation sur std::ops::Drop explique tout sur l’implémentation a écrire. Il n'y a aucun tour a deviner, la documentation de Drop est complète.

Si l'on implémente Drop pour une structure, ce n'est pas pour faire le boulot de nettoyage de la mémoire a la place du compilateur, mais seulement pour exécuter une action que l'on juge utile au moment de la destruction de la structure. Cela peut aussi aider parfois a débugger.

NB: ll est intéressant de comprendre pourquoi la fonction drop_remote() détruit la structure de la mémoire.

Les structures: Implémentation d'un trait

On ne traitera pas ici des Unions, qui sont unsafe.

TP Implementations

Implementations sur Vecteur

- Faire une librairie et non un exécutable

- Créer une structure publique Vector qui contient les champs i et j qui sont tous les deux des f64.

- Implementer le traits Add et AddAssign pour Vector

- Implementer le traits Display

- Implementer des tests pour Display, Add et AddAssign (directement dans la lib)

- Faire un programme exécutable qui se sert de la librairie avec un tout petit exemple.

Pointeurs et references

Unsafe or not ?

Les pointeurs

use std::ptr;

int main() {
    let mut a: u32 = 42;
    let ptr: *const u32 = &mut a;
    dbg!(ptr);
    // Easy
    unsafe {
        dbg!(*ptr);
    }
    let prim: [u32; 6] = [2, 3, 5, 7, 11, 13];
    let ptr: *const u32 = prim.as_ptr();
    // We now what we are doing (for the moment...)
    unsafe {
        dbg!(*ptr);
        dbg!(*ptr.offset(1));
        dbg!(*ptr.offset(2));
        dbg!(*ptr.offset(3));
        dbg!(*ptr.offset(4));
        dbg!(*ptr.offset(5));
    }
    // This block may lead to Segmentation fault
    unsafe {
        dbg!(*ptr.offset(66224411));
    }
}

Les pointeurs C en Rust. Nommés raw pointers.

Les pointeurs

[src/main.rs:17] ptr = 0x00007fffa4eaef94
[src/main.rs:20] *ptr = 42
[src/main.rs:26] *ptr = 2
[src/main.rs:27] *ptr.offset(1) = 3
[src/main.rs:28] *ptr.offset(2) = 5
[src/main.rs:29] *ptr.offset(3) = 7
[src/main.rs:30] *ptr.offset(4) = 11
[src/main.rs:31] *ptr.offset(5) = 13
Segmentation fault

Si l'on doit chercher pourquoi un programme plante, parcourir les parties dites unsafe est la meilleure idée. C'est le cœur de la philosophie de Rust.

Les références: syntaxe

int main() {
    int x = 10;
    int &r = x;             // initialization creates reference implicitly
    assert(r == 10);        // implicitly dereference r to see x's value
    r = 20;                 // stores 20 in x, r itself still points to x
}
fn main() {
    let x = 10;
    let r = &x;             // &x is a shared reference to x
    assert!(*r == 10);      // explicitly dereference r
}

C++ syntaxe

Rust syntaxe

fn main() {
    let x: u32 = 10;
    let r: &u32 = &x;       // &x is a shared reference to x
    assert!(*r == 10);      // explicitly dereference r
}

Types explicites

fn main() {
    let mut j: u32 = 10;
    let r: &mut u32 = &mut j;
    *r *= 20;
    dbg!(j);
}

Références mutables

Les réferences: Utilisation

int main() {
    let a = 16;
    let b = 16;
    let ra = &a;
    let rb = &b;
    assert_eq!(ra, rb);
    assert_eq!(*ra, *rb);
    assert_eq!(&ra, &rb);
    assert_eq!(&&ra, &&rb);

    // Ne compile pas: comparaison entre &integer et integer
    // assert_eq!(*ra, rb);
}

Accéder a la référence n’accède pas a son adresse mais a la valeur référencée

fn main() {
    fn dump_string(s: &String) {
        println!("{}", s);
    }
    let mut s: String = "banane".into();
    dump_string(&s);
    s.clear();
    dump_string(&s);
}

NB: Si l'on veut connaître l’adresse mémoire d'une référence, on doit utiliser des méthodes de std::ptr.

Afin de ne pas move accidentellement la String lors de l'appel de la fonction dump_string, on lui envoit non plus la String mais une référence de cette dernière

Les références implicites

#[derive(Debug, Copy, Clone)]
struct Vector {
    i: i32,
    j: i32,
}

impl Vector {
    fn add_assign(&mut self, other: Self) {
        self.i += other.i;
        self.j += other.j;
    }
}

fn main() {
    let v = Vector{i: 10, j: -3};
    dbg!(v);
    v.add_assign(Vector{i: 2, j: 4});
    dbg!(v);
}

Prenons l'exemple suivant:

La methode add_assign prend une référence mutable de Vector, pourtant on a envoyé un type Vector et non une référence.

A la ligne 17, l’opérateur . retourne implicitement la référence (mutable ici) de v.

C'est strictement équivalent a la ligne suivante:

    (&mut v).add_assign(Vector{i: 2, j: 4});

L'on dit que l’opérateur . emprunte (borrowing) une référence mutable de v.

Les références: mutabilité et exclusivité

Il ne peut pas y avoir de référence Null en Rust. Le compilateur s'engage a vérifier si la donnée référencée est toujours 'en vie' cf. notion de lifetime et qui sont ceux qui l'ont emprunté cf. notions d'ownership et de borrowing.

 

Rust a posé comme règle qu'avoir une référence mutable sur une variable DOIT garantir l’accès exclusif a cette dernière via CETTE référence mutable tant qu'elle vit cf: lifetime. Cela permet au compilateur de procéder a des optimisations qui n'auraient pas pu être faite sans cette garantie. cf: Concurrence.

    let mut num = 5;
    let numRef = &mut num;

En langue Rustienne, on dit que l'espace mémoire de num a été emprunté de façon mutable par numref. mutably borrowed.

C'est sans doute, parmi les aspects de Rust un des plus difficiles à vraiment saisir  !

Les références: mutabilité et exclusivité

fn main() {
    let mut num = 5;
    let numRef = &mut num;
    dbg!(num);
    *numRef = 6;
}

Prenons ce bout de code par exemple:

    let mut num = 5;
    let numRef = &mut num;
     dbg!(num);
    *numRef = 6;

Ici, après avoir déclaré l'entier num, on donne la garanti de l’exclusivité sur l'espace mémoire de num a la référence mutable numRef.

Puis on tente d’accéder (pourtant seulement en lecture) a num....

... alors que numref vit toujours ici

Le comportement ne serait pas le même si l'on avait plus tenté d’accéder a numref. Pour le compilateur, la référence mutable serait sans doute déjà morte !

Les références: mutabilité et exclusivité

Retour du compilateur:

fn main() {
    let mut num = 5;
    let numRef = &mut num;
    dbg!(*numRef);
    *numRef = 6;
}

Ici, après avoir déclaré l'entier num, on donne la garanti de l’exclusivité sur l'espace mémoire de num a la référence mutable numRef.

On peut généraliser cela a tous les cas de figure, des que l'on a une référence mutable sur quelque chose, l’exclusivité doit être garanti. Que ce soit vis-a-vis d'autres références mutables ou même non mutables !

C'est peut être le message d'erreur qu'un développeur Rust même très bon voit le plus de sa vie...

Le code corrigé ci-dessous quand a lui compile très bien:

Les références: paramètres lifetime

Référence dans une structure:

struct A {
    r: &i32,
}

Rust semble va avoir besoin de savoir combien de temps vivra la référence.

Les références: paramètres lifetime

Le code suivant compile très bien mais dans ce cas, l'on a déclaré que la référence est sur une variable globale statique et cela ne peut donc pas fonctionner sur une variable locale.

struct A {
    r: &'static i32,
}

Le compilateur nous invite a définir la structure ainsi:

struct A<'a> {
    r: &'a i32,
}

'a est un lifetime anonyme, en faisant cela, on dit au compilateur que la référence ne vivra pas plus longtemps que la variable qu'elle réfère.

Les références: paramètres lifetime

Considérons l'exemple suivant:

#[derive(Debug)]
struct A<'a> {
    i: &'a i32,
}
    

fn main() {
    let r: A;
    {
        let i: i32 = 42;
        r = A{
            i: &i
        };
    }
    dbg!(r);
}

L'allocation dynamique

Boxes, Vec, String et Arc

Box

pub struct Box<T, A = Global>(_, _) 
where
    A: Allocator,
    T: ?Sized;
struct Vector {
    i: i32,
    j: i32,
}

fn main() {
    let vec = Box::new(Vector{ i: 10, j: -5});
}

Une Box est la structure allouée en mémoire la plus simple en Rust, c'est un pointeur vers une donnée allouée sur la tas. cf malloc

A la différence de malloc qui prends une taille en octets, on donne a Box::new() le type de donnée qui sera allouée. Le type en question doit être obligatoirement de taille fixe, c'est a dire dont la taille est connue a l'avance par le compilateur.

Il n'y a pas de garbage collector en Rust, ni besoin de libérer manuellement la mémoire, a la fin du bloc ou elle est déclarée. Si l'on veut faire vivre plus longtemps la Box, il est nécessaire que la fonction  la retourne.

Box

fn make_box(vec: Vector) -> Box<Vector> {
    Box::new(vec)
}

fn take_box(b: Box<Vector>) {
    // La box ne vivra plus a la fin du ce bloc
}

fn main() {
    let b = make_box(Vector{ i: 10, j: -20});
    // Ici la box vit
    dbg!(&b);
    take_box(b);
    // A ce point du code, la box a deja ete liberee
}

Une Box est la structure allouée en mémoire la plus simple en Rust, c'est un pointeur vers une donnée allouée sur la tas. cf malloc

Enfin, l'on accède aux données contenues dans une box de façon transparente, comme si la box n'avait pas été faite.

fn main() {
    let mut v = Vector{i: 9, j: -3};
    v.i = 87;
    v,j = -12;
    let mut b = Box::new(Vector{i: 10, j: -20});
    b.i = 12;
    b.j = -11;
}

Vec

Un Vecteur est une collection d'elements de même TYPE.

On peut voir le T pour type generique. Il existe plusieurs façons de créer un vecteur, initialise ou non.

fn main() {
    // A partir d'un vecteur vide
    let mut v1 = Vec::new();
    v1.push(1);
    v2.push(2);
    v3.push(3);
    // A partir d'un tableau
    let mut v2 = [1, 2, 3].to_vec();
    // A partir de la macro vec!
    let mut v3 = vec!(1, 2, 3);
}

Vec

On peut voir que les mechanismes d’inférence de type fonctionnent, puisque qu'aucun type n'a été déclare precedement. Cela donne avec les types.

Il est dans la pratique très rare d'avoir a déclarer explicitement le type contenu dans la collection.

fn main() {
    // A partir d'un vecteur vide
    let mut v1: Vec<u64> = Vec::new();
    v1.push(1);
    v1.push(2);
    // Utilisation ici de la syntaxe literale pour definir un type
    v1.push(3_u64);
    // A partir d'un tableau
    let mut v2: Vec<u8> = [1, 2, 3].to_vec();
    // A partir de la macro vec!
    let mut v3: Vec<u32> = vec!(1, 2, 3);
    // Utisation du turbofish ::<>
    let mut v4 = Vec::<u64>::new();
    v4.push(1);
    v4.push(2);
    v4.push(3_u64);
}

Vec, accès aux éléments

La façon  la plus triviale pour accéder aux éléments est la notation crochet.

Afin de pouvoir éviter de paniquer, l'on préféré utiliser la methode get(), qui retourne une Option.

fn main() {
    // A partir d'un vecteur vide
    let mut v1: Vec<u64> = Vec::new();
    v1.push(1);
    v1.push(2);
    // Utilisation ici de la syntaxe literale pour definir un type
    v1.push(3_u64);
    dbg!(v1[0]);                                                                                                                       
    dbg!(v1[1]);                                                                                                                       
    dbg!(v1[2]);
    // PANIC
    dbg!(v1[3]);
}

Vec, accès aux éléments

Ce qui ressemble au code ci-dessous.

NB: La methode get() ne permet pas d’accéder uniquement a un élément mais peut retourner une liste d'elements.

fn main() {
    // A partir d'un vecteur vide
    let mut v1: Vec<u64> = Vec::new();
    v1.push(1);
    v1.push(2);
    // Utilisation ici de la syntaxe literale pour definir un type
    v1.push(3_u64);
    dbg!(v1.get(0));                                                                                                                       
    dbg!(v1.get(1));                                                                                                                       
    dbg!(v1.get(2));
    // Retourne None
    dbg!(v1.get(3));
}

Manipulations des Vecteurs

Il existe une pléiade de méthodes sur les vecteurs.

fn main() {
    let mut v = Vec::<u64>::new();
    // Ajoute 10 a la fin de la colletion
    v.push(10);
    // Retire le dernier element de la collection
    // et retoune une Option de celui-ci
    dbg!(v.pop());
    let mut v2: Vec<u64> = vec![4, 5, 6];
    // Copie les valeurs de v2 dans v1, vide v2
    v.append(&mut v2);
    // Vide la collection
    v.clear();
    // Itere sur le vecteur et dump les valeurs contenues
    v.iter().for_each(|v| {
        dbg!(v);
    });
    // Itere de facon mutable sur le vecteur, modifie ses valeurs
    v.iter_mut().for_each(|value| {                                                                                                       
        *value *= 2;                                                                                                                        
    });
}

Pour le cas des deux dernières instruction: iter() et iter_mut() retournent respectivement des structures implémentant le trait Iterator.

String

Une String est un autre type alloue, spécialise dans les chaînes de caractères. Contrairement aux Box qui ont une taille fixe, la String change de taille selon ce qu'elle contient.

int main() {
    let mut s: String = String::new();
    s.push_str("Hello");
    dbg!(&s);
    s.push(' ');
    s.push_str("World");
    dbg!(&s);
    s.clear();
    dbg!(&s);
}

Tout comme la Box et toutes les autres Structures allouées, la mémoire est libérée automatiquement quand on sort du bloc,

Voir la documentation de std::string::String pour connaître les capacités.

String

String

int main() {
    let s: String = "Hello World".into();
    dbg!(s);
    let mut s = String::from("Hello World");
    dbg!(&s);
    s = "Nouvelle chaine".into();
    dbg!(s);
}

Les implémentations de From<&str> for String et d'Into permettent d'assigner le contenu des Strings plus facilement.

int main() {
    let f = String::from("Hello") + " World".into();
    dbg!(f);
}

Comment ce code compile il ? Et Pourquoi avoir mis un from() puis ensuite un into() ?

Arc

Arc ou Atomically Reference Counter est une 'structure' allouée qui est destinée a être partagée entres différents threads.

Arc implémente le trait clone, chaque thread peut accéder indépendamment a son clone de l'Arc., chaque clone pointe sur le même type contenu. Le compteur de reference interne fonctionne ainsi: Pour chaque clone qui est fabrique, il est incrémenté de 1, pour chaque clone détruit (par la fin d'un bloc d'instruction, d'un appel a Drop ou a une fonction qui ne le retourne pas), l'on soustrait 1. Quand le compteur tombe a 0, l'Arc et le type contenu sont libérés de la mémoire. Le compteur étant dit Atomic, il ne peut pas y avoir de problème de concurrence lors de l’incrémentation ou de la décrémentation de ce dernier.

Arc

int main() {
    use std::sync::Arc;                                                                                                                 
    #[derive(Debug)]                                                                                                                    
    struct Vector {                                                                                                                     
        i: u32,                                                                                                                         
        j: u32,                                                                                                                         
    }                                                                                                                                   
    // Creation de l'Arc, le reference Counter passe a 1                                                                                
    let a = Arc::<Vector>::new(Vector {i: 12, j: 42});                                                                                  
    {                                                                                                                                   
        // Creation du clone, le reference Counter passe a 2                                                                            
        let b = a.clone();                                                                                                              
        // Passage par reference, le clone n'est pas detruit lors de l'appel                                                            
        dbg!(&b);                                                                                                                       
        // Ici le clone est detruit -> Fin du bloc d'instruction                                                                        
        // Le reference Counter repasse a 1                                                                                             
    }                                                                                                                                   
    // L'arc est envoye a la fonction dbg!, il est ainsi detruit apres le retour                                                        
    // de la fonction dbg! puisque son reference Counter passe a 0.                                                                     
    dbg!(a);
}

En pratique, l'utilisation d'un Arc est nécessaire seulement si l'on a plusieurs threads et qu'elle est conjugee avec un Mutex afin que les threads puissent modifier les données contenues.

Multithreading

concurrence

La programmation concurrente

Grâce a la documentation de std::thread, std::sync::Arc et celle de std::sync::Mutex, essayez de faire un programme contenant 2 threads pouvant modifier et lire tous les deux les mêmes données de façon safe.

fn main() {
    let mut data: i32 = 42;
    let _t = std::thread::spawn(move || {
        dbg!(data);
        data = 11;
        dbg!(data);
    });
    data = 22;
    dbg!(data);

    let mut s: String = "42".to_owned();
    let _t = std::thread::spawn(move || {
        dbg!(&s);
        s.push_str("11");
        dbg!(&s);
    });
    s.push_str("22");
    dbg!(&s);   
}

Trouver ce qui ne va pas dans ce programme qui ne peut pas compiler.

La programmation concurrente

Dans cette partie nous allons voir comment monter un programme multi thread en Rust.  En plus du thread main, nous allons créer deux threads, un content et un pas content qui panic. Et plutôt que d'utiliser un bête nutex que vous avez deja rencontre mille fois dans votre expérience de programmeur, nous allons utiliser un MPSC. Multiple Producer, Single Consumer. Nous ne chercherons pas non plus a joindre les threads, c'est trop banal aussi.

Le MPSC

Un MPSC est déjà thread safe, il est protege en interne par un Mutex ou un Sémaphore ou autre..., il faut regarder dans le code de la STD pour savoir ça ! Mais il est thread safe en tous cas.

C'est un canal de communication, le pattern habituel consiste a donner un SENDER a chaque thread crées en plus du main() et d'utiliser le RECEIVER dans le thread principal. RECEIVER qui lui est unique. Single Receiver...

La fonction channel() retourne un Sender et un Receiver. Notez le generic T. Cela implique que l'on peut envoyer ou recevoir le type de donnee que l'on veut. Ici nous allons renvoyer un tuple (enum + threadId) afin de passer un message ainsi que de connaître son producteur.

Il existe peut etre deja une methode sur le Receiver pour voir le threadID mais j'avais la flemme de chercher...

Instanciation du MPSC

use std::sync::mpsc;
use std::thread::ThreadId;

#[derive(Debug, Copy, Clone)]
enum Event {
    Hello,
    HelloAgain,
    IWantToStop,
}

fn main() {
    let (sender, receiver): (
        mpsc::Sender<(Event, ThreadId)>,
        mpsc::Receiver<(Event, ThreadId)>,
    ) = mpsc::channel();
    let sender2 = sender.clone();
}

Pour obtenir le Receiver et le Sender, on procède donc ainsi en précisant bien le type de donnée que l'on va transmettre. Notez que l'on clone déjà le sender car on sait que on va l'envoyer a deux threads. Autant cloner tant que le fer est chaud.

Création des threads

let _happy_thread = thread::spawn(move || { 
})


let _bad_thread = thread::spawn(move || { 
})

Aussi simple que ça: Notez le move et la closure || pour y penser plus tard. Un jour ces notions vous paraîtront clairs mais ne sont pas utiles pour l'instant.

            sender
                .send((Event::HelloAgain, thread::current().id()))
                .unwrap();
loop {
    let (msg, id) = receiver.recv().unwrap();
    // if condition, break
}
            sender2
                .send((Event::Hello, thread::current().id()))
                .unwrap();

Voici des exemples d'utilisation du sender, dans le thread 1 et 2 respectifs

Et le receiver du thread principal dans tout ça ?

Et le receiver du thread principal dans tout ça ?

Il attend un message, l'appel est dit bloquant et fait un tour de loop jusqu'au next....

Coder des bouts de code en plus....

L’idée peut être pour continuer et de faire communiquer régulièrement les threads, en attendant un peu entre chaque message grâce au sleep().

Peut être aussi quitter le programme quand un des threads nous dit qu'il en peut plus. (3eme variant de l'enum...)

Ou Afficher le threadId pour savoir qui est vient de parler...

 

Voir même faire panic! un thread autre que le principal au bout d'un certain temps pour voir comment le programme se comporte !

 

panic!("sa mere");

NB: Ce serait dommage qu'un thread se termine tout juste après sa création....

use std::sync::mpsc;
use std::thread::ThreadId;
use std::{thread, time};

#[derive(Debug, Copy, Clone)]
enum Event {
    Hello,
    HelloAgain,
    IWantToStop,
}

fn main() {
    let (sender, receiver): (
        mpsc::Sender<(Event, ThreadId)>,
        mpsc::Receiver<(Event, ThreadId)>,
    ) = mpsc::channel();
    let sender2 = sender.clone();

    let three_seconds = time::Duration::from_millis(3000);

    let _happy_thread = thread::spawn(move || {
        sender.send((Event::Hello, thread::current().id())).unwrap();
        loop {
            thread::sleep(three_seconds);
            sender
                .send((Event::HelloAgain, thread::current().id()))
                .unwrap();
        }
    });

    let _bad_thread = thread::spawn(move || {
        sender2
            .send((Event::Hello, thread::current().id()))
            .unwrap();
        thread::sleep(three_seconds * 3);
        sender2
            .send((Event::IWantToStop, thread::current().id()))
            .unwrap();
        panic!("sa mere");
    });

    loop {
        let (msg, id) = receiver.recv().unwrap();
        println!("message recu: {:?} de {:?}", msg, id);
        if let Event::IWantToStop = msg {
            println!("A thread want to stop");
            break;
        }
    }
    println!("Exiting Main thread normallu. Bye");
}

A quoi cela pourrait ressembler.

La sortie du programme

N'oubliez pas que Rust est extrêmement puissant dans la gestion de ses threads, ce n'est pas pour rien que l'on parle de programmation concurrente.  Tant qu'il n'y a pas de code explicitement unsafe, les threads tiennent bon. Les règles strictes de Rust qui nous font maudir ce langage lorsque nous sommes en monothreads sont de vraies benedictus en environnement multithreade.

Un petit mot sur le trait Send

fn main {
    // Tous les types contenus ici sont Send                                                                                               
    #[derive(Debug)]                                                                                                                    
    struct S {                                                                                                                          
        i: u8,                                                                                                                          
        j: u8,                                                                                                                          
        k: String,                                                                                                                      
    }                                                                                                                                   
    let s = S {                                                                                                                         
        i: 1,                                                                                                                           
        j: 2,                                                                                                                           
        k: "toto".to_string(),                                                                                                          
    };                                                                                                                                  
    let _t = std::thread::spawn(move || {                                                                                               
        dbg!(s);                                                                                                                        
    });
}

Rust détermine les types qui peuvent être passes a un thread par la condition s'ils implémentent le trait Send. Aussi si une structure possède plusieurs éléments qui implémentent tous le trait Send, le compilateur considéra que la structure est Send, on appelle cela un AUTO trait.

Un petit mot sur le trait Send

fn main {
    // Un pointeur en rust n'est pas Send
    #[derive(Debug)]
    struct S2 {
        ptr: *const u8,
    }
    let s2 = S2 {
        ptr: std::ptr::null(),
    };
    let _t = std::thread::spawn(move || {
        dbg!(s2);
    });
}

Il y a quelques types qui ne sont pas Send comme par exemple les raw pointers, ou les RC ou simple Reference Counter (a ne pas confondre avec les ARC). Si l'on veut vraiment passer ces types a un thread, l'on sera oblige d'implementer Send manuellement pour une structure englobant ces types, cette instruction est par nature UNSAFE.

 

le code ci-dessous ne compile donc pas:

    unsafe impl Send for S2

Il est necessaire de rajouter cette ligne:

Rust fonctionnel

Un pattern très courant

use std::num::ParseIntError;

fn parse_all(a: &str, b: &str, c: &str) -> Result<u32, ParseIntError> {
    let r1: u32 = a.parse::<u32>()?;
    dbg!(r1);
    let r2: u32 = b.parse::<u32>()?;    
    dbg!(r2);
    let r3: u32 = c.parse::<u32>()?;    
    dbg!(r3);
    Ok(r1 + r2 + r3)
}

fn main() {
    match parse_all("1637", "42", "toto21") {
        Ok(res) => println!("La somme vaut {}", res),
        Err(err) => println!("Erreur: {}", err),
    }
    println!("Hello, world!");
}

Lorsque nous avons vu les Result<T, E> la dernière fois, nous sommes complètement passe a cote de la notation ? Pourtant elle est extrêmement fréquente en Rust parce que très pratique.

Ici, nous parsons 3 chaînes de caractères pour en extraire des u32, enfin, si tout s'est bien passe, nous les additionnons et renvoyons le résultat.

Le point d'interrogation

    let r3 = match c.parse::<u32>() {
        Ok(res) => res,
        Err(e) => return Err(e),
    };

Mais que fait ce petit symbole au juste ?

C'est simple, la fonction parse retourne un Result<u32, ParseIntError>, le point d’interrogation signifie que si le résultat est Ok(_), on met le résultat dans les variables r1, r2 ou r3, sous forme de u32 et non de Result.

Cependant, si au contraire, nous avons Err(quelque chose), on quitte la fonction directement et l'erreur est retournée, elle sera gérée par l'appelant de la fonction.

Alors au lieu d'avoir ces 4 lignes de code:

On a ça:

    let r3 = c.parse::<u32>()?;

Ceci fait partie des patterns que l'on dit fonctionnels, sympa non ?

L’esthétique du code

   let total = v.iter().fold(0, |total, next| total + next);

C'est justement un des points essentiels de la programmation fonctionnelle, elle permet de faire souvent en une seule ligne élégante du code qui aurait été bien plus long et laid a voir.

En fonctionnel, çà donne:

    let mut total = 0;
    for i in 0..v.len() {
        total += v[i];
    }

Autre exemple connu. Si j'ai une collection de nombre, disons dans un tableau et que je veux additionner dans utiliser l'approche fonctionnelle. mon code serait:

L'on dit aussi qu'elle est extrêmement puissante et forte de sens pour gérer des collections entières de données. Elle simplifie le code.

Fold est juste magique, il faut absolument le voir dans la documentation.

L’esthétique du code

Cette autre fonction fait le carre de chacun des nombres entre 1 et 10 et stocke les résultats dans une liste de données, ici un Vec.

let squared: Vec<u32> = (1..10).map(|x| x * x).collect();

Les closures

fn main() {
    let mut v: Vec<u8> = vec!(1, 2, 4, 8);
    v.iter_mut().for_each(|inner| *inner *= 2);
    dbg!(&v);
}

Il existe en Rust un type de fonction un peu spéciales, ce sont les closures. Contrairement aux fonctions, elles peuvent être anonymes, les paramètres sont notes entre | | plutôt qu'entre parenthèses ( ) et elles ont surtout la capacité de capturer les variables de leur environnement, contrairement aux fonctions. La capture se fait soit par référence &, soit par référence mutable &mut soit par transfert d'ownnership, nous allons voir ces différents cas de figure.

 

Prenons le prototype de la fonction for_each() pour du trait Iterator:

Bien qu'il soit possible d'envoyer une reference d'une fonction a for_each, en pratique on utilisera toujours une closure a la place.

Les closures

fn main() {
    // Declaration de la variable s en dehors de la closure
    let s = "MaString".to_string();
    // Ici on definit la closure avant de l'utiliser
    let closure = |inner: &mut u8| {
        *inner *= 2;
        // Ici la variable s est capturee par reference &s
        println!("One other iteration: {}", s);
    };
    let mut v: Vec<u8> = vec!(1, 2, 4, 8);
    v.iter_mut().for_each(closure);
    // s est toujours accessible ici
    dbg!(&s);

    // Ici la closure est anonyme
    v.iter_mut().for_each(|inner| {
        *inner *= 2;
        // Ici la variable s est capturee par reference &s
        println!("One other iteration: {}", s);
    });
    // s est toujours accessible ici
    dbg!(&s);
}

Mais contrairement aux fonctions, ici l'on va par exemple prendre capturer une référence sur une String qui est a l’extérieur du bloc de la closure.

Ici, une référence non-mutable de s est passée implicitement a la closure, il n'y a guère de différence de code si cela avait été une référence mutable

Les closures

fn main() {
    // Declaration de la variable s en dehors de la closure
    let s = "MaString".to_string();
    // Ici on definit la closure avec MOVE avant de l'utiliser
    let closure = move |inner: &mut u8| {
        *inner *= 2;
        // Ici, l'ownership de la variable s est passe au thread
        println!("One other iteration: {}", s);
    };
    v.iter_mut().for_each(closure);
    // s n'est plus accessible ici, le programme ne compile donc pas !
    dbg!(s);
}

On peut forcer le passage par ownership en utilisant le mot clef move avant les paramètres de la closure, comme c'est le cas pour le prototype de thread.

Il serait en effet assez illogique étant donne les garantis de securite du langage que prodigue Rust qu'un thread puisse recevoir une référence d'une variable extérieure a celui-ci, si cette dernière réfère sur une variable présente sur la pile du thread appelant et non sur le tas, il est possible qu'au moment ou le nouveau thread tente d'y accéder, la pile de la fonction appelante soit modifiée.

Les Iterator mutables et exclusivité

fn main() {
    let mut v: Vec<u8> = vec!(1, 2, 4, 8);
    v.iter_mut().for_each(|inner| *inner *= 2);
    dbg!(&v);
}

Souvenons-nous de la règle concernant les références mutables qui dit qu'on ne peut acceder a l'objet référence qu'EXCLUSIVEMENT au travers de cette unique référence mutable. Dans le cas de la fonction iter_mut sur Vec<T>, qui créer une itérateur mutable, elle prends en paramètre une référence mutable de self, soit du Vec ici.

Les Iterator mutables et exclusivité

fn main() {
    let mut v: Vec<u8> = vec!(1, 2, 4, 8);
    v.iter_mut().enumerate().for_each(|(index, inner)| {
        if index == 0 {
            v.push(16);
        }
        *inner *= 2;
    });
    dbg!(&v);
}

Admettons que je tente d'ajouter une case a mon Vecteur lorsque je suis a l’intérieur du bloc  d’itération.

Quel problème pourrait poser ce genre de code ?

Les Iterator mutables et exclusivité

Le compilateur ne veut pas car des effets de bords en tout genre pourraient apparaître, comme par exemple si j'etend a chaque itération mon vecteur tout en itérant dessus, on aurait ainsi une belle boucle infinie et non parfaitement prévisible. Le tout termine par un crash du programme due au fait qu'il n'y aurait plus de mémoire disponible !

 

Il est aussi important de noter qu'en Rust que si j'ai une référence mutable sur un champ d'une structure,  je peux avoir une référence mutable sur un autre champs de cette même structure, mais plus sur la structure entière.

Les Iterator mutables et exclusivité

fn main() {
    #[derive(Debug)]
    struct S {
        i: i32,
        j: i32,
    }
    let mut s = S {
        i: -1,
        j: -2,
    };
    let ref_i = &mut s.i;
    let ref_s = &mut s;
    *ref_i = -11;
    dbg!(s);
}

Ceci n'est pas possible:

    let ref_i = &mut s.i;
    let ref_j = &mut s.j;
    *ref_i = -11;
    *ref_j = -12;
    *ref_i = -21;
    *ref_j = -22;
    dbg!(s);

Mais ceci l'est:

Ses propres Traits

Un trait bien dangereux...

On cherchera a faire un trait qui dump les valeurs hexadécimales sous forme de byte dans la mémoire occupée par une variable de type generic T. En theorie... N'importe quoi pourra utiliser le trait et révéler ses valeurs cachées.

unsafe {
    print!("{:#04x} ", *ptr.offset(byte as isize));
}

Le trait se nomme HexDump qui defnit la methode hex_dump.

pub trait HexDump {
    fn hex_dump(&self);
}
impl HexDump for u32 {
    fn hex_dump(&self) {}
}

Son implémentation generique se note ainsi:

impl <T>HexDump for T {
    fn hex_dump(&self) {}
}

Une implémentation non-generique aurait été telle quelle:

En y reflechissant, il n'y a pas trop de raison qu'un type ne puisse pas fonctionner puisque la seule chose que l'on risque de faire, c'est de recuperer une adresse mémoire.

Les elements

La macro std::ptr::addr_of! semble pouvoir aider a recuperer l'adresse mémoire.

Le type retourne ne serait-il pas un *const T ?

Si l'on veut afficher octet par octet, ne serait il pas mieux d'avoir un *const u8 ?

 

as *const u8; peut etre....

La taille de T

Si l'on commence a dumper comme ça la mémoire, il est mieux de connaître la taille de la donnée T en entree,

On sait tout ce qu'il faut pour faire le trait maintenant...

Le code

pub trait HexDump {
    fn hex_dump(&self);
}

impl<T> HexDump for T {
    fn hex_dump(&self) {
        let ptr = std::ptr::addr_of!(*self) as *const u8;
        for byte in 0..std::mem::size_of::<Self>() {
            unsafe {
                print!("{:#04x} ", *ptr.offset(byte as isize));
            }
        }
        println!();
    }
}

Le test

fn main() {
    let r: u32 = 42;
    r.hex_dump();
    let r: u32 = 0;
    r.hex_dump();
    let r: f64 = 42.12;
    r.hex_dump();
}

Durée de vie

Les cas implicites

fn main() {
    fn first_element(slice: &[u8]) -> &u8 {
        &slice[0]
    }
    let v = vec![1, 2, 3];
    dbg!(first_element(v));
}

La plupart du temps, rust inférera les durée de vie sans que l'utilisateur n'est a s'en soucier. Rappelons que la regle principale des duree de vie est qu'un objet référence DOIT vivre au moins aussi longtemps que les references sur celui-ci. C'est la garanti qu'il n'y aura jamais de reference Null ou invalides.

 

Prenons par exemple ce bout de code:

Cette fonction retourne une reference sur le premier élément de la collection. Rust en interne va la réécrire comme ci-dessous,

fn main() {
    fn first_element<'a>(slice: &'a [u8]) -> &'a u8 {
        &slice[0]
    }
    let v = vec![1, 2, 3];
    dbg!(first_element(v));
}

L’entrée doit vivre au moins aussi longtemps que la sortie.

Les cas implicites

fn main() {
    struct MaStruct {
        inner_a: u32,
        inner_b: u32,
    }
    fn get_inner_a(t: &MaStruct) -> &u32 {
        &t.inner_a
    }
    fn get_inner_a_by_rust<'a'>(t: &'a MaStruct) -> &'a u32 {
        &t.inner_a
    }
}

Si on prend l'exemple d'une structure:

Ces deux fonctions sont strictement équivalentes.

 

Les mêmes règles s'appliqueront dans le cas d'une methode d'une structure (&self ou &mut self) retournant une reference sur un champs de cette dernière. la structure en entree doit vivre au moins aussi longtemps que la reference en sortie:

Les cas implicites

forme implicite:

Ces deux fonctions sont strictement équivalentes.

 

NB: la durée de vie  'a doit toujours être déclaré après le nom de la fonction

 

 

    impl MaStruct {
        fn get_inner_a(&self) -> &u32 {
            &self.inner_a
        }
    }

forme explicite:

    impl MaStruct {
        fn get_inner_a<'a>(&'a self) -> &'a u32 {
            &self.inner_a
        }
    }

quand il faut être explicite

Cependant si j'ai par exemple une fonction qui prend 2 references en entrée,  et une en sortie, Rust ne saura pas comment déterminer quelles durées de vie doivent être liées.

Ce qui provoque comme erreur:

    fn dump_a_string_and_get_inner_a(t: &MaStruct, s: &str) -> &u32 {                                                                          
        println!("{}", s);                                                                                                              
        &t.inner_a                                                                                                                      
    }

quand il faut être explicite

Nous voyons que le compilateur suggère d'attribuer la même durée de vie a tous les paramètres en entrée MAIS cette suggestion ne doit pas etre toujours suivie a la lettre. En effet, on peut très bien accepter le fait que la référence de s n'ai pas a survivre aussi longtemps que celle de la structure T.

Ce qui provoque comme erreur:

    fn dump_a_string_and_get_inner_a<'a>(t: &'a MaStruct, s: &'a str) -> &'a u32 {
        println!("{}", s);
        &t.inner_a
    }
    fn get_output(t: &MaStruct) -> &u32 {
        let s = String::from("Toto");
        let output = dump_a_string_and_get_inner_a(&t, &s);
        output
    }
    let t = MaStruct {
        inner_a: 1,
        inner_b: 2,
    };
    dbg!(get_output(&t));

Pourquoi ce na compile pas ?!? WTF

quand il faut être explicite

La solution consiste a donner un second lifetime (ici 'b) au second paramètre, on précise aussi que le lifetime du premier paramètre est lie a celui de la sortie.

Ce qui provoque comme erreur:

    fn dump_a_string_and_get_inner_a<'a, 'b>(t: &'a T, s: &'b str) -> &'a u32 {
        println!("{}", s);
        &t.inner_a
    }
    fn get_output(t: &T) -> &u32 {
        let s = String::from("Toto");
        let output = dump_a_string_and_get_inner_a(&t, &s);
        output
    }
    let t = T {
        inner_a: 1,
        inner_b: 2,
    };
    dbg!(get_output(&t));

Ici l'on dit que 'a et 'b sont deux lifetime différents. Il existe aussi la possibilité de préciser qu'une référence DOIT vivre au moins plus longtemps qu'une autre:

    fn dump_a_string_and_get_inner_a<'a: 'b, 'a>(t: &'a T, s: &'b str) -> &'a u32

ce qui veut dire: 'b ne doit pas survivre a 'a. Le contraire ne compilerait pas dans notre contexte.

    fn dump_a_string_and_get_inner_a<'a, 'b: 'a>(t: &'a T, s: &'b str) -> &'a u32

quand il faut être explicite

Pour résumer, le bon emploi des lifetime ne se résume a suivre aveuglement ce que le compilateur suggère. Il faut comprendre comment nos references sont agencées les unes par rapports aux autres dans notre programme.

la macro

!

La macro hello World

// Une simple macro nommée `say_hello_world`.
macro_rules! say_hello_world {
    // `()` indique que la macro ne prend aucun argument.
    () => (
        // La macro étendra le contenu de ce bloc.
        println!("Hello World!");
    )
}

fn main() {
    // Cet appel va étendre `println!("Hello World");`.
    say_hello_world!()
}

Exemple de RustByExample

    Finished dev [unoptimized + debuginfo] target(s) in 0.19s
     Running `target/debug/functionnal`
Hello World!

Sortie standard du programme:

NB: Il existe des règles spécifiques d'export et d'import de macro en Rust depuis d'autres fichiers et des dépendances.

Macro sur une seule ligne

Trois cas de figure d'utilisation de serial_println!

/// Prints to the host through the serial interface, appending a newline.
#[macro_export]
macro_rules! serial_println {
    () => ($crate::serial_print!("\n"));
    ($fmt:expr, $($arg:tt)*) => ($crate::serial_print!(concat!($fmt, "\n"), $($arg)*));
    ($fmt:expr) => ($crate::serial_print!(concat!($fmt, "\n")));
}
/// Vide
/// -> Juste un simple retour a la ligne
serial_println!();

/// Une chaine de caractere avec des arguments
/// Affixher la chaine en developpant les arguments
serial_println!("{} banane(s)", 42);

/// Une chaine de caractere seule
/// Afficher la chaine de caractere
serial_println!("Toto21")

En entrée: Avant le =>                                     => En sortie: On utilise les identifiants $xxx

- () veut dire vide

-$___::expr veut dire 'expression formatée', chaîne de caractère quoi.

- la virgule dans le second cas est le délimiteur utilise entre le 1er et le 2nd argument.

- $___:tt veut dire arguments quelconque

- le wildcard * veut dire qu'il peut y en avoir toute une liste

/// Implements a new C style enum with his try_from boilerplate
macro_rules! safe_convertible_enum {
    (#[$main_doc:meta]
     #[$derive:meta]
     #[repr($primitive_type:tt)]
     enum $name:ident {
         $(
             #[$doc:meta]
             $variant:ident = $value:expr,
         )*
     }) => {
        #[$main_doc]
        #[$derive]
        #[repr($primitive_type)]
        pub enum $name {
            $(
                #[$doc]
                $variant = $value,
            )*
        }

        impl core::convert::TryFrom<$primitive_type> for $name {
            type Error = Errno;
            fn try_from(n: $primitive_type) -> Result<Self, Self::Error> {
                use $name::*;
                match n {
                    $($value => Ok($variant),)*
                    _ => Err(Errno::EINVAL),
                }
            }
        }
    }
}

Commentaire de Doc

#[derive(Debug, etc...)]

#[repr(u32)]

Commentaire de Doc

(au dessus de ch variant)

Entree

Sortie

Commentaire de Doc

#[repr(u32)]

#[derive(Debug, etc...)]

Commentaire de Doc

(au dessus de ch variant)

ident = Type

value = Valeur numerique

delimiteur =

$main_doc

$derive

$primitive_type

$doc

$variant

$value

Variables

Une macro un poil plus complexe

Forme littéral de l'enum

Présentée comme dans l'appel de la macro plus bas. avec pub en plus

Implémentation TryFrom

impl TryFrom<EnumType> for Type { ... }

match Type => EnumType

Utilisation de la macro

safe_convertible_enum!(
    /// This list contains the sockets associated function types
    #[derive(Debug, Copy, Clone)]
    #[repr(u32)]
    enum CallType {
        /// Create an endpoint for communication
        SysSocket = 1,
        /// Bind a name to a socket
        SysBind = 2,
        /// Initiate a connection on a socket. Client connection-oriented
        SysConnect = 3,
        /// Listen for connections on a socket. Server connection-oriented
        SysListen = 4,
        /// Accept a connection on a socket. Server connection-oriented
        SysAccept = 5,
        /// Send a message on a socket. Similar to write with flags. connection-oriented
        SysSend = 9,
        /// Receive a message from a socket. Similar to read with flags. connection-oriented
        SysRecv = 10,
        /// Send a message on a socket. The destination address is specified. connectionless
        SysSendTo = 11,
        /// Receive a message from a socket. The source address is specified. connectionless
        SysRecvFrom = 12,
        /// Shut down part of a full-duplex connection. connection-oriented
        SysShutdown = 13,
    }
);

FFI

Foreign Function Interface

rust bindgen

Pour la FFI, bindgen est un outil des plus efficace. En entrée on lui donne un fichier header .h et en sortie, il nous écrit un .rs a utiliser dans le programme.

cargo install bindgen

Dans sa forme la plus simple, mono fichier, on donne un .h a bindgen. Faisons un nouveau projet nomme ffi, creer un sous-dossier c_lib et y mettre le .h et le .c que je vous ai envoyé.

cd src
bindgen ../c_lib/lib.h > c_lib.rs

Le fichier .rs genere

pub type size_t = ::std::os::raw::c_ulong;
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct custom_memory_fn {
    pub allocator:
        ::std::option::Option<unsafe extern "C" fn(arg1: size_t) -> *mut ::std::os::raw::c_void>,
    pub deallocator: ::std::option::Option<unsafe extern "C" fn(arg1: *mut ::std::os::raw::c_void)>,
}

extern "C" {                                                                                                                                      
    pub fn fusion_merge_tab(                                                                                                                      
        t1: *mut ::std::os::raw::c_void,                                                                                                          
        len: ::std::os::raw::c_int,                                                                                                               
        elmt: size_t,                                                                                                                             
        cmp: ::std::option::Option<                                                                                                               
            unsafe extern "C" fn(                                                                                                                 
                arg1: *mut ::std::os::raw::c_void,                                                                                                
                arg2: *mut ::std::os::raw::c_void,                                                                                                
            ) -> ::std::os::raw::c_int,                                                                                                           
        >,                                                                                                                                        
        mem: *mut custom_memory_fn,                                                                                                               
    ) -> *mut ::std::os::raw::c_void;                                                                                                             
}  

Seules ces parties vont vraiment nous intéresser.

Ce sont les définitions de la structure a remplir et la fonction C a appeler.

Linkage du C avec le Rust

use std::env;
use std::path::Path;
use std::process::Command;

fn main() {
    let out_dir = env::var("OUT_DIR").unwrap();
    // Note that there are a number of downsides to this approach, the comments
    // below detail how to improve the portability of these commands.
    Command::new("gcc")
        .args(&["c_lib/merge_sort.c", "-c", "-fPIC", "-o"])
        .arg(&format!("{}/merge_sort.o", out_dir))
        .status()
        .unwrap();
    Command::new("ar")
        .args(&["crus", "libhello.a", "merge_sort.o"])
        .current_dir(&Path::new(&out_dir))
        .status()
        .unwrap();

    println!("cargo:rustc-link-search=native={}", out_dir);
    println!("cargo:rustc-link-lib=static=hello");
}

On va faire un script de linkage, on utilise pour cela un fichier build.rs que l'on place a la racine du projet.

D'abord il compile le fichier .c pour en faire une librairie et il contient des paramètres qui seront passes au compilateur rustc pour qu'il procède au linkage.

La librairie est linkee a l’exécutable

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct custom_memory_fn {
    pub allocator:
        ::std::option::Option<unsafe extern "C" fn(arg1: size_t) -> *mut ::std::os::raw::c_void>,
    pub deallocator: ::std::option::Option<unsafe extern "C" fn(arg1: *mut ::std::os::raw::c_void)>,
}

Si tout s'est bien passe, le cargo run devrait fonctionner normalement avec un simple Hello World. Interessons nous maintenant au code C a appeler.

D'abord il y a une sorte de structure qui prend un allocateur et un desallocateur en paramètre. std::alloc semble faire le boulot.

use std::alloc::{alloc, dealloc, Layout};

unsafe extern "C" fn allocator(arg1: c_lib::size_t) -> *mut ::std::os::raw::c_void {
    let layout = Layout::array::<u64>(arg1 as usize / std::mem::size_of::<u64>());
    let ptr = alloc(layout.unwrap());
    ptr as *mut ::std::os::raw::c_void
}

unsafe extern "C" fn deallocator(arg1: *mut ::std::os::raw::c_void) {
    let layout = Layout::new::<[u64; TAB_SIZE]>();
    dealloc(arg1 as *mut u8, layout);
}

Écrivons ces deux fonctions dans le main.rs

Fonction de comparaison

unsafe extern "C" fn cmp(
    arg1: *mut std::os::raw::c_void,
    arg2: *mut std::os::raw::c_void,
) -> std::os::raw::c_int {
    if *(arg1 as *mut u64) < *(arg2 as *mut u64) {
        0
    } else {
        1
    }
}

Pareil, apparemment, on doit envoyer l’adresse d'une fonction de comparaison. Ecrivons-la.

Tout est a peu près unsafe la

Et enfin le main...

const TAB_SIZE: usize = 1024 * 2;

fn main() {
    let mut custom = c_lib::custom_memory_fn {
        allocator: Some(allocator),
        deallocator: Some(deallocator),
    };

    let layout = Layout::array::<u64>(TAB_SIZE);
    let v = unsafe {
        let ptr = alloc(layout.unwrap());
        let mut v = Vec::<u64>::from_raw_parts(ptr as *mut u64, TAB_SIZE, TAB_SIZE);
        // remplit le vecteur avec des valeurs decroissantes
        for i in 0..(TAB_SIZE) {
            v[i] = ((TAB_SIZE) - i) as u64;
        }
        let old_ptr = v.as_ptr();
        let ptr = c_lib::fusion_merge_tab(
            v.as_ptr() as *mut ::std::os::raw::c_void,
            TAB_SIZE as i32,
            std::mem::size_of::<u64>() as u64,
            Some(cmp),
            &mut custom as *mut c_lib::custom_memory_fn as *mut c_lib::custom_memory_fn,
        );

        println!("Finito !");
        dbg!(old_ptr);
        dbg!(ptr);
        if old_ptr != ptr as *const u64 {
            v.leak(); // !!!
            v = Vec::<u64>::from_raw_parts(ptr as *mut u64, TAB_SIZE, TAB_SIZE);
        }
        v
    };
    for i in v.into_iter() {
        dbg!(i);
    }
}

std::process

Jouer avec les processus

std::Process

Text

Syntaxe des déclarations

fn main() {
    let arr: [u32; 5] = [1, 2, 3, 4, 5];
    let arr = [1, 2, 3, 4, 5];
    let arr: [&str; 3] = ["Terre", "Feu", "Air"];
    let arr = ["Terre", "Feu", "Air", "Eau"];
}

Simples Array

fn main() {
    let arr = vec![1, 2, 3, 4, 5];
    let arr = vec![1, 2, 3, 4, 5];
    let arr = vec!["Terre", "Feu", "Air", "Eau"];
    let arr = vec!["Terre", "Feu", "Air", "Eau"];
}

Utilisation de la macro vec!. un vecteur stocke un ensemble de données de type T

fn main() {
    let s: String = "Une String".into();
    let slice1 = &s[..2];
    let slice2 = &s[0..6];
    let slice3 = &s[1..len];
    let slice4 = &s[3..];
    let s = "Ceci est aussi une slice";
}

Slices: &str est une slice, une slice peut aussi référencer une String.

NB: L'index d'une slice est toujours en octet, elle peut couper une séquence utf8

Modules et fichiers

Organisation du projet

SESSION RUST

By AdapTeach

SESSION RUST

  • 178