Ce qui a bien fonctionné
- La réécriture s’est faite par petites étapes (progressive, stop-and-go), elle fonctionne bien, et le nouveau code est devenu plus facile à lire et à comprendre
- Le fait d’avoir une vue d’ensemble de tout le code a permis d’identifier des opportunités d’optimisation des performances
- Environ 1/3 à 1/2 du code inutilisé a été supprimé. Les langages de programmation modernes comme Rust ou Go détectent mieux le dead code et le signalent aux développeurs
- Plus d’inquiétude concernant les accès hors limites ou les overflow/underflow
- Le framework de tests intégré est très utile
- Ravi de pouvoir supprimer les fichiers CMake
Ce qui a moins bien fonctionné
Il faut toujours traquer les comportements indéfinis
- Lors d’une réécriture progressive de C/C++ vers Rust, il a fallu utiliser beaucoup de pointeurs bruts et de blocs
unsafe{}
- Les règles de Rust s’appliquent aussi dans
unsafe, mais comme le compilateur ne les vérifie pas, il est facile d’introduire des comportements indéfinis
- Dans
unsafe, il est facile de violer la règle « plusieurs pointeurs en lecture seule XOR un pointeur mutable »
- Miri a joué le rôle de sauveur en détectant cela
Miri ne fonctionne pas toujours et il faut encore utiliser Valgrind
- Si l’on utilise des bibliothèques qui contiennent des parties écrites en C ou en assembleur, comme les bibliothèques de chiffrement, Miri ne fonctionne pas
- Il reste beaucoup de code
unsafe que Miri ne vérifie pas
- Certains tests ont dû être exécutés avec
valgrind
Il faut toujours traquer les fuites mémoire
- Un motif courant des API C consiste à allouer de la mémoire dans
MYLIB_init() puis à la libérer dans MYLIB_release(), et il est facile d’oublier d’appeler MYLIB_release
- Les développeurs Rust ont envie de créer des objets wrapper avec RAII, mais dans les tests qui utilisent l’API C, cette approche n’est pas utilisable
- Dans une logique complexe, il est difficile d’appeler systématiquement la fonction de nettoyage. En C, on résout cela avec
goto, mais Rust ne le permet pas
- Le problème a été résolu avec le crate
defer, mais le borrow checker n’apprécie pas vraiment
La cross-compilation ne fonctionne pas toujours
- Comme pour Miri, si l’on utilise des bibliothèques qui comportent des parties implémentées en C ou en assembleur,
cargo build --target=... ne fonctionne pas immédiatement
Cbindgen ne fonctionne pas toujours
- Cbindgen est largement utilisé pour générer des en-têtes C à partir d’une base de code Rust, mais il a des limites ou des bugs
ABI instable
- Des types utiles de la bibliothèque standard comme
Option n’ont pas d’ABI stable, il faut donc les répliquer manuellement avec l’annotation repr(C)
Absence de prise en charge des allocateurs mémoire personnalisés
- De nombreuses bibliothèques C permettent à l’utilisateur de fournir un allocateur à l’exécution. En Rust, on ne peut choisir un allocateur global qu’à la compilation
- Les problèmes de nettoyage des ressources peuvent être résolus avec un allocateur d’arène, mais ce n’est pas idiomatique en Rust et ce n’est pas intégré à la bibliothèque standard
Complexité
- Il faut utiliser des éléments comme
UnsafeCell, RefCell, MaybeUninit ou Pin pour gérer le FFI, ce qui augmente la complexité
- Le Rust pur est déjà complexe, et si l’on ajoute en plus une couche FFI, cela devient une bête
- Certains développeurs ont même refusé de travailler sur cette base de code à cause de la complexité de Rust
Conclusion
- Dans l’ensemble, je suis satisfait de la réécriture en Rust, mais certains aspects ont été décevants, et cela a demandé bien plus d’efforts que prévu
- Rust qui interagit beaucoup avec du C ressemble à un langage complètement différent de Rust pur. Il y a beaucoup de friction et de pièges. Beaucoup de problèmes de C++ que Rust prétend résoudre ne le sont en réalité pas du tout
- Une profonde gratitude aux développeurs de Rust, Miri, cbindgen, etc. Ils ont accompli un travail énorme. Malgré cela, le langage et les outils pour faire beaucoup de FFI C manquent de maturité et donnent presque l’impression d’être avant la v1.0
- Si l’ergonomie de
unsafe, la bibliothèque standard, la documentation, les outils et l’ABI instable s’améliorent à l’avenir, l’expérience pourrait devenir plus agréable
- Microsoft et Google semblent eux aussi avoir ressenti tous ces points, puisqu’ils investissent réellement des fonds dans ce domaine
- Si vous ne connaissez pas encore Rust, mieux vaut utiliser du Rust pur pour un premier projet et éviter les sujets liés au FFI
- Au départ, l’idée était d’envisager Zig ou Odin pour cette réécriture, mais je ne voulais pas utiliser un langage pré-v1.0 dans une base de code de production en entreprise. Maintenant, je me demande si l’expérience aurait vraiment été pire qu’avec Rust. Il semble surtout que le modèle de Rust ne s’accorde pas vraiment avec le modèle de C (ou de C++), et que les frictions soient trop fortes lorsqu’on les utilise ensemble
- Si je devais refaire un travail similaire à l’avenir, j’envisagerais sérieusement Zig. Chaque fois que quelqu’un dira « réécrivez simplement ça en Rust », montrez-lui cet article et demandez-lui si cela lui a fait changer d’avis
12 commentaires
Même si Zig est encore en pré-v1, il permet d’utiliser de nombreuses bibliothèques C, donc c’est plus utilisable qu’on ne le pense. Pour ajouter quelque chose à un projet existant basé sur C, Zig peut être un meilleur choix que Rust.
Quand j’ai commencé à m’intéresser à Rust, au moment même où j’ai vu le mot-clé
unsafe, j’ai eu un drôle de frisson...Je pense que Rust ne peut pas résoudre les problèmes chroniques de C++. C’est moins une question de syntaxe qu’une question de réalité du terrain.
Les raisons sont les suivantes :
Beaucoup trop de systèmes en production utilisent déjà C/C++. Et ils fonctionnent de manière stable. La plupart ne cherchent donc pas particulièrement à les porter vers Rust.
À la base, le matériel n’est pas conçu en partant du principe d’un comptage de références. Dans beaucoup de cas où l’on utilise C/C++, c’est pour contrôler rapidement le matériel, l’OS, les pilotes ou la couche binaire. Or, pour prendre en charge Rust, les développeurs bas niveau doivent malgré tout gérer eux-mêmes le cycle de vie des ressources en utilisant
unsafe, ce qui représente aussi un coût important.Je pense que l’expérience de l’auteur est plus importante que la valeur potentielle du langage ou les discussions théoriques.
J’ai l’impression que, dans les domaines où un langage du niveau de C/C++ est nécessaire, le niveau de gestion des ressources ressemble à quelque chose de trop ambigu pour être remplacé par Rust.
Cet article aussi part d’une mauvaise compréhension de Rust.
À voir le contenu, il s’agit d’une bibliothèque qui doit communiquer fréquemment avec l’extérieur de Rust, et à partir de là, c’est déjà inévitablement sale... À la base, il n’existe pratiquement aucun langage natif qui ne soit pas sale sur ce point, et comme Rust l’encadre de manière sûre au niveau du langage, cet avantage disparaît en grande partie à mesure que les points de contact avec l’extérieur du langage se multiplient.
C’est vrai dans une certaine mesure, mais dans l’environnement de développement du texte original, il était inévitable qu’ils ne soient pas résolus ; à mon avis, le vrai problème est d’avoir abordé Rust comme une solution miracle.
J’ai eu l’impression que cela signifiait : comme on est de toute façon obligé d’utiliser
unsafedans le processus de remplacement progressif de C/C++ par Rust, passer à Rust n’a pas vraiment de sens ; plutôt que de migrer progressivement vers Rust, je choisirais Zig. Mais à quel endroit du texte est-il écrit qu’il s’agit d’une bibliothèque qui doit fréquemment communiquer avec l’extérieur de Rust ?Utiliser la FFI signifie, en soi, communiquer avec l’extérieur de Rust.
Et à en juger par le contenu principal, il ne s’agit pas simplement d’échanger un état ou quelques données simples, mais plutôt d’une interaction complexe entre l’intérieur et l’extérieur.
S’il s’agit de remplacer progressivement une bibliothèque écrite en C par du Rust, l’utilisation de la FFI est-elle inévitable ? Il faudra bien remplacer de petites parties du programme par du Rust et gérer le reste en C via la FFI ; est-ce cela que vous appeliez une communication avec l’extérieur ? Si c’est le cas, je pense qu’il est assez naturel que l’auteur original en vienne à douter de Rust. À moins de réécrire tout le code d’un seul coup, on ne bénéficie pas vraiment des avantages de Rust, d’où la recommandation de Zig, j’imagine.
^-^
Comme les parties explicitement
unsafesont signalées dans le code source, je m’attendais à ce que ce soit utile, puisque l’étendue de l’impact de la FFI peut être entièrement identifiée à partir du point d’entrée du programme, à moins d’utiliser des blocsunsafe. Mais apparemment, cela n’a pas vraiment parlé à l’auteur.Dès le moment où l’on a utilisé la FFI, une conception sûre n’était de toute façon plus envisageable.
Oui, c’est vrai.
C'est ça, oui : ils écrivent fièrement qu'ils en ont mis partout avec
unsafe, et pourtant ça n'a pas été résolu...