27 mars 2025

Portage de code C# vers C++ : L'implémentation de SmartPtr

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 :

  1. Le code ne se traduit pas en C++ - le traducteur se termine par une erreur.
  2. Le code se traduit en C++, mais il ne compile pas.
  3. Le code compile, mais l'édition des liens échoue.
  4. Le code s'édite et s'exécute, mais les tests échouent, ou des plantages à l'exécution se produisent.
  5. Les tests réussissent, mais des problèmes surviennent pendant leur exécution qui ne sont pas directement liés à la fonctionnalité du produit. Exemples : fuites de mémoire, performances médiocres, etc.

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.

Type de référence comme partie de l'état du pointeur

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 :

  1. Référence forte : le compteur de références est encapsulé dans l'objet ;
  2. Référence faible : le compteur de références est externe à l'objet.

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 :

  1. Stockage de références fortes : gestion de la durée de vie des objets via le comptage de références.
  2. Stockage de références faibles pour un objet.
  3. Sémantique intrusive_ptr : n'importe quel nombre de pointeurs créés pour le même objet partagera un unique compteur de références.
  4. Déréférencement et opérateur flèche (->) : pour accéder à l'objet pointé.
  5. Un ensemble complet de constructeurs et d'opérateurs d'affectation.
  6. Séparation de l'objet pointé et de l'objet compté en références (constructeur d'alias) : étant donné que les bibliothèques de nos clients travaillent avec des documents, il est souvent nécessaire qu'un pointeur vers un élément de document maintienne l'ensemble du document en vie.
  7. Un ensemble complet de casts.
  8. Un ensemble complet d'opérations de comparaison.
  9. Affectation et suppression de pointeurs : opèrent sur des types incomplets.
  10. Un ensemble de méthodes pour vérifier et modifier l'état du pointeur : mode alias, mode de stockage de référence, nombre de références de l'objet, etc.

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 :

  1. Le déplacement (constructeur de déplacement, opérateur d'affectation par déplacement) ne change pas l'état complet ; il préserve le type de référence (faible/forte).
  2. L'accès à un objet via une référence faible ne nécessite pas de verrouillage (création d'une référence forte temporaire), car une approche où l'opérateur flèche retourne un objet temporaire dégrade sévèrement les performances pour les références fortes.

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.

Préparation du code pour la traduction

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 :

  1. Nous pouvons déclarer une méthode de service dans le code C# qui ne fait rien, et pendant la traduction, la remplacer par un équivalent écrit manuellement qui effectue l'opération nécessaire :
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);
    }
};
  1. Nous pouvons placer des commentaires formatés spécialement dans le code C#, que le traducteur convertit en code C++ :
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.

Nouvelles connexes

Vidéos associées

Articles liés