27 März 2025
Von Anfang an bestand die Aufgabe darin, mehrere Projekte mit bis zu mehreren Millionen Codezeilen zu portieren. Im Wesentlichen lief die technische Spezifikation für den Translator auf den Satz hinaus: “Stellen Sie sicher, dass all dies portiert wird und in C++ korrekt läuft”. Die Arbeit derjenigen, die für die Veröffentlichung von C++-Produkten verantwortlich sind, umfasst das Übersetzen des Codes, das Ausführen von Tests, die Vorbereitung von Release-Paketen und so weiter. Die dabei auftretenden Probleme fallen typischerweise in eine von mehreren Kategorien:
Die Abarbeitung dieser Liste erfolgt von oben nach unten – zum Beispiel ist es ohne die Behebung von Kompilierungsproblemen im übersetzten Code unmöglich, dessen Funktionalität und Leistung zu überprüfen. Folglich wurden viele langjährige Probleme erst in den späteren Phasen der Arbeit am Projekt CodePorting.Translator Cs2Cpp entdeckt.
Anfänglich, bei der Behebung einfacher Speicherlecks, die durch zirkuläre Abhängigkeiten zwischen Objekten verursacht wurden, wendeten wir das CppWeakPtr
-Attribut auf Felder an, was zu Feldern vom Typ WeakPtr
führte. Solange WeakPtr
durch Aufruf der lock()
-Methode oder implizit (was syntaktisch bequemer ist) in SharedPtr
konvertiert werden konnte, verursachte dies keine Probleme. Später mussten wir jedoch auch Referenzen, die in Containern enthalten sind, mithilfe einer speziellen Syntax für das CppWeakPtr
-Attribut schwach machen, und dies führte zu einigen unangenehmen Überraschungen.
Das erste Anzeichen für Probleme mit unserem Ansatz war, dass aus C++-Sicht MyContainer<SharedPtr<MyClass>>
und MyContainer<WeakPtr<MyClass>>
zwei verschiedene Typen sind. Folglich können sie nicht in derselben Variablen gespeichert, an dieselbe Methode übergeben, von ihr zurückgegeben werden und so weiter. Das Attribut, das ursprünglich nur dazu gedacht war, die Speicherung von Referenzen in Objektfeldern zu steuern, tauchte in immer seltsameren Kontexten auf und beeinflusste Rückgabewerte, Argumente, lokale Variablen usw. Der Translator-Code, der dafür verantwortlich war, wurde von Tag zu Tag komplexer.
Das zweite Problem war ebenfalls etwas, womit wir nicht gerechnet hatten. Für C#-Programmierer stellte es sich als natürlich heraus, eine einzige assoziative Sammlung pro Objekt zu haben, die sowohl eindeutige Referenzen auf Objekte enthielt, die dem aktuellen Objekt gehören und anderweitig unzugänglich sind, als auch Referenzen auf übergeordnete Objekte. Dies geschah, um Leseoperationen aus bestimmten Dateiformaten zu optimieren, aber für uns bedeutete es, dass dieselbe Sammlung sowohl starke als auch schwache Referenzen enthalten konnte. Der Zeigertyp war nicht mehr der alleinige Bestimmungsfaktor für seinen Betriebsmodus.
Offensichtlich konnten diese beiden Probleme nicht innerhalb des bestehenden Paradigmas gelöst werden, und die Zeigertypen wurden überdacht. Das Ergebnis dieses überarbeiteten Ansatzes war die SmartPtr
-Klasse, die eine set_Mode()
-Methode bietet, die einen von zwei Werten akzeptiert: SmartPtrMode::Shared
und SmartPtrMode::Weak
. Alle SmartPtr
-Konstruktoren akzeptieren dieselben Werte. Folglich kann jede Zeigerinstanz in einem von zwei Zuständen existieren:
Das Umschalten zwischen den Modi kann zur Laufzeit und zu jedem beliebigen Zeitpunkt erfolgen. Der Zähler für schwache Referenzen wird erst erstellt, wenn mindestens eine schwache Referenz auf das Objekt existiert.
Die vollständige Liste der von unserem Zeiger unterstützten Funktionen lautet wie folgt:
intrusive_ptr
-Semantik: Beliebig viele Zeiger, die für dasselbe Objekt erstellt werden, teilen sich einen einzigen Referenzzähler.->
): Für den Zugriff auf das Objekt, auf das gezeigt wird.Die SmartPtr
-Klasse ist als Template implementiert und enthält keine virtuellen Methoden. Sie ist eng mit der System::Object
-Klasse gekoppelt, die die Speicherung des Referenzzählers übernimmt, und arbeitet ausschließlich mit deren abgeleiteten Klassen.
Es gibt Abweichungen vom typischen Zeigerverhalten:
Um die Kompatibilität mit altem Code aufrechtzuerhalten, wurde der Typ SharedPtr
zu einem Alias für SmartPtr
. Die Klasse WeakPtr
erbt nun von SmartPtr
, fügt keine Felder hinzu und überschreibt lediglich die Konstruktoren, um immer schwache Referenzen zu erstellen.
Container werden nun immer mit MyContainer<SmartPtr<MyClass>>
-Semantik portiert, und die Art der gespeicherten Referenzen wird zur Laufzeit ausgewählt. Für Container, die manuell auf Basis von STL-Datenstrukturen geschrieben wurden (hauptsächlich Container aus dem System
-Namespace), wird der Standard-Referenztyp mithilfe eines benutzerdefinierten Allokators festgelegt, wobei der Modus weiterhin für einzelne Containerelemente geändert werden kann. Für übersetzte Container wird der notwendige Code zum Umschalten des Referenzspeichermodus vom Translator generiert.
Die Nachteile dieser Lösung umfassen hauptsächlich eine reduzierte Leistung bei Erstellungs-, Kopier- und Löschoperationen von Zeigern, da zur üblichen Referenzzählung eine obligatorische Überprüfung des Referenztyps hinzugefügt wird. Spezifische Zahlen hängen stark von der Teststruktur ab. Derzeit laufen Diskussionen über das Generieren von optimalerem Code an Stellen, an denen sich der Zeigertyp garantiert nicht ändert.
Unsere Portierungsmethode erfordert das manuelle Platzieren von Attributen im C#-Quellcode, um zu markieren, wo Referenzen schwach sein sollen. Code, in dem diese Attribute nicht korrekt platziert sind, wird nach der Übersetzung Speicherlecks und in einigen Fällen andere Fehler verursachen. Code mit Attributen sieht etwa so aus:
struct S {
MyClass s; // Starke Referenz auf Objekt
[CppWeakPtr]
MyClass w; // Schwache Referenz auf Objekt
MyContainer<MyClass> s_s; // Starke Referenz auf einen Container mit starken Referenzen
[CppWeakPtr]
MyContainer<MyClass> w_s; // Schwache Referenz auf einen Container mit starken Referenzen
[CppWeakPtr(0)]
MyContainer<MyClass> s_w; // Starke Referenz auf einen Container mit schwachen Referenzen
[CppWeakPtr(1)]
Dictionary<MyClass, MyClass> s_s_w; // Starke Referenz auf einen Container, bei dem Schlüssel durch starke und Werte durch schwache Referenzen gespeichert werden
[CppWeakPtr, CppWeakPtr(0)]
Dictionary<MyClass, MyClass> w_w_s; // Schwache Referenz auf einen Container, bei dem Schlüssel durch schwache und Werte durch starke Referenzen gespeichert werden
}
In einigen Fällen ist es notwendig, manuell den Aliasing-Konstruktor der SmartPtr
-Klasse oder deren Methode aufzurufen, die den gespeicherten Referenztyp festlegt. Wir versuchen, das Bearbeiten des portierten Codes zu vermeiden, da solche Änderungen nach jedem Translator-Lauf erneut angewendet werden müssen. Stattdessen zielen wir darauf ab, solchen Code im C#-Quellcode zu belassen. Dafür haben wir zwei Möglichkeiten:
class Service {
public static void SetWeak<T>(T arg) {}
}
class Service {
public:
template <typename T> static void SetWeak(SmartPtr<T> &arg)
{
arg.set_Mode(SmartPtrMode::Weak);
}
};
class MyClass {
private Dictionary<string, object> data;
public void Add(string key, object value)
{
data.Add(key, value);
//CPPCODE: if (key == u"Parent") data->data()[key].set_Mode(SmartPtrMode::Weak);
}
}
Hier gibt die data()
-Methode in System::Collections::Generic::Dictionary
eine Referenz auf die zugrunde liegende std::unordered_map
dieses Containers zurück.