Introduction Ă  Rust

  • rust

Published at 2017-11-28

Récemment je me suis intéressé au langage Rust. Il est édité par la fondation Mozilla qui l'utilise à travers le moteur de rendu Servo de Firefox 57. Cette utilisation leur a permis de doubler la rapidité du chargement des pages tout en consommant moins de ressource.

Voici donc une courte introduction pour vous faire découvrir ce langage innovant.

Dans ce tutoriel (loin d'ĂȘtre complet), je pars du principe que vous avez de solides notions en programmation.

Sommaire

  • TOC {:toc}

Introduction

Rust est un langage de programmation qui se veut sûr et ultra-rapide. Il est:

  • compilĂ©, il faut donc passer par une Ă©tape de compilation
  • avec un typage fort & statique
  • orientĂ© bas niveau

Il a Ă©tĂ© conçu pour ĂȘtre "un langage sĂ©curisĂ©, concurrent, pratique", supportant les styles de programmation purement fonctionnels, procĂ©duraux et orientĂ©s objet.

Wikipedia

Pourquoi utiliser Rust?

Rust bĂ©nĂ©ficie d'une grande communautĂ© et a d’ailleurs Ă©tĂ© Ă©lu le langage le plus apprĂ©ciĂ© des dĂ©veloppeurs en 2016 et mĂȘme en 2017 par le sondage de Stackoverflow.

A l'inverse du C et du C++, Rust simplifie grandement la libĂ©ration de la mĂ©moire avec son systĂšme d’appartenance. ConcrĂštement cela signifie:

  • Les performances du C sans la gestion de la mĂ©moire
  • protection contre les fuites de mĂ©moire

Bénéficiant de performances proche du C, pourquoi s'en priver?

Pourquoi ne pas utiliser Rust?

De plus, Rust est un langage bas niveau qui est, pour l'instant, peu adapté au développement web par exemple.

Rust est un langage encore trÚs jeune. Du fait qu'il soit jeune, les librairies existantes sont souvent peu documentées et il faut souvent mettre les mains dans le code pour comprendre le fonctionnement de celles-ci.

De plus, Rust est encore peu utilisé dans le monde professionnel. Si vous recherchez un langage à mettre en avant dans une recherche d'emploi, ce n'est actuellement pas le meilleur choix.

C'est aussi un langage difficile Ă  maĂźtriser et bas niveau. Il n'est pas dĂ©diĂ© au web ou bien au scripting (mĂȘme s'il en est capable).

Installation

L’installation se fait trùs facilement. Sur Linux, une ligne de commande suffit:

curl https://sh.rustup.rs -sSf | sh

Ensuite pour vérifier que tout fonctionne on crée un Hello World

// hello_world.rs
fn main() {
    println!("Hello, world!");
}

Pour compiler on utilise rustc:

rustc hello_world.rs

Cela va nous crĂ©er un fichier binaire exĂ©cutable hello_world qu'il suffit d’exĂ©cuter.

./hello_world
Hello, world!

Les bases

Les variables & les types

On retrouve tous les types de base:

  • Entier
    • unsigned: i8, i16, i32, i64, isize (ne peut pas ĂȘtre nĂ©gatif)
    • signed: u8, u16, u32, u64, usize
  • DĂ©cimal f32 et f64
  • boolĂ©en: bool
  • caractĂšre: instanciĂ© avec des '

Les variables s’instancient avec let:

let name = "alexandre";

Elles sont typées.

let age = 25;// un nombre entier
let age : i8 = 25;// un entier de 8 bytes
let age = 25i8;// un entier de 8 bytes

Pour les constantes on utilise const mais le type doit ĂȘtre dĂ©fini:

const MAX_AGE: u32 = 100_000;

Types pour grouper

Les Tuples sont des tableaux sans limite de taille et sans contrainte de type

let tup: (i32, f64, u8) = (500, 6.4, 1);
let first_element = tup.1;// => 6.4

Les Array sont des tableaux avec taille fixe et les Ă©lĂ©ments doivent ĂȘtre de la mĂȘme famille

let girlfriends            = ["Pamela", "Clara", "Roger"];
// s"Ă©crit aussi
let girlfriends [String;3] = ["Pamela", "Clara", "Roger"];
let first_element    = girlfriends[0];// => "Pamela"

Les slices sont des pointeurs vers une zone d'un girlfriends:

let girlfriends = ["Pamela", "Clara", "Roger"];
println!("{:?}", &girlfriends[1..3]);// => ["Clara", "Roger"]

Les vecteur sont des tableaux d’élĂ©ments de la mĂȘme famille mais sans contrainte de taille.

let mut vector: Vec<String> = Vec::new();
vector.push("Pamela");
vector.push("Clara");
vector.push("Roger");
println!("{:?}", vector);//["Pamela", "Clara", "Roger"]
println!("{:?}", &vector[2..3]);//["Roger"]

Mutabilité

Toutes les variables sont immutables par dĂ©faut. Si vous ne le spĂ©cifiez pas, elles ne pourront ĂȘtre modifiĂ©es. Par exemple ce code ne pourra pas ĂȘtre compilĂ©.

let age = 25;
age = age + 25;
// => re-assignment of immutable variable `age`

Il faut déclarer la variable comme mutable avec le mot clé mut:

let mut age = 25;
age = age + 25;

Un autre moyen consiste à utiliser le shadowing (je n'ai pas trouvé de traduction à ce terme). Cela consiste à redéfinir la variable avec let. Ceci, à la différence du mut, permet de définir le type de la variable.

let age = 25;
let age = age + 1;

Les conditions

Ici rien de spécifique à Rust, on retrouve le if, else et le else if. Cependant, contrairement à d'autres langages, la condition ne s'entoure pas de parenthÚses

let age = 25;

if age < 20 {
    println!("You are too young!");
} else if age == 25  {
    println!("Seem perfect");
} else {
    println!("I don't care");
}

Il est possible d'utiliser des valeurs de retour pour les conditions

let condition = true;
let age = if condition {
    5
} else {
    6
};

Pour lier plusieurs conditions, on peut utiliser le match:

let age = 25i8;

match age {
    10...100 => println!("You seem alive"),
    5 | 8    => println!("5 or 8 year is too young!!"),
    _ => println!("I don't care"),
}

Les boucles

Là aussi, pas de différence. Nous pouvons utiliser le while:

let mut number = 3;

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

    number = number - 1;
}

... ou la boucle for:

let a = [10, 20, 30, 40, 50];

for element in a.iter() {
    println!("the value is: {}", element);
}

Les fonctions

Définies par le mot clé fn, les paramÚtres et la valeur de retour sont typés. La valeur de retour est la derniÚre ligne qui ne comporte pas de ; à la fin. Pour plus de lisibilité, on peut utiliser le mot clé return.

fn multiply(a: i8, b: i8) -> i8 {
  a * b
}

Un peu plus loin dans le langage

Nous avons vu toutes les notions de base de Rust. Jusqu'ici, il y avait beaucoup de similitudes par rapport au C. Attaquons les notions plus avancées (et il y en a!).

Référence

Comme le C ou bien le C++, Rust utilise les pointeurs. Au lieu de copier la variable, nous travaillons directement sur une référence de celle-ci.

Pour utiliser une référence, il suffit de préfixer la variable d'un &.

let mut hello = "Bonjour";
println!("{:?}", hello); // => "Bonjour"
println!("{:?}", &hello);// => "Bonjour"
hello = "Holla";
println!("{:?}", &hello);// => "Holla"

Gestion de la mémoire

Rust gÚre la mémoire pour nous mais n'utilise pas de garbage collector qui passe afin de libérer la mémoire. Il utilise un systÚme d'Ownership. Les variables "vivent" dans un scope limité et la mémoire est libérée au fur et à mesure.

if true {
    let me = "Alex";
}
println!("{:?}", alex);
// => cannot find value `alex` in this scope

Dandling pointers

Il s'agit lĂ  d'une des plus grandes forces de Rust. L'un des plus gros problĂšmes des pointers sont les Dandling pointers.

Imaginez que vous utilisez un pointer vers une variable définie dans un scope. Nous avons vu qu'en sortant du scope (fn, if, etc..) cette variable est libérée. Nous obtenons donc un pointer vers une référence nulle. Ceci augmente la mémoire utilisée par votre programme et crée une fuite de mémoire.

Voici un exemple avec Rust.

// Fonction qui va créer un dangling pointer
fn dangle() -> &String {
    // on crée une variable limitée au scope de la fonction
    let s = String::from("hello");
    // on renvoie une réference qui pointe sur la variable
    // limitée au scope de la fonction
    &s
}
fn main() {
    let reference_to_nothing = dangle();
    // => missing lifetime specifier
}

Une des plus grandes forces de Rust est qu'il ne vous laissera pas compiller ce code.

Le systĂšme d'appartenance

Un autre gros point fort de Rust est le systÚme d'appartenance. Nous avons vu que grùce à celui-ci, la mémoire est libérée au fur à mesure. Le systÚme d'appartenance nous protÚge aussi contre les comportement inattendu. Ainsi une fonction ne peut pas modifier un pointeur sans que cela soit explicitement indiqué.

Prenons cet exemple. Une fonction change permet d'ajouter le text ", world" à la fin d'un texte donné en paramÚtre. Nous utilisons un pointeur vers le texte qui sera modifié.

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

    change(&s);
}

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

Rust refusera de compiler ce code car la fonction modifie le contenu de some_string sans que cela soit indiqué.

cannot borrow immutable borrowed content *some_string as mutable

Pour que ce code compile, il faut spĂ©cifier que la valeur pourra ĂȘtre modifiĂ©e par la fonction avec la mention &mut.

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

    change(&mut s);
}

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

