Расширения языка C++/CLI

95

C++/CLI - это набор расширений языка C++, позволяющий создавать гибридные управляемые и низкоуровневые библиотеки DLL. С применением расширений C++/CLI вы сможете определять управляемые и неуправляемые классы и функции в пределах одного файла .cpp, использовать управляемые и низкоуровневые типы C и C++, как в обычном программном коде на C++, то есть простым подключением заголовочного файла и связыванием с библиотекой. Эти широчайшие возможности можно использовать для создания управляемых типов-оберток, пригодных для использования в любом языке .NET, а так же низкоуровневые классы-обертки и функции (доступные через файлы .dll, .lib и .h), пригодные для использования в программном коде на C/C++.

При использовании расширения C++/CLI маршалинг выполняется вручную, благодаря чему разработчик имеет более полный контроль и более полное представление о накладных расходах. Расширение C++/CLI с успехом можно использовать там, где механизм P/Invoke оказывается бесполезен, например, для маршалинга структур переменной длины. Еще одно преимущество расширения C++/CLI состоит в том, что оно позволяет имитировать интерфейс объединения запросов, даже если у вас нет доступа к исходным текстам вызывающего кода, многократно вызывая низкоуровневые методы без необходимости каждый раз пересекать границу между управляемым и неуправляемым кодом.

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

#include <msclr/marshal.h>
#include <string>
#include <wchar.h>
#include <time.h>


using namespace System;
using namespace System::Runtime::InteropServices;

class NativeEmployee {
    public:
        NativeEmployee(const wchar_t *employeeName, int age)
            : _employeeName(employeeName), _employeeAge(age) { }
        
        void DoWork(const wchar_t **tasks, int numTasks) {
            for (int i = 0; i < numTasks; i++) {
                wprintf(L"Пользователь %s работает в задаче %s\n",
                    _employeeName.c_str(), tasks[i]);
            }
        }
        
        int GetAge() const {
            return _employeeAge;
        }
        
        const wchar_t *GetName() const {
            return _employeeName.c_str();
        }
        
    private:
        std::wstring _employeeName;
        int _employeeAge;
};

#pragma managed

