De C# à C++ : Comment nous avons automatisé la conversion de projets – Partie 2

Développement

La conception et le développement du traducteur de code C# vers C++ ont été réalisés exclusivement par CodePorting. Cela a nécessité de nombreuses recherches, l'application de plusieurs approches et des tests différents, en fonction du modèle de mémoire et d'autres aspects. Au final, deux solutions ont été choisies. L'une d'entre elles est actuellement utilisée pour les versions C++ des produits Aspose.

Technologies

Il est maintenant temps d'expliquer les technologies que nous utilisons dans le traducteur de code. Le traducteur est une application console écrite en C#, ce qui facilite son intégration dans des scripts effectuant des séquences typiques telles que traduire-compiler-test. Il existe également un composant GUI vous permettant de faire la même chose en cliquant sur les boutons.

L'analyse syntaxique est effectuée par la bibliothèque NRefactory dans la génération obsolète du traducteur et par Roslyn dans la nouvelle version.

Le traducteur utilise plusieurs parcours d'arbres AST pour collecter des informations et générer du code C++ en sortie. Pour le code C++, il n'y a pas de représentation AST créée, à la place, nous gérons le code de sortie sous forme de texte brut.

Il existe de nombreux cas où des informations supplémentaires sont nécessaires pour affiner le traducteur. Ces informations sont transmises via des options et des attributs. Les options s'appliquent à l'ensemble du projet. Elles sont généralement utilisées pour spécifier le nom du macro d'exportation de classe ou les symboles conditionnels C# utilisés lors de l'analyse du code. Les attributs s'appliquent aux types et aux entités et fournissent des informations spécifiques pour celles-ci, par exemple : indiquer quels membres de classe nécessitent des qualificateurs const ou mutable dans le code traduit, ou quelles entités doivent être exclues de la traduction.

Les classes et structures C# sont converties en classes C++. Leurs membres et leur code source sont convertis en analogues les plus proches. Les types et méthodes génériques C# sont mappés sur des modèles C++. Les références C# sont traduites en pointeurs intelligents (partagés ou faibles). Les classes de référence sont définies dans la bibliothèque. D'autres détails internes du traducteur de code seront décrits dans un article séparé.

Ainsi, le projet traduit de C# vers C++ dépend de notre bibliothèque au lieu des bibliothèques .NET :

C# to C++

Pour construire la bibliothèque du traducteur de code et les projets traduits, nous utilisons Cmake. Actuellement, nous prenons en charge les compilateurs VS 2017 et 2019 (Windows), GCC et Clang (Linux).

Comme mentionné précédemment, la plupart de nos implémentations .NET sont de minces adaptateurs sur des bibliothèques tierces, notamment :

  • Skia — support graphique.
  • Botan — fonctions de chiffrement.
  • ICU — gestion des chaînes, des pages de code et des cultures.
  • Libxml2 — opérations XML.
  • PCRE2 — support des expressions régulières.
  • zlib — fonctions de compression.
  • Boost — diverses utilisations.
  • Quelques autres bibliothèques.

Le traducteur et la bibliothèque sont tous deux couverts par de nombreux tests. Les tests de la bibliothèque utilisent le framework GoogleTest. Les tests du traducteur sont principalement écrits avec NUnit/xUnit et sont répartis dans plusieurs catégories, ce qui garantit que :

  • La sortie du traducteur correspond à sa cible sur des données d'entrée spécifiques.
  • La sortie des programmes traduits correspond à leur cible.
  • Les tests NUnit/xUnit des projets d'entrée sont traduits en tests GoogleTest et réussissent.
  • L'API des projets traduits fonctionne correctement en C++.
  • Les options et attributs du traducteur fonctionnent comme prévu.

Nous utilisons GitLab comme système de contrôle de version. Pour l'intégration continue, nous utilisons Jenkins. Les produits traduits sont disponibles sous forme de packages NuGet et d'archives téléchargeables.

Problèmes

