Pull to refresh

C++/CLI — «клейкий» язык

Reading time 5 min
Views 40K
В этом топике я расскажу про C++/CLI — промежуточный язык для склеивания кода на C/C++ и .NET

Это довольно распространённая задача, ведь на 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!
Tags:
Hubs:
+40
Comments 17
Comments Comments 17

Articles