28 декабря 2024
Наш фреймворк CodePorting.Translator Cs2Cpp позволяет выпускать библиотеки, разработанные для платформы .NET, на языке C++. В этой статье будет рассказано о том, как нам удалось согласовать модели памяти этих двух языков и добиться корректной работы транслированного кода в необычном для него неуправляемом окружении.
Вы узнаете о том, какие умные указатели мы используем, и почему нам пришлось разработать для них собственные реализации. А также о процессе подготовки кода C# к портированию с точки зрения управления временем жизни объектов, о некоторых проблемах, с которыми мы столкнулись, и о специфических способах диагностики, которыми нам приходится пользоваться при работе.
Код C# выполняется в управляемой среде со сборкой мусора. Для целей трансляции это, прежде всего, означает, что программист C#, в отличие своего коллеги из числа разработчиков C++, освобождён от необходимости заботиться о возвращении системе выделенной на куче памяти, которая более не используется. Это за него делает сборщик мусора (GC) — компонент среды CLR, периодически проверяющий, какие из объектов ещё используются в программе, и очищающий те, на которые больше нет активных ссылок.
Активной считается ссылка:
Алгоритм работы сборщика мусора обычно описывают как прохождение по графу объектов, начиная со ссылок, расположенных на стеке и в статических полях, с последующим удалением всего, что не было достигнуто на предыдущем этапе. Чтобы защититься от инвалидации графа связей во время работы GC, выполняется остановка мира. Это описание может быть упрощённым, однако для наших целей его достаточно.
В отличие от умных указателей, подход с уборкой мусора свободен от проблемы перекрёстных или циклических ссылок. Если два объекта ссылаются друг на друга, возможно через некоторое количество промежуточных объектов, это не удерживает GC от удаления их в тот момент, когда на всю группу, именуемую островом изоляции, не остаётся активных ссылок. Отсюда следует, в частности, то, что у программистов C# не существует каких-либо предубеждений против того, чтобы связывать объекты друг с другом в любой момент и в любых комбинациях.
Типы данных в C# делятся на ссылочные и значимые. Экземпляры значимых типов всегда располагаются на стеке, либо в статической области памяти, либо непосредственно в полях структур и объектов. В то время как экземпляры ссылочных типов всегда создаются в куче, а на стеке, в статической памяти и в полях хранятся только их ссылки (адреса). К значимым типам относятся структуры, примитивные арифметические значения, а также ссылки. К ссылочным — классы и, исторически, делегаты. Исключение из этого правила составляют разве что случаи боксинга, когда структура или другой значимый тип копируются в кучу для использования в специфическом контексте.
Момент уничтожения объекта не определён — язык гарантирует лишь, что он не настанет раньше, чем будет удалена последняя активная ссылка на объект. Если в конвертированном коде объект будет уничтожаться сразу в момент удаления последней активной ссылки, это никак не нарушит работу программы.
Ещё один важный момент связан с поддержкой обобщённых (generic) типов и методов в C#. C# позволяет писать дженерики один раз и затем использовать их как со ссылочными, так и со значимыми типами-параметрами. Как будет показано далее, этот момент оказывается важен.
Несколько слов о том, как мы отображаем типы C# на типы C++. Поскольку фреймворк CodePorting.Translator Cs2Cpp предназначен для портирования библиотек, а не приложений, важным требованием для нас является как можно более точное воспроизведение API оригинального .NET проекта. Поэтому мы превращаем классы, интерфейсы и структуры C# в классы C++, наследующие соответствующие базовые типы.
Например, рассмотрим следующий код:
interface I1 {}
interface I2 {}
interface I3 : I2 {}
class A {}
class B : A, I1 {}
class C : B, I2 {}
class D : C, I3 {}
class Generic<T> { public T value; }
struct S {}
Он будет транслирован так:
class I1 : public virtual System::Object {};
class I2 : public virtual System::Object {};
class I3 : public virtual I2 {};
class A : public virtual System::Object {};
class B : public A, public virtual I1 {};
class C : public B, public virtual I2 {};
class D : public C, public virtual I3 {};
template <typename T> class Generic { public: T value; };
class S : public System::Object {};
Класс System::Object
является системным и объявлен в библиотеке поддержки транслятора, внешней по отношению к конвертированному проекту. Классы и интерфейсы наследуются от него виртуально, чтобы избежать проблемы ромба. Структуры в транслированном C++ коде наследуются от System::Object
, в то время как в C# они наследуются от него через System.ValueType
, но лишнее наследование убрано с целью оптимизации. Обобщённые типы и методы транслируются в шаблонные классы и методы соответственно.
Для экземпляров транслированных типов должны выполняться те же гарантии, которые давал C#. Экземпляры классов должны создаваться на куче, и время их жизни должно определяться временем жизни активных ссылок на них. Экземпляры структур должны создаваться на стеке, за исключением случаев боксинга. Делегаты являются особым и достаточно тривиальным случаем, выходящим за рамки данной статьи.
Мы рассмотрели, как модель управления памятью в C# влияет на процесс преобразования кода в C++. В следующей части статьи будет рассказано, как было принято решение о способе управления временем жизни объектов, аллоцированных на куче, и как этот подход был реализован.