Von C# nach C++: Wie wir die automatisierte Projektumwandlung realisiert haben – Teil 1

Kunden schätzen Aspose-Produkte, die die Manipulation von Protokollen und Dateien in beliebten Formaten ermöglichen. Die meisten von ihnen wurden ursprünglich für .NET entwickelt. Gleichzeitig laufen Geschäftsanwendungen für Dateiformate in verschiedenen Umgebungen. Dieser Artikel beschreibt, wie es uns gelungen ist, die Veröffentlichung von Aspose-Produkten für C++ einzurichten, indem wir einen Rahmen für die Codeübersetzung von C# aus aufgebaut haben. Die Aufrechterhaltung der Funktionalität der .NET-Versionen für diese Produkte war technisch anspruchsvoll.

Wir haben die notwendige Infrastruktur selbst entwickelt, um die Codeübersetzung zwischen Sprachen und die Emulation von .NET-Bibliotheksfunktionen zu ermöglichen. Dadurch haben wir ein Problem gelöst, das normalerweise als akademisch betrachtet wird. Dies ermöglichte es uns, monatlich .NET-Produkte für die C+±Sprache zu veröffentlichen, wobei der Code für jede Version aus dem entsprechenden C#-Code stammt. Darüber hinaus werden die Tests, die den ursprünglichen C#-Code abdeckten, zusammen mit ihm übersetzt, um die Funktionalität der resultierenden Lösung zu überwachen, auf Augenhöhe mit speziell geschriebenen Tests in C++.

Vorgeschichte

Der Erfolg des C#-zu-C+±Codeübersetzers basiert auf den erfolgreichen Erfahrungen, die das CodePorting-Team bei der Einrichtung der automatisierten C#-zu-Java-Codeübersetzung gemacht hat. Der erstellte Rahmen wandelte C#-Klassen in Java-Klassen um und ersetzte dabei systeminterne Bibliotheksaufrufe ordnungsgemäß.

Für den Rahmen wurden verschiedene Ansätze in Betracht gezogen. Die Entwicklung reiner Java-Versionen von Grund auf würde zu viele Ressourcen erfordern. Eine Möglichkeit bestand darin, die Aufrufe vom Java-Code in die .NET-Umgebung zu marshalling, aber dies würde den Satz von Programmierplattformen einschränken, die wir in Zukunft unterstützen könnten. Damals war .NET nur auf Windows vorhanden. Das Marshalling von Aufrufen ist bequem, wenn selten auftretende Aufrufe weit verbreitete Datentypen tragen. Es wird jedoch überwältigend, wenn man mit vielen Objekten und benutzerdefinierten Datentypen arbeitet.

Stattdessen fragten wir uns, wie wir vorhandenen Code vollständig auf eine neue Plattform übersetzen können. Dies war ein aktuelles Problem, da die Code-Migration monatlich und für alle Produkte durchgeführt werden musste, was einen synchronisierten Fluss von ähnlich ausgestatteten Versionen produzierte.

Die Lösung wurde in zwei Teile aufgeteilt:

  • Übersetzer — Anwendung zur Umwandlung der C#-Syntax in die Java-Syntax, wobei .NET-Typen und -Methoden durch ordnungsgemäße Substitutionen aus den Zielbibliotheken ersetzt werden.
  • Bibliothek — Komponente zur Emulation der Teile der .NET-Bibliothek, die nicht ordnungsgemäß auf Java abgebildet werden konnten. Um die Aufgabe zu vereinfachen, konnten die verfügbaren Komponenten von Drittanbietern verwendet werden.

Die folgenden Argumente bestätigten, dass der Plan technisch machbar war:

  1. Die C#- und Java-Sprachen haben eine ähnliche Ideologie. Zumindest, wenn es um die Struktur von Typen und das Modell der Speicherverwaltung geht.
  2. Wir mussten nur die Bibliotheken übersetzen, daher war das Verschieben von GUIs auf eine andere Plattform nicht der Fall.
  3. Die übersetzten Bibliotheken enthielten hauptsächlich Geschäftslogik und Dateioperationen auf niedriger Ebene, wobei die komplexesten Abhängigkeiten System.Net und System.Drawing waren.
  4. Von Anfang an wurden die Bibliotheken entwickelt, um auf einer breiten Palette von .NET-Versionen zu arbeiten (einschließlich Framework, Standard und sogar Xamarin). Daher konnten geringfügige Plattformunterschiede ignoriert werden.

Wir werden nicht weiter auf den C#-zu-Java-Übersetzer eingehen, da dies spezielle Artikel erfordern würde. Zusammenfassend lässt sich sagen, dass die Umwandlung von C#-Produkten in Java dank des erstellten Codeübersetzers zur regelmäßigen Praxis des Unternehmens geworden war. Der Übersetzer hatte sich von einem einfachen regelbasierten Texttransformator zu einem komplizierten Codegenerator entwickelt, der mit der AST-Repräsentation des Quellcodes arbeitet.

