22 novembre 2024
Créer un traducteur de code efficace entre des langages comme C# et C++ est une tâche complexe. Le développement de l'outil CodePorting.Translator Cs2Cpp a rencontré de nombreux problèmes en raison des différences de syntaxe, de sémantique et de paradigmes de programmation de ces deux langages. Cet article discutera des principales difficultés que nous avons rencontrées et des moyens possibles pour les surmonter.
Cela concerne, par exemple, les opérateurs using
et yield
:
using (var resource = new Resource())
{
// Utilisation d'une ressource
}
public IEnumerable<int> GetAllNumbers()
{
for (int i = 0; i < int.MaxValue; i++)
{
yield return i;
}
}
Dans de tels cas, il faut soit écrire un code assez complexe pour émuler le comportement du code source, à la fois dans le traducteur et dans la bibliothèque - dans le premier cas, soit refuser de supporter de telles constructions - dans le deuxième.
Par exemple, le code source contient des méthodes génériques virtuelles ou des constructeurs qui utilisent des fonctions virtuelles :
public class A
{
public virtual T GenericMethod<T>(T param)
{
return param;
}
}
public class A
{
public A()
{
VirtualMethod();
}
public virtual void VirtualMethod()
{
}
}
public class B : A
{
public override void VirtualMethod()
{
}
}
Dans de tels cas, nous n'avons d'autre choix que de réécrire le code problématique en termes qui peuvent être traduits en C#. Heureusement, ces cas sont rares et concernent de petits fragments de code.
Cela inclut les ressources, la réflexion, le chargement dynamique des assemblages et l'importation des fonctions :
static void Main()
{
var rm = new ResourceManager("MyApp.Resources", typeof(Program).Assembly);
var value = rm.GetString("MyResource");
}
static void Main()
{
var type = typeof(MyClass);
var method = type.GetMethod("MyMethod");
var result = method.Invoke(null, null);
Console.WriteLine(result);
}
public class MyClass
{
public static string MyMethod()
{
return "Hello, World!";
}
}
static void Main()
{
var assembly = Assembly.Load("MyDynamicAssembly");
var type = assembly.GetType("MyDynamicAssembly.MyClass");
var instance = Activator.CreateInstance(type);
var method = type.GetMethod("MyMethod");
method.Invoke(instance, null);
}
Dans de tels cas, nous devons émuler les mécanismes correspondants. Cela inclut le support des ressources (intégrées dans l'assemblage sous forme de tableaux statiques et lues à travers des implémentations de flux spécialisés) et la réflexion. De toute évidence, nous ne pouvons pas connecter directement les assemblages .NET au code C++ ni importer des fonctions à partir de bibliothèques dynamiques Windows lorsqu'ils sont exécutés sur une autre plateforme, donc ce code doit être coupé ou réécrit.
Dans ce cas, nous implémentons le comportement correspondant, généralement en utilisant des implémentations de bibliothèques tierces dont les licences n'interdisent pas leur utilisation dans un produit commercial.
Dans certains cas, il s'agit de simples erreurs d'implémentation qui sont généralement faciles à corriger. Beaucoup plus grave, c'est lorsque la différence de comportement réside au niveau des sous-systèmes utilisés par le code de la bibliothèque.
Par exemple, de nombreuses de nos bibliothèques utilisent abondamment des classes de la bibliothèque System.Drawing
, construite sur GDI+. Les versions de ces classes que nous avons développées pour C++ utilisent Skia comme moteur graphique. Skia se comporte souvent différemment de GDI+, en particulier sur Linux, et nous devons consacrer des ressources significatives pour obtenir le même rendu. De même, libxml2, sur lequel est basée notre implémentation de System::Xml
, se comporte différemment dans d'autres cas, et nous devons le patcher ou compliquer nos wrappers.
Les programmeurs C# optimisent leur code pour les conditions dans lesquelles il s'exécute. Cependant, de nombreuses structures commencent à fonctionner plus lentement dans un environnement inconnu.
Par exemple, créer un grand nombre de petits objets en C# fonctionne généralement plus rapidement qu'en C++ en raison de différents schémas de gestion de la mémoire (même en considérant la collecte des ordures). Le casting de type dynamique en C++ est également un peu plus lent. Le comptage des références lors de la copie des pointeurs est une autre source de surcharge absente en C#. Enfin, l'utilisation de concepts traduits de C# (énumérateurs) au lieu des concepts optimisés intégrés en C++ (itérateurs) ralentit également les performances du code.
La manière d'éliminer les goulets d'étranglement dépend largement de la situation. Si le code de la bibliothèque peut être relativement facilement optimisé, conserver le comportement des concepts traduits tout en optimisant leurs performances dans un environnement inconnu peut être assez difficile.
Par exemple, les API publiques pourraient avoir des méthodes qui acceptent SharedPtr<Object>
, les conteneurs manquent d'itérateurs, et les méthodes de gestion des flux acceptent System::IO::Stream
au lieu de istream
, ostream
, ou iostream
, et ainsi de suite.
Nous étendons continuellement le traducteur et la bibliothèque pour rendre notre code pratique pour les programmeurs C++. Par exemple, le traducteur peut déjà générer des méthodes begin
-end
et des surcharges qui fonctionnent avec des flux standards.
Les fichiers d'en-tête C++ contiennent des types et des noms de champs privés, ainsi que le code complet des méthodes modèles. Ces informations sont généralement obfusquées lors de la publication des assemblages .NET.
Nous nous efforçons d'exclure les informations inutiles en utilisant des outils tiers et des modes spéciaux du traducteur lui-même, mais ce n'est pas toujours possible. Par exemple, la suppression des champs statiques privés et des méthodes non virtuelles n'affecte pas le fonctionnement du code client; cependant, il est impossible de supprimer ou de renommer des méthodes virtuelles sans perdre de fonctionnalité. Les champs peuvent être renommés, et leurs types peuvent être remplacés par des stubs de la même taille, à condition que les constructeurs et les destructeurs soient exportés à partir du code compilé avec des fichiers d'en-tête complets. En même temps, il est impossible de cacher le code des méthodes modèles publiques.
Les versions des produits pour le langage C++, créées en utilisant notre framework, ont été lancées avec succès pendant de nombreuses années. Initialement, nous avons publié des versions réduites des produits, mais maintenant nous parvenons à maintenir une fonctionnalité beaucoup plus complète.
En même temps, il reste encore beaucoup de place pour des améliorations et des corrections. Cela inclut la prise en charge des constructions syntaxiques et des parties de bibliothèque précédemment omises, ainsi que l'amélioration de la facilité d'utilisation du traducteur.
Outre la résolution des problèmes actuels et les améliorations prévues, nous travaillons sur la migration du traducteur vers l'analyseur de syntaxe moderne Roslyn. Jusqu'à récemment, nous utilisions l'analyseur NRefactory, qui était limité à la prise en charge des versions de C# jusqu'à 5.0. La transition vers Roslyn nous permettra de prendre en charge les constructions modernes du langage C#, telles que :
Enfin, nous prévoyons d'élargir le nombre de langues supportées, tant cibles que sources. L'adaptation des solutions basées sur Roslyn pour lire le code VB sera relativement facile, d'autant plus que les bibliothèques pour C++ et Java sont déjà prêtes. D'autre part, l'approche que nous avons utilisée pour supporter Python est beaucoup plus simple, et de la même manière, d'autres langages de script comme PHP peuvent être pris en charge.