Правила трансляции кода с C# на C++: члены классов и управляющие структуры

В этой статье мы рассмотрим, как наш транслятор конвертирует члены классов, переменные, поля, операторы и управляющие структуры C#. Также коснемся вопроса использования библиотеки поддержки транслятора для корректной конвертации типов .NET Framework в С++.

Члены классов

Методы классов ложатся на C++ напрямую. Это также касается статических методов и конструкторов. В некоторых случаях может появляться дополнительный код – например, чтобы эмулировать вызовы статических конструкторов. Методы расширения и операторы транслируются в статические методы и вызываются явно. Финализаторы становятся деструкторами.

Экземплярные поля C# становятся экземплярными полями C++. Статические поля также остаются без изменений, за исключением случаев, когда важен порядок инициализации – это реализуется трансляцией таких полей в виде синглтонов.

Свойства разбиваются на метод-геттер и метод-сеттер, или что-то одно, если второй метод отсутствует. Для автоматических свойств к ним добавляется также закрытое поле значения. Статические свойства распадаются на статический геттер и сеттер. Индексаторы обрабатываются по той же логике.

События транслируются в поля, тип которых соответствует нужной специализации System::Event. Трансляция в виде трёх методов (add, remove и invoke) была бы более правильной и, к тому же, позволила бы поддержать абстрактные и виртуальные события. Возможно, в будущем мы придём к такой модели, однако на данный момент вариант с классом Event полностью покрывает наши потребности.

Следующий пример иллюстрирует описанные выше правила:

public abstract class Generic<T>
{
    private T m_value;
    public Generic(T value)
    {
        m_value = value;
    }
    ~Generic()
    {
        m_value = default(T);
    }
    public string Property { get; set; }
    public abstract int Property2 { get; }
    public T this[int index]
    {
        get
        {
            return index == 0 ? m_value : default(T);
        }
        set
        {
            if (index == 0)
                m_value = value;
            else
                throw new ArgumentException();
        }
    }
    public event Action<int, int> IntIntEvent;
}

Результат трансляции в C++ (незначащая часть кода удалена):

template<typename T>
class Generic : public System::Object
{
public:
    System::String get_Property()
    {
        return pr_Property;
    }
    void set_Property(System::String value)
    {
        pr_Property = value;
    }
    
    virtual int32_t get_Property2() = 0;
    
    Generic(T value) : m_value(T())
    {
        m_value = value;
    }
    
    T idx_get(int32_t index)
    {
        return index == 0 ? m_value : System::Default<T>();
    }
    void idx_set(int32_t index, T value)
    {
        if (index == 0)
        {
            m_value = value;
        }
        else
        {
            throw System::ArgumentException();
        }
    }
    
    System::Event<void(int32_t, int32_t)> IntIntEvent;
    
    virtual ~Generic()
    {
        m_value = System::Default<T>();
    }

private:
    T m_value;
    System::String pr_Property;
};

Переменные и поля

Константные и статические поля транслируются в статические поля, статические константы (в некоторых случаях – constexpr) либо в статические методы, дающие доступ к синглтону. Экземплярные поля C# преобразуются в экземплярные поля C++. Все сколько-нибудь сложные инициализаторы при этом переносятся в конструкторы, иногда для этого приходится явно добавлять конструкторы по умолчанию там, где их не было в C#. Переменные на стеке переносятся как есть. Аргументы методов – тоже, за исключением того, что и ref, и out аргументы становятся ссылочными (благо, перегрузка по ним запрещена).

Типы полей и переменных заменяются их аналогами из C++. В большинстве случаев, такие аналоги генерируются самим транслятором из исходного кода C#. Библиотечные типы, включающие типы .NET Framework и некоторые другие, написаны нами на C++ и входят в состав библиотеки поддержки транслятора, поставляемой вместе с конвертированными продуктами. var транслируется в auto, кроме случаев, когда явное указание типа нужно, чтобы сгладить разницу в поведении.

Кроме того, ссылочные типы оборачиваются в SmartPtr. Значимые типы подставляются как есть. Поскольку аргументы-типы могут быть как значимыми, так и ссылочными, они также подставляются как есть, но при инстанциировании ссылочные аргументы оборачиваются в SharedPtr. Таким образом, List<int> транслируется как List<int32_t>, но List<Object> становится List<SmartPtr<Object>>. В некоторых исключительных случаях ссылочные типы транслируются как значимые. Например, наша реализация System::String написана на базе типа UnicodeString из ICU и оптимизирована для хранения на стеке.

Для примера транслируем следующий класс:

public class Variables
{
    public int m_int;
    private string m_string = new StringBuilder().Append("foobazz").ToString();
    private Regex m_regex = new Regex("foo|bar");
    public object Foo(int a, out int b)
    {
        b = a + m_int;
        return m_regex.Match(m_string);
    }
}

После трансляции он принимает следующий вид (незначащий код удален):

