Сообщений 1    Оценка 80        Оценить  
Система Orphus

Взаимодействие управляемого и неуправляемого кода

Автор: Сергей Тепляков
ООО НПП Кронос

Источник: RSDN Magazine #3-2008
Опубликовано: 12.02.2009
Исправлено: 10.12.2016
Версия текста: 1.0
Введение
1 Общие принципы
2 Доступ к управляемому коду из неуправляемого
3 Обработка исключений
4 Работа с делегатами
5 Оболочка над log4net
Заключение

Демонстрационные проекты

Введение

Появление .Net Framework значительно упростило создание многих видов приложений. Благодаря богатой библиотеке отпала необходимость в создании большого количества «велосипедов», которые до этого приходилось писать многим из нас. Однако по-прежнему существует огромное количество неуправляемого кода, написанного на «голом» С++. Этот код ничего не знает и знать не может об .Net Framework. Многие из таких приложений переписываются с использованием управляемого кода, и у многих разработчиков появляется необходимость смешивать управляемый и неуправляемый код.

О том, как взаимодействовать с неуправляемым кодом из управляемого, написано достаточно много, и это неудивительно, поскольку именно эта задача является наиболее распространенной в смешанных приложениях. Но встречается и обратная ситуация, когда неуправляемое приложение (консольное приложение, служба или приложение, написанное с использование MFC) сталкивается с необходимостью обратиться к некоторой управляемой библиотеке, написанной под .Net Framework. Как быть? Переписывать заново нет ни времени, ни возможности, перекомпилировать с использованием ключа /clr тоже не получается.

В данной статье я опишу общие принципы решения задачи обращения из неуправляемого кода к управляемому, а также реализую оболочку для работы с распространенной библиотекой log4net.

1 Общие принципы

Реализация смешанных приложений (т.е. содержащих управляемый и неуправляемый код) основана на том, что среда CLR поддерживает конструкции, не входящие в общую систему типов CLR, путем указания того, что некоторый тип является непрозрачным (opaque type). Метаданные непрозрачных типов не содержат никакой дополнительной информации о типе, вместо этого хранится только размер экземпляра в памяти.

Языки C# и Visual Basic .Net, в отличие от C++, не поддерживают создание непрозрачных типов. По умолчанию при компиляции проекта с ключом /clr все классы и структуры С++ создаются как непрозрачные. Среда CLR делает это для сохранения строгой семантики С++, в результате чего любая программа на С++ может быть перекомпилирована в модуль CLR и нормально выполнена. Чтобы указать, что тип не является непрозрачным (т.е. для создания управляемого типа), в определении типа С++ используется дополнительный модификатор (ref class, value class и др.).

Поскольку среда CLR ничего не знает о полях непрозрачных типов, возникают проблемы при работе со ссылками на управляемые объекты.

      class OpaqueObject
{
private:
  Object^ managed_;
};

При попытке компиляции этого кода вы получите следующее сообщение об ошибке: “error C3265: cannot declare a managed 'managed_' in an unmanaged OpaqueObject”. Проблема в том, что управляемый объект располагается в управляемой куче, и сборщик мусора никак не может узнать о том, что где-то в неуправляемой памяти осталась ссылка на этот объект. Если бы компиляция и выполнение этого кода были возможны, то в произвольный момент времени (после сборки мусора) в неуправляемой памяти остался бы указатель на удаленный объект. А вы ведь знаете, к чему приводит разыменовывание указателя, который указывает на удаленный объект?

Для решения этой проблемы среда CLR предоставляет тип System::Runtime::InteropServices::GCHandle, который поддерживает работу со ссылками на управляемые объекты из неуправляемой памяти. Для создания нового экземпляра GCHandle применяется статическая функция GCHandle::Alloc. В результате выполнения этой функции создается новая ссылка на управляемый объект, и дескриптор ссылки сохраняется в экземпляре типа GCHandle. Тип GCHandle поддерживает полный перечень типов ссылок на управляемый объект, включая слабые (weak references) и зафиксированные (pinned references) ссылки. Любая программа может преобразовать тип GCHandle в IntPtr (который можно безопасно хранить в непрозрачном типе) и обратно. Экземпляр типа GCHandle (и ссылка на управляемый объект) существуют до тех пор, пока не будет явно вызван метод GCHandle.Free.

