CXXI: Мост между мирами C# и C++

в 7:39, , рубрики: c++, Mono, Mono и Moonlight, where is your c++/cli now?

В рантайме Mono есть немало средств для взаимодействия с кодом на не .NET языках, но никогда не было ничего вменяемого для взаимодействия с кодом на C++.

Но это вскоре изменится благодаря работе Алекса Коррадо, Андрэа Гайта и Зольтана Варга.

Вкратце, новая технология позволяет разработчикам C#/.NET:

  • Легко и прозрачно использовать классы C++ из C# или любого иного .NET языка
  • Создавать экземпляры классов C++ из C#
  • Вызывать методы классов C++ из кода на C#
  • Вызывать инлайн-методы C++ из кода на C# (при условии, что библиотека скомпилирована с флагом -fkeep-inline-functions или если вы скомпилируете дополнительную библиотеку с их реализациями)
  • Наследовать классы C++ из C#
  • Переопределять виртуальные методы классов C++ методами на C#
  • Использовать экземпляры таких смешанных C++/C# классов как в коде на C#, так и в коде на C++

CXXI (прим. пер.: читается как «sexy») это результат двухмесячной работы под эгидой Google's Summer of Code с целью улучшить взаимодействие Mono с кодом на C++.

Альтернативы

Напоминаю, что Mono предоставляет несколько механизмов взаимодействия с кодом на не .NET языках, большей частью унаследованные из ECMA-стандарта. Эти механизмы включают:

  1. Двухсторонняя технология «Platform Invoke» (P/Invoke), позволяющая управляемому коду (C#) вызывать функции из неуправляемых библиотек, а коду этих библиотек делать callback'и обратно в управляемый код.
  2. COM Interop позволяющий коду, выполняющемуся в Mono прозрачно вызывать неуправляемый код на C или C++ до тех пор пока этот код соблюдает некоторые конвенции COM (конвенции эти довольно простые: стандартная «разметка» vtable, реализация методов Add, Release и QueryInterface, а так же использование стандартного набора типов, которые могут быть отмаршалены между Mono и COM-библиотекой).
  3. Общая технология перехвата вызовов, позволяющая перехватить вызов метода объекта и дальше самостоятельно разбираться с тем, что с ним делать.

Но когда дело доходит до того чтобы использовать в C# объекты C++, выбор остаётся не очень-то обнадёживающий. Для примера, предположим что вы хотите из C# использовать следующий класс C++:

class MessageLogger {
public:
	MessageLogger (const char *domain);
	void LogMessage (const char *msg);
}

Одним из способов предоставить этот класс коду на C# — обернуть его в COM-объект. Это может сработать для некоторых высокоуровневых объектов, но процесс оборачивания весьма нудный и рутинный. Посмотреть, как выглядит это неинтересное занятие можно тут.

Другой вариант — наклепать переходников, которые потом можно будет вызвать через P/Invoke. Для представленного выше класса выглядеть они будут примерно так:

/* bridge.cpp, компилируется в bridge.so */
MessageLogger *Construct_MessageLogger (const char *msg)
{
	return new MessageLogger (msg);
}

void LogMessage (MessageLogger *logger, const char *msg)
{
	logger->LogMessage (msg);
}

Часть на C# выглядит так:

class MessageLogger {
	IntPtr handle;

	[DllImport ("bridge")]
	extern static IntPtr Construct_MessageLogger (string msg);

	public MessageLogger (string msg)
	{
		handle = Construct_MessageLogger (msg);
	}

	[DllImport ("bridge")]
	extern static void LogMessage (IntPtr handle, string msg);

	public void LogMessage (string msg)
	{
		LogMessage (handle, msg);
	}
}

Посидите полчасика за составлением таких врапперов и захотите убить автора библиотеки, компилятора, создателей C++, C#, а затем и вовсе уничтожить этот бренный и несовершенный мир.

Наша PhyreEngine# была .NET-биндингами к C++ API к PhyreEngine от Sony. Процесс написания кода был весьма нудным, так что мы на коленке сделали что-то вроде кодогенератора.

Ко всему прочему, вышеописанные методы не позволяют вам переопределять методы классов C++ кодом на C#. Точнее, вы можете это сделать, но это потребует написания большого количества кода вручную с учётом кучи частных случаев и множеством callback-вызовов. Биндинги очень быстро станут практически неподдерживаемыми (мы столкнулись с этим сами, делая биндинги к PhyreEngine).

Вышеописанные мытарства и побудили к созданию CXXI.

Как оно работает

Доступ к классам C++ представляет из себя комплекс проблем. Кратко опишу особенности реализации кода на C++, играющие большую роль для CXXI:

  • Разметка объекта (object layout): бинарное представление объекта в памяти, может отличаться на разных платформах.
  • Разметка VTable: список указателей на реализации виртуальных методов, используемая компилятором для определения адреса метода, зависит от виртуальных методов класса и его родителей.
  • Декорированные имена: невиртуальные методы, не входящие в vtable. Компилятор генерирует обычные «сишные» функции, имя которых вычисляется на основании типа возвращаемого значения и типов аргументов. Схема декорирования зависит от компилятора.

К примеру, у нас есть вот такой класс:

class Widget {
public:
	void SetVisible (bool visible);
	virtual void Layout ();
	virtual void Draw ();
};

class Label : public Widget {
public:
	void SetText (const char *text);
	const char *GetText ();
};

Компилятор C++ для этих методов методов сгенерирует следующие имена(прим. пер.: имеются ввиду компиляторы типа GCC и Intel C++ Compiler for Linux, студийный выдаст нечто нечитаемое вроде ?h@@YAXH@Z; в случае с GCC вы можете воспользоваться утилитой c++filt):
__ZN6Widget10SetVisibleEb
__ZN6Widget6LayoutEv
__ZN6Widget4DrawEv
__ZN5Label7SetTextEPKc
__ZN5Label7GetTextEv

Вот такой код

	Label *l = new Label ();
	l->SetText ("foo");
	l->Draw ();	

Будет скомпилирован во что-то похожее на это (представлено как код на C):

	Label *l = (Label *) malloc (sizeof (Label));
	ZN5LabelC1Ev (l);   // Декорированное имя конструктора Label
	_ZN5Label7SetTextEPKc (l, "foo");

	// Эта строка вызывает Draw
	(l->vtable [METHOD_PTR_SIZE*2])();

Для того чтобы CXXI поддерживало подобные вещи, ему нужно знать точное расположение методов в vtable, знать, где и как реализован каждый из методов, и знать, как до них достучаться по декорированному имени.

Схема ниже показывает, каким образом библиотека на C++ становится доступной C# и другим языкам .NET.
CXXI: Мост между мирами C# и C++

Фактически ваш код на C++ компилируется дважды. Компилятор C++ генерирует вам неуправляемую библиотеку, а инструментарий CXXI генерирует биндинги.

Вообще говоря, CXXI нужны от вашего кода на C++ только заголовочные файлы, причём только те, которые вам надо обернуть для использования в C#. Так что если у вас есть только проприетарная библиотека и заголовочные файлы к ней, CXXI всё равно сможет сгенерировать биндинги.

Инструментарий CXXI создаёт обычную .NET-библиотеку (прим. пер.: это именно дотнетовская библиотека, содержащая MSIL и ничего кроме — никакого неуправляемого кода) которую вы можете спокойно использовать из C# и прочих .NET языков. Эта библиотека выставляет наружу C#-классы со следующими свойствами:

  • Когда вы создаёте экземпляр класса C#, его конструктор создаёт экземпляр соответствующего класса C++.
  • Эти классы могут быть базовыми для других классов C#, все методы, помеченные как virtual могут быть переопределены кодом на C#.
  • Поддерживается множественное наследование классов C++: сгенерированный класс C# реализует набор операторов преобразования типа, позволяющих достучаться до различных базовых классов C++.
  • Переопределённые методы могут использовать ключевое слово «base» C# для вызова методов базового класса C++.
  • Вы можете переопределить любые виртуальные методы классов, в т. ч. в случае множественного наследования.
  • Так же наличествует конструктор, принимающий IntPtr, на случай если вы захотите использовать уже созданный кем-то другим экземпляр класса C++.

Конвеер CXXI состоит из трёх компонент, показанных на схеме справа.
CXXI: Мост между мирами C# и C++
Компилятор GCC-XML используется для разбора вашего кода на C++ и извлечения из него необходимой информации. Сгенерированный XML затем обрабатывается утилитами CXXI чтобы сгенерировать набор partial-классов на C#, содержащих собственно мосты к классам на C++

Затем это совмещается с любым дополнительным кодом, который вы захотите добавить (например, несколько перегруженных методов для улучшения API, реализацию ToString, Async-методы, etc).

На выходе получается .NET-сборка, работающая с native-библиотекой.

Стоит отметить, что эта сборка не содержит в себе самой карты разметки объектов в памяти. Вместо этого биндер CXXI определяет это исходя из используемого в момент выполнения ABI и соответствующих ему правил преобразования. Таким образом, вам нужно скомпилировать биндинги лишь однажды, а затем спокойно использовать их на разных платформах.

Примеры

Код проекта на GitHub содержит различные тесты и пачку примеров. Один из них представляет из себя минимальные биндинги к Qt.

Что ещё осталось реализовать

К сожалению проект CXXI ещё не окончен, но это уже хороший задел для ощутимого улучшения взаимодействия кода на .NET и C++.

На текущий момент CXXI делает всю работу в рантайме, генерируя переходники через System.Reflection.Emit по мере необходимости, что позволяет динамически определять ABI, используемое компилятором библиотеки на C++.

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

CXXI на текущий момент поддерживает ABI GCC и имеет начальную поддержку ABI MSVC. Мы будем рады помощи с реализацией поддержки ABI других компиляторов и с доделыванием поддержки MSVC.

На текущий момент CXXI поддерживает только удаление объектов, созданных им самим же. Все остальные объекты считаются принадлежащими миру неуправляемого кода. Поддержка оператора delete для таких объектов так же была бы полезна.

Мы так же хотим получше документировать конвеер и API времени выполнения, а так же улучшить сами биндинги.

От переводчика

Данный метод выгодно отличается от написания тонн glue-кода на C++/CLI, тут всю работу делают за вас, да ещё и получается всё кроссплатформенно. Так же стоит отметить, что на хабре в том году мелькала статья о схожем способе пинания методов классов на C++, правда там очень многое делалось вручную. Однако по словам автора использование генерируемых на лету врапперов оказалось в полтора раза быстрее чем COM Interop (на рантайме от MS).
Ах да. В статье это не отражено, но судя по тесткейзам на гитхабе вы можете обращаться к полям объектов C++.
Насколько оно юзабельно? Теоретически, вы можете вот прямо сейчас взять любую плюсовую либу и сгенерить к ней биндинги (в случае с Windows компилить её надо будет в Cygwin). И оно будет отлично работать, если в ней нет методов, возвращающих свежесозданные экземпляры объектов, т. к. на текущий момент их нельзя будет удалить, однако в Qt у QObject есть слот deleteLater(), так что проблем быть не должно. Практически же, генерилка упала при попытке сгенерить биндинги к Irrlicht, а на OGRE упал GCCXML, не осилив что-то из std::tr1. От GCCXML вообще говоря стоило бы отказаться в пользу clang, так как обновляется GCCXML он ну очень редко, а работает, как выяснилось, криво. Зато в примерах есть работающие биндинги к некоторым классам QtGui (неполные, инфраструктуру QObject со всей метаинформацией и сигналослотами никто не делал пока).

Автор: kekekeks

Поделиться

* - обязательные к заполнению поля