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

    В этом топике я расскажу про 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!
    Поделиться публикацией
    Комментарии 17
      +1
      На втором курсе с помощью managed c++ смешал расчет формулы в Maple и визуализацию на C#. Но сейчас, хоть убей, c++ забыл как страшный сон.
        +1
        ну вот оно как раз для таких задач — смешать и забыть

        и ведь все равно находятся люди, которые недоумевают, зачем нужен C++/CLI, пытаются сравнивать его с C# и прочей такой херью занимаются :)
          +1
          C++ /cli нужен в первую очередь для «легкой» адаптации легаси C++ кода под нет, а уже потом для случаев когда с помощью P/Invoke задачу тяжело решать. Было бы неплохо еще сказать что вначале была попытка с managed c++ но она успешно провалилась. На мой взгляд в первую очередь из-за дикого синтаксиса.
          0
          Счастливчик :) Иногда всетаки приходиться смешивать. Особенно если хочешь написать плагин для нативной программы…
          0
          -> Но что, если у библиотеки нет C-интерфейса, или возможностей P/Invoke не хватает?

          А можете привести пример, когда возможности P/Invoke не хватает?..
            0
            Ну на самом деле автор видимо имел ввиду случаи когда нужно импортировать действительно много функций. А C++ /cli нужен в первую очередь для удобного использования старого плюсового кода в нете.
            • НЛО прилетело и опубликовало эту надпись здесь
                0
                Когда у вас есть нативные классы.
                +4
                Было бы неплохо раз уж сказали о финалайзерах — рассказать как их использовать (синтаксис). Интересно также что стандартная для C# реализация IDisposable в С++ /CLI может несколько удивить, а если глянуть в сгенеренный код, то и шокировать =)

                А вообще на эту тему есть очень хорошая книжка — C++ /CLI in Action.
                  0
                  Спасибо! Очень актуально.

                  Не знаете, а если ли что-то подобное для Java? Мне нужно использовать огромное количество сишных и фортрановских библиотек — все не перепишешь. Кстати, как обстноит дело с не элементарными типами? std::complexоно нормальнео переварит?
                    0
                    Смотря что значит «переварит». Использовать в плюсах — можно, вытащить наружу (чтобы из нета была видна) — нет, для этого нужно класс описывать специальным образом.
                      0
                      Ну вот и всё, собственно. Каждый внутренний класс библиотки переписывать это всё равно что её саму переписать. А если там каждый класс и функция на шаблонах, то я так понимаю что никак её не скрестишь.
                        +1
                        Что — все? Чудес то не бывает, для каждой задачи есть свои способы решения. Нужны несколько классов из плюсов — пишите менеджет враппер, нужны классы из нета — используйте плюсы, а из них нетовский фреймворк. Первоочередная цель создания C++ /CLI — облегчить переход с нативных плюсов к дот нету. А шаблонов в плюсовом понимании в нете дейсвительно нету, но это не мешает их использовать с менеджет классами.
                      +3
                      В Java есть JNI
                        +1
                        Зависит от конкретной задачи. Если вам немного Java и много C++ — для вас есть CNI, если наоборот — тогда JNI
                          0
                          Нет и не надо. Используйте JNI.

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое