11 mars 2024
Voyons comment notre traducteur convertit les constructions syntaxiques du langage C# en C++. Nous explorerons les spécificités de la traduction et les limitations qui apparaissent au cours de ce processus.
La traduction se fait sur une base par projet. Un projet C# est converti en un ou deux projets C++. Le premier projet est le miroir du projet C#, tandis que le second sert d'application googletest pour exécuter des tests s'ils existent dans le projet original. Un fichier CMakeLists.txt est généré pour chaque projet d'entrée, permettant la création de projets pour la plupart des systèmes de construction.
Habituellement, un fichier .cs correspond à un fichier .h et un fichier .cpp. Typiquement, les définitions de types vont dans les fichiers d'en-tête, tandis que les définitions de méthodes résident dans les fichiers de code source. Cependant, c'est différent pour les types de modèles, où tout le code reste dans les fichiers d'en-tête. Les fichiers d'en-tête contenant au moins une définition publique se retrouvent dans le répertoire include, accessible aux projets dépendants et aux utilisateurs finaux. Les fichiers d'en-tête avec uniquement des définitions internes vont dans le répertoire source.
En plus des fichiers de code obtenus en traduisant le code C# original, le traducteur génère des fichiers supplémentaires contenant du code de service. Les fichiers de configuration avec des entrées spécifiant où trouver les types de ce projet dans les fichiers d'en-tête sont également placés dans le répertoire de sortie. Ces informations sont nécessaires pour gérer les assemblages dépendants. De plus, un journal de traduction complet est stocké dans le répertoire de sortie.
using <typename> = ...
enum class
).System::MulticastDelegate
:public delegate int IntIntDlg(int n);
using IntIntDlg = System::MulticastDelegate<int32_t(int32_t)>;
System.Object
devient explicite.L'ensemble de ces facteurs impose plusieurs limites :
Nous comprenons que l'imitation stricte du comportement du langage C# nécessiterait une approche quelque peu différente. Néanmoins, nous avons choisi cette logique parce qu'elle aligne l'API des bibliothèques converties plus étroitement avec les paradigmes C++. L'exemple ci-dessous illustre ces caractéristiques :
Code C# :
using System;
public class Base
{
public virtual void Foo1()
{ }
public void Bar()
{ }
}
public interface IFoo
{
void Foo1();
void Foo2();
void Foo3();
}
public interface IBar
{
void Bar();
}
public class Child : Base, IFoo, IBar
{
public void Foo2()
{ }
public virtual void Foo3()
{ }
public T Bazz<T>(object o) where T : class
{
if (o is T)
return (T)o;
else
return default(T);
}
}
Fichier d'en-tête C++ :
#pragma once
#include <system/object_ext.h>
#include <system/exceptions.h>
#include <system/default.h>
#include <system/constraints.h>
class Base : public virtual System::Object
{
typedef Base ThisType;
typedef System::Object BaseType;
typedef ::System::BaseTypesInfo<BaseType> ThisTypeBaseTypesInfo;
RTTI_INFO_DECL();
public:
virtual void Foo1();
void Bar();
};
class IFoo : public virtual System::Object
{
typedef IFoo ThisType;
typedef System::Object BaseType;
typedef ::System::BaseTypesInfo<BaseType> ThisTypeBaseTypesInfo;
RTTI_INFO_DECL();
public:
virtual void Foo1() = 0;
virtual void Foo2() = 0;
virtual void Foo3() = 0;
};
class IBar : public virtual System::Object
{
typedef IBar ThisType;
typedef System::Object BaseType;
typedef ::System::BaseTypesInfo<BaseType> ThisTypeBaseTypesInfo;
RTTI_INFO_DECL();
public:
virtual void Bar() = 0;
};
class Child : public Base, public IFoo, public IBar
{
typedef Child ThisType;
typedef Base BaseType;
typedef IFoo BaseType1;
typedef IBar BaseType2;
typedef ::System::BaseTypesInfo<BaseType, BaseType1, BaseType2> ThisTypeBaseTypesInfo;
RTTI_INFO_DECL();
public:
void Foo1() override;
void Bar() override;
void Foo2() override;
void Foo3() override;
template <typename T>
T Bazz(System::SharedPtr<System::Object> o)
{
assert_is_cs_class(T);
if (System::ObjectExt::Is<T>(o))
{
return System::StaticCast<typename T::Pointee_>(o);
}
else
{
return System::Default<T>();
}
}
};
Code source C++ :
#include "Class1.h"
RTTI_INFO_IMPL_HASH(788057553u, ::Base, ThisTypeBaseTypesInfo);
void Base::Foo1()
{
}
void Base::Bar()
{
}
RTTI_INFO_IMPL_HASH(1733877629u, ::IFoo, ThisTypeBaseTypesInfo);
RTTI_INFO_IMPL_HASH(1699913226u, ::IBar, ThisTypeBaseTypesInfo);
RTTI_INFO_IMPL_HASH(3787596220u, ::Child, ThisTypeBaseTypesInfo);
void Child::Foo1()
{
Base::Foo1();
}
void Child::Bar()
{
Base::Bar();
}
void Child::Foo2()
{
}
void Child::Foo3()
{
}
La série d'alias et de macros au début de chaque classe traduite est utilisée pour émuler certains mécanismes C#, principalement GetType
, typeof
et is
. Les codes de hachage du fichier .cpp sont utilisés pour une comparaison efficace des types. Toutes les fonctions implémentant des interfaces sont virtuelles, même si cela diffère du comportement du C#.