В этом топике я расскажу про C++/CLI — промежуточный язык для склеивания кода на C/C++ и .NET
Это довольно распространённая задача, ведь на C/C++ написаны тонны проверенного временем высокопроизводительного кода, который невозможно переписать на управляемые языки.
Наша задача — обеспечить .NET-интерфейс к этим библиотекам. Но как это сделать, если они написаны на C/C++?
Microsoft предлагает два варианта решения проблемы.
Первый — это механизм Platform Invoke, с помощью которого можно использовать библиотеки с C ABI. Выглядит это как-то так:
P/Invoke обеспечивает маршаллинг (трансляцию) всех простых типов данных, а так же строк, структур с полями, и даже callback-функций (делегатов).
Но что, если у библиотеки нет C-интерфейса, или возможностей P/Invoke не хватает? На помощь приходит C++/CLI.
Это идеальный язык для генерации glue code между managed и unmanaged средами исполнения, поскольку он позволяет генерировать код для обоих сред + генерирует transition code, избавляя нас от необходимости склеивать что-то вручную.
Суффикс CLI — обозначает то, что язык реализует спецификацию Common Language Infrastructure, т.е. является полноправным членом семейства языков платформы .NET
Итак, нам понадобится Visual C++ Express 2008. Кликаем на «New Project», выбираем тип проекта — CLR. Это создаст проект с по умолчанию выставленной опцией /clr (Use Common Language Runtime). Это означает, что компилятор сгенерирует корректную MSIL-сборку и даст нам использовать новый синтаксис — который мы сейчас и рассмотрим.
По умолчанию, компилятор считает весь C++-код проекта нацеленным на компиляцию в MSIL. Скорее всего, это не будет работать (не скомпилируется), а если и скомпилируется, то это с большой вероятностью не то, чего вам хотелось бы.
Специальные команды препроцессора позволяют указать, какую часть кода надо компилировать в x86, а какую — в MSIL.
Скорее всего, если вы подключаете какую-то библиотеку, вам придется сначала скомпилировать её в статический .lib и не забыть обернуть её заголовки в блок
Снова препроцессор:
Здесь так же, как в обычном C++:
Это то, что в C# называется «struct».
Кстати, что интересно, C++/CLI поддерживает некоторые фичи CLR, которые не реализованы в языке C#. Например, property indexers — можно определять индексаторы (оператор []) для отдельных свойств, а не только для класса целиком. Вот только такой код нельзя будет вызвать из C# :)
То, что в C# называется «interface»:
Здесь ключевое слово generic используется аналогично template в C++:
Очень частая задача. Используем паттерн IDisposable.
Обратите особое внимание, что С++-деструктор в ref-классе автоматически транслируется в метод Dispose (). Для финализаторов используется другой синтаксис.
Эта операция в архитектуре CLR называется «pinning». При «прибивании» объекту запрещается перемещаться в куче при сборке мусора и уплотнении кучи. Это позволяет неуправляемому коду воспользоваться адресом объекта и записать/прочитать что-нибудь по этому адресу.
Будьте осторожны, так как прибивание объектов мешает сборщику мусора и сильно фрагментирует кучу. Эта операция предназначена для лишь для кратковременных манипуляций с объектами кучи.
«Pinned»-указатели реализованы в C++/CLI как шаблонный RAII-тип
Это довольно распространённая задача, ведь на C/C++ написаны тонны проверенного временем высокопроизводительного кода, который невозможно переписать на управляемые языки.
Наша задача — обеспечить .NET-интерфейс к этим библиотекам. Но как это сделать, если они написаны на C/C++?
Microsoft предлагает два варианта решения проблемы.
P/Invoke
Первый — это механизм Platform Invoke, с помощью которого можно использовать библиотеки с C ABI. Выглядит это как-то так:
[DllImport ("user32.dll")] static extern bool MessageBeep (System.UInt32 type);
P/Invoke обеспечивает маршаллинг (трансляцию) всех простых типов данных, а так же строк, структур с полями, и даже callback-функций (делегатов).
C++/CLI
Но что, если у библиотеки нет C-интерфейса, или возможностей P/Invoke не хватает? На помощь приходит C++/CLI.
Это идеальный язык для генерации glue code между managed и unmanaged средами исполнения, поскольку он позволяет генерировать код для обоих сред + генерирует transition code, избавляя нас от необходимости склеивать что-то вручную.
Суффикс CLI — обозначает то, что язык реализует спецификацию Common Language Infrastructure, т.е. является полноправным членом семейства языков платформы .NET
Итак, нам понадобится Visual C++ Express 2008. Кликаем на «New Project», выбираем тип проекта — CLR. Это создаст проект с по умолчанию выставленной опцией /clr (Use Common Language Runtime). Это означает, что компилятор сгенерирует корректную MSIL-сборку и даст нам использовать новый синтаксис — который мы сейчас и рассмотрим.
Crash course
Выделение managed/unmanaged-блоков
По умолчанию, компилятор считает весь C++-код проекта нацеленным на компиляцию в MSIL. Скорее всего, это не будет работать (не скомпилируется), а если и скомпилируется, то это с большой вероятностью не то, чего вам хотелось бы.
Специальные команды препроцессора позволяют указать, какую часть кода надо компилировать в x86, а какую — в MSIL.
/* ... управляемый код ... */
#pragma unmanaged
/* ... блок сырого С++ ... */
#pragma managed
/* ... снова управляемый код ... */
Скорее всего, если вы подключаете какую-то библиотеку, вам придется сначала скомпилировать её в статический .lib и не забыть обернуть её заголовки в блок
#pragma unmanaged
. Или собрать библиотеку в один большой .c-файл (еденицу трансляции) — как в SQLite amalgamation.Подключение MSIL-сборок
Снова препроцессор:
#using <System.dll>
#using "..\MyLocalAssembly.dll">
Namespaces
Здесь так же, как в обычном C++:
using namespace System::Collections::Generic;
Объявление value-типа
Это то, что в C# называется «struct».
public value class Vector
{
public:
int X;
int Y;
Vector (int x, int y) : X (x), Y (y) {}
};
Объявление reference-типа, методы, properties
public ref class Resource
{
public:
void PublicMethod () { ... }
property int SomeProperty
{
int get () { return ... }
void set (int value) { ... }
};
};
Кстати, что интересно, C++/CLI поддерживает некоторые фичи CLR, которые не реализованы в языке C#. Например, property indexers — можно определять индексаторы (оператор []) для отдельных свойств, а не только для класса целиком. Вот только такой код нельзя будет вызвать из C# :)
Объявление интерфейсного типа
То, что в C# называется «interface»:
public interface class IApplicationListener
{
void OnStart ();
void OnWait ();
void OnEnd ();
};
Enum-ы
public enum struct RenderMode
{
Normal = FT_RENDER_MODE_NORMAL,
Light = FT_RENDER_MODE_LIGHT,
Mono = FT_RENDER_MODE_MONO,
LCD = FT_RENDER_MODE_LCD
};
Жизнь внутри метода — базовый синтаксис
/* ссылка на GC-объект, nullptr — аналог null в C#
*/
System::String ^ string = nullptr;
/* Выброс исключения, gcnew — аналог new в C#, выделяет объект на управляемой куче
*/
throw gcnew System::Exception (L"Юникодная строка об ошибке");
Generics, type constraints, массивы
Здесь ключевое слово generic используется аналогично template в C++:
generic<typename T>
where T : value class
Buffer ^ CreateVertexBuffer (array<T> ^ elements)
{
/* Тип array<T> — CLR-массив, аналог T[] в C# */
}
Делаем обертку для неуправляемого ресурса
Очень частая задача. Используем паттерн IDisposable.
Обратите особое внимание, что С++-деструктор в ref-классе автоматически транслируется в метод Dispose (). Для финализаторов используется другой синтаксис.
public ref class Tessellator : System::IDisposable
{
internal: // эти поля не попадут в метаданные
Unmanaged::Tessellator * tess;
public:
Tessellator (int numSteps)
{
tess = new Unmanaged::Tessellator (numSteps);
}
~Tessellator () // IDisposable::Dispose ()
{
delete tess;
}
};
Получаем сырой указатель на GC-объект
Эта операция в архитектуре CLR называется «pinning». При «прибивании» объекту запрещается перемещаться в куче при сборке мусора и уплотнении кучи. Это позволяет неуправляемому коду воспользоваться адресом объекта и записать/прочитать что-нибудь по этому адресу.
Будьте осторожны, так как прибивание объектов мешает сборщику мусора и сильно фрагментирует кучу. Эта операция предназначена для лишь для кратковременных манипуляций с объектами кучи.
«Pinned»-указатели реализованы в C++/CLI как шаблонный RAII-тип
pin_ptr. Семантика похожа на std::auto_ptr (умный указатель, или смартпойнтер). При выходе экземпляра pin_ptr из области видимости, GC-объект автоматически отпиннится.
generic<typename T> where T : value class
Buffer ^ CreateVertexBuffer (array<T> ^ elements)
{
/* получаем указатель на начало массива
*/
pin_ptr<T> p = &(elements[0]);
/* получили сырой указатель,
* который можно передать в неуправляемый код
*/
void * address = p;
}
Маршаллинг строк
Конвертим System::String в wide char строку (wchar_t *):
#include <vcclr.h>
System::String ^ path = ...
/* получаем "прибитый" указатель прямо на содержимое String
*/
pin_ptr<const wchar_t> pathChars = PtrToStringChars (path);
Конвертим System::String в ANSI C строку (RAII-контейнер):
struct StringToANSI
{
private:
const char * p;
public:
StringToANSI (String ^ s) :
p ((const char*) ((Marshal::StringToHGlobalAnsi (s)).ToPointer ()))
{
}
~StringToANSI() { Marshal::FreeHGlobal (IntPtr ((void *) p)); }
operator const char * () { return p; }
};
Конвертим ANSI-строку в System::String:
const char * ptr = "ANSI string";
System::String ^ str = gcnew System::String (ptr);
Жонглируем ссылками на GC-объекты в unmanaged-коде
Часто возникает задача передать ссылку на управляемый объект куда-то в неуправляемый код. Или даже хранить её в поле неуправляемого объекта. Но компилятор C++/CLI устанавливает четкие границы сред и не поддерживает такую демократию. Поэтому, на помощь приходит вспомогательный контейнер gcroot:
#include <msclr/auto_gcroot.h>
#pragma unmanaged
class UnmanagedWindowCounterpart
{
private:
/* ссылка на управляемый объект
*/
gcroot<IInputEventListener ^> MouseEventListener;
...
};
Заключение
В этой статье я описал не всё, но уж точно самое необходимое. Остальное без труда находится в MSDN.
Happy coding!