Der Erfolg des C#-zu-Java-Übersetzers half uns, den Java-Markt zu betreten, und es wurde die Idee aufgebracht, auch für C++ nach dem gleichen Szenario zu veröffentlichen.

Anforderungen

Um die Veröffentlichung einer C+±Version unserer Produkte zu ermöglichen, musste ein Framework erstellt werden, das es uns ermöglichte, C#-Code in C++ zu übersetzen, zu kompilieren, zu testen und an den Kunden zu senden. Der Code bestand aus einer Reihe von Bibliotheken, von denen jede mehrere Millionen Zeilen Code enthielt. Die Bibliothekskomponente des Codeübersetzers musste Folgendes abdecken:

  1. Emulation der .NET-Umgebung für den übersetzten Code.
  2. Anpassung des übersetzten Codes für C++: Struktur der Typen, Speicherverwaltung usw.
  3. Wechsel vom übersetzten C#-Code-Stil zum C++-Stil, um den Code für Entwickler, die nicht mit .NET-Paradigmen vertraut sind, leichter nutzbar zu machen.

Viele Leser werden sich wahrscheinlich fragen, warum wir keine vorhandenen Lösungen wie das Mono-Projekt in Betracht gezogen haben. Es gab mehrere Gründe dafür:

  1. Dies würde die zweiten und dritten Anforderungen nicht abdecken.
  2. Mono ist in C# implementiert und hängt von seiner Laufzeitumgebung ab.
  3. Die Anpassung von Fremdcode an unsere Bedürfnisse (API, Typsystem, Speicherverwaltungsmodell, Optimierung usw.) würde eine vergleichbare Zeit wie die Erstellung unserer Lösung erfordern.
  4. Unsere Produkte erfordern keine vollständige .NET-Implementierung. Wenn wir eine vollständige Implementierung hätten, wäre es schwer zu unterscheiden, welche Methoden und Klassen wir benötigen und welche nicht. Wir würden viel Zeit damit verbringen, Funktionen zu reparieren, die wir nie verwenden.

Theoretisch könnten wir unseren Übersetzer verwenden, um eine vorhandene Lösung in C++ zu konvertieren. Dies würde jedoch erfordern, dass von Anfang an ein voll funktionsfähiger Übersetzer vorhanden ist, da es unmöglich ist, jeden übersetzten Code ohne eine Systembibliothek zu debuggen. Außerdem würden die Optimierungsprobleme noch wichtiger als für den übersetzten Produktdaten-Code, da Systembibliotheksaufrufe zu Engpässen tendieren.

Kehren wir zu unseren Anforderungen an den Code-Übersetzer zurück. Aufgrund der Unfähigkeit, .NET-Typen auf STL-Typen abzubilden, haben wir uns entschieden, benutzerdefinierte Bibliothekstypen als Ersatz zu verwenden. Die Bibliothek wurde als Satz von Adaptern entwickelt, die es ermöglichen, Funktionen von Drittanbieterbibliotheken über eine .NET-ähnliche API zu nutzen (wie auch in Java).

Da wir die Bibliotheken mit bestehender API übersetzten, war es eine wichtige Anforderung, dass der übersetzte Code innerhalb jeder Kundenanwendung laufen sollte. Daher konnten wir keine Garbage Collection für den übersetzten Code verwenden, da sie die gesamte Anwendung abdecken würde. Stattdessen musste unser Speicherverwaltungsmodell für C+±Entwickler verständlich sein. Die Verwendung von Smart Pointern wurde als Kompromiss gewählt. Wir werden beschreiben, wie es uns gelungen ist, das Speichermodell in einem separaten Artikel zu ändern.

CodePorting hat eine starke Testabdeckungskultur, und die Fähigkeit, die für C#-Code geschriebenen Tests auf C+±Produkte anzuwenden, würde die Fehlersuche erheblich vereinfachen. Der Code-Übersetzer musste auch in der Lage sein, die Tests zu übersetzen.

Anfangs erlaubte die manuelle Korrektur des übersetzten Java-Codes, die Entwicklung und die Produktveröffentlichungen zu beschleunigen. Langfristig erhöhte dies jedoch die Ausgaben, die benötigt wurden, um jede Version für die Veröffentlichung vorzubereiten, da jeder Übersetzungsfehler jedes Mal behoben werden musste, wenn er auftrat. Dies könnte handhabbar sein, indem der resultierende Java-Code mit Patches gefüttert wird, die als Differenz zwischen den Ausgaben des Übersetzers für zwei aufeinanderfolgende C#-Code-Revisionen berechnet werden, anstatt ihn jedes Mal von null zu konvertieren. Dennoch wurde entschieden, die Behebung des C+±Frameworks gegenüber der Behebung des resultierenden Codes zu priorisieren, sodass jeder Übersetzungsfehler nur einmal behoben wurde.

In Verbindung stehende Artikel