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.
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
- unsigned:
- Décimal
f32
etf64
- 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).