Compilateur Rust vers C : le taux de réussite des tests atteint 95,9 %
(fractalfir.github.io)rustc_codegen_clrest un projet qui génère du code Rust vers C et .NET, et se prépare à une présentation à la Rust Week tout en améliorant son taux de réussite aux tests- Le taux de réussite des tests Rust core est passé de 92 % il y a deux mois à 95,9 %, et les quelque 65 tests restants semblent avoir des causes similaires
- Les correctifs récents se sont concentrés sur les intrinsics d’entiers 128 bits, l’arithmétique vérifiée et le subslicing, tandis que la partie .NET peut désormais exécuter 96,3 % des tests Rust core
- Le projet vise une sortie proche du C99 ou de l’ANSI C et l’usage des API POSIX standard afin de cibler davantage de compilateurs C, et a déjà réussi à exécuter des programmes Rust très simples sur certains compilateurs ANSI C
- Des optimisations pour réduire la taille des fichiers lors de la génération de gros volumes de code C, ainsi qu’un refactoring de l’IR interne, sont en cours et posent les bases pour élargir l’exécution de Rust à des plateformes anciennes ou spécialisées
Présentation à la Rust Week et état d’avancement du projet
- Une présentation du projet
rustc_codegen_clrest prévue à la Rust Week organisée à Utrecht, aux Pays-Bas - La présentation cherche un équilibre entre accessibilité pour les débutants et traitement d’un sujet avancé comme la compilation de Rust vers C
- Ces derniers mois, le travail a progressé en parallèle sur les correctifs de tests, la compatibilité avec les compilateurs C, les performances et le refactoring interne
Hausse du taux de réussite des tests Rust core
- Le taux de réussite des tests Rust core est passé de 92 % il y a deux mois à 95,9 %
- Il reste encore environ 65 tests, mais leurs causes semblent similaires, ce qui réduit relativement le champ des correctifs à apporter
- La partie .NET bénéficie elle aussi de ces correctifs et peut désormais exécuter 96,3 % des tests Rust core
Correctifs sur les entiers 128 bits et l’arithmétique vérifiée
- Une grande partie des progrès vient des correctifs sur les intrinsics 128 bits, l’arithmétique vérifiée et le subslicing
- En C, il existe trois intrinsics
popcount:__builtin_popcount,__builtin_popcountlet__builtin_popcountll__builtin_popcountll, contrairement à ce que son nom laisse penser, fonctionne surunsigned long longet non sur__int128_t- Sur x86_64 Linux avec GCC,
unsigned longetunsigned long longfont tous deux 64 bits
- L’implémentation précédente tronquait silencieusement les entiers 128 bits à 64 bits dans l’intrinsic de comptage de bits, produisant ainsi des résultats erronés
- Le
popcount128 bits est émulé en comptant séparément les bits à 1 des 64 bits de poids faible et des 64 bits de poids fort, puis en additionnant les deux résultats - La vérification d’overflow pour la multiplication 128 bits est traitée de manière simple, faute d’avoir trouvé une méthode plus efficace
- si
bn’est pas 0 et que(a * b) / b == a, alors on considère qu’il n’y a pas d’overflow - ce n’est pas une méthode révolutionnaire, mais elle suffit à faire passer quelques tests supplémentaires
- si
Bug de subslicing et intrinsics de fallback
- Le bug de subslicing venait d’un oubli de
sizeof, qui décalait le pointeur de données d’un slice en octets au lieu de le faire par élément - Ce bug ne cassait le comportement que lors d’un subslicing depuis la fin du slice et non depuis le début, une forme surtout utilisée dans le pattern matching, ce qui explique qu’il soit resté longtemps inaperçu
- Pour les byte slices et les string slices, la taille d’un octet et d’une unité de code UTF-8 étant de 1 octet, les tests internes passaient, et le problème n’est apparu que dans la suite complète de tests du compilateur Rust
- Certains intrinsics peuvent utiliser directement l’implémentation de fallback du compilateur Rust au lieu d’être réimplémentés
carrying_mul_adddoit effectuer la multiplication sur un entier deux fois plus large que les entrées- jusqu’à 64 bits, c’est gérable, mais avec des entrées 128 bits il faut des entiers 256 bits
- LLVM prend en charge les entiers 256 bits, contrairement à C et à .NET
- L’implémentation de fallback du compilateur Rust réalise une multiplication et une addition 256 bits à l’aide d’entiers 128 bits, ce qui peut servir de référence pour émuler des opérations 128 bits avec seulement des entiers 64 bits
Prise en charge de davantage de compilateurs C
- Le projet cherche à accroître les chances que du code Rust puisse fonctionner avec davantage de compilateurs C spécialisés
- Il existe déjà des exemples où du code Rust s’exécute sur Game Boy, se compile avec de simples commandes
move, ou tourne avec le Holly C de Temple OS - Il n’est pas possible de prendre directement en charge des compilateurs C propriétaires inaccessibles, mais élargir la compatibilité avec des compilateurs C obscurs augmente les chances que du code Rust puisse aussi tourner dans ces environnements
- Beaucoup de plateformes ne sont pas prises en charge faute de documentation ou d’accessibilité
- Dans le cadre des discussions sur l’écriture d’une partie de Git en Rust, des plateformes propriétaires comme NonStop ne prennent en charge ni Rust, ni LLVM, ni GCC, ce qui peut entraîner une dégradation voire une disparition du support de Git
- Compiler Rust vers C signifie qu’en théorie, Rust pourrait tourner partout où il existe un compilateur C
- il reste toutefois incertain de savoir si tous les problèmes pourront être contournés sur toutes les plateformes
- obtenir légalement un compilateur pour ces plateformes aurait nécessité l’achat d’un serveur, bien au-delà du budget disponible
- il semble donc peu probable que Rust y fonctionne prochainement
Stratégie actuelle de sortie C
- Le plan actuel consiste à générer du code aussi proche que possible d’un C99 conforme au standard ou de l’ANSI C
- Une certaine prise en charge du threading est nécessaire pour l’initialisation thread-local, d’où l’orientation vers l’usage exclusif des API POSIX standard
- Le projet dispose de ses propres implémentations de fallback pour certains intrinsics, et cette liste s’allonge progressivement
- L’exécution de programmes Rust très simples a déjà réussi sur certains compilateurs ANSI C
- L’objectif est d’atteindre un état où même les plateformes difficiles à prendre en charge aujourd’hui pourront être ajoutées assez facilement si le besoin se présente
Petites améliorations de performances et de taille de fichiers
- Pour l’affichage des littéraux entiers, il a été pris en compte que les entiers inférieurs à 2^32 sont aussi courts, voire plus courts, en décimal qu’en hexadécimal
255est plus court d’un octet que0xFF65536est aussi plus court que0xFFFF- à cause du préfixe
0x, la représentation hexadécimale ne devient pas plus compacte avant 2^32
- Dans le cas extrême de la conversion du compilateur Rust complet en C, des fichiers source C allant jusqu’à 1 Go ont déjà été générés, si bien qu’une petite réduction relative de la taille a un impact réel
- La directive
#line, utilisée pour insérer les debug info, a également été rendue plus intelligente- le nom du fichier source n’est inclus que lorsqu’il change
- cela peut réduire fortement la taille des fichiers lors de l’utilisation des debug info
Refactoring interne et nettoyage de l’IR
- Certaines fonctionnalités internes de
rustc_codegen_clront été séparées dans une crate distincte afin d’accélérer les builds incrémentaux - Une migration vers un IR interné plus économe en mémoire est également en cours
- L’ancien IR contenait des rvalue/lvalue atypiques qui se mappaient mal vers C, et les problèmes devenaient plus marqués avec des fonctionnalités Rust comme les types de taille dynamique
- Par exemple, avec un type dynamique personnalisé comme
MyStr,&self.sparaît simple mais implique en réalité la gestion des métadonnées d’un fat pointer- en C99, un compound literal permet de créer un
struct FatPtr_strsur une seule ligne - en ANSI C, il faut placer séparément
dataetmetadans des variables temporaires avant de retourner le résultat
- en C99, un compound literal permet de créer un
- Comme une seule ligne de Rust et de MIR peut se transformer en plusieurs lignes de C, l’ancien IR gérait cela au moyen de scopes internes avec des variables locales temporaires et des sous-instructions
- La nouvelle approche rend l’étape de préparation plus complexe, mais simplifie l’IR dans son ensemble ; une fois les derniers cas difficiles résolus, la fonctionnalité correspondante de l’ancien IR pourra être supprimée
Prochaines étapes
- Le travail sur le projet dure depuis environ 1,5 an, et plus le nombre de bugs diminue, plus il faut de temps pour trouver ceux qui restent
- Un deuxième volet de “Rust panics under the hood” est en préparation afin d’expliquer pas à pas le fonctionnement des panic en Rust
- La seule explication de la génération des panic messages représente déjà dix minutes de contenu, ce qui amène à envisager de scinder l’article en deux
- Un profileur mémoire petit et précis pour Rust est également en cours de développement
- le code représente environ 2K LOC
- le calendrier est serré, mais l’auteur espère écrire à ce sujet dans les prochaines semaines
1 commentaires
Avis sur Hacker News
À l’origine, le projet rustc_codegen_gcc l’avait promis, mais il n’a finalement jamais tenu cette promesse
[0] https://github.com/rust-lang/compiler-team/issues/458
Côté GCC/Clang, ce n’est « que » 80 %
| .NET Core tests | 1764 | 48 | 20 | 96.29% |
| C Core tests | 1712 | 71 | 8 | 95.59% |
Ce hack est un back-end de compilateur Rust. Comme un back-end prend en entrée des instructions propres à la plateforme, le code C non trivial généré ne sera pas portable
Les utilisateurs devront recevoir des sources spécifiques à leur plateforme déjà générées, ou installer le compilateur Rust et ce back-end pour les générer eux-mêmes
extern "c"?« Ma représentation IR .NET se mappe bien vers C, ce qui m’a permis d’ajouter la prise en charge de la compilation de Rust vers C avec 2 à 3 milliers de lignes de code. Je réutilise presque toute la base de code ; le code spécifique à C et à .NET n’existe qu’à la toute dernière étape de la compilation »
Au départ, c’était un back-end .NET, mais il s’est avéré que l’approche pouvait aussi facilement prendre en charge la génération de code C, donc cela a été ajouté. Ce que rustc lui transmet est converti en une représentation intermédiaire (IR) propre au projet, puis traité
Les garanties dynamiques comme les vérifications de bornes peuvent aussi être implémentées sans problème dans le runtime C
unsafeC ne transmet pas toutes les informations qu’il faudrait connaître explicitement pour que ça compile en Rust
https://www.youtube.com/watch?v=1VgptLwP588
unsafeet que le code obtenu n’est généralement pas très idiomatique en RustPar exemple, transformer une machine à états basée sur des variables globales en une machine à états plus idiomatique en Rust, avec quelque chose comme des énumérations nommées, serait très difficile
Avec l’aide d’une IA suffisamment puissante, ce serait peut-être possible, mais l’IA est encore loin de réellement faire ce qu’on attend d’elle, donc il est difficile de dire que c’est prêt. Il faudrait aussi assez de mémoire pour faire tenir l’ensemble des bases de code C et Rust dans la fenêtre de contexte ; dès que le code dépasse une certaine taille, il faudrait vite du matériel très coûteux. Sinon, comme avec beaucoup de LLM d’aide au code, on finit par générer indépendamment du code incompatible d’un morceau à l’autre
Cela dit, si vous voulez étendre un projet C avec Rust ou le réécrire progressivement, https://c2rust.com/ est utilisable dès maintenant
Rust est un langage moderne avec une gestion de paquets, des outils intégrés de build/test simplifiés, beaucoup moins de fardeau historique, et des fonctionnalités et une syntaxe de haut niveau que les gens apprécient vraiment
C est aussi propre, mais les bases de code complexes bénéficient d’un langage moderne qui aide à créer des abstractions robustes tout en conservant la vitesse du C. Sans oublier bien sûr le borrow checker et la sécurité mémoire
Un transpileur Rust→C qui génère du code C suffisamment standard pourrait combler cet écart