Правила трансляции кода с C# на C++: основы

Поговорим о том, каким образом наш транслятор преобразует синтаксические конструкции языка C# в C++. Рассмотрим особенности преобразования и возникающие ограничения.

Проекты и единицы компиляции

Трансляция производится попроектно. Один проект C# преобразуется в один или два проекта C++. Первый проект аналогичен проекту C#, второй представляет собой googletest-приложение для запуска тестов, если они присутствуют в исходном проекте. Для каждого входного проекта генерируется файл CMakeLists.txt, который позволяет создавать проекты для большинства сборочных систем.

В большинстве случаев одному файлу .cs соответствует один файл .h и один файл .cpp. Обычно определения типов попадают в заголовочный файл, а определения методов – в файл исходного кода, но это не так для шаблонных типов, весь код которых остаётся в заголовочных файлах. Заголовочные файлы, в которых присутствует, по крайней мере, одно публичное определение, попадают в каталог включаемых файлов, доступных зависимым проектам и конечным пользователям. Заголовочные файлы, включающие лишь непубличные (internal) определения, попадают в каталог с исходниками.

Кроме файлов с кодом, полученных трансляцией оригинального кода C#, транслятор создаёт дополнительные файлы, содержащие некоторый сервисный код. Также в выходной каталог помещаются конфигурационные файлы с записями о том, в каких заголовочных файлах искать типы из данного проекта. Эта информация требуется при преобразовании зависимых сборок. Также в выходную директорию ложится полный лог транслирования.

Общая структура исходного кода

  1. Пространства имён C# отображаются в пространства имён C++. Операторы использования пространств имён превращаются в аналоги из C++.
  2. Комментарии переносятся как есть, кроме документации к типам и методам, обрабатываемой отдельно.
  3. Форматирование сохраняется частично.
  4. Директивы препроцессора не переносятся, поскольку при построении синтаксического дерева необходимо уже задать все константы.
  5. В начале каждого файла находится список включаемых файлов, а после него – список предварительных объявлений типов. Данные списки формируются на основании того, какие типы упоминаются в текущем файле, с тем расчётом, чтобы список включений был по возможности минимальным.
  6. Метаданные к типам генерируются в виде специальных структур данных, доступных во время выполнения. Поскольку безусловная генерация метаданных существенно увеличивает объём скомпилированных библиотек, она включается вручную для отдельных типов по мере необходимости.

Определения типов

  1. Псевдонимы типов транслируются с использованием синтаксиса using <typename> = ...
  2. Перечисления C# транслируются в перечисления C++14 (синтаксис enum class).
  3. Делегаты преобразуются в псевдонимы для специализаций класса System::MulticastDelegate:
public delegate int IntIntDlg(int n);
using IntIntDlg = System::MulticastDelegate<int32_t(int32_t)>;
  1. Классы и структуры C# отображаются на классы C++. Интерфейсы превращаются в абстрактные классы. Структура наследования соответствует таковой в C#, а неявное наследование от System.Object становится явным.
  2. Свойства и индексаторы разбиваются на геттеры и сеттеры, представленные отдельными методами.
  3. Виртуальные функции C# отображаются на виртуальные функции C++. Реализация интерфейсов также производится с использованием механизма виртуальных функций.
  4. Обобщённые (generic) типы и методы превращаются в шаблоны C++.
  5. Финализаторы переходят в деструкторы.

Ограничения

Всё это вместе задаёт несколько ограничений:

  1. Трансляция виртуальных обобщённых методов не поддерживается.
  2. Реализация интерфейсных методов виртуальна, даже если в исходном коде это не так.
  3. Введение новых (new) методов с именами и сигнатурами, повторяющими имена и сигнатуры существующих виртуальных и/или интерфейсных методов, невозможно. Однако транслятор позволяет переименовывать такие методы.
  4. Если методы базового класса используются для реализации интерфейсов дочернего класса, в дочернем классе появляются дополнительные определения, которых нет в C#.
  5. Вызов виртуальных методов на стадиях конструирования и финализации ведёт себя иначе после преобразования, и его нужно избегать.

Понятно, что строгая имитация поведения C# требовала бы несколько иного подхода. Тем не менее, мы предпочли следовать именно такой логике, поскольку в этом случае API конвертированных библиотек в наиболее полной мере соответствует парадигмам C++. Приведённый ниже пример демонстрирует эти особенности:

Код 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);
    }
}

Заголовочный файл 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++:

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

Серия псевдонимов и макросов, в начале каждого транслированного класса, нужна для эмуляции некоторых механизмов C#, прежде всего: GetType, typeof и is. Хэш-коды из файла .cpp используются для быстрого сравнения типов. Все функции, реализующие интерфейсы, виртуальны, хотя в C# это не так.

Связанные статьи