Avec Rust, vous vous bùterez souvent contre le compilateur mais celui ci vous protégera d'une majorité des Runtime Errors

Structure

Si vous venez d'un langage orienté objet (Ruby, PHP, Java, etc..), les structures correspondent un peu aux classes.

Prenons une Human qui possÚde un name et un sex (jusque là ça se tient).

#[derive(Debug)]
struct Human {
    name: String,
    sex: String,
}

fn main() {
    let alex = Human {
        name: String::from("Alexandre"),
        sex: String::from("Big"),
    };
    println!("{:?}", alex);
    // => Human { name: "Alexandre", sex: "Big" }
}

Comme en POO, nous pouvons ajouter des méthodes sur ces structures avec les implémentations. Pour cela on utilise impl. Pour les fonctions, si elle prennent en paramÚtre &self, ce sont des méthodes d'instances. Si elle ne prennent pas en paramÚtre &self, ce sont des méthodes statiques.

Essayons ça avec une une méthode describe qui permet d'afficher les informations.

struct Human {
    name: String,
    sex: String,
}

impl Human {
    fn describe(&self) {
        println!("My name is {} and my sex is {}", self.name, self.sex);
    }
}


fn main() {
    let alex = Human {
        name: String::from("Alexandre"),
        sex: String::from("Big"),
    };
    alex.describe();
    // => My name is Alexandre and my sex is Big
}

Les enum

alex est bien sympa, mais au lieu de remplir le champs sex par Male ou Female, il a mis "big" (le con!).

Pour parer à cela, nous pouvons forcer le choix à des types définis avec un enum ressemblant à cela

#[derive(Debug)]
enum Sex {
    Male,
    Female
}

Et pour implémenter l'enum à notre classe, il suffit de changer le type de l'attribut sex:

struct Human {
    name: String,
    sex: Sex,
}

impl Human {
    fn describe(&self) {
        println!("My name is {} and my sex is {:?}", self.name, self.sex);
    }
}

Et maintenant l'instanciation de notre alex ne peut plus choisir autre chose que les deux sexes proposé:

fn main() {
    let alex = Human {
        name: String::from("Alexandre"),
        sex: Sex::Male,
    };
    alex.describe();
    // => My name is Alexandre and my sex is Male
}

Les options

Imaginons qu'un Human puisse ne pas avoir de sex (pourquoi pas?). Rust implémente la notion d'Option

struct Human {
    name: String,
    sex: Option<Sex>,
}

Maintenant que le sex est optionnel, nous pouvons créer alex sans sex:

fn main() {
    let alex = Human {
        name: String::from("Alexandre"),
        sex: None
    };
    alex.describe();
    // => My name is Alexandre and my sex is None
}

Pattern matching & Gestion des erreurs

Reprenons l'exemple prĂ©cĂ©dent. Nous voulons implĂ©menter une fonction d'instance print_my_sex afin de savoir Ă  tout moment si l'Human possĂšde un Sex ou non. LĂ  oĂč dans la majoritĂ© des langages on teste le rĂ©sultat renvoyĂ© par la fonction, avec Rust on utilise le pattern matching avec match. Voici l'implĂ©mentation:

impl Human {
    // affichage du sexe
    pub fn print_my_sex(&self) {
        match self.sex {
            Some(ref _sex) => println!("J'ai un sexe :)"),
            None => println!("Je n'ai pas de sexe :'("),
        }
    }
}

Maintenant on teste la fonction

fn main() {
    let mut alex = Human::new();
    alex.print_my_sex();// => "Je n'ai pas de sexe :'("
    // maintenant, on lui définit un sexe
    alex.sex = Some(Sex::Male);
    alex.print_my_sex();// => "J'ai un sexe :)"
}

Qu'importe la situation, notre code gÚre la situation. Cette méthode peut sembler lourde mais ainsi notre code est bulletproof!

Notre premiĂšre librairie

Notre Human nous servira trĂšs certainement dans nos futurs projets.

Essayons maintenant de créer un Crate afin que notre Human soit ré-utilisable. Pour cela on utilise Cargo, le gestionnaire de dépendance de Rust.

cargo new human

Cargo nous crée un dossier human avec une arborescence de ce type

human/
├── Cargo.toml
└── src
    └── lib.rs
  • Cargo.toml contient les informations et les dĂ©pendances de notre librairie
  • src/lib.rs contient

Voyons de plus prĂšs:

cd human

On commence par créer un fichier main.rs dans le dossier src.

// src/main.rs
fn main() {
    println!("My first crate");
}

Plus besoin de compiler nos fichiers Ă  la main, Cargo s'en charge pour nous

cargo run

My first crate

Le module

Maintenant nous allons créer un nouveau fichier pour notre Human. Il suffit de créer notre nouveau fichier human.rs dans la dossier src.

// src/human.rs

pub struct Size{
    pub length: i8
}

pub enum Sex{
    Male(Size),
    Female
}

pub struct Human {
    pub name: String,
    pub sex: Option<Sex>,
}

impl Human {

    pub fn print_my_sex(&self) {
        match self.sex {
            Some(ref _sex) => println!("J'ai un sexe :)"),
            None => println!("Je n'ai pas de sexe :'("),
        }
    }
}

Je n'ai pas parlĂ© des pub. Ils signifient que la dĂ©finition est publique et donc qu'elle peut ĂȘtre utilisĂ©e en dehors du fichier. On modifie un peu notre main.rs pour utiliser notre Human.

Pour importer notre fichier human.rs dans notre main.rs, il suffit alors d'utiliser mod. Dans le fichier main.rs, pour utiliser Human nous devons le préfixer du module. Comme ceci human::Human.

// src/main.rs

mod human;

fn main() {

    let size = human::Size{length: 8};

    let alex = human::Human {
        name: String::from("Alexandre"),
        sex: Some(human::Sex::Male(size))
    };
    alex.print_my_sex();// => "J'ai un sexe :)"
}

Et voilĂ . Il suffira d'ajouter ce crate Ă  votre projet et de l'appeler en faisant

extern crate human;

Conclusion

Le gros avantage de Rust est bien Ă©videment son compilateur qui nous protĂšge vraiment des runtime erreurs en empĂȘchant la compilation en amont. Il nous apporte des performances proche du C ce qui en fait un excellent choix pour les projets nĂ©cessitants de bonne performances (embarquĂ©, jeux-vidĂ©os, etc..).

De plus, le code Ă©cris en Rust est rĂ©utilisable et portable car le compilateur peut mĂȘme ĂȘtre installĂ© sur des architecture ARM (Raspberry PI).

Liens utiles