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

Les clients apprécient les produits Aspose, qui permettent de manipuler des protocoles et des fichiers de formats populaires. La plupart de ces produits ont été initialement développés pour .NET. Parallèlement, les applications métier pour les formats de fichiers s'exécutent dans différents environnements. Cet article décrira comment nous avons réussi à mettre en place les versions d'Aspose pour C++, en construisant un cadre de traduction de code depuis le C#. Le maintien de la fonctionnalité des versions .NET de ces produits était techniquement complexe.

Nous avons développé l'infrastructure nécessaire nous-mêmes, permettant la traduction de code entre les langages et l'émulation des fonctions de la bibliothèque .NET. Ce faisant, nous avons résolu un problème qui est généralement considéré comme académique. Cela nous a permis de commencer à publier mensuellement des produits .NET pour le langage C++, en obtenant le code de chaque version à partir du code C# correspondant. De plus, les tests qui couvraient le code C# original sont traduits en parallèle, garantissant que la fonctionnalité de la solution résultante est surveillée, au même titre que les tests spécialement écrits en C++.

Contexte

Le succès du traducteur de code C# vers C++ repose sur l'expérience réussie de l'équipe CodePorting lors de la mise en place de la traduction automatisée du code C# vers Java. Le cadre créé transformait les classes C# en classes Java tout en remplaçant correctement les appels de bibliothèque système.

Différentes approches ont été envisagées pour le cadre. Le développement de versions Java pures à partir de zéro aurait nécessité trop de ressources. Une option consistait à effectuer un appel de méthodes depuis le code Java vers l'environnement .NET, mais cela aurait limité les plates-formes de programmation que nous pouvions prendre en charge à l'avenir. À l'époque, .NET était présent uniquement sur Windows. Les appels de méthodes sont pratiques pour les appels peu fréquents utilisant des types de données largement utilisés. Cependant, cela devient complexe lorsqu'il s'agit de travailler avec de nombreux objets et types de données personnalisés.

Au lieu de cela, nous nous sommes demandé comment traduire entièrement le code existant vers une nouvelle plate-forme. C'était un problème d'actualité car la migration du code devait être effectuée mensuellement et pour tous les produits, produisant un flux synchronisé de versions présentant des fonctionnalités similaires.

La solution a été divisée en deux parties :

  • Traducteur : une application pour transformer la syntaxe C# en syntaxe Java, remplaçant les types et méthodes .NET par des substitutions appropriées provenant des bibliothèques du langage cible.
  • Bibliothèque : un composant pour émuler les parties de la bibliothèque .NET qui ne pouvaient pas être mappées correctement en Java. Pour simplifier la tâche, des composants tiers disponibles ont été utilisés.

Les arguments suivants ont confirmé que le plan était techniquement viable :

  1. Les langages C# et Java ont une idéologie similaire. Du moins, en ce qui concerne la structure des types et le modèle de gestion de la mémoire.
  2. Nous devions traduire uniquement les bibliothèques, donc le déplacement des interfaces graphiques vers une plate-forme différente n'était pas nécessaire.
  3. Les bibliothèques traduites contenaient principalement de la logique métier et des opérations de fichiers de bas niveau, avec les dépendances les plus complexes étant System.Net et System.Drawing.
  4. Dès le début, les bibliothèques ont été développées pour fonctionner sur une large gamme de versions .NET (y compris Framework, Standard et même Xamarin). Par conséquent, les différences mineures de plate-forme pouvaient être ignorées.

Nous n'entrerons pas dans les détails du traducteur de C# vers Java, cela nécessiterait des articles dédiés. En résumé, la conversion des produits C# en Java est devenue une pratique courante de l'entreprise, grâce au traducteur de code créé. Le traducteur est passé d'un simple transformateur de texte basé sur des règles à un générateur de code complexe qui fonctionne avec la représentation AST du code source.

Le succès du traducteur de C# vers Java nous a permis de pénétrer le marché Java, et le sujet a été soulevé pour commencer à publier des produits pour C++ en utilisant le même scénario.

