Le développeur connu sur le nom Zack Radisic a voulu tester si le langage de programmation Zig est une meilleure alternative à Rust. Il montre quel point l'écriture non sécurisée de Rust serait difficile en construisant un projet qui nécessiterait une quantité substantielle de code non sécurisé. Ensuite, il a réécrit le projet en Zig pour voir si c'était plus facile ou meilleur. Après avoir terminé les deux versions, il a trouvé que « l'implémentation en Zig était plus sûre, plus rapide et plus facile à écrire. »Notons que Rust est un langage de programmation compilé multiparadigme, conçu par Graydon Hore alors employé chez Mozilla Research. Utilisé par plusieurs grandes entreprises et par de nombreux développeurs dans le monde, Rust est devenu le langage de base pour certaines des fonctionnalités indispensables du navigateur Firefox et de son moteur Gecko, ainsi que pour le moteur Servo de Mozilla.
Avec Rust, il est possible de développer des pilotes de périphériques, des systèmes embarqués, des systèmes d'exploitation, des jeux, des applications web, et bien d'autres choses encore. Des centaines d'entreprises dans le monde entier utilisent Rust en production pour des solutions multiplateformes et économes en ressources. Des logiciels connus et appréciés, comme Firefox, Dropbox, et Cloudflare, utilisent ce langage. De la startup à la multinationale, du système embarqué au service web à haute disponibilité, Rust serait une excellente solution.
Zig est un langage de programmation open source conçu par Andrew Kelley pour garantir les performances telles que la robustesse et la maintenabilité. Andrew a annoncé dans une courte introduction au langage Zig qu’il l’a créé dans le but de concurrencer, voire remplacer à l’avenir, le redoutable langage C dans le cadre de la programmation système. Ainsi, il dit avoir bâti Zig sur quatre principaux aspects afin qu’il soit un langage de programmation pragmatique, optimal, un coffre-fort en matière de sécurité et un langage le plus lisible possible.
« Je ne suis pas si ambitieux et mon objectif est de créer un nouveau langage de programmation qui sera plus pragmatique que le C. C'est comme essayer d'être plus diabolique que le diable lui-même », a écrit Andrew en introduction à la présentation du langage Zig. Lorsqu’il parle de langage plus pragmatique, il fait allusion au fait « que tout ce qui compte à la fin, c’est de savoir si le langage vous a aidé à faire ce que vous tentiez de faire et d’une manière plus simple que les autres langages ».
Zig, une meilleure alternative à Rust ?
Le récupérateur de mémoire est la partie importante, il est difficile de le faire fonctionner et d'être rapide et sûr parce que c'est fondamentalement un problème qui ne joue pas bien avec le vérificateur d'emprunts. Il existe deux façons de le faire de façon sûre en Rust : en utilisant le référence-counting et en utilisant arenas+handles, mais les deux semblent être plus lents que l'approche traditionnelle mark/sweep.
L'implémentation spécifique de l'interpréteur de bytecode est tirée du livre : Crafting Interpreters : Crafting Interpreters. En particulier, il s'agit d'une VM basée sur la pile pour un langage qui supporte les fonctions, les fermetures, les classes/instances, etc.
L'implémentation non sécurisée de Rust
De l'avis de Zack Radisic, l'implémentation non sécurisée de Rust est difficile. Beaucoup plus difficile que le C, il a beaucoup de règles nuancées sur le comportement non défini - grâce au vérificateur d'emprunts - qui rendent facile d'introduire des bogues. En effet, le compilateur effectue des optimisations en supposant que ses règles de propriété soient suivies. « Mais si vous les enfreignez, cela est considéré comme un comportement non défini, et le compilateur continue, appliquant ces mêmes optimisations et transformant potentiellement le code en quelque chose de dangereux. »
Pour rendre les choses encore plus compliquées, « Rust ne sait pas exactement quel comportement est considéré comme non défini », soutient Zack Radisic. Vous pouvez donc écrire du code qui présente un comportement non défini sans même le savoir.
Une façon d'y remédier est d'utiliser Miri, un interprète pour la représentation intermédiaire de Rust qui peut détecter les comportements non définis. Plus précisément, Miri est un interpréteur expérimental pour la représentation intermédiaire de niveau moyen (MIR) de Rust. Il peut exécuter les binaires et les suites de tests des projets cargo et détecter certaines classes de comportements non définis. « La source la plus difficile de comportement non défini que j'ai rencontré était liée aux règles d'aliasing de Rust », déclare Zack Radisic.
Comme mentionné précédemment, Rust utilise ses règles d'emprunt et de propriété pour optimiser le compilateur. Si vous enfreignez ces règles, vous obtenez un comportement non défini. L'astuce consiste à utiliser des pointeurs. Ils n'ont pas les mêmes contraintes d'emprunt que les références Rust classiques, ce qui permet de contourner le vérificateur d'emprunt.
Par exemple, vous pouvez avoir autant de pointeurs bruts mutables (*mut T) ou immuables (*const T) que vous le souhaitez.
Si vous transformez un pointeur brut en référence (μt T ou &T), vous devez vous assurer qu'il respecte les règles de ce type de référence pendant toute sa durée de vie. Par exemple, une référence mutable ne peut pas exister tant que d'autres références mutables/immuables existent également.
| Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | fn do_something(value: *mut Foo) {
// Turn the raw pointer into a mutable reference
let value_mut_ref: &mut Foo = value.as_mut().unwrap();
// If I create another ref (mutable or immutable) while the above ref
// is alive, that's undefined behaviour!!
// Undefined behaviour!
let value_ref: &Foo = value.as_ref().unwrap();
} |
Il serait très facile d'enfreindre cette règle. Vous pouvez faire une référence mutable à certaines données, appeler quelques fonctions, et ensuite, 10 couches plus loin dans la pile d'appels, une fonction peut faire une référence immuable à ces mêmes données, et provoque un comportement indéfini. Le problème est que les pointeurs bruts n'ont pas la même ergonomie que les références. Tout d'abord, vous ne pouvez pas avoir de fonctions associées qui prennent le self comme pointeur brut :
| Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | struct Class {
/* fields... */
}
impl Class {
// Regular associated function
fn clear_methods(&mut self) {
/* ... */
}
fn clear_methods_raw(class: *mut Class) {
/* ... */
}
}
unsafe fn test(class: *mut Class) {
let class_mut_ref: &mut Class = class.as_mut().unwrap();
// This syntax is nice and ergonomic
class_mut_ref.clear_methods();
// But with raw pointers you'll have to just call the function like in C
Class::clear_methods_raw(class);
} |
Ensuite, il n'y a pas de syntaxe de déréférencement de pointeur comme ptr->field en C....
La fin de cet article est réservée aux abonnés. Soutenez le Club Developpez.com en prenant un abonnement pour que nous puissions continuer à vous proposer des publications.