Использование интерфейсов при работе с 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);
}
Этой информации вполне достаточно, чтобы вы почувствовали все удобство использования
интерфейсов. Удачного программирования!
|