Exigences

Pour rendre possible la sortie de la version C++ de nos produits, il était nécessaire de créer un cadre qui nous permettrait de traduire le code C# en C++, de le compiler, de le tester et de l'envoyer au client. Le code se composait de bibliothèques, chacune comportant jusqu'à quelques millions de lignes de code. Le composant Bibliothèque du traducteur de code devait couvrir les points suivants :

  1. Émuler l'environnement .NET pour le code traduit.
  2. Adapter le code traduit pour C++ : structure des types, gestion de la mémoire, etc.
  3. Passer du style de code C# traduit au style C++, afin de faciliter son utilisation pour les développeurs non familiers avec les paradigmes .NET.

De nombreux lecteurs se demanderont probablement pourquoi nous n'avons pas envisagé d'utiliser des solutions existantes, telles que le projet Mono. Plusieurs raisons expliquent notre choix :

  1. Cela ne couvrirait pas les deuxième et troisième exigences.
  2. Mono est implémenté en C# et dépend de son runtime.
  3. Adapter du code tiers à nos besoins (API, système de types, modèle de gestion de la mémoire, optimisation, etc.) nécessiterait autant de temps que la création de notre propre solution.
  4. Nos produits n'exigent pas une implémentation complète de .NET. Cependant, si nous avions une implémentation complète, il serait difficile de distinguer les méthodes et classes dont nous avons besoin de celles dont nous n'avons pas besoin. Nous passerions beaucoup de temps à corriger des fonctionnalités que nous n'utilisons jamais.

Théoriquement, nous pourrions utiliser notre traducteur pour convertir une solution existante en C++. Cependant, cela nécessiterait d'avoir un traducteur pleinement fonctionnel dès le départ, car il est impossible de déboguer un code traduit sans bibliothèque système. De plus, les problèmes d'optimisation deviendraient encore plus essentiels que pour le code des produits traduits, car les appels de bibliothèque système tendent à devenir des goulots d'étranglement.

Revenons à nos exigences pour le traducteur de code. En raison de l'incapacité de faire correspondre les types .NET aux types STL, nous avons décidé d'utiliser des types personnalisés de la bibliothèque comme substitutions. La bibliothèque a été développée sous forme d'adaptateurs permettant l'utilisation des fonctionnalités de bibliothèques tierces via une API similaire à celle de .NET (comme en Java).

Pendant que nous traduisions les bibliothèques avec l'API existante, une exigence importante pour le code traduit était qu'il devait s'exécuter dans n'importe quelle application cliente. Par conséquent, nous ne pouvions pas utiliser la collecte des déchets (garbage collection) pour le code traduit, car cela couvrirait l'ensemble de l'application. À la place, notre modèle de gestion de la mémoire devait être clair pour les développeurs C++. L'utilisation de pointeurs intelligents (smart pointers) a été choisie comme compromis. Nous décrirons comment nous avons réussi à modifier le modèle de mémoire dans un article séparé.

CodePorting a une forte culture de couverture de tests, et la possibilité d'appliquer les tests écrits pour le code C# aux produits C++ simplifierait considérablement le dépannage. Le traducteur de code devait également être capable de traduire les tests.

Initialement, la correction manuelle du code Java traduit a permis d'accélérer le développement et les sorties de produits. Cependant, à long terme, cela a considérablement augmenté les dépenses nécessaires pour préparer chaque version avant la sortie, car chaque erreur de traduction devait être corrigée à chaque apparition. Cela aurait pu être géré en alimentant le code Java résultant avec les correctifs calculés comme la différence entre les sorties du traducteur générées pour deux révisions de code C# consécutives au lieu de le convertir à partir de zéro à chaque fois. Néanmoins, il a été décidé de donner la priorité à la correction du framework C++ plutôt qu'à la correction du code résultant, afin de ne corriger chaque erreur de traduction qu'une seule fois.

Articles liés :