Приведенный ниже С++-класс демонстрирует использование типа GCHandle для взаимодействия управляемого и неуправляемого кода.

      #include <iostream>
#include <stddef.h>
#include <gcroot.h>

// Компилируется с ключом /clr// "Непрозрачный" или неуправляемый класс, который использует GCHandle// для доступа к управляемому объекту.class OpaqueObject
{
public: //   Конструкторы и деструкторы
  OpaqueObject(Object^ managed)
  {
    GCHandle handle = GCHandle::Alloc(managed);
    managedHandle_ = (intptr_t)GCHandle::ToIntPtr(handle);
  }
  ~OpaqueObject()
  {
    GCHandle handle = GCHandle::FromIntPtr((IntPtr)managedHandle_);
    handle.Free();
  }
public: // Public-интерфейсint GetHashCode() const
  {
    GCHandle handle = GCHandle::FromIntPtr((IntPtr)managedHandle_);
    Object^ managedObject = handle.Target;
    return managedObject->GetHashCode();
  }
private: // Private-поля
  intptr_t managedHandle_;
};
int main(array<System::String ^> ^args)
{
  String^ str = gcnew String("Hello, Managed!");
  OpaqueObject opaque(str);
  std::cout<<"opaque.GetHashCode(): "<<opaque.GetHashCode()<<std::endl;
  std::cin.get();
  return 0;
}

Поскольку работа с типом GCHandle может быть несколько утомительной, более разумным способом доступа к управляемому объекту из неуправляемой памяти является использование класса gcroot<T>, который реализован в заголовочном файле gcroot.h. Класс gcroot<T> является своего рода «умным указателем», работа которого основана на использовании типа GCHandle. В конструкторе класса gcroot<T> вызывается функция GCHandle::Alloc, а в деструкторе вызывается GCHandle::Free. Кроме того, gcroot поддерживает оператор неявного преобразованию к типу T, оператор разыменовывания gcroot->, а также реализует конструктор копирования и оператор присваивания. Используя gcroot<T>, можно переписать предыдущий пример следующим образом:

      // Компилируется с ключом /clr
      // "Непрозрачный" или неуправляемый класс, который использует gcroot<Object^>
      // для доступа к управляемому объекту.
      class OpaqueObject
{
public: //   Конструкторы и деструкторы
  OpaqueObject(Object^ managed)
    : managed_(managed)
  {}
public: // Public-интерфейсint GetHashCode() const
  {
    return managed_->GetHashCode();
  }
private: // Private-поля
  gcroot<Object^> managed_;
};

Как видите, получить доступ к управляемому коду из неуправляемого не так и сложно. Но для того, чтобы этот доступ был возможен при компиляции приложения без ключа /clr, придется немного потрудиться.

2 Доступ к управляемому коду из неуправляемого

В предыдущем разделе описаны основные принципы взаимодействия управляемого и неуправляемого кода. Но все примеры компилировались с ключом компилятора /clr, но как быть, если это невозможно? Что делать, если имеется консольное Win32-приложение, служба или приложение, разработанное с использованием MFC, но нет возможности перекомпилировать его с ключом /clr?

Разработаем dll-оболочку для набора управляемых классов, которая будет экспортировать неуправляемый интерфейс. Это позволит использовать эту библиотеку без ключа компиляции /clr (например, в простом консольном приложении), хотя для выполнения такого приложения .Net Framework, конечно же, понадобится (рисунок 1).


Рисунок 1. Взаимодействие управляемого и неуправляемого кода.

Для начала рассмотрим набор управляемых классов, написанных на языке C#.

      // Class1.cs
      public
      class Class1
{
  public Class1(int i, string str)
  {
    this.i = i;
    this.str = str;
  }

  publicvoid ShowMessageBox()
  {
    MessageBox.Show(ToString(), "Hello from managed world!");
  }
  public override string ToString()
  {
    return String.Format("i = {0}, str = {1}", i, str);
  }

  privateint i;
  private string str;
}

Этот класс ничего не делает, это всего лишь пример, с помощью которого можно проверить взаимодействие управляемого и неуправляемого кода.

      // Class2.cs
