Regeln für die Übersetzung von Code von C# nach C++: Grundlagen

Lassen Sie uns darüber sprechen, wie unser Übersetzer syntaktische Konstrukte aus der C#-Sprache in C++ umwandelt. Wir werden die spezifischen Übersetzungsregeln und die auftretenden Einschränkungen erkunden.

Projekte und Kompilationseinheiten

Die Übersetzung erfolgt pro Projekt. Ein C#-Projekt wird in ein oder zwei C++-Projekte umgewandelt. Das erste Projekt spiegelt das C#-Projekt wider, während das zweite als googletest-Anwendung für die Ausführung von Tests dient, sofern sie im ursprünglichen Projekt vorhanden sind. Für jedes Eingangsprojekt wird eine CMakeLists.txt-Datei generiert, die die Erstellung von Projekten für die meisten Build-Systeme ermöglicht.

Normalerweise entspricht eine .cs-Datei einer .h-Datei und einer .cpp-Datei. Typdefinitionen werden in der Regel in Headerdateien platziert, während Methodendefinitionen in Quelldateien verbleiben. Bei Vorlagetypen ist dies jedoch anders, hier bleibt der gesamte Code in Headerdateien. Headerdateien mit mindestens einer öffentlichen Definition landen im include-Verzeichnis und sind für abhängige Projekte und Endbenutzer zugänglich. Headerdateien mit nur internen Definitionen werden in das source-Verzeichnis verschoben.

Neben den aus der ursprünglichen C#-Codeübersetzung gewonnenen Code-Dateien generiert der Übersetzer zusätzliche Dateien mit Servicecode. Konfigurationsdateien mit Einträgen, die angeben, wo im Headerdateien die Typen dieses Projekts zu finden sind, werden ebenfalls im Ausgabeverzeichnis platziert. Diese Informationen sind für die Verarbeitung abhängiger Assemblys erforderlich. Zusätzlich wird ein umfassendes Übersetzungsprotokoll im Ausgabeverzeichnis gespeichert.

Allgemeine Struktur des Quellcodes

  1. C#-Namensräume werden auf C++-Namensräume abgebildet. Die Verwendung von Namensraumoperatoren wird in ihre C++-Äquivalente umgewandelt.
  2. Kommentare werden unverändert übertragen, mit Ausnahme der Typ- und Methodendokumentation, die separat behandelt wird.
  3. Formatierung wird teilweise beibehalten.
  4. Präprozessor-Direktiven werden nicht übertragen, da alle Konstanten während des Aufbaus des Syntaxbaums definiert sein müssen.
  5. Jede Datei beginnt mit einer Liste der eingebundenen Dateien, gefolgt von einer Liste der Vorwärtsdeklarationen von Typen. Diese Listen werden basierend auf den im aktuellen File erwähnten Typen generiert, um die Anzahl der Einbindungen so gering wie möglich zu halten.
  6. Typ-Metadaten werden als spezielle Datenstrukturen generiert, die zur Laufzeit zugänglich sind. Da bedingungslose Metadatengenerierung die Größe der kompilierten Bibliotheken erheblich erhöht, wird sie bei Bedarf manuell für bestimmte Typen aktiviert.

Typdefinitionen

  1. Typ-Aliase werden mit der Syntax using <typename> = ... übersetzt.
  2. C#-Aufzählungen werden auf C++14-Aufzählungen abgebildet (unter Verwendung der Syntax enum class).
  3. Delegaten werden in Aliase für Spezialisierungen der Klasse System::MulticastDelegate umgewandelt:
public delegate int IntIntDlg(int n);
using IntIntDlg = System::MulticastDelegate<int32_t(int32_t)>;
  1. C#-Klassen und Strukturen werden als C++-Klassen dargestellt. Schnittstellen werden zu abstrakten Klassen. Die Vererbungsstruktur spiegelt die von C# wider, und die implizite Vererbung von System.Object wird explizit gemacht.
  2. Eigenschaften und Indexer werden in separate Methoden für Getter und Setter aufgeteilt.
  3. Virtuelle Funktionen in C# entsprechen virtuellen Funktionen in C++. Die Implementierung von Schnittstellen erfolgt ebenfalls über den Mechanismus virtueller Funktionen.
  4. Generische Typen und Methoden werden in C++-Templates umgewandelt.
  5. Finalizer werden in Destruktoren konvertiert.

Einschränkungen

All diese Faktoren zusammen führen zu mehreren Einschränkungen:

  1. Die Übersetzung von virtuellen generischen Methoden wird nicht unterstützt.
  2. Implementierungen von Schnittstellenmethoden sind virtuell, auch wenn sie im ursprünglichen C#-Code nicht so waren.
  3. Das Einführen neuer Methoden mit denselben Namen und Signaturen wie vorhandene virtuelle und/oder Schnittstellenmethoden ist nicht möglich. Der Übersetzer ermöglicht jedoch das Umbenennen solcher Methoden.
  4. Wenn Basisklassenmethoden in einer abgeleiteten Klasse zur Implementierung von Schnittstellen verwendet werden, erscheinen zusätzliche Definitionen in der abgeleiteten Klasse, die im C#-Code nicht vorhanden waren.
  5. Das Aufrufen virtueller Methoden während der Konstruktion und Finalisierung verhält sich nach der Übersetzung anders und sollte vermieden werden.

Wir verstehen, dass eine strikte Nachahmung des C#-Verhaltens einen etwas anderen Ansatz erfordern würde. Dennoch haben wir diese Logik gewählt, weil sie die API der konvertierten Bibliotheken näher an die C++-Paradigmen anpasst. Das folgende Beispiel veranschaulicht diese Funktionen:

C#-Code:

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);
    }
}

C++-Headerdatei:

#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>();
        }
    }
};

C++-Quellcode:

#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()
{
}

Die Reihe von Aliassen und Makros am Anfang jeder übersetzten Klasse dient dazu, bestimmte C#-Mechanismen zu emulieren, hauptsächlich GetType, typeof und is. Hash-Codes aus der .cpp-Datei werden für effiziente Typvergleiche verwendet. Alle Funktionen, die Schnittstellen implementieren, sind virtuell, obwohl dies vom C#-Verhalten abweicht.

In Verbindung stehende Artikel