« Rust: the Future of Systems Programming » par Josh Triplett à l’Open Source Technology Summit 2019

Logo officle Rust

Voici un résumé complété de quelques notes d’une vidéo de Josh Triplett, ingénieur système chez Intel, sur l’utilisation de Rust dans ce qu’il appelle la programmation système.

Définition du « systems programming »

Josh Triplett définit le « systems programming » (programmation système) par le développement logiciel de composants tels que : BIOS, firmware, bootloaders, kernel, systèmes d’exploitation, équipements embarqués, machines virtuelles et (surprise) navigateurs webs ! De toute évidence ce dernier point ne fait pas l’unanimité. Selon Josh Triplett, la grande complexité des navigateurs web et leurs nombreuses fonctionnalités et interactions justifient leur intégration dans la définition. Ce n’est pas la 1ère fois que les navigateurs webs sont comparés à de véritables systèmes d’exploitation, d’ailleurs c’est sur cette base que Google a créé son système d’exploitation ChromeOS pour ses ordinateurs Chromebooks.

Le systems programming est autrefois passé de l’assembleur au langage C

Les ingénieurs ont longtemps continué à développer les BIOS, firmwares et bootloaders directement en assembleur tandis que pour les autres logiciels on utilisait déjà des langages de programmation avancés. Aujourd’hui, ils sont quasiment intégralement programmés en C et complétés en assembleur.

Que faut-il pour permettre un changement de langage ?

Un basculement d’un langage de programmation à un autre n’est possible qu’à 2 conditions : des fonctionnalités (features) inédites suffisamment intéressantes dans le nouveau langage, et la parité (parity) c’est-à-dire un nouveau langage au moins aussi « capable » ou autrement dit qui permet de faire tout ce que permet l’ancien langage aussi bien, sinon mieux.

Fonctionnalités intéressantes

Il est important que les apports soient non importants afin de compenser l’effort nécessaire pour changer de langage.
Exemples de fonctionnalités intéressantes : sûreté du typage (type safety), des constructions abstraites (high-level constructs), du code plus lisible, …

Parité du C avec l’assembleur

C peut faire à peu près tout ce que fait l’assembleur, n’est pas sensiblement moins rapide dans la plupart des cas, et permet si besoin d’exécuter directement du code assembleur. Sans cela, il n’aura probablement jamais remplacé l’assembleur en tant que principal langage de programmation système !

C is the new assembly

Aujourd’hui, de nombreux développeurs veulent utiliser d’autres langages que le C principalement pour 2 raisons :
– Les difficultés du C (notamment les buffer overflows ou la gestion de la mémoire mais pas seulement)
– Les fonctionnalités offertes par d’autres langages
La gestion automatique de la mémoire est une obligation incontournable pour tout langage de programmation de plus haut niveau que le C pour la programmation système mais ne suffit pas pour être une alternative intéressante.
La facilité d’utilisation et l’efficacité d’un langage vont de pair avec la sécurité : écrire moins de code permet de limiter les risques d’erreur. De la même manière, automatiser des tâches difficiles et promptes à l’erreur (exemple : la gestion de la mémoire) limite l’erreur humaine.

Hors, la parité est jugée difficile à atteindre par Josh pour la programmation système.

Is Rust the new C?

Rust peut être considéré comme une alternative intéressante au C pour la programmation système selon Josh.

Genèse du Rust

Développé par Mozilla, le langage de programmation Rust a été pensé pour être fiable et très performant pour permettre de construire des systèmes complexes comme un navigateur web.
Il faut savoir que Firefox est programmé dans plus d’une dizaine de langages de programmation ce qui rend très complexe certaines tâches de développement ou de maintenance !

Gestion de la mémoire automatique : la façon Rust

Rust gère automatiquement la mémoire, contrairement au langage C. Rust n’utilise pas de garbage collector (processus automatisé qui libère régulièrement la mémoire qui n’est plus référencée, au détriment des performances) comme les langages Python, C#, Go, Java ni de runtime comme le langage Java (on détaille à la suite les inconvénients d’un runtime).

Rust utilise son concept original de propriété (ownership).
Tout objet dans la mémoire a un propriétaire. La propriété d’un objet a toujours un périmètre donné / une durée de vie ou validité (appelé lifetime). Le propriétaire d’un objet peut être typiquement la variable qui « est » ou pointe vers cet objet, qu’il soit dans la pile ou sur le tas (selon le type d’objet). Si on veut manipuler un objet existant, on peut le faire de 2 manières : en l’empruntant ou en le copiant.

L’emprunt

Il existe 2 types d’emprunt (borrowing) : mutable (modifiable) ou partagée (non mutable). Si on emprunte un objet en mutable, on ne peut pas l’emprunter par ailleurs simultanément (ni en mutable ni en partagé). On peut par contre avoir pour un même objet plusieurs emprunts partagés simultanés (comme le nom l’indique).
Chaque emprunt a un lifetime. Ce concept permet de garantir le partage fiable (safe) de la mémoire entre les variables et fonctions d’un programme via les emprunts.

La copie

Si on copie un objet, on est le propriétaire de la copie. On ne manipule plus l’objet original. Par exemple, copier un integer de 32 bits crée un nouvel integer de 32 bits sur la pile. Lorsqu’un objet est détruit, toute la mémoire est libérée (dans la pile et sur le tas suivant le type d’objet), en général sans impact notable sur la performance à l’exécution.

Au contraire en C, la mémoire est gérée manuellement par les développeurs. Un exemple est donné de scénario de problème de gestion de mémoire : dans le cas de l’usage d’une bibliothèque, la documentation peut préciser qu’une fonction renvoie un nouvel objet qu’il faudra libérer après usage, ou renvoie une référence à une partie d’un objet qu’il ne faudra pas libérer avant d’avoir terminer avec l’objet englobant. Une erreur d’un développeur peut alors mener à des bugs pouvant même affecter la sécurité du logiciel, hors l’erreur est humaine ! Au contraire, Rust s’occupe de ces aspects à la place du développeur.

Absence de runtime en Rust

L’absence de runtime est nécessaire en programmation système pour rivaliser avec le C. Un runtime ne permet pas efficacement la programmation système car il implique d’initialiser certains éléments avant l’exécution de tout code et ne permet pas de maitriser complètement tout le code qui est exécuté.

Programmation concurrente fiable

Rust permet plus facilement qu’avec d’autres langages comme le C de faire de la programmation concurrente fiable (« safe »). Il est utilisé par Firefox Quantum notamment pour mettre en forme les pages web via le CSS de manière fragmentée, en traitant en parallèle différentes parties de la page. C’est une tâche très complexe car elle nécessite de tracer exactement les interdépendances entre les différents éléments de la page. L’article Mozilla plus bas témoigne de plusieurs échecs de Mozilla de développer une telle fonctionnalité avec d’autres langages. Il se trouve par que la gestion automatique de la mémoire de Rust permet également de facilement suivre l’interdépendance des processus, threads et objets du langage.

Quelques enquêtes avec des chiffres intéressants sur Rust et les problèmes de gestion de la mémoire :
– Rust est le langage favori des développeurs depuis 3 ans (version 1.0) selon Stack Overflow
70% des bugs affectant la sécurité des produits Microsoft sont liés à la gestion de la mémoire
– 73.9% des bugs de sécurité du composant de firefox appliquant les feuilles CSS aux pages web n’auraient pas pu exister, le composant fut-il écrit en Rust

La parité du Rust vis-à-vis du C

Pour certains types de développement, Rust n’est pas encore aussi capable que le C.
Voici 3 fonctionnalités de C incontournables que Rust permet et qui les rendent interopérables :
– Les fonctions : Rust peut utiliser les fonctions C ou fournir des fonctions que C peut utiliser
– Structures : Rust permet de construire des structures compatibles avec les structures C et peut manipuler les structures C
– Pointeurs : Rust permet de manipuler les pointeurs directement, comme le C

Rust est donc souvent utilisé par-dessus du C pour construire des structures plus complexes (et compatibles avec le code C existant) comme des APIs.

Josh travaille actuellement à la parité du Rust avec le C. Il a travailler à l’intégration de l »union (fonctionnalité du C) à Rust dans le cadre d’un projet de machines virtuelles en Rust. Il a créé et fait partie d’un working group pour la full parity entre Rust et le C, notamment ses plus communes extensions. Il travaille à la réduction de la taille des binaires produits par le compilateur de Rust, l’intégration de l’assembleur dans le Rust de manière plus fiable que le C, la meilleure maitrise des fonctionnalités offertes par les processeurs sous-jacent : vérifier que certaines fonctionnalités sont disponibles lors de la compilation, utiliser des formats nouveaux comme le bfloat16 (utilisé par exemple en machine-learning par Tensorflow sur des processeurs Intel).

Beaucoup d’autres fonctionnalités sont actuellement en cours de développement pour permettre la full parity, et plus encore.

Notes de fin

Première article sur un langage de programmation, le Rust est plutôt en vogue. Mozilla semble avoir frappé fort, espérons qu’ils continuent à contribuer à l’informatique en développant des technologies fiables et sécurisées !
Merci à Victor pour ses éclaircissements sur le concept de propriété du Rust.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.