public
      class Class2
{
  public Class2(Class1 class1)
  {
    this.class1 = class1;
  }

  publicvoid ShowMessageBox()
  {
    System.Windows.Forms.MessageBox.Show("Message from Class2", "Hello from managed world");
    class1.ShowMessageBox();
  }
  private Class1 class1;
}

Класс Class2 также не слишком сложен, но он принимает другой управляемый класс в качестве параметра конструктора.

Наиболее простой способ получить доступ к управляемому объекту из неуправляемого класса – воспользоваться интеллектуальным указателем gcroot<T>.

      #include <vcclr.h>
class ManagedObject
{
public:
  ManagedObject(Object^ managed);
private:
  gcroot<Class1 ^> managed_;
};

Этот код компилируется и прекрасно работает, но для его компиляции необходимо использовать ключ /clr, поэтому он не может быть использован в неуправляемом приложением.

Для решения этой проблемы можно воспользоваться тем, что gcroot<T> является тонкой оболочкой над указателем, и единственным членом этого класса является intptr_t. Поэтому экземпляр класса gcroot<T> и intptr_t гарантированно имеют одинаковое представление в памяти.

Другой особенностью управляемых приложений является то, что при их компиляции константа _MANAGED определена и равна 1.

При всем этом класс ManagedObject будет выглядеть следующим образом.

      #ifdef _MANAGED
#include <vcclr.h>
#define GCROOT(T) gcroot<T^>
#else#define GCROOT(T) intptr_t
#endif//_MANAGEDclass ManagedObject
{
public:
  ManagedObject(Object^ managed);
private:
  GCROOT(Class1) managed_;
};

Но проблема еще решена не полностью. Дело в том, что когда клиент из неуправляемого кода создает экземпляр класса ManagedObject, компилятор автоматически генерирует встроенные функции конструктора копирования и оператора присваивания, которые реализуют побитовое копирование объекта. Но побитовое копирование intptr_t приведет к тому, что два объекта gcroot<T> будут содержать один и тот же дескриптор управляемого объекта. В таком случае при удалении второго экземпляра gcroot<T> вы получите неопределенное поведение, что в большинстве случаев выражается в нарушении доступа к памяти.

Решение задачи состоит в том, чтобы объявить экспортируемые функции конструктора копирования и оператора присваивания в заголовочном файле, а реализовать их в соответствующем cpp-файле.

      //ManagedObject.h
      #ifdef _MANAGED
#using <mscorlib.dll>
#using <System.dll>
#include <vcclr.h>
#endif//_MANAGED#ifdef _MANAGED
#define GCROOT(T) gcroot<T^>
#else#define GCROOT(T) intptr_t
#endif//_MANAGED#ifdef _MANAGED
//Экспортировать функции этих классов нужно только из управляемого кода#define DLL_EXPORT _declspec(dllexport) 
#else#define DLL_EXPORT 
#endif//_MANAGEDclass ManagedObject
{
  //Нужно запретить все попытки создания объетков этого класса//из неуправляемого кода
  DLL_EXPORT ManagedObject();
public: //Public-интерфейс#ifdef _MANAGED
  //Эти функции существуют только в управляемом коде
  ManagedObject(System::Object^ managed);
  System::Object^ GetObject() const {return managed_;}
#endif//_MANAGED//Объявлены в этом файле, реализованы в ManagedObject.cpp
  DLL_EXPORT ManagedObject(const ManagedObject& rhs);
  DLL_EXPORT ManagedObject& operator=(const ManagedObject& rhs);
private: //Private-поля//Разворачивается в gcroot<System::Object^> или в intptr_t, которые//имеют одинаковое представление в памяти и в native и в managed коде
  GCROOT(System::Object) managed_;
};

//ManagedObject.cpp//Находится в проекте SampleManagedAssemblyWrapper и компилируется с ключем /clr#include <ManagedObject.h>

ManagedObject::ManagedObject(System::Object^ managed)
  : managed_(managed)
{}

ManagedObject::ManagedObject(const ManagedObject& rhs)
  : managed_(rhs.managed_)
{
}
ManagedObject& ManagedObject::operator=(const ManagedObject& rhs)
{
  managed_ = rhs.managed_;
  return *this;
}

