Расширения языка C++/CLI
95C# и .NET Framework --- Оптимизация приложений .NET Framework --- Расширения языка C++/CLI
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). Давайте подытожим все приведенные ранее советы, относительно небезопасного кода.
Ниже перечислены наиболее эффективные приемы реализации взаимодействий:
проектируя интерфейсы, старайтесь сводить к минимуму количество переходов через границу между управляемым и неуправляемым кодом, например, объединяя задания;
уменьшайте количество взаимодействий, совмещая несколько вызовов простых функций в одном вызове;
реализуйте интерфейс IDisposable, если неуправляемые ресурсы сохраняются между вызовами;
используйте пулы памяти и выделяйте блоки неуправляемой памяти;
используйте небезопасный код для интерпретации данных (например, в сетевых протоколах);
явно именуйте вызываемые функции и используйте ExactSpelling=true;
используйте параметры двоично совместимых типов, где это возможно;
избегайте преобразования Юникода в ANSI, когда это возможно;
вручную преобразуйте строки в/из IntPtr;
используйте расширение C++/CLI, обеспечивающее лучшие управляемость и производительность при взаимодействиях с кодом на C/C++ и COM-объектами;
указывайте атрибуты [In] и [Out], чтобы избежать ненужного маршалинга;
избегайте закрепления долгоживущих объектов;
используйте метод ReleaseComObject при необходимости;
используйте атрибут SuppressUnmanagedCodeSecurityAttribute в окружениях, заслуживающих доверия;
используйте утилиту tlbimp.exe с ключом /unsafe в окружениях, заслуживающих доверия;
избегайте или старайтесь уменьшать количество вызовов через границы подразделений COM;
при возможности используйте атрибут AspCompat в приложениях ASP.NET, чтобы уменьшить количество вызовов через границы подразделений COM.