« Rust » : différence entre les versions
Ligne 1 774 : | Ligne 1 774 : | ||
Un grand merci à sebcrozet pour ses connaissances sur le fonctionnement de Rust (qui s’est inscrit sur Linuxfr juste pour l’occasion !), à olivierweb et à Olivier Renaud pour leurs innombrables corrections, ainsi qu’à tous les autres contributeurs bien entendu ! | Un grand merci à sebcrozet pour ses connaissances sur le fonctionnement de Rust (qui s’est inscrit sur Linuxfr juste pour l’occasion !), à olivierweb et à Olivier Renaud pour leurs innombrables corrections, ainsi qu’à tous les autres contributeurs bien entendu ! | ||
<br/> | |||
<br/> | |||
'''<b>[[Dev-index|@ Retour à la rubrique Développement]]</b>''' | |||
<br/> | |||
{{Copy|2013|[[Utilisateur:Sinma|Sinma]]|CC-BY-SA}} | {{Copy|2013|[[Utilisateur:Sinma|Sinma]]|CC-BY-SA}} |
Version du 2 décembre 2013 à 20:35
Présentation de Rust
par Sinma
Introduction
Rust est un langage de programmation multi-paradigme (procédural, fonctionnel, orienté objet), compilé et orienté système. Il se veut donc un concurrent sérieux de langages tels que le C, C++, D et Go.
Centré sur la sûreté, la concurrence et la praticité, il est développé par Mozilla Research (ils ne font pas que du web !) et une communauté de bénévoles. Il est publié sous double licence Apache 2.0 et MIT.
Il me semblait donc nécessaire de faire le point sur ce langage à la communauté dynamique et qui semble très prometteur. La sortie de la version 0.8 est donc l’occasion rêvée pour vous faire une présentation complète du langage. D’ailleurs, ce document est sûrement, à la date de la publication, le plus gros document francophone concernant Rust.
Qu’est-ce que Rust ?
Histoire
En 2006, Graydon Hoare commence un projet personnel, Rust, qui le restera pendant trois ans. À ce stade, le langage fonctionne assez bien pour faire tourner quelques exemples basiques. Il fut donc jugé suffisamment mature pour être pris sous l’aile de Mozilla.
Le compilateur était à l’origine écrit en OCaml, mais a été réécrit en Rust en 2010. On appelle cela un compilateur auto-hébergé parce qu’il est capable de se compiler lui-même. Le nouveau compilateur est basé sur l’excellente infrastructure LLVM, utilisée notamment au sein de Clang.
À terme, le langage devrait rivaliser en termes de vitesse avec du C++ idiomatique tout en étant plus sûr, et dépasser la vitesse du C++ à sûreté égale. En effet, l’écrasante majorité des vérifications de sûreté sont effectuées à la compilation, et il reste des tas d’optimisations à faire un peu partout. La sémantique du Rust (plus riche que celle du C++) permet en outre à LLVM de faire quelques optimisations supplémentaires.
Pour suivre les évolutions des performances de Rust, c’est par là.
Servo
On peut se demander pour quelle raison la fondation Mozilla a choisi d’investir dans le développement d’un nouveau langage. La raison est que les développeurs de Mozilla ont besoin de produire du code à la fois efficace, sécurisé, et parallélisable ; et le langage C++ qu’utilisent habituellement les développeurs Mozilla atteint rapidement ses limites sur ces deux derniers points. Plus particulièrement, Mozilla a commencé début 2012 à développer Servo, un moteur de rendu de pages web (HTML et CSS) dont les objectifs principaux sont justement la sécurité et la parallélisation. Servo est écrit en Rust, et par conséquent Rust a été fortement influencé par les besoins de Servo, puisque ces deux projets ont évolué ensemble. Cette situation n’est pas sans rappeler la symbiose qu’il y eu à l’époque entre le langage C et le projet Unix, qui ont été développés de concert.
L’architecture de Servo permet d’avoir de nombreux composants isolés (le rendu, la mise en page, l’analyse syntaxique du HTML, le décodage des images, etc) qui tournent en parallèle, pour obtenir un maximum de vitesse et surtout de stabilité. Le 3 avril dernier, Mozilla et Samsung ont annoncé leur collaboration pour développer ce projet.
Pour le moment, Mozilla n’a aucune intention d’utiliser Servo dans Firefox, car il est encore très loin d’être fonctionnel (d’après le wiki, encore un bug à corriger pour passer le test acid1 et plusieurs dizaines pour l’acid2), et aussi parce que ça demanderait beaucoup de travail pour l’intégrer au sein de Firefox.
Quels sont les buts du langage ?
Tout d’abord, c’est un langage plutôt orienté système (fonctionnalités de bas niveau, proches du matériel), mais avec une bonne sécurité par défaut (contrairement au C et au C++). La syntaxe du langage et les vérifications du compilateur empêchent énormément d’erreurs courantes. C’est simple : à long terme, le langage offre les mêmes garanties de libération de mémoire qu’un ramasse-miettes ce qui protège de la plupart des fuites de mémoire (memory leaks), il sera impossible de provoquer des dépassements de tampon (buffer overflow), ou des erreurs de segmentation (segfault) grâce à une gestion de la mémoire très bien pensée. Pour le moment, c’est juste très difficile !
C’est aussi un langage qui se parallélise aussi bien voire mieux que ce qui se fait dans les autres langages modernes. Il est facile de créer des tâches légères qui n’ont pas de mémoire partagée, mais un système de déplacement de variable d’une tâche à une autre.
Enfin, il réutilise des concepts connus et éprouvés, la « rouille » (rust), même s’il y a quand même quelques nouveautés. Néanmoins, certains langages, dont sont issus ces concepts, sont relativement peu connus et le mélange de fonctionnalités est inédit et le moins que l’on puisse dire c’est que tout se marie très bien !
Il a principalement été inspiré par le C++ (pointeurs intelligents), l’Erlang (système de tâches légères), le Haskell (le système de trait), les langages fonctionnels en général (filtrage par motif (pattern matching) et éléments de syntaxe), Python (quelques éléments de syntaxe), et sans doute d’autres.
C’est une véritable volonté de réutiliser ce que la riche histoire des langages de programmation leur avait laissé, et non de réinventer la roue une énième fois sans tirer des leçons du passé !
Mais Rust ne fait pas tout…
Ce qui suit est plus ou moins une traduction d’une partie de la FAQ présente sur Github. Certaines choses ne font pas partie des objectifs de Rust :
- Utiliser des techniques innovantes : comme dit précédemment, Rust a très peu de nouvelles fonctionnalités, et au contraire se focalise sur l’exploitation de techniques connues, des écrits et des études sur le sujet, pour l’intégrer de façon cohérente au langage.
- L’expressivité, le minimalisme ou l’élégance ne sont pas des buts en soi et ne sont donc pas plus importants que les autres buts du langage. En effet, le langage est performant, parallélisable et sûr en premier lieu.
- Couvrir toutes les fonctionnalités bas niveau des « langages système » pour écrire un noyau de système d’exploitation. Bien que ce ne soit pas son but, nous verrons toutefois plus bas qu'il se prête plutôt bien à l’exercice.
- Posséder toutes les fonctionnalités du C++ (la santé mentale des développeurs compte aussi !). Le langage fournit des fonctionnalités qui sont utiles dans la majorité des cas. On peut remarquer que c’est la même philosophie actuellement suivie dans Firefox.
- Être 100% statique, 100% sûr ou 100% réflexif, et en règle générale, être trop dogmatique. Les compromis existent. Le langage a vocation à être pratique, et non « pur ».
- Tourner sur n’importe quelle plateforme. Il devrait fonctionner sans trop de problèmes sur la plupart des plateformes matérielles et logicielles. Nous verrons plus bas qu’il est même possible de faire tourner des programmes Rust sur des plateformes matérielles un peu plus exotiques que la moyenne.
Montrez-moi le code !
Par rapport aux versions précédentes
Le langage commence à arriver à maturité, c’est pour cela qu’une bonne partie de la syntaxe reste identique par rapport aux versions précédentes (quand je dis les versions précédentes, je parle de deux ou trois versions en arrière). En effet, les évolutions ont surtout été de l’ordre des améliorations incrémentales de la syntaxe et de gros travaux dans la bibliothèque standard.
“Hello, world!” (what else?)
À quoi ressemble l’habituel et incontournable Hello world en Rust ?
fn main() {
println("Hello, world");
}
Par convention, les sources Rust ont l’extension .rs, se compilent en faisant rustc hello.rs et produisent un fichier hello.
Vous pouvez aussi directement lancer la commande rust run hello.rs qui compilera et lancera l’exécutable.
Commentaires
En Rust, on utilise le même type de commentaires qu’en C (et beaucoup d’autres langages).
// Ceci est un commentaire sur une seule ligne /* Ceci est un commentaire sur plusieurs lignes */
Déclarations de constantes et de variables
Types de base, vec et str
Les déclarations se font avec le mot clef let. Dans la plupart des cas il n’est pas nécessaire de donner le type de la variable, car il est déduit à la compilation (inférence de type).
let a = 3; // a est de type int (entier) let b = "Rust"; // b est de type str (chaine de caractères) let c = [1, 2, 3]; // c est de type vec (tableau)
On peut aider un peu le compilateur en suffixant les valeurs :
let a = 1u; // a est de type uint (entier non-signé) let b = 1i; // b est un int (entier signé) let b32 = 1i32; // b est de type i32 (entier sur 32 bits)
let c = 1.0; // c est de type f64 (nombre flottant sur 64 bits) // c’est le type double dans les autres langages let d = 1f32; // d est de type f32 // c’est le type float dans les autres langages let e = 1e-14f64; // e est de type f64 vaut 1×10^-14 let f = true; // f est de type bool (booléen) let g = "Ceci n’est pas un texte !"; // g est de type str (chaine de caractères en UTF-8) let h = [1u, 2, 3]; // pour plusieurs éléments de même type, un seul suffixe suffit
Le type peut être déterminé à partir de l’utilisation qui en est faite ensuite. En général, on n'utilise cette propriété que si l'on peut déterminer le type de la variable à partir du code juste en dessous (et pas 500 lignes plus bas).
let mut x = 1.0; // sans indication, le type déduit est f64 x = 1f32; // le compilateur devine que x est en fait un f32
let mut x = 1; // le type déduit est int x = 2u; // finalement, c’est un uint
let mut x = ~[]; // vecteur de type inconnu (ne compile pas tout seul) x = ["Ceci", "est", "un", "test"]; // vecteur de str
Sinon, on peut simplement donner le type explicitement :
let a: uint = 2; // annotation de type let b: [f64, ..3] = [1f64, 2.0, 2.437]; // tableau de f64 de taille 3 // conversion (_cast_) d’un entier non-signé vers un entier signé // méthode to_int() qui existe pour beaucoup de types de base let c = fonction_qui_retourne_un_uint().to_int(); // À utiliser uniquement si on ne peut pas faire autrement let d = fonction_qui_retourne_un_uint() as int;
Vous remarquerez assez vite que la conversion de type implicite n’existe pas en Rust, même entre les types numériques de base. Loin d’être un fardeau, c’est la garantie de trouver rapidement d’où vient son problème (et pas d’un bug qui provient d’une conversion implicite, bugs en général très difficiles à repérer).
Je viens de vous parler de vec mais sachez qu’il y a de nombreux autres conteneurs : des équivalents à map et set, une file à double fin (on peut ajouter et enlever à la fin ou au début, au choix), et une file ordonnée par une clé.
Mutabilité
En Rust, les données sont immuables par défaut. Le compilateur nous garantit que la valeur d’une variable ne pourra pas être modifiée pendant toute la durée de vie de cette variable. C’est une garantie bien plus forte que le const de C++, qui ne fait qu’interdire les modifications de la valeur au travers de cette variable const : il est toujours possible de modifier une structure de données déclarée const si on accède à son contenu depuis un pointeur qui n’est lui-même pas marqué const.
En Rust, le système de typage rend même le pointage non-constant d’une valeur constante impossible. Cette propriété du langage élimine toute une classe d’erreurs potentielles. Par exemple, cela supprime le problème d’invalidation d'itérateurs, qui est une source d’erreurs fréquentes en C++.
Si on veut pouvoir modifier sa valeur par la suite, il faut utiliser le mot-clé mut :
let mut total = 0; total += 1;
En C++, il peut être plutôt difficile d’avoir un code qui respecte la const-correctness (concept cher aux développeurs C++ expérimentés qui consiste à marquer const tout ce qui peut l’être). Cela permet d’avoir un code plus sûr, plus facile à maintenir, et ça peut aider le compilateur à faire quelques optimisations.
Bref, vous le verrez également plus bas, le compilateur Rust assure que la mutabilité est correcte par défaut !
Variables statiques
Les variables statiques sont des variables globales définies directement dans un module à l’aide du mot clef static :
static toto: int = 42;
fn main() { println!("Ma variable statique: {}", toto); }
Il est possible de définir une variable statique mutable. Ce faisant, il est possible de la modifier depuis n’importe quel point du programme. Étant donné que dans un environnement multitâche une variable statique est partagée entre les taches, son accès n’est pas synchronisé et donc potentiellement dangereux. C’est pour cela qu’il est nécessaire d’effectuer toute manipulation d’une variable statique dans un bloc unsafe :
static mut toto: int = 42;
fn main() {
unsafe { toto = 0; println!("Ma variable statique: {}", toto); }
}
Notez qu’il est possible de définir des variables statiques mutable locales à chaque tâche. On appelle ça le Task-Local Storage, qui s’effectue grâce à une table associative attachée à chaque tâche. Pour plus de détails sur l’utilisation des TLS, ça se passe ici.
Guide de nommage
Au niveau du style, il est recommandé d’écrire les noms de fonctions, variables, et modules en minuscule en utilisant des tirets-bas (underscore) pour aider à la lisibilité, et d’utiliser du CamelCase pour les types. Les noms peuvent contenir des caractères UTF-8 tels que des accents, tant qu’ils ne provoquent pas d’ambigüités.
Vous pouvez aussi voir les conventions utilisées pour les dépôts concernant Rust.
Afficher du texte
Point de System.out.println(); ici ! Rust a des fonctions d’affichage de texte très bien conçues, qui font beaucoup penser à Python, et dont les noms font moins de 18 caractères !
print("Affichage simple"); println("Affichage simple + saut de ligne");
print!("Affichage avec syntaxe pour afficher des {}", "variables."); // résultat : Affichage avec syntaxe pour afficher des variables. println!("Affichage avec syntaxe pour afficher des {}", "variables + saut de ligne."); // résultat : Affichage avec syntaxe pour afficher des variables + saut de ligne.
// On peut donner le type plutôt que de faire un ma_variable.to_str() println!("La réponse est {:i}", 42); println!("La réponse est {:s}", "42");
// On peut aussi… ne pas le donner ! println!("La réponse est {:?}", 42); println!("La réponse est {:?}", "42");
// On peut donner un nom aux emplacements, très utile pour les traductions println!("La réponse est {réponse:i}", réponse = 42);
Il y a encore bien d’autres choses, mais si vous souhaitez en savoir plus, je vous conseille de vous référer à la documentation.
Fonctions
Une fonction se déclare de la façon suivante :
fn ma_fonction(param1: Type, param2: Type) -> TypeDeRetour {
// mon code
}
Les fonctions qui n’ont pas de type de retour sont généralement marquées avec le type de retour unit (aussi appelé « nil »). En Rust, les deux notations ci-dessous sont équivalentes :
fn ma_fonction(param1: Type, param2: Type) -> () {
// mon code
}
fn ma_fonction(param1: Type, param2: Type) {
// mon code
}
La syntaxe ressemble furieusement à du Python (avec annotations de type qui rappelons-le ne sont pas interprétées par Python).
Comme dans les langages fonctionnels, il est aussi possible d’omettre le mot clef return à la fin de la fonction en supprimant le point-virgule. Dans ce cas, le bloc de plus haut niveau (le plus imbriqué dans des accolades) de la fonction produit l’expression qui sert de valeur de retour à la fonction. Ainsi, les deux fonctions suivantes sont équivalentes :
fn mult(m: int, n: int) -> int {
return m * n
}
fn mult(m: int, n: int) -> int {
m * n
}
Enfin, il est possible d’écrire des fonctions imbriquées (nested functions, fonctions à l’intérieur d’autres fonctions), contrairement au C, C++ ou Java.
Les structures de contrôle
On retrouve la plupart des structures de contrôle habituelles. À noter que les conditions des structures de contrôle ne nécessitent pas de parenthèses et doivent être de type booléen (rappel : pas de conversions implicites). Le corps de la structure de contrôle doit obligatoirement être entre accolades.
Le classique if/else
if false { println("étrange"); } else if true { println("bien"); } else { println("ni vrai ni faux ?!"); }
On peut combiner la possibilité de ne pas utiliser de mot-clé return à la puissance du if/else (ça permet aussi avec match que l’on verra plus bas) afin d’éviter quelques lourdeurs :
// retourne la valeur absolue fn abs(x: int) -> uint { if x > 0 { x } else { -x }
}
On peut aussi l’utiliser pour assigner des valeurs :
let est_majeur = true; // Pas besoin d’opérateur ternaire // En C++ ça donnerait: // int x = (est_majeur)? "+18": "-18"; let x = if est_majeur { "+18" } else { "-18" };
match : switch puissance 1 000
match permet de faire du filtrage par motif (pattern matching) ainsi que déstructurer les structures de données (c’est-à-dire récupérer individuellement les valeurs).
match mon_nombre {
0 => println("zéro"), 1 | 2 => println("un ou deux"), 3..10 => println("de 3 à 10"), _ => println("quelque chose d’autre")
}
Mais match est une des killer features de Rust, car un match qui ne traite pas toutes les possibilités ne compile pas.
// ne compile pas : on ne prend pas en compte les nombres négatifs et supérieur à 10 match mon_nombre {
0 => println("zéro"), 1 | 2 => println("un ou deux"), 3..10 => println("de 3 à 10"),
}
// compile : tous les cas sont pris en compte grâce au joker _ match mon_nombre {
0 => println("zéro"), 1 | 2 => println("un ou deux"), 3..10 => println("de 3 à 10"), _ => {} // ne fait rien
}
// compile : Rust a vérifié que l’on n'avait pas oublié de possibilités match mon_nombre {
x if x < 0 => println("strictement inférieur à zéro"); 0 => println("zéro"), 1 | 2 => println("un ou deux"), 3..10 => println("de 3 à 10"), x if x > 0 => {}
}
Pour ce qui est des performances, le filtrage par motif peut être assimilé à une union en C++, avec un tag (un nombre entier automatiquement généré par le compilateur, différent pour chaque entrée de l’union) permettant sélectionner la bonne entrée de l’union.
Boucle while
let mut nbr_gâteaux = 8; while nbr_gâteaux > 0 { nbr_gâteaux -= 1;
}
Boucle for
// permet de boucler sur les éléments contenus dans un itérateur. (voir plus bas) // l’itérateur que renvoie range va de 0 à 9 // _ est un joker : aucune variable ne prend les valeurs « renvoyées » par range for _ in range(0, 10) { println("blam!"); }
// on peut bien sûr parcourir des vecteurs let mon_vecteur = [-1, 0, 1]; // la méthode iter() permet de récupérer un itérateur // invert permet d’inverser le sens de l’itérateur for i in mon_vecteur.iter().invert() {
println(i.to_str());
} // Cela affichera donc : // 1 // 0 // -1
Itérateurs
Un petit point sur les itérateurs tout de même. On peut obtenir de n’importe quel conteneur un itérateur, mais on pourrait imaginer un itérateur sur n’importe quelle suite mathématique.
De plus, les itérateurs ont certaines méthodes bien pratiques…
let xs = [1, 9, 2, 3, 14, 12]; // un vec quelconque // La méthode fold permet d’accumuler les valeurs d’un itérateur let result = xs.iter().fold(0, |accumulator, item| accumulator - *item); // result vaut -41
Pour plus d’infos, c’est par ici.
Boucle loop, ça c’est nouveau
loop permet de faire des boucles infinies ! En fait, c’est l’équivalent de while true, il permet de remplacer do {} while(); (si on met un if qui contient un break juste avant la fin de la boucle) tout en étant plus flexible.
let mut x = 5u; loop { x += x - 3; if x % 5 == 0 { break; } println(x.to_str()); }
Les structures de données
struct
Compatible avec les struct en C, c’est une structure de données qui permet de regrouper plusieurs variables.
struct Magicien { pv: uint, pm: uint }
let mut mon_magicien = Magicien { pv: 2, pm: 3 }; mon_magicien.pv = 3; // pv de mon_magicien vaut désormais 3
// On peut aussi créer des `struct` vides (pour faire des tests par exemple) struct StructVide; let ma_struct_vide = StructVide;
On peut implémenter des méthodes sur des struct, ce qui nous donne plus ou moins une classe.
impl Magicien { // par convention, `new` crée, initialise et renvoie une structure // on met `mut` devant le nom du paramètre pour pouvoir le modifier fn new(mut pv_initiaux: uint, mut pm_initiaux: uint) -> Magicien { // on vérifie qu’on ne viole pas les invariants de classe if pv_initiaux == 0 || pv_initiaux > 10 { pv_initiaux = 2; } if pm_initiaux == 0 || pm_initiaux > 20 { pm_initiaux = 3; }
// finalement on crée la structure que l’on va renvoyer Magicien { pv: pv_initiaux, pm: pm_initiaux } }
// si notre magicien perd de la vie fn perd_vie(&mut self, vie_perdu: uint) { if vie_perdu > self.pv { self.pv = 0; } else { self.pv -= vie_perdu; } }
// on veut pouvoir récupérer ses pv pour l’affichage ou le debug fn get_pv(&self) -> uint { self.pv }
// La méthode `new` ne prend pas `&self` en paramètre. // C’est l’équivalent d’une méthode statique. // On l’appelle donc comme ceci : let mut mon_magicien = Magicien::new(2, 4);
// La méthode `perd_vie()` prend mut `&self` en paramètre. // Cela signifie qu’on désigne une instance de la structure // et que l’on souhaite pouvoir la modifier. // On l’appelle donc comme ceci : mon_magicien.perd_vie(2);
// La méthode `get_pv(`) prend `&self` en paramètre. // Cela signifie qu’on opère sur une instance de la structure // mais cette fois, on ne souhaite pas la modifier. println(mon_magicien.get_pv().to_str());
On remarquera que certaines méthodes prennent un self en premier paramètre. Il s’agit d’un identifiant représentant la structure courante (un peu comme le pointeur this dans la plupart des langages orientés objet. Sauf qu’ici on a plus de libertés sur la sémantique de son passage en argument : par référence avec &self, par mouvement avec self, etc). Par exemple dans mon_magicien.perd_vie(2), on aura self égal à mon_magicien. Une méthode sans paramètre self est une méthode statique.
Remarque : si on crée une instance de structure sans passer par new, il est quand même possible d’utiliser les méthodes définies dans le bloc impl. En fait, new n’est rien d’autre qu’une méthode statique comme les autres qu’on aurait très bien pu appeler create, bob voire choux_fleur. Ça n’a rien à voir avec les constructeurs ou la surcharge de l’opérateur d’allocation new en C++.
enum
Dans son utilisation la plus simple, une enum Rust est comparable à une enum de C. Il est également possible d’implémenter des méthodes dessus (un peu comme en Java).
enum Coup { Pierre, Feuille, Ciseaux }
impl Coup { fn to_str(&self) -> ~str { // on renvoie une chaine de caractère match *self { // on utilise l’étoile pour accéder à la valeur Pierre => ~"pierre", Feuille => ~"feuille", Ciseaux => ~"ciseaux" } } }
Chaque variante d’un enum peut avoir une valeur numérique… comme en C.
enum Coleur { Rouge = 0xff0000, Vert = 0x00ff00, Bleu = 0x0000ff }
Enfin, un enum peut faire des choses beaucoup plus puissantes, car elle permet en réalité de définir des types algébriques.
struct Point { x: int, y: uint }
// l’enum sert à choisir entre deux structures de données // qui ont une représentation mémoire différente enum Forme { Cercle(Point, f64), Rectangle(Point, Point) }
// On peut aussi choisir entre plusieurs `struct`s.
enum Forme { Cercle { centre: Point, rayon: f64 }, Rectangle { haut_gauche: Point, bas_droit: Point } }
// Dans ce cas-là, on déconstruit en utilisant les accolades fn aire(forme: Forme) -> f64 { // calcule l’aire de la figure match(forme) { Cercle { rayon: rayon, _ } => f64::consts::pi * square(rayon), Rectangle { haut_gauche: haut_gauche, bas_droite: bas_droite } => { (bas_droite.x - haut_gauche.x) * (haut_gauche.y - bas_droite.y) } } }
Tuples
Des tuples, comme en Python.
let position1 = (2, 4.0); // laisse le compilateur deviner les types let position2: (int, f32) = (2, 4.0); // donne le type explicitement
// tuple vide, renvoyé par les fonctions censées ne rien renvoyer (on vous a menti !) let tuple1 = (); // tuple d’une seule valeur, pas très utile let tuple2 = (4); // Préférez une `struct` si vous avez beaucoup de valeurs let tuple3 = (23, 8, -1, 78, -4);
// On peut extraire des valeurs des tuples let tuple = (5, -6, 4); let (premier, _) = tuple; // premier vaut 5, le _ indique qu’on ne se préoccupe pas de la seconde valeur du tuple
match position1 { (x, y) if x > 0 && y > 0.0 => {} (_, y) if y < 0.0 => {} (_, _) => {} }
Là aussi, match est capable de savoir si toutes les possibilités ont été épuisées.
Tuple struct
Les tuple structs sont tout simplement des tuples auxquels on donne un nom.
struct tuple_struct(int, int, f32); let mon_tuple = tuple_struct(1, 4, 30.0); match mon_tuple { tuple_struct(a, b, c) => println!("a: {:i}, b: {:i}, c: {:f}", a, b, c) }
Le tuple struct à un seul champ est un cas particulier très utile pour définir un nouveau type (appelé comme cela d’après la fonctionnalité d’Haskell newtype). Le compilateur conservera la même représentation mémoire pour le type contenu dans le tuple, et le tuple lui-même. En revanche, il s’agit d’un tout nouveau type : on peut lui ajouter de nouvelles méthodes alors que celles du type contenu ne sont accessibles que par déconstruction du tuple grâce à l’opérateur de déréférencement *.
Il ne faut pas le confondre avec type IdBidule = int; qui crée simplement un alias de type, comme typedef en C ou using en C++11.
// On crée et on déclare de la même façon struct IdBidule(int); let mon_id_bidule: IdBidule = IdBidule(10); // cette syntaxe est valide pour le cas particulier des nouveaux types. let id_int: int = *mon_id_bidule; // déconstruit le tuple-struct pour en extraire l’entier.
C’est très utile pour différencier des données de même type mais qui doivent être utilisées différemment.
struct Pouces(int); struct Centimètres(int);
Type Option
Autre killer feature du Rust, le type Option permet de gérer les cas d’erreurs où on utiliserait des pointeurs nuls ou des exceptions dans les autres langages ! Ils sont remplacés par Option, type que l’on peut déstructurer :
// fonction_dangereuse_en_C renvoie un Option<int> nbr = match fonction_dangereuse_en_C() { // x est du type int, on peut le manipuler entre les accolades Some(x) => { x } // si ça a réussi, nbr = x None => { 0 } // sinon, on met une valeur par défaut (nbr = 0) }
Le compilateur optimise automatiquement certains types Option comme Option<~int> afin qu’ils soient réellement représentés en mémoire par des pointeurs nuls (et non plus une union taguée).
Nous n’aborderons pas ici les autres moyens de gérer les erreurs en Rust, qui peuvent mieux convenir dans certains cas mais qui sont moins utilisés et plus complexes.
Récupérer des informations depuis l’entrée standard
Il y a des opérations basiques :
use std::io; // pour utiliser le module d’entrées/sorties // à terme, on utilisera std::rt::io (qui n’est pas encore fini)
// io::stdin().read_line() renvoie l’entrée utilisateur (chaine de caractères) let arg = io.stdin().read_line();
Mais dans pas mal de cas il faut passer par le type Option que l’on vient de voir :
// from_str::<int> convertit la chaine en entier et la renvoie dans un Option<int> let arg = from_str::<int>(io::stdin().read_line());
// On peut aussi le faire de cette façon : let arg: Option<int> = FromStr::from_str(io::stdin().read_line()); // Il faut ensuite utiliser un match pour récupérer le résultat
Exemples
Voici quelques exemples classiques (et surtout qui servent à quelque chose) reprenant la plupart des concepts vu ci-dessus.
Fizz Buzz
Voici un exemple de la puissance du Rust:
fn main() { for i in range(0u, 101) { match (i % 3 == 0, i % 5 == 0) { (true, true) => println("Fizz Buzz"), (true, false) => println("Fizz"), (false, true) => println("Buzz"), (false, false) => println(i.to_str()) } } }
Vous voulez plus de Fizz Buzz ?
Récupérer une saisie utilisateur
Ici on veut récupérer la valeur absolue du nombre que l’utilisateur a entré. Ça va vous permettre de jeter un œil aux fonctions utilisées pour les entrées en Rust. C’est surtout l’occasion de voir comment régler proprement un problème qu’on s’est forcément posé une fois quand on était débutant.
// Pour pouvoir utiliser certaines parties de la bibliothèque standard use std::io; use std::num;
fn main() { let mut nbr; println("Entrez un nombre s’il vous plait : ");
loop { let arg = from_str::<int>(io::stdin().read_line()); match arg { None => { println("Ce n’est pas un nombre."); }, Some(x) => { nbr = num::abs(x).to_uint(); // num::abs() renvoie un entier break; // on sort de la boucle } } // sinon on a un message d’erreur println("Veuillez entrer une valeur correcte : "); } }
Clôture (closure)
Les clôtures, ce sont des fonctions qui peuvent capturer des variables de la portée (scope) en dessous de la leur, c’est-à-dire qu’elles peuvent accéder aux variables déclarées au même niveau que la clôture. De plus, on peut passer des clôtures à une autre fonction, un peu comme une variable.
fn appeler_clôture_avec_dix(b: &fn(int)) { b(10); }
let var_capturée = 20; let clôture = |arg| println!("var_capturée={}, arg={}", var_capturée, arg);
appeler_clôture_avec_dix(clôture);
Des fois, il est nécessaire d’indiquer le type :
// fonction carré, renvoie le carré du nombre en paramètre let carré = |x: int| -> uint { (x * x) as uint };
On peut aussi faire des clôtures anonymes :
let mut max = 0; [1, 2, 3].map(|x| if *x > max { max = *x });
Parallélisation
do spawn
Pour lancer une nouvelle tâche, il suffit d’écrire do spawn, puis de mettre tout ce qui sera exécuter dans la nouvelle tâche entre accolades.
// Lancement un traitement dans une autre tâche do spawn { // Gros traitement }
// Lancer pleins de trucs en parallèle for i in range(1, 100) { // Pour les entiers de 1 à 99 do spawn { // On crée une tâche qui affiche l’entier à l’écran println(i.to_str()); } }
Canal
Pour communiquer entre processus en C, on utilise les tubes (pipes). En Rust, on utilise les canaux pour communiquer entre les tâches.
// on crée le canal de communication let (port, chan): (Port<int>, Chan<int>) = stream();
do spawn { let result = some_expensive_computation(); chan.send(result); // on envoie le résultat // notez qu’on ne peut plus utiliser chan dans la tâche principale // car elle a été « capturée » par la tâche secondaire }
some_other_expensive_computation(); // calcul dans la tâche principale let result = port.recv(); // on reçoit le résultat de la tâche secondaire
Un exemple de la « capture » des variables par la tâche :
let (port, chan) = stream(); // on n’a pas précisé le type, en effet il peut être déduit
do spawn { chan.send(some_expensive_computation()); // on envoie le résultat du calcul }
// Erreur, car le bloc `do spawn` précédent possède la variable `chan` do spawn { chan.send(some_expensive_computation()); }
Canal partagé
Pour lancer plein de tâches, mais tout récupérer au même endroit :
let (port, chan) = stream(); let chan = SharedChan::new(chan); // chan devient un canal partagé
for init_val in range(0u, 3) { // Create a new channel handle to distribute to the child task let child_chan = chan.clone(); do spawn { child_chan.send(some_expensive_computation(init_val)); } }
let result = port.recv() + port.recv() + port.recv();
Notez qu’on peut utiliser les itérateurs pour changer la dernière ligne et rendre notre code beaucoup plus flexible…
// Cela fonctionne pour n’importe quel nombre de tâches secondaires let result = ports.iter().fold(0, |accum, port| accum + port.recv() );
Retour vers le futur
Il est possible de faire un calcul en arrière-plan pour le récupérer quand on en a besoin grâce à future.
fn fib(n: uint) -> uint { // calcule le nombre de Fibonacci de n // long calcul qui renvoie un uint }
let mut fib_différé = extra::future::Future::spawn (|| fib(50) ); faire_un_sandwich(); println!("fib(50) = {:?}", fib_différé.get())
Les boites et les pointeurs
Jusqu’à maintenant, on créait des variables et des structures de données sur la pile. Cela signifie que si on passe cette variable à une fonction par exemple, on effectue forcément une copie. Pour de grosses structures ou des objets mutables, il peut être intéressant d’avoir une seule copie de la donnée sur la pile ou sur le tas et de la référencer par un pointeur.
En Rust, on a les pointeurs qui se contentent de pointer sur une valeur (comme son nom l’indique), et les boites (correspondant aux pointeurs intelligents du C++) qui vont avoir une influence sur la durée de vie de la valeur (si une valeur n’a plus de boite qui la référence, elle est supprimée). La différence n’est pas essentielle, mais ça permet de mieux comprendre le fonctionnement de Rust.
Pointeur unique (Owned pointer)
C’est une boite qui correspond à peu près à unique_ptr<T> en C++. Concrètement, la boite « possède » la valeur sur laquelle il pointe, et si on décide d’utiliser une autre boite ou un autre pointeur sur cette variable, on ne pourra plus utiliser l’ancienne. On appelle cela la sémantique de mouvement. Quand le pointeur est détruit, la valeur sur laquelle il pointe sera détruite (la durée de vie de l’objet pointé est celle de sa boite unique).
{ // on déclare un pointeur unique avec un ~ devant la valeur let x = ~"Test"; } // x et la chaine de caractères sur laquelle il pointe sont détruites
{ let x = ~"Test"; let y = x; // on passe la propriété de la chaine de caractères à y // erreur de compilation, c’est y qui possède la chaine de caractères x = "Plus test"; } // x est supprimé // y est supprimé ainsi que la chaine de caractères qu’elle possède
{ let mut x; { let y = ~"Test"; x = y; // on ne peut plus utiliser y } // y est supprimé // mais x possède la chaine de caractères qui n’est donc pas détruite } // x et la chaine de caractères sur laquelle il pointe sont supprimés
Boite partagée (Managed box)
C’est une boite qui correspond à peu près au shared_ptr<T> en C++ et au système utilisé dans Python, Java, Ruby… Plusieurs boites différentes peuvent référencer une même valeur, et lorsque la dernière référence est détruite, un ramasse-miette s’occupe de libérer la mémoire.
{ let mut x;
{ let y = @"Un str dans une boite partagée"; let x = y; // x et y pointent vers la même chaine de caractères } // y est supprimé } // x est supprimé ainsi que la chaine de caractères // en effet, x et y ont tous les deux été supprimés
Rust fait très attention à la mutabilité…
let w = @0; let x = @mut 1; let mut y = @2; let mut z = @mut 3;
z = y; // impossible, la valeur de z est mutable alors que celle de y ne l’est pas y = z; // bien entendu, ça ne fonctionne pas non plus dans l’autre sens
Deux particularités devraient cependant retenir votre attention. D’une part on choisit ce qui sera géré par le ramasse-miettes, ce qui fait qu’il ne gère que ce qui est nécessaire (il est donc plus rapide). D’autre part, il n’y a pas un ramasse-miettes global, mais un ramasse-miettes par tâche qui le nécessite (possible car il n’y a pas de mémoire partagée), ce qui signifie qu’un programme multitâche (multithreadé) ne sera jamais complètement arrêté.
C’est une fonctionnalité presque indispensable au sein d’un moteur de rendu comme Servo. Pour le moment, c’est un simple compteur de références qui ne gère pas correctement les références circulaires, mais dans le futur, un vrai ramasse-miettes sera implémenté.
Il est intéressant de noter que l'API standard de Rust n’utilise que très rarement des boites partagées. En fait, il est relativement courant qu’un programme Rust n’utilise que des valeurs sur la pile et des pointeurs uniques, ce qui au final revient à ne pas utiliser de ramasse-miettes. Le fait de pouvoir se passer totalement de ramasse-miettes, et ceci sans avoir à trop restreindre l’utilisation de l'API standard, est un point fort pour développer dans certains domaines (jeux, temps réel).
Pointeur emprunté (Borrowed pointer)
Correspond à la référence en C++. C’est simplement un pointeur sur la mémoire appartenant à une autre boite ou pointeur. Il est surtout utilisé pour les fonctions, on peut alors lui passer en paramètre n’importe quelle valeur, boite ou pointeur :
// Le & devant le type signifie qu’on accepte n’importe quelle valeur, boite ou pointeur fn test(mon_vecteur: &[uint]) { // mon code }
// un vecteur alloué sur la pile, et deux boites (allouées sur le tas) let x = [1, 2, 3]; let y = ~[1, 2, 3]; let z = @[1, 2, 3];
// Grâce au pointeur emprunté, les trois appels ci-dessous sont valides test(x); test(y); test(z);
Ça permet aussi de « geler » temporairement une variable :
let mut x = 5; // je peux modifier x { let y = &x; let x = 6; // Erreur, x ne peut être utilisé tant que y existe let y = 7; // Erreur, y n’est pas mutable } // Je peux à nouveau utiliser x
// Le cas de la boite partagée est intéressant… let mut x = @mut 5; { // notez l’étoile, on cherche l’adresse de la valeur et non celle de la boite let y = &*x; // si on essaie de modifier x ou y, le programme va lancer un échec // http://static.rust-lang.org/doc/master/tutorial-conditions.html#failure // il va « planter » proprement. En effet, l’état de gel des boites partagées // est vérifié à l’exécution. }
Pointeur brut (Raw pointer)
Quand nous vous avions dit tout au début que Rust était un langage totalement sûr, nous vous avions menti ! En effet, il est possible d’écrire du code non-sûr mais seulement dans un bloc ou une fonction marquée unsafe. Ils sont principalement utilisés pour FFI (pour appeler des fonctions d’un code écrit dans un autre langage, voir plus bas) ou, rarement, pour des opérations qui nécessitent plus de performance.
Le mot-clé unsafe (ce qui signifie « non-sûr ») permet en effet d’avoir accès à un pointeur non sécurisé (risque de fuite mémoire, multiple désallocation, valeur déréférencée, désallocation du pointeur pas claire), le type de pointeur utilisé en C (*). Le déréférencement est non sécurisé pour ce type.
Ce genre de pointeur est aussi utile pour définir ses propres types de pointeurs intelligents. Par exemple la bibliothèque étendue extra fournie avec le compilateur contient deux autres pointeurs intelligents : Rc et Arc, respectivement pour le comptage de référence et le partage de données entre plusieurs taches s’exécutant en parallèle.
Si vous souhaitez en savoir plus sur le code non-sûr, consultez le manuel.
Plus de détails sur le fonctionnement des pointeurs
Déréférencement de boites et de pointeurs
Si on crée une boite ou un pointeur pour une valeur, on veut pouvoir modifier son contenu. Pour y accéder, il y a deux manières :
let mut x = ~10; x = ~10; // on assigne à nouveau une valeur dans une boite x = 10; // invalide *x = 10; // l’étoile permet d’accéder à la valeur comme en C
Cela fonctionne de la même façon pour les struct et les méthodes.
struct Test { x: int, y: int } impl Test { pub fn test() { print("Un petit test."); } }
let mon_test = ~Test { x: 10, y: 5 } (*mon_test).x = 4; (*mon_test).test();
Mais rassurez-vous, Rust fait du déréférencement automatique ! Cela signifie que vous n’avez pas à utiliser l’étoile lorsque vous voulez accéder à une valeur ou une méthode d’une struct. Ainsi, le code suivant est parfaitement valide :
mon_test.x = 4; mon_test.test();
Les durées de vie
Les durées de vie sont peut-être la fonctionnalité inédite du Rust. Ils permettent de créer des pointeurs sur à peu près n’importe quoi (y compris sur la pile), tout en garantissant qu’ils ne soient jamais invalides.
En fait, tous les pointeurs empruntés ont une durée de vie. La plupart du temps, le compilateur les déduit automatiquement.
struct UneGrosseStructure { donnée_énorme_à_ne_pas_copier: f64 // imaginez qu’à la place de f64 on ait vraiment //une donnée de plusieurs Mo. }
fn main() { let donnée = UneGrosseStructure { donnée_énorme_à_ne_pas_copier: 42.0 }; pointeur_emprunté_vers_la_donnée = &donnée.donnée_énorme_à_ne_pas_copier;
// À partir de maintenant, le compilateur utilise les durées de vie // pour s’assurer que le pointeur emprunté ne survive pas après // la destruction de la donnée.
// compile car la structure donnée existe encore ! println(pointeur_emprunté_vers_la_donnée.to_str());
}
En revanche il est des situations où le compilateur ne peut inférer correctement les durées de vie. Cela arrive systématiquement lorsque l’on essaie de retourner un pointeur emprunté vers une donnée interne à une structure.
struct UneGrosseStructure { donnée_énorme_à_ne_pas_copier: f64 // imaginez qu’à la place de f64 on ait vraiment une donnée de plusieurs Mo. }
impl UneGrosseStructure { fn get_data_ref(&self) -> &f64 { // ceci ne compile pas &self.donnée_énorme_à_ne_pas_copier } }
Ceci ne peut pas compiler étant donné que rien n’indique à l’appelant de la méthode get_data_ref que le pointeur qu’il retourne pointe vers l’intérieur de la structure. En effet, lorsqu’on appelle get_data_ref de l’extérieur, on a besoin de savoir que le &f64 retourné n’est valide que tant que &self est lui-même valide. Cette synchronisation de validité de pointeurs se fait par le biais d’une annotation de durée de vie explicite :
struct UneGrosseStructure { donnée_énorme_à_ne_pas_copier: f64 // imaginez qu’à la place de f64 on ait vraiment une donnée de plusieurs Mo. }
impl UneGrosseStructure { fn get_data_ref<'a>(&'a self) -> &'a f64 { // ceci compile! // Le pointeur retourné et self ont le même tag: 'a. &'a self.donnée_énorme_à_ne_pas_copier } }
Vous pouvez voir le 'a (annotation de durée de vie) comme un tag de pointeur qui va dire que « tous les pointeurs tagués par un 'a doivent vivre au plus aussi longtemps que le self tagué avec un 'a. ». Il sera ainsi impossible à la structure dont on a pris un pointeur interne d’être détruite avant que le pointeur interne lui-même ait été détruit.
Voici un autre exemple, utilisant la même structure que précédemment, de ce que l’on aurait pu faire (à tort) sans la notion de durée de vie. Si on avait le droit d’écrire fn get_data_ref(&self) -> &f64, on aurait été capable d’écrire cela :
/* * Ceci est ce que l’on aurait pu faire si la notion * de durée de vie de pointeur n’existait pas. */ fn créer_un_pointeur_invalide() -> &f64 { // on crée la donnée let donnée = UneGrosseStructure { donnée_énorme_à_ne_pas_copier: 42.0 };
// on prend une référence let référence = donnée.get_data_ref();
// on fait plein de trucs fun avec println(référence.to_str());
// et… on la retourne ! référence }
fn main() { let pointeur_invalide = créer_un_pointeur_invalide();
println(pointeur_invalide.to_str()); }
Si ceci était autorisé, il est évident que le pointeur_invalide est invalide étant donné qu’il pointe sur la pile allouée pour l’appel de fonction créer_un_pointeur_invalide.
Voyons comment, en ayant défini fn get_data_ref<'a>(&'a self) -> &'a f64, les durées de vie nous aident ici :
/* * Ceci est du code Rust qui ne compilera pas, grâce aux durées de vie */ fn créer_un_pointeur_invalide() -> &f64 { // on crée la donnée let donnée = UneGrosseStructure { donnée_énorme_à_ne_pas_copier: 42.0 };
// on prend une référence // `donnée` et `référence` sont synchronisés par 'a. let référence = donnée.get_data_ref() // 'a est toujours vivante
// on fait plein de trucs marrants avec // 'a est toujours valide. println(référence.to_str()); // 'a est toujours valide.
// et … on ne peut pas la retourner ! (erreur de compilation) // 'a est toujours valide référence // 'a n’est _plus_ valide à la sortie de la fonction ! }
Ici, le 'a permet de suivre pendant combien de temps donnée est valide. On ne peut pas retourner le pointeur puisque référence est de type &'a f64 alors que le type de retour de la fonction est &f64. On voit bien que les durées de vie ne sont pas les mêmes.
La sémantique de mouvement
Il faut noter qu’en Rust, la méthode de passage d’argument par défaut n’est ni par copie, ni par référence. Il s’agit d’un passage par déplacement, c’est-à-dire en utilisant la sémantique de mouvement. C’est un peu comme si on appelait la fonction C++ std::move sur chacun des paramètres avant l’appel de fonction.
Cette sémantique de mouvement s’applique pour les pointeurs uniques (rappel : indiqué par le préfixe ~), les structures contenant de tels pointeurs, et les types génériques (cf. la section suivante). Tous les autres types sont copiés implicitement (les pointeurs @ et @mut effectuent une copie légère).
En effet, comme on sait que les pointeurs uniques ne peuvent pas être partagés (un seul pointeur dessus à la fois), on peut effectuer l’opération de déplacement sans risque. L’avantage principal de ce comportement est de permettre au programmeur de toujours savoir exactement à quel moment une copie nécessitant une allocation est réalisée.
struct Toto { données: ~[int], // grosse quantité de données qu’on veut éviter de copier autre_chose: int }
fn extraire_données(t: Toto) -> ~[int] { t.données }
fn main() { let toto = Toto { données: ~[10, 20, 40, 80], autre_chose: 23 }; // toto avec // un gros vecteur let données = extraire_données(toto); // pas de copie ici !
// À partir d’ici, toto a été déplacé et n’est plus utilisable !
println(données.to_str()); // on affiche les données }
Dans cet exemple, le vecteur données n’est jamais copié ! Il est simplement déplacé hors de la variable toto. Ceci rend toto inutilisable après l’appel à extraire_données (et c’est vérifié par le compilateur). Si on souhaite éviter cette sémantique de mouvement, il est nécessaire de passer Toto en utilisant un pointeur, et de copier explicitement les données avec la méthode clone :
struct Toto { données: ~[int], // grosse quantité de données qu’on veut éviter de copier autre_chose: int }
fn extraire_données(t: &Toto) -> ~[int] { t.données.clone() // copie explicite }
fn main() { let toto = Toto { données: ~[ 10, 20, 40, 80 ], autre_chose: 42 }; // toto avec // un gros vecteur let données = extraire_données(&toto); // on fait copie ici !
// À partir d’ici, toto est toujours utilisable !
println(données.to_str()); // on affiche les données }
Interactions avec les autres langages
Appeler du code d’un autre langage
Il est possible d’utiliser toutes les fonctions de la libc directement depuis Rust.
De plus, il est très facile d’appeler du code C grâce à FFI, il suffit de faire une fonction pour « envelopper » l’appel, mettre éventuellement du code non-sûr (souvent pour les pointeurs), s’occuper des correspondances entre les types de C et les types de Rust et deux-trois petits trucs supplémentaires.
Par exemple, si on veut faire la fork bomb la plus courte en Rust :
#[fixed_stack_segment] fn main() { loop { // boucle infinie unsafe { std::libc::fork(); } // le fork, merci la libc } }
Vous remarquerez que c’est quand même compliqué de faire des bêtises en Rust !
Il y a des discussions en cours pour amener le support C++ au même niveau que le C en s’inspirant du D, mais pour le moment, aucun autre langage que le C n’est supporté. Il faut donc créer un binding (c’est-à-dire refaire l’opération décrite ci-dessus pour toute la bibliothèque) en C pour ce code puis faire un binding Rust qui appelle ces fonctions C. C’est le même fonctionnement assez similaires aux autres langages de programmation.
Appeler du code Rust depuis un autre langage
On peut appeler du code Rust depuis n’importe quel langage qui peut appeler du code C en déclarant ses fonctions extern "C" fn foo(…) {}.
Néanmoins, vous ne pouvez utiliser qu’un sous-ensemble de Rust. Les tâches, les échecs et les pointeurs partagées notamment ne fonctionneront pas, car le runtime n’a pas été initialisé.
De plus, les (rares) parties de la bibliothèque standard qui utilisent les pointeurs partagés ne fonctionneront pas, notamment la partie io. Dans le futur, Rust sera plus facilement utilisable depuis un autre langage, mais cela nécessite du travail.
Si cela vous intéresse, voilà comment utiliser du Rust à partir de Ruby (certaines informations sont obsolètes).
La généricité
La généricité est la capacité d’écrire du code une seule fois et qui fonctionne pour de plusieurs types de données différents.
Les traits
Un trait est un outil permettant de contraindre un autre type à fournir un certain nombre de méthodes. C’est l’équivalent des interfaces de Java, des typeclasses d’Haskell.
En C++, on pensera plutôt aux classes abstraites et de ce qu’aurait pu être la notion de concept en C++1 (qui n’a pas été retenue par le comité de standardisation). Il y a également le système de templates qui n’a pas vraiment d’équivalent Rust (mais rassurez-vous, on se débrouille sans !).
Supposons que vous faites un moteur de rendu. Vous voudrez par exemple avoir des structures désignant quelque chose qui peut être dessiné. En d’autres termes, il est nécessaire d’imposer à un type d’avoir une méthode draw (dessiner en français). Pour cela, on va dans un premier temps créer un trait.
trait Draw { fn draw(&self); }
Ensuite, tous les objets qui devraient pouvoir être dessinés ont simplement à implémenter le trait Draw :
struct A { data_a: int }
struct B { data_b: f64 }
impl Draw for A { fn draw(&self) { println(self.data_a.to_str()); } }
impl Draw for B { fn draw(&self) { println(self.data_b.to_str()); } }
Ensuite il devient possible d’écrire des fonctions génériques qui fonctionneront aussi bien pour A que pour B et toute autre structure implémentant Draw.
fn draw_object<T: Draw>(object: &T) { println("Je vais dessiner sur la console un objet qui implémente Draw !"); object.draw(); println("Ça y est, j’ai fini ! :p"); }
fn main() { let a = A { data_a: 42 }; let b = B { data_b: 42.0 }; draw_object(a); // OK, A implémente Draw. draw_object(b); // OK, B implémente Draw. draw_object(42.0); // erreur de compilation: f64 n’implémente _pas_ Draw ! }
Notez-le <T: Draw>. Cela signifie que la fonction draw_object accepte n’importe quel type que l’on nomme abstraitement T, et que ce type doit implémenter le trait Draw.
Pour manipuler des éléments du type Draw lui-même, il est possible d’utiliser l’opérateur as pour que le compilateur considère la structure implémentant le trait Draw comme étant de type ~Draw. On appelle les instances du type ~Draw (ou @Draw et &Draw) des trait-object (des traits vus comme des objets).
let liste_de_trucs_qui_peuvent_être_dessinés = ~[~Draw]; // une liste hétérogène ! liste_de_trucs_qui_peuvent_être_dessinés.push(~A { data_a: 42 } as ~Draw); liste_de_trucs_qui_peuvent_être_dessinés.push(~B { data_b: 42.0 } as ~Draw);
Les détails compliqués
Le comportement du compilateur vis-à-vis des fonctions (et structures) génériques est similaire au C++ : les fonctions polymorphiques (génériques) sont rendues monomorphiques pour chaque type d’argument avec lequel il est appelé. Pour faire simple, c’est exactement comme si le compilateur générait automatiquement les fonctions non-génériques :
fn draw_object(object: &A) { // Généré par le compilateur lorsqu’il voit // que draw_object(a) est appelé. // ... }
fn draw_object(object: &B) { // Généré par le compilateur lorsqu’il voit // que draw_object(b) est appelé. // ... }
Cela est très important pour les performances étant donné que la résolution des fonctions est réalisée au moment de la compilation et non lors de l’exécution. C’est pour cela que les traits sont très différents des interfaces en Java, ou des classes abstraites en C++. Pour faire simple : les traits en Rust font l’objet de dispatch statique de fonction, alors que les interfaces en Java font l’objet de dispatch dynamique.
Les traits sont l’objet de dispatch statique de fonction. Le dispatch dynamique, comme les interfaces de Java, (utilisées pour les listes hétérogènes dans le dernier exemple de la partie précédente) est assuré grâce au mécanisme de trait-object.
Pour résumer, on peut avoir du dispatch statique en utilisant une contrainte de type <T: Draw>, et de dispatch dynamique en utilisant un trait-objet ~Draw. Il s’agit d’un pont entre statique et dynamique très élégant en Rust que l’on trouve dans peu de langages.
Bien entendu, ceci n’est qu’un aperçu de la généricité en Rust, il est possible de faire beaucoup de choses puissantes comme de l’héritage de trait, les méthodes par défaut, les structures génériques, etc.
Les catégories (Kind)
Les catégories sont des traits un peu particuliers étant donné qu’ils ne pourraient pas être déclarés par un utilisateur du langage : ils nécessitent un support particulier de compilateur. Ceux-ci permettent principalement de contraindre la durée de vie des types ou de ce qu’ils contiennent (dans le cas où ils contiendraient des pointeurs empruntés).
Il n’est pas forcément nécessaire d’entrer dans les détails des catégories ici, il faut juste réaliser qu’elles permettent quelques actes de magie très puissants. Notamment Rc les utilise afin de s’assurer, au moment de la compilation, qu’il n’y aura pas de références circulaires (car les références circulaires et le comptage de référence ne font pas bon ménage).
Les catégories existantes sont: Freeze, Send, 'static et Drop.
Les caisses et les modules : programmer dans plusieurs fichiers
Une caisse (crate) est une unité de compilation. Cela signifie que c’est un programme ou une bibliothèque. rustc ne compile qu’une caisse à la fois.
Un module, c’est simplement une sous-partie d’une caisse. Chaque fichier représente un module, mais on peut aussi en déclarer manuellement, cela permet d’avoir le même rôle qu’un namespace de C++.
Mais voyons comment utiliser les définitions contenues dans un fichier dans un autre fichier.
mod truc; // on peut désormais accéder au fichier `truc.rs`
truc::fonction_truc(); // on peut accéder à ce qu’il y a dans truc avec `truc::`
Si on veut pouvoir utiliser ce que contient le fichier truc.rs, on peut importer les noms de fonctions et de variables dans la portée courante.
use truc::fonction_truc; // on peut utiliser fonction_truc directement // l’import global est expérimental et potentiellement bugué // il faut placer `#[feature(globs)];` en haut du fichier pour l’activer use truc::*;
mod truc; // les `use` doivent être placés tout en haut, avant les `mod`.
Ainsi, si vous voulez utiliser std::io::stdin().read_line(), vous pouvez utiliser use std::io; pour utiliser directement io::stdin().read_line(). Dans la bibliothèque standard, les modules de std sont importés par défaut si utilisés, contrairement à extra. De plus, certaines méthodes sont déjà importés, comme std::io::print et ses dérivées.
Quand nous ne sommes plus dans le fichier principal, les use ne marchent plus comme on s’y attend… En effet, les use dépendent du fichier dans lequel on est. Si on est dans truc.rs et qu’on souhaite utiliser des choses de machin.rs, on fera (dans truc.rs) :
use self::machin::Machin; // self se réfère au fichier principal de la caisse mod machin;
La convention est que le nom d’un module s’écrit en minuscule. Par ailleurs, nommer un fichier de la même façon qu’une déclaration dudit fichier peut causer quelques problèmes.
Pour créer des modules manuellement, on doit utiliser mod et placer le contenu du module entre accolades :
mod foo { fn foo_foo() {}
mod bar { fn bar_bar() {} } }
// pour importer bar_bar, on utilisera `use foo::bar::bar_bar;`
Les extensions de syntaxe
La syntaxe de Rust est relativement simple, d’ailleurs les concepteurs du langage ont beaucoup travaillé dans ce sens en unifiant ou en supprimant des concepts redondants, ou encore en réduisant au maximum le nombre de mots-clés du langage. Cependant, il est parfois tentant d’enrichir la syntaxe de Rust pour des besoins particuliers.
Rust propose de modifier localement sa syntaxe, grace a des extensions de syntaxe. Concrètement, une extension de syntaxe est de la forme nom_de_l_extension!(…), où le contenu des parenthèses a une syntaxe spécifique à l’extension.
La bibliothèque standard inclut plusieurs extensions de syntaxe. println! est un équivalent au printf de C :
let answer = 42 println!("la reponse est {}.", answer); println!("la reponse est {v}.", v=answer);
En C, printf est implementé par une fonction à nombre variable d’argument, et la vérification du nombre et du type d’arguments s’effectue au runtime. Le println de Rust a quant à lui l’énorme avantage d’être vérifié lors de la compilation. C’est en quelque sorte un mini langage embarqué dans le langage Rust, mais compilé en même temps que lui.
Il existe par exemple l’extension asm!, qui permet au développeur d’intégrer du code assembleur en ligne, comme le fait le C via le mot-clé dédié __asm__.
#[cfg(target_os = "linux")] fn helloworld() { unsafe { asm!(".pushsection .rodata msg: .asciz \"Hello World!\" .popsection lea msg(%rip), %rdi call puts"); } } fn main() { helloworld(); }
Les extensions error!, warn!, info! et debug! permettent d’ajouter des traces de log, activables et désactivables via une variable d’environnement.
Les extensions de syntaxe offrent des possibilités gigantesques, et cela sans perturber le langage. Il est par exemple prévu d’implémenter une extension de syntaxe pour les expressions régulières, ce qui permettrait d’avoir des regex compilées en même temps que son programme, et donc à la fois optimisée et vérifiées à la compilation !
Enfin, il est possible à un développeur Rust d’écrire ses propres extensions de syntaxe. On appelle cela des macros. Attention, le terme macro se rapproche ici beaucoup plus des macros de Lisp que des macros du C. Les macros permettent de définir sa propre syntaxe, et de spécifier quel sera le code généré à partir de cette syntaxe.
Par exemple, un utilisateur de Rust a écrit sa macro range_type!, qui permet de définir simplement un type numérique restreint à une plage de valeur :
range_type!(Percent(float) = 0.0 .. 1.0) // définit le type Percent, qui est toujours compris entre 0 et 1
Une autre macro est même capable de parser du HTML simple :
let _page = html! ( <html> <head><title>This is the title.</title></head> <body>
This is some text
</body> </html> ); // ceci est du Rust valide !
Créer ses propres macros
Il peut arriver que l’on soit obligé d’écrire beaucoup de code redondant, du style :
match entrée_1 { special_a(x) => { return x; } _ => {} } // ... match entrée_2 { special_b(x) => { return x; } _ => {} }
Le système de macros permet de supprimer le code redondant. Par exemple, le code suivant est équivalent au premier :
// `macro_rules!` pour indiquer qu’on va créer une macro // retour_tôt est le nom de la macro qu’on utilisera pour l’appeler macro_rules! retour_tôt( ($inp:expr $sp:ident) => ( match $inp { $sp(x) => { return x; } _ => {} } ); ) // … retour_tôt!(input_1 special_a); // … retour_tôt!(input_2 special_b);
Plus précisément, les macros permettent de générer du code à la compilation. Ainsi, l’exemple ci-dessus va générer les deux fonctions de départ (strictement les mêmes).
Le $ indique une variable (un peu comme en PHP). Cette syntaxe spéciale permet de différencier le code de la macro et le code Rust en lui-même.
Je ne rentrais pas dans les détails, mais le ($inp:expr $sp:ident), c’est comme la définition des arguments d’une fonction, ça indique le « type » de ce qu’on va donner comme argument. Ici, ça indique que inp est une expression et sp un identifiant de variable (on ne peut donc pas faire n’importe quoi dans les macros).
Mais on peut vraiment faire des choses poussées, plus d’informations sur la documentation.
Modificateurs de visibilités
Si on veut accéder à quelque chose déclaré dans un autre fichier, il faut qu’il soit marqué pub (pour public). Par défaut, tout est privé (on peut l’indiquer en marquant priv).
On remarquera qu’encore une fois, les mots-clés sont concis mais toujours tout à fait compréhensibles (et c’est vachement agréable au quotidien).
pub struct Test { // on peut mettre un indicateur de visibilité sur une struct x: uint, // mais ça n’a pas de sens en Rust pour un attribut y: uint }
impl Test { // ça n’a pas de sens non plus pour une `impl` pub new( … ) -> Test { … } // mais on peut sur des méthodes }
priv trait Truc { // le trait ne sera accessible que dans son fichier fn machin( … ) { … } // la méthode est forcément publique dans un trait }
Outils
Attributs
Les méta-données concernant le code sont passées au compilateur et au générateur de documentation par le biais d’une syntaxe spéciale : les attributs.
#[test] // la fonction est un test unitaire (voir plus bas pour les détails)
#[crate_type = "lib"]; // pour compiler en tant que bibliothèque
#[license = "MIT/ASL2"]; // Le point-virgule indique un attribut global // les autres attributs ne s’appliquent qu’à la déclaration suivante // cet attribut sert à la documentation et aux paquets (voir plus bas)
#[desc = "Projet Machin"]; #[author = "Jean Dupont"]; // autres attributs pour la documentation
#[cfg(target_os = "linux")] // la déclaration qui suit est ignorée si le système sur lequel on compile // n’est pas basé sur Linux.
#[cfg(target_arch = "x86")] // la même chose, mais concernant l’architecture matérielle
Tests unitaires
Pour écrire des tests unitaires, il suffit de placer #[test] sur la ligne précédant une fonction. La fonction ne doit prendre aucun argument et ne rien renvoyer. Si on souhaite que la fonction échoue, il faut mettre en plus #[should_fail].
Les fonctions check, fail, assert (ainsi que assert_eq, assert_approx_eq, etc) sont très utiles pour les tests unitaires.
#[test] fn test_quelques_opérations() { let x = ~[10]; x.push(5); x.pop(); }
#[test] #[should_fail] fn test_l’échec_hors_des_limites() { let v: [int] = []; v[0]; }
Il existe un type de tests unitaires un peu spécial : les benchmarks (tests de performances). Il faut utiliser l’attribut #[bench] mais aussi un peu plus que ça…
#[bench] fn test_trucmuche(b: &mut extra::test::BenchHarness) { // on va utiliser l’argument // le code de préparation do b.iter() { // le code dont vous souhaitez mesurer les performances } }
De la même façon que le code qu’on compile ou non en fonction de la plateforme, il existe un mécanisme similaire pour les tests unitaires. Il faut utiliser #[ignore())], par exemple #[ignore(cfg(target_os = "win32"))].
Ensuite, il faut utiliser rustc avec l’option --test :
- rustc --test main.rs -o tests
Vous pouvez obtenir ça :
# ./tests running 30 tests running driver::tests::mytest1 ... ok running driver::tests::mytest2 ... ignored ... snip ... running driver::tests::mytest30 ... ok
result: ok. 28 passed; 0 failed; 2 ignored
Ou ça :
# ./test running 30 tests running driver::tests::mytest1 ... ok running driver::tests::mytest2 ... ignored ... snip ... running driver::tests::mytest30 ... FAILED
result: FAILED. 27 passed; 1 failed; 2 ignored
Il est possible de placer les tests dans un module et de n’exécuter que ceux-ci :
# ./tests mytest1 running 11 tests running driver::tests::mytest1 ... ok running driver::tests::mytest10 ... ignored ... snip ... running driver::tests::mytest19 ... ok
result: ok. 11 passed; 0 failed; 1 ignored
Pour les tests de performance :
# ./tests --bench running 2 tests test bench_sum_1024_ints ... bench: 709 ns/iter (+/- 82) test initialise_a_vector ... bench: 424 ns/iter (+/- 99) = 19320 MB/s test result: ok. 0 passed; 0 failed; 0 ignored; 2 measured
rustdoc
rustdoc est un outil livré avec Rust qui permet de générer de la documentation à partir des commentaires du code.
Pour qu’un commentaire soit considéré comme de la documentation, il faut utiliser une syntaxe spéciale :
/// Ceci est un commentaire de documentation mono-ligne /** * Ceci est un commentaire de documentation * sur plusieurs lignes */
La syntaxe utilisée pour la mise en forme est le Markdown (comme sur Linuxfr), et la version HTML est générée avec Pandoc.
Il y a quelques règles cependant :
- La première phrase d’un commentaire sera prise comme résumé de la suite du commentaire ;
- Seuls les titres indiqués avec un seul # (et non plusieurs # ou plusieurs = en dessous du texte) sont interprétés par rustdoc. Cette règle sera assouplie dans le futur.
Il y a également quelques conventions :
La première phrase doit décrire succinctement ce que l’élément fait. Si ça n’est pas suffisant, la suite devra décrire quoi et pourquoi l’élément fait ce qu’il fait, les entrées-sorties, et mentionner sous quelles conditions le code va échouer.
On doit utiliser des titres standards quand le texte devient long : « Arguments », « Return value » (valeur renvoyée), « Failure » (échec), « Example », « Safety notes » (notes sur la sûreté), et « Performance notes » (notes sur la performance). Les arguments doivent être écrit de la façon suivante :
# Arguments * `arg1` - pour faire tel truc * `arg2` - pour connaitre telle chose
Enfin, pour écrire du code, on utilise la syntaxe suivante :
~~~~ Mettez votre code ici ~~~~
Les autres façons d’écrire du code en Markdown ne fonctionnent pas (```) ou sont ambigües (quatre espaces devant le code) et peuvent donc ne pas fonctionner.
Pour générer la documentation, rien de plus simple : il suffit d’ajouter #[link(name = "Nom de votre projet")] en en-tête de votre fichier main.rs (ou le fichier principal de votre projet) et d’utiliser la commande rustdoc main.rs. Cela vous donnera une documentation au format HTML.
Mais on peut fournir bien plus d’informations… Je vous laisse jeter un coup d’œil à la configuration utilisée dans la bibliothèque standard de Rust :
#[link(name = "std", vers = "0.9-pre", uuid = "c70c24a7-5551-4f73-8e37-380b11d80be8", url = "https://github.com/mozilla/rust/tree/master/src/libstd")];
#[comment = "The Rust standard library"]; #[license = "MIT/ASL2"]; #[crate_type = "lib"];
#[doc(html_logo_url = "http://www.rust-lang.org/logos/rust-logo-128x128-blk.png", html_favicon_url = "http://www.rust-lang.org/favicon.ico", html_root_url = "http://static.rust-lang.org/doc/master")];
Note : en réalité, la syntaxe de documentation n’est que du sucre syntaxique pour dire #[doc = "Description blablabla"].
rustpkg
rustpkg est un outil qui permet de faire des paquets Rust, largement inspiré du gestionnaire de paquets de Go. On peut donner des informations à cet outil grâce aux attributs (par exemple, #[licence = "ma_licence"] et #[link(vers = "mon_numéro_de_version")]).
Un espace de travail valide contient les dossiers suivants :
- src/, qui contient un dossier par paquet (ex : src/foo/main.rs) ;
- lib/, rustpkg install va y installer les bibliothèques nécessaires dans un sous-dossier (ex : si libbar est nécessaire à foo, alors elle sera installée à lib/x86_64-apple-darwin/libbar-[hash].dylib) ;
- bin/, pour les exécutables (ex : bin/foo) ;
- build/, rustpkg build va y stocker les fichiers temporaires de compilation (ex : build/x86_64-apple-darwin/foo/main.o).
L’ID d’un paquet prend la forme d’une URL (par exemple, github.com/mozilla/rust si c’est un dépôt distant ou /foo/bar/ si c’est un dépôt local). Une version peut être précisée :
- Un tag (ex : github.com/mozilla/rust#0.3). Dans ce cas, rustpkg va vérifier que le dépôt contient bien un tag nommé 0.3 ;
- Une révision particulière (ex : github.com/mozilla/rust#release-0.7). Comme ça n’est pas un nombre décimal, rustpkg passe la refspec (ce qu’il y a après le #) au système de gestion de version sans l’interpréter. Cela compte comme un ID de paquet à part entière, là où une nouvelle version peut satisfaire la dépendance envers * une ancienne version ;
- Une révision particulière (ex : github.com/mozilla/rust#5c4cd30f80). La _refspec est également passée directement au système de gestion de version.
Une fois bien paramétré, on a accès aux commandes rustpkg build, rustpkg clean, rustpkg install, et rustpkg test. Autant dire que ça automatise pas mal de choses !
L’état actuel du projet
Gros travaux avant la 1.0
La version 1 du langage arrive à grands pas, et il reste pas mal de travail. Côté développeur, la syntaxe ne change presque pas mais à chaque version il y a des incompatibilités, heureusement très souvent mineures.
Les développements se focalisent sur les corrections de bugs, l’organisation et le nettoyage du code, mais aussi compléter la bibliothèque standard et améliorer les performances.
Cependant il y a aussi un énorme travail à faire sur la documentation, qui, bien que très complète, nécessite plus d’efforts de mise en page. Les tutoriels nécessitent quant à eux beaucoup plus de travail pour les rendre plus accessibles, complets et simples.
Tester (ou faire des trucs sérieux avec) Rust
Pour l’instant, le seul moyen de tester Rust est d’utiliser l’exécutable de la version 0.8 pour Windows, ou de compiler, au choix, la version 0.8 ou la version en cours de développement (conseillée si vous démarrez un projet un peu important en Rust) pour MacOS.
Si vous êtes sous Ubuntu, vous pouvez ajouter un PPA qui fournit plusieurs versions de Rust dont des nightlies.
Pour Arch Linux, Rust a récemment intégré le dépôt [community]. De plus, un des développeurs de Rust a mis en place un dépôt contenant les compilations quotidiennes de la version de développement. Il suffit d’ajouter :
[thestinger] SigLevel = Optional Server = http://pkgbuild.com/~thestinger/repo/$arch
à votre /etc/pacman.conf et d’installer le paquet rust-git.
Il est aussi possible dans Gentoo de rajouter le paquet en version 0.8 et développement en utilisant l'overlay rust (ajouter dev-lang/rust dans /etc/portage/packages.keywords) :
layman -a rust emerge =dev-lang/rust-0.8
Environnements de développement
Coloration syntaxique
Des configurations pour la coloration syntaxique et l’indentation sont disponibles pour Vim, Emacs, Sublime Text 2 et Kate. Il y a aussi un support pour ctags, Eclipse, NetBeans et autres.
Débugueur
On peut également noter qu’il est possible de configurer Eclipse (ou d’autres IDE) pour débuguer du code Rust via GDB et LLDB.
Projets basés sur Rust
Dans cette partie, je vais vous présenter rapidement une sélection de projets réalisés en Rust, déjà parce qu’il y a beaucoup trop de projets différents et d’autre part parce que tous les projets ne sont pas forcément intéressants ou utilisables. Et j’ai d’autres choses à faire dans la vie aussi. :p
Logiciels
Un certain nombre de logiciels bas niveau ont été créés en Rust, démontrant la polyvalence du langage et l’intérêt du langage dans ce domaine. Plusieurs personnes se sont montrées intéressées dans le remplacement du C par le Rust pour le bas niveau, dans le domaine de l’embarqué par exemple. Il y a aussi quelques jeux vidéo en cours de développement (notamment un émulateur NES) ainsi qu’un traqueur de bug.
zero.rs est un projet de moins de 300 lignes qui permet de lancer des programmes Rust sans système d’exploitation. Nécessite seulement l’accès à quelques fonctions C de base (les tâches, les échecs et le ramasse-miettes ne fonctionnent pas).
Deux projets utilisent zero.rs : Rust.ko, un module minimal pour le noyau Linux et rustboot, un système d’exploitation dont la seule fonctionnalité est de peindre l’écran en rouge — ce qui a beaucoup intéressé les développeurs de GNOME.
Nous avons également sprocketnes, un émulateur NES qui sert de preuve de concept, ne vous attendez pas à ce que vos jeux fonctionnent parfaitement dessus. C’est le genre de programme qui devrait fonctionner avec zero.rs.
Enfin, Evict-BT est un système de suivi de bugs.
Bibliothèques et bindings généralistes
Toujours dans une volonté de ne pas réinventer la roue (ou par flemme ?), on trouve des bindings à la pelle mais peu de bibliothèques en Rust.
Dans les bindings, on a SQLite3, PostGreSQL, MongoDB (écrit par les développeurs de MongoDB eux-mêmes !), Cocoa (le framework objet de Mac OS X), wxWidgets (bibliothèque d’interface graphique basée sur GTK), PCRE (Perl Compatible Regular Expression, expressions régulières compatibles Perl), libpng, OpenCL, etc.
J’ai trouvé deux bibliothèques Rust intéressantes : RustyMem, bibliothèque client pour se connecter à un serveur Memcached et RustyXML, un parseur XML qui fournit une API de type SAX.
Jeux vidéos
L’intérêt de la communauté autour du langage Rust pour faire des jeux vidéo justifie à lui tout seul cette partie ! Il y a déjà pas mal de ressources, et le Rust risque d’être très intéressant dans ce domaine…
Il y a des bindings pour SDL1, SDL2, SFML2, Allegro5, etc. On peut aussi faire de l’OpenGL, on peut utiliser OpenAL et PortAudio.
On a bien sûr quelques bibliothèques Rust, comme kiss3d (moteur graphique 3D simple et stupide — KISS), nphysics (moteur physique temps réel de corps rigides 2 et 3D), cgmath-rs et nalgebra (mathématiques pour les graphismes et la physique temps réel).
Et bien sûr on a des jeux ! On a deux projets de rogue-like, deux moteurs de FPS en développement, un projet de jeu de rôle en 3D, et un jeu de rythme qui est une traduction quasi-directe du C vers le Rust.
Mais je perds mon temps alors que tout cela est mis à jour régulièrement sur le wiki !
Quel avenir pour Rust ?
Le langage D, bien que très prometteur, n’a jamais véritablement percé. Pourquoi en serait-il autrement avec le Rust ?
Le langage D, c’était un langage avec un compilateur officiel au frontal non-libre, qui avait des fuites de mémoire et un développement fermé (au départ, ça se passe sur Github maintenant), une communauté qui dès ses débuts s’est scindée pour développer deux bibliothèques standard incompatibles et un langage dont l’intérêt ne saute pas aux yeux car similaire au C++ à première vue.
Comme vous pouvez le déduire des projets ci-dessus, la communauté derrière Rust est très active. Et c’est sans doute le plus gros point fort du langage pour lui permettre de percer, de décoller rapidement et de ne pas s’essouffler comme le D.
Le développement du langage se déroule entièrement sur Github, au moins 25 commits par jour tous les jours (des fois ça peut atteindre 50 !), des projets sérieux qui se basent sur le langage avec Mozilla derrière, des tas de projets fun aussi, et des dizaines de bindings… Et un langage aussi bon techniquement, sinon meilleur que le D.
Rust se différencie également de Go, le langage de Google. Par exemple, en Go il n’est pas possible de se passer du garbage collector ; à l'inverse en Rust les notions de boîtes/pointeurs sont plus nombreuses. Rust semble plus rapide et plus sûr que Go. Si cela vous intéresse, la FAQ du wiki explique ce que Rust a en plus et cet article montre plutôt les similitudes. Au niveau de la lisibilité, le code Rust est probablement beaucoup plus lisible que le C, C++ ou D (mais c’est peut-être dû en partie à coloration syntaxique différente).
En conclusion, même si on parle assez peu de Rust, il est beaucoup plus connu que le D ne l’était au même stade de développement. De plus, il a vraiment beaucoup d’atouts de son côté : exploitation de fonctionnalités peu connues mais qui existent déjà dans un autre langage (pas d’inconnues et d’expérimentations), haut et bas niveau, beaucoup de vérifications à la compilation, une communauté pour le moment restreinte mais active (avec beaucoup de petits projets mais aussi Servo), etc.
Reste à savoir s’il saura s’imposer dans son domaine de prédilection grâce à sa sûreté (face à C, C++) ou ailleurs grâce à sa vitesse (Java, PHP, Python, Ruby…), car les langages actuels sont très implantés.
La communauté Rust
Il y a déjà quelques endroits où l’on peut discuter du Rust : le sous-Reddit r/rust, la liste de diffusion et les canaux IRC (what else?) #rust et #rust-gamedev sur irc.mozilla.org. Ce sont des canaux très actifs, les personnes présentes sont très sympas et se feront une joie de vous aider. Il y a aussi les canaux #rust-internals et #servo pour les développeurs.
Bref, tout cela ne vous dispense pas d’aller lire le putain de manuel (que vous trouverez sur le site de Rust) !
Contribuer
- faire connaitre le projet ;
- traduire le tutoriel et la documentation de Rust en français. J’ai commencé à traduire le tutoriel en français ;
- écrire un binding pour pouvoir utiliser une bibliothèque sympa depuis Rust ;
- réaliser un projet codé en Rust ;
- écrire du code : compilateur, bibliothèque standard ou outils du projet.
Conclusion
C’est un langage moderne, lisible, performant. Et surtout, il semble avoir un avenir prometteur.
Mais le mieux, c’est de tester par soi-même !
À propos de cette doc
Cette documentation est issue d'une dépêche publiée initialement sur LinuxFR sous le titre Présentation de Rust 0.8. Il a par la suite été enrichi et complété.
Un grand merci à sebcrozet pour ses connaissances sur le fonctionnement de Rust (qui s’est inscrit sur Linuxfr juste pour l’occasion !), à olivierweb et à Olivier Renaud pour leurs innombrables corrections, ainsi qu’à tous les autres contributeurs bien entendu !
@ Retour à la rubrique Développement
Copyright
© 2013 Sinma
Ce document est publié sous licence Creative Commons Attribution, Partage à l'identique 4.0 : https://creativecommons.org/licenses/by-sa/4.0/ |