Тогда, если в неуправляемом коде встретится выражение следующего вида:

ManagedObject obj1 = …
ManagedObject obj2 = obj1;

управление будет передано в dll, и в режиме управляемого выполнения будет вызван конструктор копирования класса gcroot<T>.

С помощью класса ManagedWrapper написать оболочку управляемого класса не составит труда. Но лучше создать несколько макросов, ускоряющих этот процесс.

      #ifdef _MANAGED
//Этот макрос разворачивается только в управляемом коде//В результате получаем gcroot<Object^> и вспомогательный метод, возвращающий//управляемый объект указанного типа#define DECLARE_WRAPPER(MANAGED_TYPE) \
public: \
    MANAGED_TYPE^ GetManaged() const \
    {\
    returnstatic_cast<MANAGED_TYPE^>(managed_.GetObject()); \
    }\
private: \
    ManagedWrapper::ManagedObject managed_; \

#else//В неуправляемом коде виден только intptr_t#define DECLARE_WRAPPER(MANAGED_TYPE) \
private: \
    ManagedWrapper::ManagedObject managed_; \

#endif

При наличии класса-оболочки управляемого объекта и макроса DECLARE_WRAPPER реализация оболочки над Class1 становится тривиальной.

      //Class1Wrapper.h
      //Оболочка управляемого класса Class1
      class Class1Wrapper
{
  // Конструкторы и деструкторы
  DECLARE_WRAPPER(SampleManagedAssembly::Class1)public: 
  DLL_EXPORT Class1Wrapper(int i, constchar* str);
public: //Public-интерфейс
  DLL_EXPORT void ShowMessageBox();
};

//Class1Wrapper.cpp
//Находится в проекте SampleManagedAssemblyWrapper и компилируется с ключем /clr
#include"Class1Wrapper.h"

Class1Wrapper::Class1Wrapper(int i, constchar* str)
  : managed_(gcnew SampleManagedAssembly::Class1(i, gcnew System::String(str)))
{}
void Class1Wrapper::ShowMessageBox()
{
  GetManaged()->ShowMessageBox();
}

Ненамного сложнее оболочка для класса Class2.

      //Class2Wrapper.h
      //Оболочка управляемого класса Class2
      class Class2Wrapper
{
  // Конструкторы и деструкторы
  DECLARE_WRAPPER(SampleManagedAssembly::Class2)public: 
  DLL_EXPORT Class2Wrapper(const Class1Wrapper &class1);
public: //Public-интерфейс
  DLL_EXPORT void ShowMessageBox();
};

//Class2Wrapper.cpp
//Находится в проекте SampleManagedAssemblyWrapper и компилируется с ключем /clr
#include"Class2Wrapper.h"

Class2Wrapper::Class2Wrapper(const Class1Wrapper &class1)
: managed_(gcnew SampleManagedAssembly::Class2(class1.GetManaged()))
{
}

void Class2Wrapper::ShowMessageBox()
{
  GetManaged()->ShowMessageBox();
}

Использовать классы оболочек достаточно просто.

      //UnmanagedTest.cpp
//Находится в проекте UnmanagedTest 
// и компилируется без ключа /clr
      #include <iostream>
#include"..\SampleManagedAssemblyWrapper\Class1Wrapper.h"#include"..\SampleManagedAssemblyWrapper\Class2Wrapper.h"#pragma comment(lib, "SampleManagedAssemblyWrapper")

int _tmain(int argc, _TCHAR* argv[])
{
  Class1Wrapper c1(12, "Empty");
  Class2Wrapper c2(c1);
  c1.ShowMessageBox();
  c2.ShowMessageBox();
  std::cin.get();
	return 0;
}

В результате выполнения этого кода получим следующий результат (рисунок 2).


Рисунок 2. Работа с классами-оболочками из неуправляемого кода.

Во время разработки смешанных приложений я столкнулся с проблемой отладки. Для отладки dll-оболочки из неуправляемого приложения необходимо установить значение DebuggerType в Mixed. Значение по умолчанию (Auto) определяет тип загружаемого отладчика по EXE-файлу, поэтому при запуске неуправляемого EXE-файла IDE запускает неуправляемый отладчик, и отлаживать управляемый код нельзя. А когда вы указываете значение Mixed, IDE загружает оба отладчика, и проблемы с отладкой управляемых модулей не возникает.