class Variables : public System::Object
{
public:
    int32_t m_int;
    System::SharedPtr<System::Object> Foo(int32_t a, int32_t& b);
    Variables();
private:
    System::String m_string;
    System::SharedPtr<System::Text::RegularExpressions::Regex> m_regex;
};
System::SharedPtr<System::Object> Variables::Foo(int32_t a, int32_t& b)
{
    b = a + m_int;
    return m_regex->Match(m_string);
}
Variables::Variables()
    : m_int(0)
    , m_regex(System::MakeObject<System::Text::RegularExpressions::Regex>(u"foo|bar"))
{
    this->m_string = System::MakeObject<System::Text::StringBuilder>()->
        Append(u"foobazz")->ToString();
}

Управляющие структуры

Подобие основных управляющих структур сыграло нам на руку. Такие операторы, как if, else, switch, while, do-while, for, try-catch, return, break и continue в большинстве случаев переносятся как есть. Исключением в данном списке является разве что switch, требующий пары специальных обработок. Во-первых, C# допускает его использование со строковым типом – в C++ мы в этом случае генерируем последовательность if-else if. Во-вторых, относительно недавно добавилась возможность сопоставлять проверяемое выражение шаблону типа – что, впрочем, также легко разворачивается в последовательность if-ов.

Интерес представляют конструкции, которых нет в C++. Так, оператор using даёт гарантию вызова метода Dispose() при выходе из контекста – в C++ мы эмулируем это поведение, создавая объект-часового на стеке, который вызывает нужный метод в своём деструкторе. Перед этим, правда, нужно перехватить исключение, вылетевшее из кода, бывшего телом using, и сохранить exception_ptr в поле часового – если Dispose() не бросит своё исключение, будет переброшено то, которое мы сохранили. Это как раз тот редкий случай, когда вылет исключения из деструктора оправдан и не является ошибкой. Блок finally транслируется по похожей схеме, только вместо метода Dispose() вызывается лямбда-функция, в которую транслятор обернул его тело.

Ещё один оператор, которого нет в C# и который мы вынуждены эмулировать – это foreach. Изначально, мы транслировали его в эквивалентный while, вызывающий метод MoveNext() у перечислителя, что универсально, но довольно медленно. Поскольку в большинстве своём C++ реализации контейнеров из .NET используют структуры данных STL, мы пришли к тому, чтобы там где это возможно, использовать их оригинальные итераторы, конвертируя foreach в range-based for. В тех случаях, когда оригинальные итераторы недоступны (например, контейнер реализован на чистом C#), используются итераторы-обёртки, внутри себя работающие с перечислителями. Раньше за выбор нужного способа итерации отвечала внешняя функция, написанная с использованием техники SFINAE, сейчас мы близки к тому, чтобы иметь правильные версии методов begin-end во всех контейнерах, в том числе транслированных.

Операторы

Как и в случае с управляющими структурами, большинство операторов (по крайней мере, арифметических, логических и присваивания) не требуют особой обработки. Хотя, тут есть тонкий момент: в C# порядок вычисления частей выражения детерминирован, тогда как в C++ в некоторых случаях возникает неопределённое поведение. Например, следующий транслированный код ведёт себя неодинаково после компиляции разными инструментами:

auto offset32 = block[i++] + block[i++] * 256 + block[i++] * 256 * 256 +
    block[i++] * 256 * 256 * 256;

К счастью, подобные проблемы возникают достаточно редко. У нас есть планы научить транслятор бороться с такими моментами, но из-за сложности анализа, выявляющего выражения с побочными эффектами, это пока не было реализовано.

Впрочем, даже простейшие операторы требуют специальной обработки, когда они применяются к свойствам. Как было показано выше, свойства разбиваются на геттеры и сеттеры, и транслятору приходится вставлять нужные вызовы в зависимости от контекста:

obj1.Property = obj2.Property;
string s = GetObj().Property += "suffix";
obj1->set_Property(obj2->get_Property());
System::String s = System::setter_add_wrap(static_cast<MyClass*>(GetObj().GetPointer()),
    &MyClass::get_Property, &MyClass::set_Property, u"suffix")

В первой строке замена оказалась тривиальной. Во второй пришлось использовать обёртку setter_add_wrap, гарантирующую, что функция GetObj() будет вызвана всего один раз, а результат конкатенации вызова get_Property() и строкового литерала будет передан не только в метод set_Property() (который возвращает void), но и далее для использования в выражении. Тот же подход применяются при обращении к индексаторам.

Операторы C#, которых нет в C++: as, is, typeof, default, ??, ?., и так далее, эмулируются при помощи функций библиотеки поддержки транслятора. В тех случаях, когда требуется избежать двойного вычисления аргументов, например, чтобы не разворачивать GetObj()?.Invoke() в GetObj() ? GetObj().Invoke() : nullptr, используется подход, подобный показанному выше.

Оператор доступа к члену (.) в зависимости от контекста может заменяться на аналог из C++: на оператор разрешения области видимости (::) или на “стрелку” (->). При доступе к членам структур такая замена не требуется.

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