Lors de la réalisation de ce projet, nous avons rencontré de nombreux problèmes différents. Certains étaient attendus, tandis que d’autres ont été découverts en cours de route :

  1. Différences de système de types entre .NET et C++.
    En C++, il n'existe aucune substitution pour le type Object, et la plupart des classes de bibliothèque ne disposent pas de RTTI (Run-Time Type Information). Cela rend impossible la mise en correspondance des types .NET avec ceux de la STL (Standard Template Library) en C++.
  2. Les algorithmes de traduction sont complexes.
    De nombreuses subtilités non triviales doivent être découvertes dans le code traduit. Par exemple, en C#, l'ordre de calcul des arguments d'une méthode est défini, tandis qu'en C++, cela peut entraîner un comportement indéfini (UB).
  3. Le dépannage est difficile.
    Déboguer du code traduit nécessite des compétences spécifiques. Des nuances comme celle décrite ci-dessus peuvent avoir un impact crucial sur le fonctionnement d'un programme, produisant des erreurs difficiles à expliquer. D'un autre côté, elles peuvent facilement se transformer en bogues cachés et persister longtemps.
  4. Les systèmes de gestion de la mémoire diffèrent.
    En C++, il n'y a pas de ramasse-miettes (garbage collection). Par conséquent, davantage de ressources sont nécessaires pour que le code traduit se comporte comme l'original.
  5. La discipline est requise pour les développeurs C#
    Les développeurs C# doivent s'habituer aux limitations imposées par le processus de traduction du code. Les raisons de ces limitations sont les suivantes :
    • La version du langage doit être prise en charge par l'analyseur syntaxique du traducteur.
    • Les constructions de code non prises en charge par le traducteur sont interdites (par exemple, yield).
    • Le style du code est limité par la structure du code traduit (par exemple, chaque champ de référence doit être soit une référence faible, soit une référence partagée, tandis que pour le code C# arbitraire, ce n'est pas nécessairement le cas).
    • Le langage C++ impose ses propres restrictions (par exemple, en C#, les variables statiques ne sont pas supprimées avant que tous les threads principaux ne se terminent, tandis qu'en C++, ce n'est pas le cas).
  6. Un grand volume de travail.
    Le sous-ensemble de la bibliothèque .NET utilisé par nos produits est suffisamment vaste, et il faut beaucoup de temps pour implémenter toutes les classes et méthodes.
  7. Exigences spéciales pour les développeurs.
    La nécessité de plonger dans les détails complexes de la plateforme et de travailler avec deux langages de programmation ou plus limite le nombre de candidats disponibles. D'un autre côté, les développeurs intéressés par la théorie des compilateurs ou d'autres disciplines exotiques trouvent facilement leur place dans le projet.
  8. La fragilité du système.
    Bien que nous disposions de milliers de tests et de millions de lignes de code pour tester le traducteur, il arrive parfois que des modifications apportées pour corriger la compilation d'un projet entraînent des problèmes pour un autre projet. Par exemple, cela peut se produire avec des constructions de syntaxe rares et des styles de code spécifiques dans les projets.
  9. Des barrières à l'entrée élevées.
    La plupart des tâches dans le projet de traducteur de code nécessitent une analyse approfondie. En raison du grand nombre de sous-systèmes et de scénarios, chaque nouvelle tâche nécessite de se familiariser avec de nouveaux aspects du projet pendant longtemps.
  10. Problèmes de protection de la propriété intellectuelle.
    Alors qu'il existe de nombreuses solutions prêtes à l'emploi pour obfusquer efficacement le code C#, en C++, de nombreuses informations sont préservées dans les en-têtes de classe. De plus, certaines définitions ne peuvent pas être supprimées des en-têtes publics sans conséquences. La mise en correspondance des classes et méthodes génériques avec des modèles crée une autre vulnérabilité, car elle révèle les algorithmes.

Malgré tout cela, le projet de traducteur de code est très intéressant d'un point de vue technique, et sa complexité académique nous oblige à apprendre constamment de nouvelles choses.

Conclusion

En travaillant sur le projet de traducteur de code, nous avons réussi à mettre en place un système qui résout une tâche académique intéressante de traduction de code. Nous avons organisé des versions mensuelles des bibliothèques Aspose pour le langage avec lequel elles n'étaient pas censées fonctionner.

Il est prévu de publier plus d'articles sur le traducteur de code. Le prochain article expliquera en détail le processus de conversion, y compris comment les constructions concrètes en C# sont mappées sur celles en C++. Un autre article abordera le modèle de gestion de la mémoire.

Nous ferons de notre mieux pour répondre aux questions posées. Si les lecteurs sont intéressés par d'autres aspects du développement du traducteur de code, nous pourrions envisager d'écrire plus d'articles à ce sujet.

Articles liés :