3 Обработка исключений

При создании оболочек над классами Class1 и Class2 не была учтена одна важная деталь – обработка исключений. А что будет, если операция в управляемом коде завершится неудачно и будет сгенерировано исключение? Ответ простой – приложение «рухнет».

Рассмотрим класс Class3, который определяет свойство Age, причем значение этого свойства проверяется при установке, и, если оно больше 150 или меньше 0, генерируется исключение System::AgrumentException.

      // Class3.cs
public
      class Class3
{
  public Class3()
  {}
  
  publicint Age
  {
    get { return age; }
    set
    {
      if (value > 150 || value < 0)
        thrownew ArgumentException("Age can't be more than 150 or less then 0");
      age = value;
    }
  }
  
  privateint age;
}

Первое, что нужно сделать – это определить неуправляемый класс исключения, который будет в управляемом коде принимать System::ArgumentException в качестве параметра и предоставлять текстовое описание ошибки для неуправляемого кода.

      //ArgumentException.h
      //Оболочка над System::ArgumentException
      class ArgumentException
{
public:
#ifdef _MANAGED
  ArgumentException(System::ArgumentException^ e);
#endif//_MANAGED
  DLL_EXPORT ArgumentException(
    const std::string &message, const std::string &argument);
public:
  DLL_EXPORT std::string What() const;
private:
  std::string message_;
  std::string paramName_;
};

//ArgumentException.cpp
//Находится в проекте SampleManagedAssemblyWrapper 
// и компилируется с ключом /clr
#include"ArgumentException.h"//Преобразование System::String в std::string
std::string to_native_string(System::String^ source)
{
  if ( source == nullptr )
    return std::string();
  size_t i;
  size_t len = (( source->Length + 1) * 2);
  char *ch = newchar[len];
  bool result ;
  {
    pin_ptr<const wchar_t> wch = PtrToStringChars( source );
    result = wcstombs_s(&i, ch, len, wch, len ) != -1;
  }
  std::string res(ch);
  delete ch;
  return res;
}

ArgumentException::ArgumentException(System::ArgumentException^ e)
: message_(to_native_string(e->Message))
, paramName_(to_native_string(e->ParamName))
{
}
ArgumentException::ArgumentException(
  const std::string &message, const std::string &paramName)
  : message_(message)
  , paramName_(paramName)
{}

std::string ArgumentException::What() const
{
  return message_ + " " + paramName_;
}

Все, что нужно сделать в Class3Wrapper – в функции SetAge() перехватить управляемое исключение и сгенерировать соответствующее неуправляемое исключение. Если функция в управляемом коде может генерировать несколько исключений, то нужно создать оболочки для всех этих исключений, или просто перехватить исключение System::Exception. Это уже зависит от потребностей вашего приложения.

      // Class3Wrapper.h
      // Оболочка управляемого класса Class3
      class Class3Wrapper
{
  // Конструкторы и деструкторы
  DECLARE_WRAPPER(SampleManagedAssembly::Class3)public: 
  DLL_EXPORT Class3Wrapper();
public: // Public-интерфейс
  DLL_EXPORT int GetAge() const;
  DLL_EXPORT void SetAge(int age) /*throw ArgumentException*/;
}; 

// Class3Wrapper.cpp// Находится в проекте SampleManagedAssemblyWrapper
// и компилируется с ключем /clr#include"Class3Wrapper.h"

Class3Wrapper::Class3Wrapper()
  : managed_(gcnew SampleManagedAssembly::Class3())
{}


int Class3Wrapper::GetAge() const
{
  return GetManaged()->Age;
}
void Class3Wrapper::SetAge(int age) /*can throw ArgumentException*/
{
  try
  {
    GetManaged()->Age = age;
  }
  catch(System::ArgumentException^ e)
  {
    throw ArgumentException(e);
  }
}

Class3Wrapper используется аналогично любому неуправляемому классу, функция которого может генерировать исключение.

      // UnmanagedTest.cpp
