« Rust » : différence entre les versions
(Page créée avec « {{En Construction}} ») |
Aucun résumé des modifications |
||
Ligne 1 : | Ligne 1 : | ||
{{En Construction}} | {{En Construction}} | ||
= Présentation de 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, [http://huonw.github.io/isrustfastyet/ 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 [https://github.com/mozilla/servo 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 [https://blog.mozilla.org/blog/2013/04/03/mozilla-and-samsung-collaborate-on-next-generation-web-browser-engine/ 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 [https://github.com/mozilla/servo/wiki/Acid-test-features 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, il sera impossible de provoquer des fuites de mémoire (''memory leaks''), 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 ? | |||
<div class="code"> | |||
fn main() { | |||
println("Hello, world"); | |||
} | |||
</div> | |||
Par convention, les sources Rust ont l’extension <tt>.rs</tt>, se compilent en faisant <tt>rustc hello.rs</tt> et produisent un fichier <tt>hello</tt>. | |||
Vous pouvez aussi directement lancer la commande rust <tt>run hello.rs</tt> 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). | |||
<div class="code"> | |||
// Ceci est un commentaire sur une seule ligne | |||
/* Ceci est un commentaire | |||
sur plusieurs lignes */ | |||
</div> | |||
=== Déclarations de constantes et de variables === | |||
==== Types de base, vec et str ==== | |||
Les déclarations se font avec le mot clef <tt>let</tt>. 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). | |||
<div class="code"> | |||
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) | |||
</div> | |||
On peut aider un peu le compilateur en suffixant les valeurs : | |||
<div class="code"> | |||
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 | |||
</div> | |||
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). | |||
<div class="code"> | |||
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 | |||
</div> | |||
Sinon, on peut simplement donner le type explicitement : | |||
<div class="code"> | |||
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; | |||
</div> | |||
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 [http://static.rust-lang.org/doc/master/tutorial-container.html#containers 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 <tt>const</tt> de C++, qui ne fait qu’interdire les modifications de la valeur au travers de cette variable <tt>const</tt> : il est toujours possible de modifier une structure de données déclarée <tt>const</tt> si on accède à son contenu depuis un pointeur qui n’est lui-même pas marqué <tt>const</tt>. | |||
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é <tt>mut : | |||
<div class="code"> | |||
let mut total = 0; | |||
total += 1; | |||
</div> | |||
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 <tt>const</tt> 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 <tt>static</tt> : | |||
<div class="code"> | |||
static toto: int = 42; | |||
fn main() { | |||
println!("Ma variable statique: {}", toto); | |||
} | |||
</div> | |||
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 <tt>unsafe</tt> : | |||
<div class="code"> | |||
static mut toto: int = 42; | |||
fn main() { | |||
unsafe { | |||
toto = 0; | |||
println!("Ma variable statique: {}", toto); | |||
} | |||
} | |||
</div> | |||
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 [http://static.rust-lang.org/doc/master/std/local_data/index.html 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 [http://fr.wikipedia.org/wiki/CamelCase 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 [https://github.com/mozilla/rust/wiki/Note-style-guide conventions utilisées pour les dépôts concernant Rust]. | |||
=== Afficher du texte === | |||
Point de <tt>System.out.println();</tt> 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 ! | |||
<div class="code"> | |||
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. | |||
</div> | |||
<div class="code"> | |||
// 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); | |||
</div> | |||
Il y a encore bien d’autres choses, mais si vous souhaitez en savoir plus, je vous conseille de vous référer à [http://static.rust-lang.org/doc/0.8/std/fmt/index.html la documentation]. | |||
=== Fonctions === | |||
Une fonction se déclare de la façon suivante : | |||
<div class="code"> | |||
fn ma_fonction(param1: Type, param2: Type) -> TypeDeRetour { | |||
// mon code | |||
} | |||
</div> | |||
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 : | |||
<div class="code"> | |||
fn ma_fonction(param1: Type, param2: Type) -> () { | |||
// mon code | |||
} | |||
fn ma_fonction(param1: Type, param2: Type) { | |||
// mon code | |||
} | |||
</div> | |||
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 <tt>return</tt> à 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 : | |||
<div class="code"> | |||
fn mult(m: int, n: int) -> int { | |||
return m * n | |||
} | |||
fn mult(m: int, n: int) -> int { | |||
m * n | |||
} | |||
</div> | |||
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 <tt>if</tt>/<tt>else</tt> ==== | |||
<div class="code"> | |||
if false { | |||
println("étrange"); | |||
} else if true { | |||
println("bien"); | |||
} else { | |||
println("ni vrai ni faux ?!"); | |||
} | |||
</div> | |||
On peut combiner la possibilité de ne pas utiliser de mot-clé <tt>return</tt> à la puissance du <tt>if</tt>/<tt>else</tt> (ça permet aussi avec <tt>match</tt> que l’on verra plus bas) afin d’éviter quelques lourdeurs : | |||
<div class="code"> | |||
// retourne la valeur absolue | |||
fn abs(x: int) -> uint { | |||
if x > 0 { x } | |||
else { -x } | |||
} | |||
</div> | |||
On peut aussi l’utiliser pour assigner des valeurs : | |||
<div class="code"> | |||
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" }; | |||
</div> | |||
==== <tt>match</tt> : <tt>switch</tt> puissance 1 000 ==== | |||
<tt>match</tt> 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). | |||
<div class="code"> | |||
match mon_nombre { | |||
0 => println("zéro"), | |||
1 | 2 => println("un ou deux"), | |||
3..10 => println("de 3 à 10"), | |||
_ => println("quelque chose d’autre") | |||
} | |||
</div> | |||
Mais <tt>match</tt> est une des ''killer features'' de Rust, car '''un <tt>match</tt> qui ne traite pas toutes les possibilités ne compile pas'''. | |||
<div class="code"> | |||
// 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 => {} | |||
} | |||
</div> | |||
Pour ce qui est des performances, le filtrage par motif peut être assimilé à une <tt>union</tt> 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 <tt>while</tt> ==== | |||
<div class="code"> | |||
let mut nbr_gâteaux = 8; | |||
while nbr_gâteaux > 0 { | |||
nbr_gâteaux -= 1; | |||
} | |||
</div> | |||
==== Boucle <tt>for</tt> ==== | |||
<div class="code"> | |||
// 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 | |||
</div> | |||
==== 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… | |||
<div class="code"> | |||
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 | |||
</div> | |||
Pour plus d’infos, c’est [http://static.rust-lang.org/doc/master/tutorial-container.html#iterators par ici]. | |||
==== Boucle loop, ça c’est nouveau ==== | |||
loop permet de faire des boucles infinies ! En fait, c’est l’équivalent de <tt>while true</tt>, il permet de remplacer <tt>do {} while();</tt> (si on met un <tt>if</tt> qui contient un <tt>break</tt> juste avant la fin de la boucle) tout en étant plus flexible. | |||
<div class="code"> | |||
let mut x = 5u; | |||
loop { | |||
x += x - 3; | |||
if x % 5 == 0 { break; } | |||
println(x.to_str()); | |||
} | |||
</div> | |||
=== Les structures de données === | |||
==== <tt>struct</tt> ==== | |||
Compatible avec les <tt>struct</tt> en C, c’est une structure de données qui permet de regrouper plusieurs variables. | |||
<div class="code"> | |||
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; | |||
</div> | |||
On peut implémenter des méthodes sur des <tt>struct</tt>, ce qui nous donne plus ou moins une classe. | |||
<div class="code"> | |||
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 | |||
} | |||
</div> | |||
<div class="code"> | |||
// 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()); | |||
</div> | |||
On remarquera que certaines méthodes prennent un <tt>self</tt> en premier paramètre. Il s’agit d’un identifiant représentant la structure courante (un peu comme le pointeur <tt>this</tt> 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 <tt>&self</tt>, par mouvement avec <tt>self</tt>, etc). Par exemple dans <tt>mon_magicien.perd_vie(2)</tt>, on aura <tt>self</tt> égal à <tt>mon_magicien</tt>. Une méthode sans paramètre self est une méthode statique. | |||
Remarque : si on crée une instance de structure sans passer par <tt>new</tt>, il est quand même possible d’utiliser les méthodes définies dans le bloc <tt>impl</tt>. En fait, <tt>new</tt> n’est rien d’autre qu’une méthode statique comme les autres qu’on aurait très bien pu appeler <tt>create</tt>, <tt>bob</tt> voire <tt>choux_fleur</tt>. Ça n’a rien à voir avec les constructeurs ou la surcharge de l’opérateur d’allocation <tt>new</tt> en C++. | |||
==== <tt>enum</tt> ==== | |||
Dans son utilisation la plus simple, une enum Rust est comparable à une <tt>enum</tt> de C. Il est également possible d’implémenter des méthodes dessus (un peu comme en Java). | |||
<div class="code"> | |||
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" | |||
} | |||
} | |||
} | |||
</div> | |||
Chaque variante d’un <tt>enum</tt> peut avoir une valeur numérique… comme en C. | |||
<div class="code"> | |||
enum Coleur { | |||
Rouge = 0xff0000, | |||
Vert = 0x00ff00, | |||
Bleu = 0x0000ff | |||
} | |||
</div> | |||
Enfin, un <tt>enum</tt> peut faire des choses beaucoup plus puissantes, car elle permet en réalité de définir des [http://fr.wikipedia.org/wiki/Type_alg%C3%A9brique_de_donn%C3%A9es types algébriques]. | |||
<div class="code"> | |||
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 } | |||
} | |||
</div><div class="code"> | |||
// 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) | |||
} | |||
} | |||
} | |||
</div> | |||
==== Tuples ==== | |||
Des tuples, comme en Python. | |||
<div class="code"> | |||
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); | |||
</div><div class="code"> | |||
// 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 => {} | |||
(_, _) => {} | |||
} | |||
</div> | |||
Là aussi, <tt>match</tt> 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. | |||
<div class="code"> | |||
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) | |||
} | |||
</div> | |||
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 <tt>*</tt>. | |||
Il ne faut pas le confondre avec <tt>type IdBidule = int;</tt> qui crée simplement un alias de type, comme <tt>typedef</tt> en C ou <tt>using</tt> en C++11. | |||
<div class="code"> | |||
// 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. | |||
</div> | |||
C’est très utile pour différencier des données de même type mais qui doivent être utilisées différemment. | |||
<div class="code"> | |||
struct Pouces(int); | |||
struct Centimètres(int); | |||
</div> | |||
==== Type Option ==== | |||
Autre killer feature du Rust, le type <tt>Option</tt> 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 : | |||
<div class="code"> | |||
// 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) | |||
} | |||
</div> | |||
Le compilateur optimise automatiquement certains types <tt>Option</tt> comme <tt>Option<~int></tt> afin qu’ils soient réellement représentés en mémoire par des pointeurs nuls (et non plus une <tt>union</tt> taguée). | |||
Nous n’aborderons pas ici [http://static.rust-lang.org/doc/master/tutorial-conditions.html 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 : | |||
<div class="code"> | |||
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(); | |||
</div> | |||
Mais dans pas mal de cas il faut passer par le type Option que l’on vient de voir : | |||
<div class="code"> | |||
// 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 | |||
</div> | |||
=== 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: | |||
<div class="code"> | |||
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()) | |||
} | |||
} | |||
} | |||
</div> | |||
[http://composition.al/blog/2013/03/02/fizzbuzz-revisited/ 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. | |||
<div class="code"> | |||
// 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 : "); | |||
} | |||
} | |||
</div> | |||
=== 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. | |||
<div class="code"> | |||
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); | |||
</div> | |||
Des fois, il est nécessaire d’indiquer le type : | |||
<div class="code"> | |||
// fonction carré, renvoie le carré du nombre en paramètre | |||
let carré = |x: int| -> uint { (x * x) as uint }; | |||
</div> | |||
On peut aussi faire des clôtures anonymes : | |||
<div class="code"> | |||
let mut max = 0; | |||
[1, 2, 3].map(|x| if *x > max { max = *x }); | |||
</div> | |||
=== Parallélisation === | |||
==== <tt>do spawn</tt> ==== | |||
Pour lancer une nouvelle tâche, il suffit d’écrire <tt>do spawn</tt>, puis de mettre tout ce qui sera exécuter dans la nouvelle tâche entre accolades. | |||
<div class="code"> | |||
// 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()); | |||
} | |||
} | |||
</div> | |||
==== Canal ==== | |||
Pour communiquer entre processus en C, on utilise les tubes (''pipes''). En Rust, on utilise les canaux pour communiquer entre les tâches. | |||
<div class="code"> | |||
// 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 | |||
</div> | |||
Un exemple de la « capture » des variables par la tâche : | |||
<div class="code"> | |||
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()); | |||
} | |||
</div> | |||
==== Canal partagé ==== | |||
Pour lancer plein de tâches, mais tout récupérer au même endroit : | |||
<div class="code"> | |||
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(); | |||
</div> | |||
Notez qu’on peut utiliser les itérateurs pour changer la dernière ligne et rendre notre code beaucoup plus flexible… | |||
<div class="code"> | |||
// Cela fonctionne pour n’importe quel nombre de tâches secondaires | |||
let result = ports.iter().fold(0, |accum, port| accum + port.recv() ); | |||
</div> | |||
==== 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 à <tt>future</tt>. | |||
<div class="code"> | |||
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()) | |||
</div> | |||
=== 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 à <tt>unique_ptr<T></tt> 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). | |||
<div class="code"> | |||
{ | |||
// 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 | |||
</div><div class="code"> | |||
{ | |||
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 | |||
</div> | |||
==== Boite partagée (Managed box) ==== | |||
C’est une boite qui correspond à peu près au <tt>shared_ptr<T></tt> 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. | |||
<div class="code"> | |||
{ | |||
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 | |||
</div> | |||
Rust fait très attention à la mutabilité… | |||
<div class="code"> | |||
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 | |||
</div> | |||
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 : | |||
<div class="code"> | |||
// 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); | |||
</div> | |||
Ça permet aussi de « geler » temporairement une variable : | |||
<div class="code"> | |||
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. | |||
} | |||
</div> | |||
==== 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 <tt>unsafe</tt>. Ils sont principalement utilisés pour [http://static.rust-lang.org/doc/0.8/tutorial-ffi.html FFI] (pour appeler des fonctions d’un code écrit dans un autre langage, [[Rust#interactions-avec-les-autres-langages|voir plus bas]]) ou, rarement, pour des opérations qui nécessitent plus de performance. | |||
Le mot-clé <tt>unsafe</tt> (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 (<tt>*</tt>). 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 <tt>extra</tt> fournie avec le compilateur contient deux autres pointeurs intelligents : <tt>Rc</tt> et <tt>Arc</tt>, 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, [http://static.rust-lang.org/doc/master/rust.html#unsafety 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 : | |||
<div class="code"> | |||
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 | |||
</div> | |||
Cela fonctionne de la même façon pour les <tt>struct</tt> et les méthodes. | |||
<div class="code"> | |||
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(); | |||
</div> | |||
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 <tt>struct</tt>. Ainsi, le code suivant est parfaitement valide : | |||
<div class="code"> | |||
mon_test.x = 4; | |||
mon_test.test(); | |||
</div> | |||
==== 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. | |||
<div class="code"> | |||
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()); | |||
} | |||
</div> | |||
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. | |||
<div class="code"> | |||
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 | |||
} | |||
} | |||
</div> | |||
Ceci ne peut pas compiler étant donné que rien n’indique à l’appelant de la méthode <tt>get_data_ref</tt> que le pointeur qu’il retourne pointe vers l’intérieur de la structure. En effet, lorsqu’on appelle <tt>get_data_ref</tt> de l’extérieur, on a besoin de savoir que le <tt>&f64</tt> retourné n’est valide que tant que <tt>&self</tt> 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 : | |||
<div class="code"> | |||
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 | |||
} | |||
} | |||
</div> | |||
Vous pouvez voir le <tt>'a</tt> (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 <tt>self</tt> tagué avec un <tt>'a</tt>. ». 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 <tt>fn get_data_ref(&self) -> &f64</tt>, on aurait été capable d’écrire cela : | |||
<div class="code"> | |||
/* | |||
* 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()); | |||
} | |||
</div> | |||
Si ceci était autorisé, il est évident que le <tt>pointeur_invalide</tt> est invalide étant donné qu’il pointe sur la pile allouée pour l’appel de fonction <tt>créer_un_pointeur_invalide</tt>. | |||
Voyons comment, en ayant défini <tt>fn get_data_ref<'a>(&'a self) -> &'a f64</tt>, les durées de vie nous aident ici : | |||
<div class="code"> | |||
/* | |||
* 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 ! | |||
} | |||
</div> | |||
Ici, le <tt>'a</tt> permet de suivre pendant combien de temps <tt>donnée</tt> est valide. On ne peut pas retourner le pointeur puisque <tt>référence</tt> est de type <tt>&'a f64</tt> alors que le type de retour de la fonction est <tt>&f64</tt>. 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++ <tt>std::move</tt> 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 <tt>~</tt>), 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 <tt>@</tt> et <tt>@mut</tt> 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. | |||
<div class="code"> | |||
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 | |||
} | |||
</div> | |||
Dans cet exemple, le vecteur <tt>données</tt> n’est jamais copié ! Il est simplement déplacé hors de la variable <tt>toto</tt>. Ceci rend <tt>toto</tt> inutilisable après l’appel à <tt>extraire_données</tt> (et c’est vérifié par le compilateur). Si on souhaite éviter cette sémantique de mouvement, il est nécessaire de passer <tt>Toto</tt> en utilisant un pointeur, et de copier explicitement les <tt>données</tt> avec la méthode <tt>clone</tt> : | |||
<div class="code"> | |||
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 | |||
} | |||
</div> | |||
=== Interactions avec les autres langages === | |||
==== Appeler du code d’un autre langage ==== | |||
[http://static.rust-lang.org/doc/master/std/libc/index.html 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 à [http://static.rust-lang.org/doc/0.8/tutorial-ffi.html 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 [http://static.rust-lang.org/doc/master/tutorial-ffi.html deux-trois petits trucs supplémentaires]. | |||
Par exemple, si on veut faire la [http://fr.wikipedia.org/wiki/fork%20bomb fork bomb] la plus courte en Rust : | |||
<div class="code"> | |||
#[fixed_stack_segment] | |||
fn main() { | |||
loop { // boucle infinie | |||
unsafe { std::libc::fork(); } // le fork, merci la libc | |||
} | |||
} | |||
</div> | |||
Vous remarquerez que c’est quand même compliqué de faire des bêtises en Rust ! | |||
Il y a des discussions en cours pour [https://github.com/mozilla/rust/issues/5853 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 [http://fr.wikipedia.org/wiki/Binding#Binding_de_langage 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 <tt>extern "C" fn foo(…) {}</tt>. | |||
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 <tt>io</tt>. Dans le futur, Rust sera plus facilement utilisable depuis un autre langage, mais cela nécessite du travail. | |||
Si cela vous intéresse, [http://brson.github.io/2013/03/10/embedding-rust-in-ruby/ 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 <tt>interface</tt>s de Java, des <tt>typeclasse</tt>s d’Haskell. | |||
En C++, on pensera plutôt aux classes abstraites et de ce qu’aurait pu être la notion de <tt>concept</tt> en C++1 (qui n’a pas été retenue par le comité de standardisation). Il y a également le système de <tt>template</tt>s 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 <tt>draw</tt> (dessiner en français). Pour cela, on va dans un premier temps créer un <tt>trait</tt>. | |||
<div class="code"> | |||
trait Draw { | |||
fn draw(&self); | |||
} | |||
</div> | |||
Ensuite, tous les objets qui devraient pouvoir être dessinés ont simplement à implémenter le trait <tt>Draw</tt> : | |||
<div class="code"> | |||
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()); | |||
} | |||
} | |||
</div> | |||
Ensuite il devient possible d’écrire des fonctions génériques qui fonctionneront aussi bien pour <tt>A</tt> que pour <tt>B</tt> et toute autre structure implémentant <tt>Draw</tt>. | |||
<div class="code"> | |||
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 ! | |||
} | |||
</div> | |||
Notez-le <tt><T: Draw></tt>. Cela signifie que la fonction <tt>draw_object</tt> accepte n’importe quel type que l’on nomme abstraitement T, et que ce type doit implémenter le trait <tt>Draw</tt>. | |||
Pour manipuler des éléments du type <tt>Draw</tt> lui-même, il est possible d’utiliser l’opérateur <tt>as</tt> pour que le compilateur considère la structure implémentant le trait Draw comme étant de type <tt>~Draw</tt>. On appelle les instances du type <tt>~Draw</tt> (ou <tt>@Draw</tt> et <tt>&Draw</tt>) des ''trait-object'' (des traits vus comme des objets). | |||
<div class="code"> | |||
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); | |||
</div> | |||
===== 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 : | |||
<div class="code"> | |||
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é. | |||
// ... | |||
} | |||
</div> | |||
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 <tt><T: Draw></tt>, et de dispatch dynamique en utilisant un trait-objet <tt>~Draw</tt>. 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 <tt>Rc</tt> 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: <tt>Freeze</tt>, <tt>Send</tt>, <tt>'static</tt> et <tt>Drop</tt>. | |||
=== 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. <tt>rustc</tt> 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 <tt>namespace</tt> de C++. | |||
Mais voyons comment utiliser les définitions contenues dans un fichier dans un autre fichier. | |||
<div class="code"> | |||
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::` | |||
</div> | |||
Si on veut pouvoir utiliser ce que contient le fichier <tt>truc.rs</tt>, on peut importer les noms de fonctions et de variables dans la portée courante. | |||
<div class="code"> | |||
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`. | |||
</div> | |||
Ainsi, si vous voulez utiliser <tt>std::io::stdin().read_line()</tt>, vous pouvez utiliser <tt>use std::io;</tt> pour utiliser directement <tt>io::stdin().read_line()</tt>. Dans la bibliothèque standard, les modules de [http://static.rust-lang.org/doc/master/std/index.html std] sont importés par défaut si utilisés, contrairement à [http://static.rust-lang.org/doc/master/extra/index.html extra]. De plus, certaines méthodes sont déjà importés, comme <tt>std::io::print</tt> et ses dérivées. | |||
Quand nous ne sommes plus dans le fichier principal, les <tt>use</tt> ne marchent plus comme on s’y attend… En effet, les <tt>use</tt> dépendent du fichier dans lequel on est. Si on est dans truc.rs et qu’on souhaite utiliser des choses de <tt>machin.rs</tt>, on fera (dans <tt>truc.rs</tt>) : | |||
<div class="code"> | |||
use self::machin::Machin; // self se réfère au fichier principal de la caisse | |||
mod machin; | |||
</div> | |||
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 <tt>mod</tt> et placer le contenu du module entre accolades : | |||
<div class="code"> | |||
mod foo { | |||
fn foo_foo() {} | |||
mod bar { | |||
fn bar_bar() {} | |||
} | |||
} | |||
</div> | |||
// 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 <tt>nom_de_l_extension!(…)</tt>, où le contenu des parenthèses a une syntaxe spécifique à l’extension. | |||
La bibliothèque standard inclut plusieurs extensions de syntaxe. <tt>println!</tt> est un équivalent au <tt>printf</tt> de C : | |||
<div class="code"> | |||
let answer = 42 | |||
println!("la reponse est {}.", answer); | |||
println!("la reponse est {v}.", v=answer); | |||
</div> | |||
En C, <tt>printf</tt> 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 <tt>println</tt> 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 <tt>asm!</tt>, qui permet au développeur d’intégrer du code assembleur en ligne, comme le fait le C via le mot-clé dédié <tt>__asm__</tt>. | |||
<div class="code"> | |||
#[cfg(target_os = "linux")] | |||
fn helloworld() { | |||
unsafe { | |||
asm!(".pushsection .rodata | |||
msg: .asciz \"Hello World!\" | |||
.popsection | |||
lea msg(%rip), %rdi | |||
call puts"); | |||
} | |||
} | |||
fn main() { | |||
helloworld(); | |||
} | |||
</div> | |||
Les extensions <tt>error!</tt>, <tt>warn!</tt>, <tt>info!</tt> et <tt>debug!</tt> 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 <tt>range_type!</tt>, 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 : | |||
<div class="code"> | |||
let _page = html! ( | |||
<html> | |||
<head><title>This is the title.</title></head> | |||
<body> | |||
<p>This is some text</p> | |||
</body> | |||
</html> | |||
); // ceci est du Rust valide ! | |||
</div> | |||
==== Créer ses propres macros ==== | |||
Il peut arriver que l’on soit obligé d’écrire beaucoup de code redondant, du style : | |||
<div class="code"> | |||
match entrée_1 { | |||
special_a(x) => { return x; } | |||
_ => {} | |||
} | |||
// ... | |||
match entrée_2 { | |||
special_b(x) => { return x; } | |||
_ => {} | |||
} | |||
</div> | |||
Le système de macros permet de supprimer le code redondant. Par exemple, le code suivant est équivalent au premier : | |||
<div class="code"> | |||
// `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); | |||
</div> | |||
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 <tt>$</tt> 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 (<tt>$inp:expr $sp:ident</tt>), 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 <tt>inp</tt> est une expression et <tt>sp</tt> 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 [http://static.rust-lang.org/doc/master/tutorial-macros.html la documentation]. | |||
== Outils == | |||
=== Attributs === |
Version du 2 décembre 2013 à 19:40
Présentation de 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, il sera impossible de provoquer des fuites de mémoire (memory leaks), 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.