namespace EmployeeLib {
    public ref class Employee {
    {
    public:
        Employee(String ^employeeName, int age) {
            // Вариант 1:
            // IntPtr pEmployeeName = Marshal::StringToHGlobalUni(employeeName);
            // m_pEmployee = new NativeEmployee(
            // reinterpret_cast<wchar_t *>(pEmployeeName.ToPointer()), age);
            // Marshal::FreeHGlobal(pEmployeeName);
            
            // Вариант 2 (прямой указатель на закрепленную 
            // управляемую строку, самый быстрый):
            pin_ptr<const wchar_t> ppEmployeeName = PtrToStringChars(employeeName);
            _employee = new NativeEmployee(ppEmployeeName, age);
        }
        
        ~Employee() {
            delete _employee;
            _employee = nullptr;
        }
        
        int GetAge() {
            return _employee->GetAge();
        }
        
        String ^GetName() {
            // Вариант 1:
            // return Marshal::PtrToStringUni(
            // (IntPtr)(void *) _employee->GetName());
            
            // Вариант 2:
            return msclr::interop::marshal_as<String ^>(_employee->GetName());
            
            // Вариант 3 (самый быстрый):
            return gcnew String(_employee->GetName());
        }
        
        void DoWork(array<String^>^ tasks) {
            // marshal_context - это управляемый класс, размещаемый 
            // (в динамической памяти сборщика мусора) с использованием 
            // семантики, напоминающей стек. Его деструктор IDisposable::Dispose()
            // будет вызван после выхода из области видимости этой функции
            msclr::interop::marshal_context ctx;
            const wchar_t **pTasks = new const wchar_t*[tasks->Length];
            
            for (int i = 0; i < tasks->Length; i++) {
                String ^t = tasks[i];
                pTasks[i] = ctx.marshal_as<const wchar_t *>(t);
            }
            
            m_pEmployee->DoWork(pTasks, tasks->Length);
            
            // деструктор контекста освободит неуправляемую 
            // память, выделенную методом marshal_as
            delete[] pTasks;
        }
        
    private:
        NativeEmployee *_employee;
    };
}

Взглянув на код, можно увидеть, что конструктор Employee демонстрирует два способа преобразования управляемых строк в неуправляемый: первый основан на выделении памяти с помощью GlobalAlloc, которую необходимо будет освободить явно, а второй временно закрепляет управляемую строку в памяти и возвращает прямой указатель. Второй способ выполняется быстрее, но его можно использовать, только когда неуправляемый код принимает строки, завершающиеся нулевым символом, в кодировке UTF-16, и он не записывает ничего в память по указателю. Кроме того, закрепление управляемых объектов на длительное время может привести к фрагментации памяти, поэтому, если перечисленные требования не удовлетворяются, вам придется прибегнуть к копированию строк.

Метод GetName() класса Employee демонстрирует три способа преобразования неуправляемых строк в управляемые: первый основан на использовании класса System.Runtime.InteropServices.Marshal, второй использует функцию marshal_as, объявленную в заголовочном файле msclr/marshal.h, и, наконец, третий использует конструктор класса System.String, являющийся наиболее быстрым.

Метод DoWork() класса Employee принимает управляемый массив (или управляемые строки) и преобразует его в массив указателей типа wchar_t, указывающих на строки; фактически это массив строк в стиле языка C. Преобразование управляемых строк в неуправляемые выполняется с применением метода marshal_as класса объекта marshal_context. В отличие от глобальной функции marshal_as, объект marshal_context используется для преобразований, требующих освобождения занимаемых при этом ресурсов. Обычно для преобразования управляемых данных в неуправляемые метод marshal_as выделяет неуправляемую память, которую следует освободить после выполнения операции. Объект marshal_context содержит связанный список операций освобождения ресурсов, которые выполняются в момент уничтожения объекта.

Подводя итоги можно сказать, что расширение C++/CLI обеспечивает полный контроль над маршалингом и не требует дублирования объявлений функций, что чревато ошибками, особенно когда часто приходится изменять сигнатуры неуправляемых функций.

Вспомогательная библиотека marshal_as

В этом разделе мы остановимся на вспомогательной библиотеке marshal_as, входящей в состав версии Visual C++ 2008 и выше. marshal_as - это библиотека шаблонов, упрощающая реализацию маршалиига управляемых типов в неуправляемые и обратно. Она способна преобразовывать многие неуправляемые строковые типы, такие как char*, wchar_t*, std::string, std::wstring, CStringT<char>, CStringT<wchar_t>, BSTR, bstr_t и CComBSTR, в управляемые типы и обратно. Она способна автоматически выполнять преобразование символов Юникода и ANSI, а также выделять и освобождать память.

Библиотека объявлена и реализована в файлах marshal.h (базовые типы), marshal_windows.h (типы Windows), marshal_cppstd.h (типы данных STL) и marshal_atl.h (типы данных ATL).

Имеется возможность расширять библиотеку marshal_as реализацией преобразований пользовательских типов. Это помогает избежать дублирования кода, когда требуется организовать маршалинг одного и того же типа во многих местах в программе, и дает возможность обеспечить единообразие синтаксиса маршалинга разных типов.

В следующем фрагменте демонстрируется пример расширения библиотеки marshal_as поддержкой преобразования управляемого массива строк в эквивалентный неуправляемый массив строк:

namespace msclr {
    namespace interop {
        template<>
        ref class context_node<const wchar_t**, array<String^>^> 
            : public context_node_base 
        {
        private:
            const wchar_t** _tasks;
            marshal_context _context;
            
        public:
            context_node(const wchar_t**& toObject, array<String^>^ fromObject) 
            {
                // здесь начинается логика преобразования
                _tasks = NULL;
                const wchar_t **pTasks = 
                    new const wchar_t*[fromObject->Length];
                    
                for (int i = 0; i < fromObject->Length; i++) 
                {
                    String ^t = fromObject[i];
                    pTasks[i] = _context.marshal_as<const wchar_t *>(t);
                }
                
                toObject = _tasks = pTasks;
            }
            
            ~context_node() {
                this->!context_node();
            }
            
        protected:
            !context_node() 
            {
                // При удалении контекста будет освобождена память, 
                // выделенная для строк (и принадлежащая marshal_context),
                // поэтому массив - единственная память, 
                // которую требуется освободить
                if (_tasks != nullptr) {
                    delete[] _tasks;
                    _tasks = nullptr;
                }
            }
        };
    }
}


// Теперь можно переписать метод Employee::DoWork:
void DoWork(array<String^>^ tasks) 
{
    // Вся неуправляемая память освобождается автоматически, 
    // как только marshal_context выйдет из области видимости
    msclr::interop::marshal_context ctx;
    _employee->DoWork(ctx.marshal_as<const wchar_t **>(tasks), tasks->Length);
}

Код на языке IL и неуправляемый код

Неуправляемый класс по умолчанию будет скомпилирован расширением C++/CLI в код на языке IL, а не в машинный код. Это может ухудшать производительность в сравнении с оптимизированным машинным кодом, потому что компилятор Visual C++ способен оптимизировать код лучше, чем JIT-компилятор.

Чтобы повлиять на процедуру компиляции можно, добавив объявление #pragma unmanaged или #pragma managed перед требуемым разделом кода. Кроме того, в проектах VC++ имеется возможность включать поддержку C++/CLI для отдельных единиц компиляции (файлов .cpp).

Эффективные приемы взаимодействий

Итак, в этой и предыдущих статьях вы познакомились с небезопасным кодом, особенностями реализации различных механизмов взаимодействий, влиянием на производительность каждой из особенностей и узнали, как можно ослабить это влияние. Вашему вниманию были представлены наиболее эффективные приемы увеличения производительности операций взаимодействий и упрощения их реализации (например, с помощью библиотеки marshal_as и генератора сигнатур для P/Invoke). Давайте подытожим все приведенные ранее советы, относительно небезопасного кода.

Ниже перечислены наиболее эффективные приемы реализации взаимодействий:

Пройди тесты
Лучший чат для C# программистов