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

Original author: Miguel de Icaza
  • Translation
В рантайме 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.


Фактически ваш код на 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 состоит из трёх компонент, показанных на схеме справа.

Компилятор 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 со всей метаинформацией и сигналослотами никто не делал пока).
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 18

    0
    C++/CLI разве не решает эту проблему проще?
      +2
      Если написание glue-кода вручную и наличие в итоговой сборке native-кода, привязанного к архитектуре процессора и неработающего нигде кроме Windows, проще, то да.
        +2
        В любом случае откомпилированная библиотека на C++ будет привязана к архитектуе процессора и в ней будет native код.

        Я не предлагаю писать все на C++/CLI, но налаживать на нем мосты довольно удобно.
          0
          Ну там вам надо делать мост, тут вам дают возможность без написания дополнительного кода получить доступ к классам C++. В любом случае сборки C++/CLI гвоздями приколочены к MSVCRT, так что для Mono (в рамках которого и пилят CXXI) они бесполезны.
            0
            Я несколько опасаюсь автоматических прокси-генераторов неуправляемого кода в управляемый — у них раньше была тенденция злоупотреблять IntPtr.

            Возможно, если эти ребята побороли эту проблему, то штука на самом деле полезная.
        0
        основной вопрос здесь не в написании байндингов, а в их автоматической генерации.
        примерно такой же подход использует проект SharpDX.
        API генерируется напрямую из заголовочных файлов DirectX SDK.
        image
        +2
        Лично я настроен скептично. Ну то есть я верю, что они допишут это всё и оно даже будет работать. Мне не нравится идеология. Люди, которые осознанно решают использовать из C# кода классы C++ делают это по вполне понятным причинам: им не хватает производительности или управляемости или ручного управления памятью. Потому они садятся, и пишут руками на С++ все плохо реализуемые в С# вещи (включая взаимодействие С# и С++ кода). А тут этот значительный и серьёзный кусок за них сгенерит какая-то непонятная хрень. Ну и о каком контроле управляемости\производительности\памяти может идти речь?
          0
          Люди, которые осознанно решают использовать из C# кода классы C++ делают это по вполне понятным причинам: им не хватает производительности или управляемости или ручного управления памятью.
          Эм. Есть тонны готового, работающего и протестированного кода на C++. Зачем его переписывать на шарп? Возьмите то же Qt — переписать всё это на шарп да ещё и так, чтобы работало на всех поддерживаемых Qt платформах не представляется возможным. Таким образом CXXI — главным образом инструмент для создания биндингов к готовым библиотекам, а не смешанных приложений.
          +1
          Идея занятная, но, честно говоря, я не очень понял восторгов насчет кроссплатформенности.
          Судя по нарисованной цепочке — управляемая сборка, содержащая биндинги к нативному коду здорово зависит от используемого компилятора С++, точнее от результата компиляции. С++ является кроссплатформенным языком, но под каждую платформу собираются отдельные модули. Из этого следует, что придется собирать биндинги отдельно под каждую платформу.
          Или я что-то неправильно понимаю?
            0
            Вы невнимательно читали. Метаинформация содержит только типы и порядок полей и методов класса, конкретное их расположение в памяти вычисляется в рантайме на основании правил для ABI конкретного компилятора.
              0
              А как в рантайме определяются правила обращения к нативной библиотеке? в нативных dll где-то хранится информация о компиляторе?
                0
                Скорее всего методом перебора по декорированным именам функций. Компиляторы с одинаковыми методами декорирования совместимы и по ABI, как ни странно.
            –1
            Почему в статье нет ни слова о managed C++?
              0
              Потому что он
              1) устарел и заменён C++/CLI
              2) не работает нигде окромя форточек
              0
              Неплохо бы продемонстрировать процесс на простом примере. Раньше как-то не было необходимости ставить mono под cygwin под windows, все примеры по сборке достаточно старые и противоречивые.
                0
                Вообще говоря CXXI работает и на .NET FW, просто ABI студийного компилятора C++ пока не поддерживается, сами либы нужно собирать под cygwin. А примеры есть на GitHub.
                0
                А зачем cygwin? Не проще нативный MSYS взять?
                  +1
                  Идея напоминает SWIG

                  Only users with full accounts can post comments. Log in, please.