// Находится в проекте UnmanagedTest 
// и компилируется без ключа /clr
      try
{
  Class3Wrapper c3;
  c3.SetAge(10);
  std::cout<<"Age: "<<c3.GetAge()<<std::endl;
  c3.SetAge(240);
}
catch(ArgumentException &e)
{
  std::cout<<"Error: "<<e.What()<<std::endl;
}

4 Работа с делегатами

Еще одной особенностью .Net Framework, с которой может столкнуться разработчик оболочек, являются делегаты и события.

Рассмотрим класс Class4, который реализует событие SampleEvent.

      // Class4.cs
public
      class Class4
{
  public Class4()
  {}

  publicdelegatevoid SampleEventHandler(int i);
  publicevent SampleEventHandler SampleEvent;
      publicevent EventHandler<EventArgs> SampleEvent2;  
      publicvoid FireSampleEvent(int i)
  {
    if (SampleEvent != null)
      SampleEvent(i);
  }
      
      publicvoid FireSampleEvent2()
      {
            if (SampleEvent2 != null)
                SampleEvent2(this, EventArgs.Empty);
      }
}

Там, где в управляемом коде используются делегаты, в неуправляемом коде применяются различные варианты обратного вызова. В С++ это могут быть указатели на функцию, указатели на функцию-член, boost::function (а теперь уже и std::tr1::function, для этого нужно скачать с сайта Microsoft Visual C++ 2008 Feature Pack) или любой другой функциональный объект.

Для простоты я воспользовался указателем на функцию, вы же в реальном приложении можете воспользоваться любым другим вариантом, на ваше усмотрение.

      //Class4Wrapper.h
      //Оболочка управляемого класса Class4
      class Class4Wrapper
{
public:
  // Я определил указатель на функцию, вы можете воспользоваться// указателем на функцию-член, boost::function, std::tr1::function// или любой другой вариацией на тему обратного вызова в С++typedefvoid (*SampleEventHandler)(int i);
  DLL_EXPORT Class4Wrapper(SampleEventHandler eventHandler);
  //...  
  DLL_EXPORT void FireSampleEvent(int i);
      //...private:
      SampleEventHandler eventHandler_;
};

// Class4Wrapper.cpp// Находится в проекте SampleManagedAssemblyWrapper 
// и компилируется с ключем /clr#include"Class4Wrapper.h"usingnamespace System;

// Вспомогательный класс, без которого не удастся создать обработчик события
ref class EventHandlerWrapper
{
public:
  EventHandlerWrapper(Class4^ managed, Class4Wrapper *nativeTarget)
        : nativeTarget_(nativeTarget)
  {
    // подписываюсь на событие управляемого класса
    managed->SampleEvent += gcnew Class4::SampleEventHandler(
      this, &EventHandlerWrapper::SampleEventHandler);
  }
  ~EventHandlerWrapper()
  {}
private:
  void SampleEventHandler(int i)
  {
            nativeTarget_->EventHandler(i);
  }
private:
        Class4Wrapper* nativeTarget_;
};

Class4Wrapper::Class4Wrapper(SampleEventHandler eventHandler)
: managed_(gcnew SampleManagedAssembly::Class4())
, eventHandler_(eventHandler){
  // переменная-член типа EventHandlerWrapper не нужна, т.к.// указатель на этот объект сохраняется в классе Class4 при подписке// на событие SampeEvent, поэтому объект класса EventHandlerWrapper// будет удален только вместе с объектом класса Class4
  gcnew EventHandlerWrapper(GetManaged(), eventHandler);
}
void Class4Wrapper::EventHandler(int i)
{
      // вызываю неуправляемую функцию обратного вызоваif ( eventHandler_ != NULL )
          (eventHandler_)(i);
}
void Class4Wrapper::FireSampleEvent(int i)
{
  GetManaged()->FireSampleEvent(i);
}

// UnmanagedTest.cpp// Находится в проекте UnmanagedTest 
// и компилируется без ключа /clrvoid Class4WrapperEventHandler(int i)
{
  std::cout<<"EventHandler in native code!"<<std::endl
    <<"i = "<<i<<std::endl;
}
//...  
Class4Wrapper c4(Class4WrapperEventHandler);
c4.FireSampleEvent(12);

