27 mars 2025
Dès le début, la tâche impliquait le portage de plusieurs projets contenant jusqu'à plusieurs millions de lignes de code. Essentiellement, la spécification technique pour le traducteur se résumait à la phrase "s'assurer que tout cela soit porté et s'exécute correctement en C++". Le travail des responsables de la publication des produits C++ implique la traduction du code, l'exécution des tests, la préparation des paquets de livraison, etc. Les problèmes rencontrés se classent généralement dans l'une des catégories suivantes :
La progression dans cette liste se fait de haut en bas — par exemple, sans résoudre les problèmes de compilation dans le code traduit, il est impossible de vérifier sa fonctionnalité et ses performances. Par conséquent, de nombreux problèmes de longue date n'ont été découverts que lors des dernières étapes du travail sur le projet CodePorting.Translator Cs2Cpp.
Initialement, lors de la correction de simples fuites de mémoire causées par des dépendances circulaires entre objets, nous appliquions l'attribut CppWeakPtr
aux champs, résultant en des champs de type WeakPtr
. Tant que WeakPtr
pouvait être converti en SharedPtr
en appelant la méthode lock()
ou implicitement (ce qui est syntaxiquement plus pratique), cela ne posait pas de problèmes. Cependant, nous avons dû plus tard rendre faibles également les références contenues dans les conteneurs, en utilisant une syntaxe spéciale pour l'attribut CppWeakPtr
, et cela a conduit à quelques surprises désagréables.
Le premier signe de problème avec notre approche adoptée était que, du point de vue C++, MyContainer<SharedPtr<MyClass>>
et MyContainer<WeakPtr<MyClass>>
sont deux types différents. Par conséquent, ils ne peuvent pas être stockés dans la même variable, passés à la même méthode, retournés par celle-ci, etc. L'attribut, initialement destiné uniquement à gérer la manière dont les références sont stockées dans les champs d'objet, a commencé à apparaître dans des contextes de plus en plus étranges, affectant les valeurs de retour, les arguments, les variables locales, etc. Le code du traducteur responsable de sa gestion devenait de plus en plus complexe jour après jour.
Le deuxième problème était également quelque chose que nous n'avions pas anticipé. Pour les programmeurs C#, il s'est avéré naturel d'avoir une seule collection associative par objet, contenant à la fois des références uniques aux objets détenus par l'objet courant et inaccessibles autrement, ainsi que des références aux objets parents. Cela a été fait pour optimiser les opérations de lecture à partir de certains formats de fichiers, mais pour nous, cela signifiait que la même collection pouvait contenir à la fois des références fortes et faibles. Le type de pointeur a cessé d'être le déterminant final de son mode de fonctionnement.
Clairement, ces deux problèmes ne pouvaient pas être résolus dans le paradigme existant, et les types de pointeurs ont été reconsidérés. Le résultat de cette approche révisée a été la classe SmartPtr
, dotée d'une méthode set_Mode()
qui accepte l'une des deux valeurs : SmartPtrMode::Shared
et SmartPtrMode::Weak
. Tous les constructeurs de SmartPtr
acceptent ces mêmes valeurs. Par conséquent, chaque instance de pointeur peut exister dans l'un des deux états suivants :
Le passage d'un mode à l'autre peut se produire à l'exécution et à tout moment. Le compteur de références faibles n'est créé que lorsqu'au moins une référence faible à l'objet existe.
La liste complète des fonctionnalités prises en charge par notre pointeur est la suivante :
intrusive_ptr
: n'importe quel nombre de pointeurs créés pour le même objet partagera un unique compteur de références.->
) : pour accéder à l'objet pointé.La classe SmartPtr
est générique (template) et ne contient aucune méthode virtuelle. Elle est étroitement couplée à la classe System::Object
, qui gère le stockage du compteur de références, et fonctionne exclusivement avec ses classes dérivées.
Il existe des écarts par rapport au comportement typique des pointeurs :
Pour maintenir la compatibilité avec l'ancien code, le type SharedPtr
est devenu un alias pour SmartPtr
. La classe WeakPtr
hérite maintenant de SmartPtr
, n'ajoutant aucun champ, et surcharge simplement les constructeurs pour toujours créer des références faibles.
Les conteneurs sont désormais toujours portés avec la sémantique MyContainer<SmartPtr<MyClass>>
, et le type de références stockées est choisi à l'exécution. Pour les conteneurs écrits manuellement basés sur les structures de données STL (principalement les conteneurs de l'espace de noms System
), le type de référence par défaut est défini à l'aide d'un allocateur personnalisé, tout en permettant toujours de changer le mode pour des éléments individuels du conteneur. Pour les conteneurs traduits, le code nécessaire pour changer le mode de stockage des références est généré par le traducteur.
Les inconvénients de cette solution incluent principalement une réduction des performances lors des opérations de création, de copie et de suppression de pointeurs, car une vérification obligatoire du type de référence est ajoutée au comptage de références habituel. Les chiffres spécifiques dépendent fortement de la structure du test. Des discussions sont actuellement en cours sur la génération de code plus optimal aux endroits où le type de pointeur est garanti de ne pas changer.
Notre méthode de portage nécessite de placer manuellement des attributs dans le code source C# pour marquer où les références doivent être faibles. Le code où ces attributs ne sont pas correctement placés provoquera des fuites de mémoire et, dans certains cas, d'autres erreurs après la traduction. Le code avec des attributs ressemble à ceci :
struct S {
MyClass s; // Référence forte à l'objet
[CppWeakPtr]
MyClass w; // Référence faible à l'objet
MyContainer<MyClass> s_s; // Référence forte à un conteneur de références fortes
[CppWeakPtr]
MyContainer<MyClass> w_s; // Référence faible à un conteneur de références fortes
[CppWeakPtr(0)]
MyContainer<MyClass> s_w; // Référence forte à un conteneur de références faibles
[CppWeakPtr(1)]
Dictionary<MyClass, MyClass> s_s_w; // Référence forte à un conteneur où les clés sont stockées par références fortes et les valeurs par références faibles
[CppWeakPtr, CppWeakPtr(0)]
Dictionary<MyClass, MyClass> w_w_s; // Référence faible à un conteneur où les clés sont stockées par références faibles et les valeurs par références fortes
}
Dans certains cas, il est nécessaire d'appeler manuellement le constructeur d'alias de la classe SmartPtr
ou sa méthode qui définit le type de référence stocké. Nous essayons d'éviter de modifier le code porté, car de telles modifications doivent être réappliquées après chaque exécution du traducteur. Au lieu de cela, nous visons à conserver ce code dans la source C#. Nous avons deux façons d'y parvenir :
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);
}
}
Ici, la méthode data()
dans System::Collections::Generic::Dictionary
retourne une référence à la std::unordered_map
sous-jacente de ce conteneur.