On-Line Библиотека www.XServer.ru - учебники, книги, статьи, документация, нормативная литература.
       Главная         В избранное         Контакты        Карта сайта   
    Навигация XServer.ru






 

Использование интерфейсов при работе с DLL.

Как вы, наверное, знаете, в динамически подключаемых библиотеках (DLL) используются соглашения языка C при объявлении экспортируемых объектов, в то время как в C++ применяется несколько иная система генерации имен при компиляции, так что нельзя просто экспортировать функции - методы класса С++ и затем использовать их в коде приложения-клиента (здесь и далее под клиентом подразумевается приложение, использующее DLL). Однако это можно сделать при помощи интерфейсов, доступных и DLL, и клиентскому приложению. Этот метод очень мощный и в то же время элегантный, т.к. клиент видит только абстрактный интерфейс, а фактический класс, который реализует все функции может быть любым. Microsoft'овская технология COM (Component Object Model) построена на подобной идее (плюс дополнительная функциональность, конечно). В этой статье будет рассказано, как использовать "классовый" подход с применением интерфеса, похожего на COM, при раннем (на этапе компиляции) и позднем (во время работы программы) связывании.

Если вы хоть раз работали с DLL, то уже знаете, что DLL имееет особенную функцию DllMain(). Эта функция подобна WinMain, или main() в том смысле, что это своего рода точка входа в DLL. Операционная система автоматически вызывает эту функцию в случаае, если DLL загружается и выгружается. Обычно эту функцию ни для чего другого не используют.

Существует два метода подключения DLL к проекту - это раннее (на этапе компиляции программы) и позднее (во время выполнения программы) связывание. Методы различаются способом загрузки DLL и способом вызова функций, реализованных и экспортированных из DLL.

Раннее связывание (во время компиляции программы)

При таком методе связывания операционная система автоматически загружает DLL во время запуска программы. Однако требуется, чтобы в разрабатываемый проект был включен .lib файл (библиотечный файл), соответствующий данной DLL. Этот файл определяет все экспортируемые объекты DLL. Объявления могут содержать обычные функции C или классы. Все, что нужно клиенту - использовать этот .lib файл и включить заголовочный файл DLL - и ОС автоматически загрузит эту DLL. Как видно, этот метод выглядит очень простым в использовании, т.к. все прозрачно. Однако вы должны были заметить, что код клиента нуждается в перекомпиляции всякий раз, когда изменяется код DLL и, соответственно, генерируется новый .lib файл. Удобно ли это для вашего приложения - решать вам. DLL может объявить функции, которые она хочет экспортировать, двумя методами. Стандартный метод - использование .def файлов. Такой .def файл - это просто листинг функций, экспортируемых из DLL.


  
//============================================================
// .def файл

LIBRARY       myfirstdll.dll
DESCRIPTION  'My first DLL'
EXPORTS
              MyFunction


//============================================================
// заголовок DLL, который будет включен в код клиента

bool MyFunction(int parms);


//============================================================
// реализация функции в DLL

bool MyFunction(int parms)
{
   // делаем все, что нужно
   ............
}


Я думаю, можно не говорить, что в данном примере экспортируется только одна функция MyFunction. Второй метод объявления экспортируемых объектов специфичен, но намного мощнее: вы можете экспортировать не только функции, но также классы и переменные. Давайте посмотрим на на фрагмент кода, сгенерированный при создании DLL AppWizard'ом VisualC++. Комментариев, включенных в листинг, вполне хватает, чтобы понять , как все это работает.


  
//============================================================
// Заголовок DLL, который должен быть включен в код клиента

/*
Следующий блок ifdef - стандартный метод создания макроса, 
который  далает экспорт из DLL проще. Все файлы этой  DLL 
компилируются с определенным ключом  MYFIRSTDLL_EXPORTS.
Этот ключ не определяется для любого из проектов, использующих эту DLL.
Таким образом, любой проект, в который включен это файл, видит функции
MYFIRSTDLL_API как импортируемые из  DLL, тогда как сама DLL 
эти же функции видит как экспортируемые.
*/

#ifdef MYFIRSTDLL_EXPORTS
#define MYFIRSTDLL_API __declspec(dllexport)
#else
#define MYFIRSTDLL_API __declspec(dllimport)
#endif

// Класс экспортируется из  test2.dll
class MYFIRSTDLL_API CMyFirstDll {
public:
   CMyFirstDll(void);
   // TODO: здесь можно добавить свои методы.
};