К сожалению, без определения дополнительного управляемого класса никак не обойтись. Это связано с тем, что при создании делегата нужно передать указатель на функцию-член управляемого класса. Указатель на неуправляемую функцию не может быть передан в качестве аргумента.

В Visual C++ есть заголовочный файл \Msclr\Event.h – универсальное решение этой проблемы. Определения из этого файла можно использовать всякий раз, когда нужно обрабатывать .Net-событие в методе неуправляемого C++-класса. Недостатком этого решения является необходимость компиляции с ключом /clr, что в нашем случае является неприемлемым ограничением.

Я немного исправил этот набор классов и макросов. Это позволило обрабатывать .Net-события в неуправляемом коде, с возможностью компиляции без ключа /clr.

С использованием вспомогательных макросов, Class4Wrapper будет реализован следующим образом.

      // Class4Wrapper.h
      // Оболочка управляемого класса Class4
      class Class4Wrapper
{
      DECLARE_WRAPPER(SampleManagedAssembly::Class4)
      // Начало карты делегатов
      BEGIN_DELEGATE_MAP_WRAPPER(Class4Wrapper)
      // Используется для обработки событий с двумя аргументами
      EVENT_DELEGATE_ENTRY_WRAPPER(EventHandler2, System::Object^, System::EventArgs^)
      // Используется для обработки событий с одним аргументом
      EVENT_DELEGATE_ENTRY_WRAPPER1(EventHandler, int)
      // Окончание карты делегатов
      END_DELEGATE_MAP_WRAPPER()
      //...public:
#ifdef _MANAGED
      // Обработчики управляемых событий должны быть видны только// управляемому кодуvoid EventHandler2(System::Object^, System::EventArgs^);
      void EventHandler(int);
#endifprivate: //Private-поля
      SampleEventHandler eventHandler_;
      SampleEventHandler2 eventHandler2_;
};

// Class4Wrapper.cpp// Находится в проекте SampleManagedAssemblyWrapper // и компилируется с ключем /clr
Class4Wrapper::Class4Wrapper(
  SampleEventHandler eventHandler, SampleEventHandler2 eventHandler2)
  : managed_(gcnew SampleManagedAssembly::Class4())
  , eventHandler_(eventHandler)
  , eventHandler2_(eventHandler2)
{
  GetManaged()->SampleEvent += 
    MAKE_DELEGATE_WRAPPER(Class4::SampleEventHandler, EventHandler);
  GetManaged()->SampleEvent2 += 
    MAKE_DELEGATE_WRAPPER(System::EventHandler<System::EventArgs^>, EventHandler2);
}

void Class4Wrapper::EventHandler2(System::Object^, System::EventArgs^)
{
  if ( eventHandler2_ != NULL )
    (eventHandler2_)();
}

void Class4Wrapper::EventHandler(int i)
{
  // вызываю неуправляемую функцию обратного вызоваif ( eventHandler_ != NULL )
  (eventHandler_)(i);
}

5 Оболочка над log4net

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

Прежде всего, нужно создать оболочку над интерфейсом log4net::ILog.

      // ILog.h
      // Оболочка над log4net::ILog
      class ILog
{
      DECLARE_WRAPPER(log4net::ILog)public:
#ifdef _MANAGED
  ILog(log4net::ILog^ managed);
#endif//_MANAGED
  DLL_EXPORT bool IsDebugEnabled() const;
  DLL_EXPORT bool IsInfoEnabled() const;
  DLL_EXPORT bool IsErrorEnabled() const;
  DLL_EXPORT bool IsFatalEnabled() const;
  DLL_EXPORT bool IsWarnEnabled() const;

  DLL_EXPORT void Debug(constchar* message) const;
  DLL_EXPORT void Info(constchar* message) const;
  DLL_EXPORT void Warn(constchar* message) const;
  DLL_EXPORT void Error(constchar* message) const;
  DLL_EXPORT void Fatal(constchar* message) const;
};

