Reglas para traducir código de C# a C++: Conceptos básicos

Vamos a discutir cómo nuestro traductor convierte construcciones sintácticas del lenguaje C# a C++. Exploraremos las particularidades de la traducción y las limitaciones que surgen durante este proceso.

Proyectos y unidades de compilación

La traducción se realiza por proyecto. Un proyecto de C# se convierte en uno o dos proyectos de C++. El primer proyecto es un espejo del proyecto de C#, mientras que el segundo sirve como una aplicación googletest para ejecutar pruebas si existen en el proyecto original. Se genera un archivo CMakeLists.txt para cada proyecto de entrada, permitiendo la creación de proyectos para la mayoría de los sistemas de construcción.

Normalmente, un archivo .cs corresponde a un archivo .h y un archivo .cpp. Por lo general, las definiciones de tipos van en archivos de cabecera, mientras que las definiciones de métodos residen en archivos de código fuente. Sin embargo, esto es diferente para los tipos de plantilla, donde todo el código permanece en archivos de cabecera. Los archivos de cabecera que contienen al menos una definición pública terminan en el directorio include, accesible para proyectos dependientes y usuarios finales. Los archivos de cabecera con solo definiciones internas van al directorio de fuente.

Además de los archivos de código obtenidos de la traducción del código C# original, el traductor genera archivos adicionales que contienen código de servicio. Los archivos de configuración con entradas que especifican dónde encontrar tipos de este proyecto en archivos de cabecera también se colocan en el directorio de salida. Esta información es necesaria para manejar ensamblajes dependientes. Además, un registro de traducción completo se almacena en el directorio de salida.

Estructura general del código fuente

  1. Los espacios de nombres de C# se asignan a espacios de nombres de C++. Los operadores de uso de espacio de nombres se transforman en sus equivalentes de C++.
  2. Los comentarios se transfieren tal cual, excepto la documentación de tipos y métodos, que se maneja por separado.
  3. El formato se conserva parcialmente.
  4. Las directivas del preprocesador no se transfieren porque todas las constantes deben definirse durante la construcción del árbol de sintaxis.
  5. Cada archivo comienza con una lista de archivos incluidos, seguida de una lista de declaraciones anticipadas de tipos. Estas listas se generan en base a los tipos mencionados en el archivo actual para que la lista de inclusiones sea lo más mínima posible.
  6. Los metadatos de tipo se generan como estructuras de datos especiales accesibles en tiempo de ejecución. Debido a que la generación incondicional de metadatos aumenta significativamente el tamaño de las bibliotecas compiladas, se habilita manualmente para tipos específicos según sea necesario.

Definiciones de tipo

  1. Los alias de tipos se traducen utilizando la sintaxis using <typename> = ...
  2. Las enumeraciones C# se mapean a enumeraciones C++14 (usando la sintaxis enum class).
  3. Los delegados se transforman en alias para especializaciones de la clase System::MulticastDelegate:
public delegate int IntIntDlg(int n);
using IntIntDlg = System::MulticastDelegate<int32_t(int32_t)>;
  1. Las clases y estructuras de C# se representan como clases de C++. Las interfaces se convierten en clases abstractas. La estructura de herencia refleja la de C#, y la herencia implícita de System.Object se convierte en explícita.
  2. Las propiedades e indexadores se dividen en métodos separados para getters y setters.
  3. Las funciones virtuales de C# se corresponden con las funciones virtuales de C++. La implementación de interfaces también se consigue utilizando el mecanismo de las funciones virtuales.
  4. Los tipos y métodos genéricos se transforman en plantillas de C++.
  5. Los finalizadores se convierten en destructores.

Limitaciones

Todos estos factores juntos imponen varias limitaciones:

  1. No se admite la traducción de métodos genéricos virtuales.
  2. Las implementaciones de métodos de interfaz son virtuales, aunque no lo fueran en el código C# original.
  3. No es posible introducir nuevos métodos con los mismos nombres y firmas que los métodos virtuales y/o de interfaz existentes. Sin embargo, el traductor permite renombrar dichos métodos.
  4. Si se utilizan métodos de la clase base para implementar interfaces en una clase derivada, aparecen definiciones adicionales en la clase derivada que no estaban presentes en C#.
  5. Llamar a métodos virtuales durante la construcción y la finalización se comporta de forma diferente tras la traducción, y debería evitarse.

Entendemos que imitar estrictamente el comportamiento de C# requeriría un enfoque algo diferente. Sin embargo, elegimos esta lógica porque alinea la API de las bibliotecas convertidas más estrechamente con los paradigmas de C++. El siguiente ejemplo ilustra estas características:

Código 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);
    }
}

Archivo de cabecera 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>();
        }
    }
};

Código fuente 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 serie de alias y macros al principio de cada clase traducida se utilizan para emular ciertos mecanismos de C#, principalmente GetType, typeof y is. Los códigos hash del archivo .cpp se utilizan para una comparación de tipos eficiente. Todas las funciones que implementan interfaces son virtuales, aunque esto difiere del comportamiento de C#.

Artículos relacionados: