@VinceOPS

Rust : Premiers retours sur le langage

rust-starting

Quand je n’écris pas en TypeScript, je m’intéresse à Rust, langage de programmation système au typage fort et statique créé par Mozilla. Entre autres, par nostalgie pour la prog. système et surtout pour son interopérabilité optimisée avec WebAssembly. À ce sujet, j’ai d’abord étudié AssemblyScript (compilation TypeScript vers WebAssembly) mais celui-ci n’est pas une option viable pour le moment.

Dans cet article, je partage quelques éléments notables et mes premières impressions sur Rust et son écosystème, en tant que développeur travaillant avec Node.js et TypeScript depuis plus de 4 ans (bien qu’ayant aussi travaillé avec PHP, C, Java, C# et d’autres).

TL;DR: Si vous connaissez déjà bien Rust, vous n’apprendrez probablement rien dans cet article. Je ne suis moi-même pas encore un expert du sujet.

👮 Disclaimer : Un minimum de connaissances en POO et en allocation mémoire est requis pour certains passages de l’article.

Installation et “Hello World”

L’installation est très simple :

curl https://sh.rustup.rs -sSf | sh
# Pour une mise à jour : `rustup update`

Vous disposez ensuite de Cargo, le gestionnaire de paquets officiel du langage (cargo <update | search | ...>).
Il permet aussi de :

  • Créer un projet : cargo new
  • Compiler le projet : cargo build
  • Générer la documentation du projet : cargo doc
  • Lancer l’exécutable du projet : cargo run
  • Lancer les tests : cargo test
  • Linter le code : cargo clippy (clippy)
  • Formatter le code cargo fmt (rustfmt)
  • etc, etc

💡 La plupart de ces tâches sont exécutées par d’autres programmes. Cargo est leur interface commune.

On crée un nouveau projet :

cargo new my_project

Le projet ainsi généré contient : un dépôt git et son .gitignore, Cargo.toml (équivalent au package.json), Cargo.lock (équivalent au package-lock.json / yarn.lock), ainsi qu’un premier Hello World dans src/main.rs.

Et c’est déjà prêt ! L’installation est simplissime et fournit un outil standard effectuant toutes les tâches usuelles du développement : 🎉. Quant au Hello world, on le compile et l’exécute avec cargo run :

// src/main.rs
fn main() {
    println!("Hello world");
}
$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/my_project`
Hello world

Remarques et impressions

Documentations

La documentation du langage (“The Book”) est très bien construite, avec un niveau de détails parfaitement jaugé. On peut y accéder, même hors-ligne, en exécutant la commande rustup doc --book.

J’en profite pour citer l’excellent Rust by Example et les Rust API Guidelines, sans oublier The Rust Reference.

Intégration avec VS Code

L’équipe en charge du Rust Language Server (comparable au TSserver) a développé un client VS Code : le plugin officiel Rust. Il apporte une intégration complète, quoiqu’encore un peu lente : coloration, autocompletion, documentation, lint, etc.

Ces plugins, bien qu’optionnels, peuvent s’avérer utiles :

  • Better TOML pour un meilleur support des fichiers TOML (Cargo.toml).
  • crates, qui fournit une interface pour la gestion des versions des crates dans Cargo.toml.

Cargo et les Crates

Les packages, en Rust, sont composés d’un à plusieurs crates (bibliothèque ou exécutable). Le registre officiel, crates.io en contient à ce jour plus de 29 000, dont certains fournis par l’équipe officielle, comme rand, time et beaucoup d’autres.

Pour ajouter une dépendance au projet, on insère (manuellement) son nom et sa version aux [dependencies] de Cargo.toml :

[dependencies]
rand = "0.7.0"

Chaque paquet est ensuite installé en exécutant cargo build, qui met aussi à jour Cargo.lock.

💡 Pour installer une dépendance en passant par la CLI, il est nécessaire d’utiliser le crate cargo-edit. Son intégration officielle est supposément en cours.

Quelques éléments notables du langage

Comme annoncé en introduction, Rust est fortement et statiquement typé. Il offre une inférence (déduction) de typage très efficace. Les trois axes mis en avant par ses créateurs sont la Performance (rapide, gestion de la mémoire efficace, absence de garbage collector…), la Fiabilité (système de typage, utilisation de la mémoire…) et la Productivité (excellente documentation, compilateur très expressif, outils optimaux…).

Immutabilité

Les variables sont immutables par défaut. On utilise le mot clé mut si l’on souhaite déclarer une variable mutable. La convention de nommage est le snake_case 🐍.

// type "i32" déduit pour les deux variables
let immutable_x = 3 + 5;
let mut mutable_x = 3 + 5;

immutable_x = 5; // err. "Cannot assign twice to immutable variable
mutable_x = 7; // OK

Le compilateur

Deux mots sur le compilateur, rustc :

  • Il repose sur LLVM pour la génération du code machine
  • L’expressivité des messages d’erreur est excellente.

Exemple ici (messages copiés en commentaire), lorsqu’on tente de ré-affecter une variable non mutable

fn main() {
    let x = 5; // ❌ error[E0384]: cannot assign twice to immutable variable `x`
     // -
     // |
     // first assignment to `x`
     // help: make this binding mutable: `mut x`

    println!("The value of x is: {}", x);
    x = 6;
    // ^^^^^ cannot assign twice to immutable variable
}
thanks

Types et Instances, Méthodes, Visibilité…

Dans les types du langage, on retrouve les :

  • Booléens (bool, 1 octet)
  • Entiers signés iX et non signés uX, X étant la taille en bits (8, 16, 32, 64, 128)
  • Flottants avec f32 et f64
  • Caractères (char 4 octets, support complet d’Unicode ('a', '火', '🔥'))
  • Tuples (let tup = ('a', 'b', 'c'), let tup = (5, 4.5))
  • Tableaux (à taille fixe; pour des tailles dynamiques, on utilise des vectors, comme en C++)

Pas de null en Rust 🎊, mais une énumération standard Option pouvant contenir une valeur (Some) ou rien (None).

On définit des types personnalisés à l’aide des enum et struct :

enum IpAddressKind {
    V4,
    V6,
}

struct IpAddress {
    kind: IpAddressKind,
    address: String,
}

En effet, pas de classes ni de Programmation orientée Objet, mais d’autres mécanismes remplacent très bien l’héritage, le polymorphisme, etc. Il n’y a d’ailleurs pas de constructeur en Rust : par convention, on définit une fonction associée new responsable de fournir une instance du type défini. C’est par exemple le cas de String::new(), du crate du même nom.
Les fonctions associées ou “Associated functions” sont l’équivalent des méthodes statiques en POO : des fonctions associées à un type, plutôt qu’à une instance. On les exécute avec l’opérateur double colon (::) : <Type>::<method_name>.

L’encapsulation est possible à l’aide du mot clé pub, qui permet de rendre publiques les modules, types, méthodes et fonctions. Par défaut, tout est privé. On peut ajouter des méthodes à un type en définissant une implémentation (impl) :

pub struct Account {
    balance: i64,
}

impl Account {
    pub fn new(amount: i64) -> Account {
        Account { balance: amount }
    }

    pub fn get_balance(&self) -> i64 {
        return self.balance;
    }
}

Le champs balance est privé : il n’est pas accessible par les autres modules constituant le projet.

Pas d’interfaces ni d’héritage, mais Rust supporte également les types génériques ainsi que les Traits.

Ownership

Cette fonctionnalité de Rust (concept unique !) qui joue un rôle majeur dans sa gestion sûre de la mémoire est basée sur 3 règles simples :

  • Chaque valeur stockée en mémoire a une variable désignée comme son “propriétaire”
  • Il n’y a qu’un seul propriétaire à la fois
  • Quand le scope du propriétaire prend fin, la valeur est supprimée

Lorsqu’une valeur ayant entrainé une allocation mémoire est réassignée ou passée en paramètre, on parle de ”move” (transfert de propriété), rendant impossible l’utilisation de la variable initiale :

fn main() {
    let name = String::from("Jean");
    say_hello(name);
    // ❌ error[E0382]: borrow of moved value: `name`
    println!("{}", name);
                // ^ value borrowed here after move
}

fn say_hello(name: String) {
    println!("Hello {}", name);
}

Pour des raisons de performances, Rust ne prend pas l’initiative de faire une copie de cette valeur en mémoire et fait un move. Certaines valeurs sont systématiquement copiées (pass by value), comme les références (et non la valeur qu’elle réfère) et les types scalaires (entiers, caractères…).

On peut passer name par référence (typée &) pour procéder à un emprunt (borrow) :

fn main() {
    let name = String::from("Jean");
    say_hello(&name);
    println!("{}", name);
}

fn say_hello(name: &String) {
    println!("Hello {}", name);
}

L’emprunteur say_hello utilise une référence de name. Lorsque la fin de son scope est atteinte, rien ne se passe : le propriétaire de la valeur n’a jamais changé.
🤔 Que se passe-t-il si l’on essaie de compiler du code modifiant la valeur pour la préfixer avec “Hello” ?

fn main() {
    let s = String::from("Jean");
    prefix_with_hello(&s);
    println!("{}", s);
}

fn prefix_with_hello(name: &String) { // ❌ error[E0596]: cannot borrow `*name` as mutable, as it is behind a `&` reference
                        // ------- help: consider changing this to be a mutable
    name.insert_str(0, "Hello ");
//  ^^^^ `name` is a `&` reference, so the data it refers to cannot be borrowed as mutable
}

Le compilateur explique (littéralement !) que name ne peut pas être emprunté en tant que valeur mutable, car passé par “référence &”. Il recommande l’emploi d’une référence mutable, de type &mut String. On applique donc 3 changements : 1- name doit être mutable, 2- passé en tant que “référence &mut”, 3- et prefix_with_hello doit accepter le bon type en paramètre :

fn main() {
    let mut s = String::from("Jean");
    prefix_with_hello(&mut s);
    println!("{}", s); // "Hello Jean"
}

fn prefix_with_hello(name: &mut String) {
    name.insert_str(0, "Hello ");
}

En résumé, l’ownership effectue un suivi de chaque partie de code utilisant des valeurs stockées dans le tas (heap), y minimise les duplications, et libère la mémoire occupée par des valeurs inutilisées.

💡 Je recommande au passage la lecture de cet excellent article (teaser : on y parle aussi un peu du gradual typing de TypeScript).

Conclusion

Encore un article trop long à lire et à écrire, bien qu’il ne contienne même pas 30% de ce que j’aurais voulu y rapporter tellement j’apprécie la découverte de ce langage ! Mais le but est seulement de partager mon enthousiasme avec quelques éléments techniques. Pour les plus curieux, je recommande à nouveau la consultation du Book pour y découvrir, entre autres :

D’autres articles sur Rust (et donc WebAssembly !) viendront. Soyez prêts !

Be prepared

Credits pour la superbe plage en pixel art 🏖


VinceOPS

Retrouvez-moi sur Twitter 🤷
@VinceOPS