extern MYFIRSTDLL_API int nMyFirstDll;

MYFIRSTDLL_API int fnMyFunction(void);

Во время компиляции DLL определен ключ MYFIRSTDLL_EXPORTS, поэтому перед объявлениями экспортируемых объектов подставляется ключевое слово __declspec(dllexport). А когда компилируется код клиента, этот ключ неопределен и перед объектами появляется префикс __declspec(dllimport), так что клиент знает, какие объекты импортируются из DLL.

В обоих случаях все, что нужно сделать клиенту - добавить файл myfirstdll.lib в проект и включить заголовочный файл, который объявляет импортируемые из DLL объекты, а затем использовать эти объекты (функции, классы и переменные) точно так же, как будто они были определены и реализованы локально в проекте. А теперь давайте разберем другой метод использования DLL, который чаще бывает удобнее и мощнее.

Позднее связывание (во время работы программы)

Когда используется позднее связывание, DLL загружается не автоматически, при запуске программы, а напрямую в коде, там, где это нужно. Не нужно использовать никакие .lib файлы, так что клиентское приложение не требует перекомпиляции при изменении DLL. Такое связывание обладает мощными возможностями именно потому, что ВЫ решаете, когда и какую DLL загрузить. Например, вы пишете игру, в которой используется DirectX и OpenGL. Вы можете просто включить весь необходимый код в исполняемый файл, но тогда разобраться в чем-нибудь будет просто невозможно. Или можно поместить код DirectX в одну DLL, а код OpenGL - в другую и статически подключить их к проекту. Но теперь весь код взаимнозависим, так что если вы написали новую DLL, содержащую код DirectX, то перекомпилировать придется и исполняемый файл. Единственным удобством будет то, что вам не нужно заботиться о загрузке (хотя неизвестно, удобство ли это, если вы загружаете обе DLL, занимая память, а в действительности нужна лишь одна из них). И наконец, на мой взгляд, лучшая идея состоит в том, чтобы позволить исполняемому файлу решить, какую DLL загрузить при запуске. Например, если программа определила, что система не поддерживает акселерацию OpenGL, то лучше загрузить DLL с кодом DirectX, иначе загрузить OpenGL. Поэтому позднее связывание экономит память и уменьшает зависимость между DLL и исполняемым файлом. Однако в этом случае накладывается ограничение на экспортируемые объекты - экспортироваться могут лишь C-style функции. Классы и переменные не могут быть загружены, если программа использует позднее связывание. Давайте посмотрим, как обойти это ограничение с помощью интерфейсов.

DLL, спроектированная для позднего связывания обычно использует .def файл для определения тех объектов, которые она хочет экспортировать. Если вы не хотите использовать .def файл, можно просто использовать префикс __declspec(dllexport) перед экспортируемыми функциями. Оба метода делают одно и то же. Клиент загружает DLL, передавая имя файла DLL в функцию Win32 LoadLibrary().Эта функция возвращает хэндл HINSTANCE, который используется для работы с DLL и который необходим для выгрузки DLL из памяти, когда она становится не нужна. После загрузки DLL клиент может получить указатель на любую функцию при помощи функции GetProcAddress(), используя в качестве параметра имя требуемой функции.


  
//============================================================
// .def файл

LIBRARY      myfirstdll.dll
DESCRIPTION   'My first DLL'
EXPORTS
      MyFunction

//============================================================
/*
Реализация функции в DLL
*/

bool MyFunction(int parms)
{
   //делаем что-нибудь
   ............
}

//============================================================
//Код клиента

/*
Объявление функции в действительности необходимо только для того, 
чтобы опредедлить параметры. Объявления функций обычно содержаться в 
заголовочном файле, поставляемом вместе с DLL. 
Ключевое слово  extern C в объявлении функции сообщает компилятору, 
что нужно использовать соглашения об именовании переменных языка C.
*/

extern "C" bool MyFunction(int parms);
typedef bool (*MYFUNCTION)(int parms);

MYFUNCTION   pfnMyFunc=0;   //указатель на  MyFunction

HINSTANCE    hMyDll = ::LoadLibrary("myfirstdll.dll");

if(hMyDll != NULL)
{
   //Определяем адрес функции
   pfnMyFunc= (MYFUNCTION)::GetProcAddress(hMyDll, "MyFunction");

   //Если неудачно - выгружаем DLL
   if(pfnMyFunc== 0)   
   {
      ::FreeLibrary(hMyDll);
      return;
   }

   //Вызываем функцию
   bool result = pfnMyFunc(parms);
   
   //Выгружаем DLL, если она больше нам не нужна
   ::FreeLibrary(hMyDll);
}