// ILog.cpp
// Находится в проекте Log4NetWrapper и компилируется с ключом /clr
#include"ILog.h"
ILog::ILog(log4net::ILog^ managed)
  : managed_(managed)
{}
bool ILog::IsDebugEnabled() const
{
  return GetManaged()->IsDebugEnabled;
}
//...void ILog::Debug(constchar* message) const
{
  GetManaged()->Debug(gcnew System::String(message));
}
//...

Реализация оболочек над log4net::LogManager и log4net::Config::XmlConfigurator еще проще, т.к. эти классы состоят только из статических функций.

      //LogManager.h
      //Оболочка над log4net::LogManager
      class LogManager
{
  //Запрещаю создание экземпляров этого класса
  DLL_EXPORT LogManager();
public: //Puplic Functions
  DLL_EXPORT static ILog GetRootLogger();
  DLL_EXPORT static ILog GetLogger(constchar* name);
};

//LogManager.cpp
//Находится в проекте Log4NetWrapper и компилируется с ключом /clr#include"LogManager.h"ILog LogManager::GetRootLogger()
{
  return ILog(log4net::LogManager::GetLogger(""));
}

ILog LogManager::GetLogger(constchar* name)
{
  return ILog(log4net::LogManager::GetLogger(gcnew System::String(name)));
}

//Оболочка над log4net::Configuration::XmlConfiguratorclass XmlConfigurator
{
  //Запрещаю создание экземпляров этого класса
  DLL_EXPORT XmlConfigurator();
public: //Public-интерфейс
  DLL_EXPORT staticvoid Configure(constchar* filename);
  DLL_EXPORT staticvoid ConfigureAndWatch(constchar* filename);
};

//XmlConfigurator.cpp
//Находится в проекте Log4NetWrapper и компилируется с ключом /clr#include"XmlConfiguration.h"
void XmlConfigurator::Configure(constchar* filename)
{
  log4net::Config::XmlConfigurator::Configure(
    gcnew System::IO::FileInfo(gcnew System::String(filename)));
}
void XmlConfigurator::ConfigureAndWatch(constchar* filename)
{
  log4net::Config::XmlConfigurator::ConfigureAndWatch(
    gcnew System::IO::FileInfo(gcnew System::String(filename)));
}

Применение оболочки над log4net также достаточно простое.

      //TestLog4NetWrapper.cpp
//Компилируется без ключа /clr
      #include <iostream>

#include"..\Log4NetWrapper\LogManager.h"#include"..\Log4NetWrapper\XmlConfigurator.h"#pragma comment(lib, "Log4NetWrapper")
usingnamespace Log4NetWrapper;
int _tmain(int argc, _TCHAR* argv[])
{
  XmlConfigurator::ConfigureAndWatch("config.log4net");
  LogManager::GetRootLogger().Debug("Hello, Debug!!!");
  LogManager::GetRootLogger().Info("Hello, Info!!!");
  LogManager::GetRootLogger().Warn("Hello, Warn!!!");
  LogManager::GetRootLogger().Error("Hello, Error!!!");
  LogManager::GetRootLogger().Fatal("Hello, Fatal!!!");
  std::cin.get();
  return 0;
}

Результат выполнения программы показан на рисунке 3.


Рисунок 3. Работа с log4net из неуправляемого кода.

Конечно, это не вся функциональность библиотеки log4net, которая может вам понадобиться. Но с помощью класса ManagedObject достаточно просто создать оболочки над другими классами.

Заключение

Создание классов-оболочек для работы с управляемым кодом из неуправляемого является не такой уж сложной задачей. Основную роль в создании классов-оболочек играет класс ManagedObject, с помощью которого вы можете создать оболочки над вашими любимыми библиотеками и использовать их в неуправляемых приложениях. Естественно, что такие оболочки предоставляют лишь ограниченную функциональность, и вы не сможете воспользоваться всей мощью .Net Framework, но во многих случаях этого будет вполне достаточно для создания достаточно сложных смешанных приложений. Кроме того, при использовании смешанных приложений нужно всегда помнить о том, что переход из управляемого кода в неуправляемый не обходится бесплатно и требует со стороны CLR дополнительных операций. Поэтому, насколько возможно, старайтесь свести к минимуму такие переходы.


Эта статья опубликована в журнале RSDN Magazine #3-2008. Информацию о журнале можно найти здесь
    Сообщений 1    Оценка 80        Оценить