22 ноября 2024
Создание эффективного транслятора кода между такими языками, как C# и C++, представляет собой сложную задачу. При разработке инструмента CodePorting.Translator Cs2Cpp возникло множество проблем, связанных с различиями в синтаксисе, семантике и парадигмах программирования этих двух языков. В этой статье будут рассмотрены ключевые трудности, с которыми мы столкнулись, а также возможные способы их преодоления.
Это относится, например, к операторам using
и yield
:
using (var resource = new Resource())
{
// Использование ресурса
}
public IEnumerable<int> GetAllNumbers()
{
for (int i = 0; i < int.MaxValue; i++)
{
yield return i;
}
}
В таких случаях нам приходится либо писать довольно сложный код для эмуляции поведения оригинального кода, как в трансляторе, так и в библиотеке — в первом случае, либо отказываться от поддержки таких конструкций — во втором.
Например, в исходном коде присутствуют виртуальные обобщённые методы, или конструкторы, использующие виртуальные функции:
public class A
{
public virtual T GenericMethod<T>(T param)
{
return param;
}
}
public class A
{
public A()
{
VirtualMethod();
}
public virtual void VirtualMethod()
{
}
}
public class B : A
{
public override void VirtualMethod()
{
}
}
В подобных случаях нам не остаётся ничего, кроме как переписывать такой проблемный код в терминах, допускающих конвертацию на C#. К счастью, такие случаи встречаются редко и касаются небольших фрагментов кода.
Это включает ресурсы, рефлексию, динамическое подключение сборок и импорт функций:
static void Main()
{
var rm = new ResourceManager("MyApp.Resources", typeof(Program).Assembly);
var value = rm.GetString("MyResource");
}
static void Main()
{
var type = typeof(MyClass);
var method = type.GetMethod("MyMethod");
var result = method.Invoke(null, null);
Console.WriteLine(result);
}
public class MyClass
{
public static string MyMethod()
{
return "Hello, World!";
}
}
static void Main()
{
var assembly = Assembly.Load("MyDynamicAssembly");
var type = assembly.GetType("MyDynamicAssembly.MyClass");
var instance = Activator.CreateInstance(type);
var method = type.GetMethod("MyMethod");
method.Invoke(instance, null);
}
В таких случаях нам приходится эмулировать соответствующие механизмы. Это включает поддержку ресурсов (внедряемых в сборку в виде статических массивов и читаемых через специализированные реализации потоков) и рефлексию. Очевидно, что напрямую подключать сборки .NET к коду C++ или импортировать функции из динамических библиотек Windows при выполнении на другой платформе мы не можем, поэтому такой код приходится урезать или переписывать.
В этом случае мы реализуем соответствующее поведение, как правило, используя реализации из сторонних библиотек, лицензии которых не запрещают использование в составе коммерческого продукта.
В некоторых случаях речь идет о простых ошибках в реализации, которые, как правило, несложно исправить. Гораздо хуже, когда разница в поведении лежит на уровне подсистем, используемых библиотечным кодом.
Например, многие наши библиотеки активно используют классы из библиотеки System.Drawing
, построенной на GDI+. Версии этих классов, разработанных нами для C++, используют Skia в качестве графического движка. Поведение Skia зачастую отличается от такового в GDI+, особенно под Linux, и на то, чтобы добиться одинаковой отрисовки, нам приходится тратить значительные ресурсы. Аналогично, libxml2, на которой построена наша реализация System::Xml
, ведёт себя в иных случаях не так, и нам приходится патчить её или усложнять свои обёртки.
Программисты на C# оптимизируют свой код под те условия, в которых он выполняется. Однако многие структуры начинают работать медленнее в необычном для себя окружении.
Например, создание большого количества мелких объектов в C# обычно работает быстрее, чем в C++, из-за иной схемы работы кучи (даже с учётом сборки мусора). Динамическое приведение типов в C++ также несколько медленнее. Подсчёт ссылок при копировании указателей — ещё один источник накладных расходов, которых нет в C#. Наконец, использование вместо встроенных, оптимизированных концепций C++ (итераторы) переводных с C# (перечислители) также замедляет работу кода.
Способ устранения бутылочных горлышек во многом зависит от ситуации. Если библиотечный код сравнительно легко оптимизировать, то сохранить поведение транслированных концепций и одновременно оптимизировать их работу в чуждом окружении порой не так просто.
Например, в публичном API присутствуют методы, принимающие SharedPtr<Object>
, у контейнеров отсутствуют итераторы, методы для работы с потоками принимают System::IO::Stream
вместо istream
, ostream
или iostream
, и так далее.
Мы последовательно расширяем транслятор и библиотеку, чтобы нашим кодом было удобно пользоваться программистам C++. Например, транслятор уже умеет генерировать методы begin
-end
и перегрузки, работающие со стандартными потоками.
В заголовочные C++ файлы попадают типы и имена закрытых полей, а также полный код шаблонных методов. Эта информация обычно обфусцируется при выпуске релизов для .NET.
Мы стараемся исключить лишнюю информацию при помощи сторонних утилит и специальных режимов работы самого транслятора, однако это не всегда возможно. Например, удаление закрытых статических полей и невиртуальных методов не влияет на работу клиентского кода, однако удалить или переименовать виртуальные методы без потери функциональности невозможно. Поля могут быть переименованы, а их тип заменён на заглушку того же размера, при условии, что конструкторы и деструкторы экспортированы из кода, собранного с полными заголовочными файлами. В то же время, скрыть код публичных шаблонных методов не представляется возможным.
Релизы продуктов для языка C++, созданные с использованием нашего фреймворка, успешно выпускаются уже много лет. Если вначале мы выпускали урезанные версии продуктов, то теперь удаётся поддерживать гораздо более полную функциональность.
В то же время, у нас остаётся достаточно много пространства для исправлений и улучшений. Это касается как поддержки ранее пропущенных синтаксических конструкций и частей библиотеки, так и повышения удобства работы с транслятором.
Помимо решения текущих проблем и плановых улучшений, мы работаем над переводом транслятора на современный синтаксический анализатор Roslyn. До недавнего времени мы использовали анализатор NRefactory, который был ограничен поддержкой версий языка C# до 5.0. Переход на Roslyn позволит нам поддерживать современные конструкции языка C#, такие как:
Наконец, мы планируем расширить число поддерживаемых языков — как целевых, так и исходных. Адаптировать решения, основанные на Roslyn, для чтения кода VB будет относительно легко, особенно учитывая, что библиотеки для C++ и Java уже готовы. С другой стороны, подход, который мы применили для поддержки Python, во многом проще, и по аналогии можно поддержать и другие скриптовые языки, такие как PHP.