Как вы видите, код довольно прямолинеен. А теперь давайте посмотрим, как может быть реализована работа с "классами". Как было указано ранее, если используется позднее связывание, нет прямого способа импортировать из DLL классы, так что нам нужно реализовать "функциональность" класса с помощью интерфейса, содержащего все открытые (public) функции, исключая конструктор и деструктор. Интерфейс будет обычной C/C++ структурой, содержащей только виртуальные абстрактные функции-члены. Фактический класс в DLL будет наследоваться от этой структуры и будет реализовывать все функции, определенные в интерфейсе. Теперь, чтобы получить доступ к этому классу из приложения - клиента, все, что нужно сделать - это экспортировать C-style функции, соответствующие экземпляру класса и связать их с определенным нами интерфейсом для того, чтобы клиент мог их использовать. Для реализации такого метода нужна еще две функции, одна из которых создаст интерфейс, а вторая удалит интерфейс после того, как с ним закончили работать. Пример реализации этой идеи приведен ниже.


  
//============================================================
// .def файл

LIBRARY      myinterface.dll
DESCRIPTION  'реализует интерфейс I_MyInterface
EXPORTS      
             GetMyInterface
             FreeMyInterface

//============================================================
// Заголовочный фал, используемый в Dll и клиенте, 
// который объявляет инетрфейс
// I_MyInterface.h

struct I_MyInterface
{
   virtual bool  Init(int parms)=0;
   virtual bool  Release()=0;
   virtual void  DoStuff() =0;
};

/*
Объявления экспортируемых функций Dll и определения типов указателей 
на функции для простой загрузки и работы с функциями. Обратите 
внимание на префикс extern "C", который сообщает компилятору о том, 
что используются С-style функции
*/

extern "C"
{
HRESULT  GetMyInterface(I_MyInterface ** pInterface);
typedef HRESULT (*GETINTERFACE)(I_MyInterface  ** pInterface);

HRESULT  FreeMyInterface(I_MyInterface ** pInterface);
typedef HRESULT (*FREEINTERFACE)(I_MyInterface ** pInterface);
}


//============================================================
//Реализация интерфейса в Dll
// MyInterface.h

class CMyClass: public I_MyInterface
{
public:
   bool  Init(int parms);
   bool  Release();
   void  DoStuff();

   CMyClass();
   ~CMyClass();

   //любые другие члены класса
   ............
private:
   //любые члены класса
   ............
};

//============================================================
// Экспортируемые функции, которые создают и уничтожают интерфейс
// Dllmain.h

HRESULT GetMyInterface(I_MyInterface ** pInterface)
{
   if(!*pInterface)
   {
      *pInterface= new CMyClass;
      return S_OK;
   }
   return E_FAIL;
}

HRESULT FreeMyInterface(I_MyInterface ** pInterface)
{
   if(!*pInterface)
      return E_FAIL;
   delete *pInterface;
   *pInterface= 0;
   return S_OK;
}

//============================================================
// Код клиента


//Объявления интерфейса и вызов функций

GETINTERFACE pfnInterface=0;//указатель на функцию GetMyInterface
I_MyInterface * pInterface  =0;//указатель на структуру MyInterface

HINSTANCE hMyDll = ::LoadLibrary("myinterface.dll");

if(hMyDll != NULL)
{
   //Определяем адрес функции
   pfnInterface= (GETINTERFACE)::GetProcAddress(hMyDll,
          "GetMyInterface");

   //Выгружаем DLL, если предыдущая операция окончилась неудачей
   if(pfnInterface == 0)   
   {
      ::FreeLibrary(hMyDll);
      return;
   }

   //Вызываем функцию
   HRESULT hr = pfnInterface(&pInterface);
   
   //Выгружаем, если неудачно
   if(FAILED(hr))   
   {   
      ::FreeLibrary(hMyDll);
      return;
   }

   //Интерфейс загружен, можно вызывать функции
   pInterface->Init(1);
   pInterface->DoStuff();
   pInterface->Release();

   //Освобождаем интерфейс

   FREEINTERFACE pfnFree = 
       (FREEINTERFACE )::GetProcAddress(hMyDll,"FreeMyInterface");
   if(pfnFree != 0)
      pfnFree(&hMyDll);

   //Выгружаем DLL
   ::FreeLibrary(hMyDll);
}

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



Литература по C & C++