Використання інтерфейсів при роботі з DLL, C / C + +, Програмування, статті

Як ви, напевно, знаєте, в динамічно підключаються бібліотеках (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);
}

Цієї інформації цілком достатньо, щоб ви відчули всю зручність використання інтерфейсів. Вдалого програмування!

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*