22 November 2024
Erstellen eines effizienten Code-Übersetzers zwischen Sprachen wie C# und C++ ist eine komplexe Aufgabe. Während der Entwicklung des CodePorting.Translator Cs2Cpp-Tools sind wir auf zahlreiche Herausforderungen gestoßen, die mit den Unterschieden in Syntax, Semantik und Programmierparadigmen dieser beiden Sprachen verbunden sind. Dieser Artikel wird die wichtigsten Schwierigkeiten, denen wir begegnet sind, und mögliche Lösungswege diskutieren.
Dies bezieht sich auf Konstrukte wie using
und yield
:
using (var resource = new Resource())
{
// Ressourcennutzung
}
public IEnumerable<int> GetAllNumbers()
{
for (int i = 0; i < int.MaxValue; i++)
{
yield return i;
}
}
In solchen Fällen müssen wir entweder recht komplexen Code schreiben, um das Verhalten des ursprünglichen Codes sowohl im Übersetzer als auch in der Bibliothek zu emulieren - im ersten Fall - oder auf die Unterstützung solcher Konstrukte verzichten - im zweiten Fall.
Zum Beispiel kann der Originalcode virtuelle generische Methoden oder Konstruktoren mit virtuellen Funktionen enthalten:
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()
{
}
}
In solchen Fällen bleibt uns keine andere Wahl, als den problematischen Code in Begriffen umzuschreiben, die eine Konvertierung nach C# ermöglichen. Glücklicherweise sind solche Fälle selten und betreffen normalerweise nur kleine Codefragmente.
Dies umfasst Ressourcen, Reflektion, dynamisches Laden von Assemblies und Funktionsimporte:
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);
}
In solchen Fällen müssen wir die entsprechenden Mechanismen nachahmen. Dies beinhaltet Unterstützung für Ressourcen (eingebettet in die Assembly als statische Arrays und durch spezialisierte Stream-Implementierungen gelesen) und Reflektion. Offensichtlich ist es nicht möglich, .NET-Assemblies direkt mit C++-Code zu verknüpfen oder Funktionen aus dynamischen Windows-Bibliotheken zu importieren, wenn sie auf einer anderen Plattform ausgeführt werden, sodass solcher Code gekürzt oder umgeschrieben werden muss.
In diesem Fall implementieren wir das notwendige Verhalten, meist unter Verwendung von Implementierungen aus Drittanbieter-Bibliotheken, deren Lizenzen die Verwendung in einem kommerziellen Produkt nicht verbieten.
In einigen Fällen handelt es sich um einfache Implementierungsfehler, die normalerweise leicht zu beheben sind. Viel schlimmer ist es jedoch, wenn der Unterschied im Verhalten auf Subsystemebene liegt, die vom Bibliothekscode verwendet wird.
Zum Beispiel verwenden viele unserer Bibliotheken aktiv Klassen aus der System.Drawing
-Bibliothek, die auf GDI+ basiert. Die von uns für C++ entwickelten Versionen dieser Klassen verwenden Skia als Grafik-Engine. Das Verhalten von Skia unterscheidet sich oft von dem von GDI+, insbesondere unter Linux, und das Erreichen einer konsistenten Darstellung erfordert erhebliche Ressourcen. Ebenso verhält sich libxml2, auf dem unsere System::Xml
-Implementierung basiert, in einigen Fällen anders, und wir müssen es patchen oder unsere Wrapper komplizieren.
C#-Programmierer optimieren ihren Code für die Bedingungen, unter denen er ausgeführt wird. Viele Strukturen beginnen jedoch in einer ungewohnten Umgebung langsamer zu laufen.
Beispielsweise funktioniert das Erstellen einer großen Anzahl kleiner Objekte in C# in der Regel schneller als in C++ aufgrund unterschiedlicher Heap-Management-Schemata (auch unter Berücksichtigung der Garbage Collection). Dynamisches Typcasting in C++ ist ebenfalls etwas langsamer. Referenzzählung beim Kopieren von Zeigern ist eine weitere Überkopfquelle, die in C# nicht vorhanden ist. Schließlich verlangsamt auch die Verwendung übersetzter Konzepte aus C# (Enumeratoren) anstelle der eingebauten, optimierten C++-Konzepte (Iteratoren) die Codeleistung.
Die Art und Weise, Engpässe zu beseitigen, hängt weitgehend von der Situation ab. Wenn der Bibliothekscode relativ leicht optimiert werden kann, kann die Beibehaltung des Verhaltens übersetzter Konzepte bei gleichzeitiger Optimierung ihrer Leistung in einer ungewohnten Umgebung ziemlich herausfordernd sein.
Zum Beispiel könnten öffentliche APIs Methoden haben, die SharedPtr<Object>
akzeptieren, Containern fehlen Iteratoren, und Stream-Handling-Methoden akzeptieren System::IO::Stream
anstelle von istream
, ostream
oder iostream
, und so weiter.
Wir erweitern kontinuierlich den Übersetzer und die Bibliothek, um unseren Code für C++-Programmierer bequem zu machen. Beispielsweise kann der Übersetzer bereits begin
-end
-Methoden und Überladungen generieren, die mit Standardstreams arbeiten.
C++-Header-Dateien enthalten Typen und Namen privater Felder sowie den vollständigen Code von Template-Methoden. Diese Informationen werden normalerweise verschleiert, wenn .NET-Assemblies veröffentlicht werden.
Wir bemühen uns, unnötige Informationen mithilfe von Drittanbieter-Tools und speziellen Modi des Übersetzers selbst auszuschließen, aber dies ist nicht immer möglich. Beispielsweise beeinträchtigt das Entfernen privater statischer Felder und nicht-virtueller Methoden nicht die Funktionsweise des Client-Codes; es ist jedoch unmöglich, virtuelle Methoden zu entfernen oder umzubenennen, ohne die Funktionalität zu verlieren. Felder können umbenannt werden, und ihre Typen können durch Stubs gleicher Größe ersetzt werden, sofern Konstruktoren und Destruktoren aus dem Code exportiert werden, der mit vollständigen Header-Dateien kompiliert wurde. Gleichzeitig ist es unmöglich, den Code öffentlicher Template-Methoden zu verbergen.
Die Veröffentlichungen von Produkten für die C++-Sprache, die mit unserem Framework erstellt wurden, wurden seit vielen Jahren erfolgreich gestartet. Anfänglich veröffentlichten wir reduzierte Versionen der Produkte, aber jetzt schaffen wir es, viel vollständigere Funktionalitäten zu erhalten.
Gleichzeitig gibt es immer noch viel Raum für Verbesserungen und Korrekturen. Dies umfasst die Unterstützung bisher ausgelassener syntaktischer Konstrukte und Bibliotheksteile sowie die Verbesserung der Benutzerfreundlichkeit des Übersetzers.
Neben der Lösung aktueller Probleme und geplanter Verbesserungen arbeiten wir daran, den Übersetzer auf den modernen Roslyn Syntax-Analysator zu migrieren. Bis vor kurzem verwendeten wir den NRefactory Analysator, der auf die Unterstützung von C#-Versionen bis 5.0 beschränkt war. Der Wechsel zu Roslyn ermöglicht es uns, moderne C#-Sprachkonstrukte zu unterstützen, wie zum Beispiel:
Schließlich planen wir, die Anzahl der unterstützten Sprachen zu erweitern – sowohl Ziel- als auch Quellsprachen. Die Anpassung von Roslyn-basierten Lösungen für das Lesen von VB-Code wird relativ einfach sein, insbesondere wenn man bedenkt, dass Bibliotheken für C++ und Java bereits bereit sind. Andererseits ist der Ansatz, den wir zur Unterstützung von Python verwendet haben, viel einfacher, und ähnlich können andere Skriptsprachen wie PHP unterstützt werden.