Создание простого приложения с плагинами

В этой статье описываются принципы и решения, применяемые при проектировании приложений, которые будут использовать внешние, динамически подключаемые, модули. Эта статья более ориентирована на тех, кто хочет использовать механизмы подключения/отключения функциональности приложения, наподобии механизма Aobe Photoshop или Far, а не просто многократного использования кода в разных приложениях.

Динамически подключаемые модули (DLL) - это модули, которые содержат функции и данные. Эти модули загружаются во время выполнения программы, использующей эти модули (хоста). В ОС Windows модули содержат внутренние и экспортируемые функции (в UNIX подобных системах все функции являются экспортируемыми). Экспортируемые функции доступны для вызова хостом, а внутренние нет. Хотя данные тоже могут быть экспортируемыми, но обычно используются экспортируемые функции для доступа данным.

Некоторые, особенно начинающие разработчики ПО, и не представляют, что при создании приложения, уже используют внешние модули. Хотя при разработке MFC приложений этот факт более очевиден. Просто компилятор сам вставляет код, который загружает системные библиотеки, иначе любое Windows приложение было бы на 20-30 Мб больше.

Итак, перейдем непосредственно к созданию механизма для использования в Ваших приложениях плагинов.

Создайте новое DLL приложение (Builder и VC позволяют выбрать тип при создании нового проекта).

Каждая библиотека имеет точку входа (но можно ее и не описывать), как функция main() в обычном приложении. Вот обычное ее описание:

HINSTANCE hDllInstance=NULL;
BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved )
{
if (ul_reason_for_call == DLL_PROCESS_ATTACH)

hDllInstance = (HINSTANCE)hModule;

return TRUE;
}

При присоединении к вызвавшему ее процессу, в эту функцию передается instance. Т.е. Вы сможете использовать ресурсы библиотеки. Используйте в этой функции только простые задачи по инициализации! Эта функция очень уязвимое место в модуле.

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

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

Например, один из возможных вариантов этих двух функций:

#ifdef __cplusplus
extern "C" {
#endif

__declspec(dllexport) void GetPluginInfo(PluginInfo* pPluginInfo,
DWORD *pdwResult);
__declspec(dllexport) void PluginHandler(DWORD dwCode,
HostInfo *pHostInfo,
DWORD *pdwResult);

#ifdef __cplusplus
}
#endif

Ключевые слова __declspec( dllexport ) обозначают, что функции являются экспортируемыми.

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

Вот пример структуры, передаваемой при инициализации:

struct PluginInfo {
// вид выполняемой плагином операции
// (если предусматривается несколько типов плагинов)

DWORD m_dwPluginType;
// передадим пункт меню для нашего плагина
char * m_pcMenuString;
unused[64];
};

А вот пример функции, которая находится в плагине и заполняет эту структуру:

void GetPluginInfo(PluginInfo* pPluginInfo, DWORD *pdwResult) {

pPluginInfo->m_dwPluginType=5;
pPluginInfo->m_pcMenuString="/Мой плагин";
*pdwResult=0;
}

Функция-обработчик в плагине:

void PluginHandler(DWORD dwCode,HostInfo *pHostInfo,DWORD *pdwResult); 

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

void PluginHandler(DWORD dwCode,HostInfo *pHostInfo,DWORD *pdwResult) {

switch(dwCode) {
case 1:
//первое действие
*pdwResult=1;
break;
case 2:
//второе действие
*pdwResult=1;
break;
default: *pdwResult=0;
}
}

Если плагин не знает переданного кода операции, он просто вернет код "Не поддерживается" и не выполнит некорректных действий.

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

Загрузим библиотеку хостом:

// приходится создавать тип для каждой экспортируемой функции
typedef void (*GetPluginInfoType)(PluginInfo*);
typedef void (*PluginHandlerType)(HostInfo*);

HMODULE hLib=LoadLibrary("MyLib.dll");
if (hLib==NULL) {
// тут обрабатываем ошибку, если библиотека не загрузилась
return FALSE;
}

GetPluginInfoType GetPluginInfo;
GetPluginInfo=(GetPluginInfoType)GetProcAddress(m_hInstance,"GetPluginInfo");
if (GetPluginInfo==NULL) {
FreeLibrary(hLib);
return FALSE;
}
DWORD dwResult;
PluginInfo PluginNfo;
memset(&PluginNfo,0,sizeof(PluginInfo));
GetPluginInfo(&PluginNfo,&dwResult);
// тут анализируем заполненную в плагине структуру
// (создаем меню для плагина, резервируем память и т.д.)

Итак, плагин загружен и готов к работе, ожидаем когда пользователь выберет пункт меню.

Помещаем в обработчик меню следующий код:

PluginHandlerType PluginHandler;
// получаем адрес функции обработчика в плагине
PluginHandler=(PluginHandlerType)GetProcAddress(m_hInstance,"PluginHandler");
if (PluginHandler==NULL) return FALSE;
// подготавливаем структуры с данными,
// которые необходимо передать в плагин для обработки

HostInfo HostNfo;
memset(&HostNfo,0,sizeof(HostInfo));
// если в плагинах будут создаваться окна,
// то необходимо передать HWND главного окна в качестве родительского

HostInfo.m_hHostWnd=theApp->m_pMainWnd->GetSafeHwnd();
// передаем адреса функций, реализованных в хосте
HostInfo.IPShowProgress=::ShowProgress;
DWORD dwResult;
try { // желательно поставить обработчик исключений
// вызываем функцию-обработчик в плагине
PluginHandler(1,&HostNfo,&dwResult);
} catch(...)
{
AfxMessageBox("В модуле произошла необрабатываемая ошибка.");
ASSERT(0);
return FALSE;
}
// не забывайте выгружать библиотеки по завершении работы хостом
FreeLibrary(hLib);

Вот собственно и все описание простого примера использования плагинов в своих программах.

Я хочу добавить несколько советов для разработчиков:

  • если планируется использовать много плагинов, то разумно будет сначала получить первоначальную информацию от плагина, а затем его выгрузить из памяти. И подгружать его в случае надобности.
  • если пользователь вводит некоторые параметры в диалогах плагинов, то разумно разработать механизм централизованного хранения последних введенных параметров.
  • делайте возможность помещения плагинов в произвольные папки внутри папки плагинов. Код рекурсивного поиска плагинов приведен ниже.
  • Задайте своим плагинам отличное от "dll" расширение. Т.к. сами плагины могут использовать внешние dll библиотеки.

А вот пример функции, которая рекурсивно находит все файлы в папке с плагинами:

// Массив со всеми файлами, включая путь относительно папки с плагинами
CStringArray PluginsArray;
// задается путь к папке с плагинами
CString sPlugInsPath="Plugins\\";
void GetPlugInFiles(CString sPath) {
if (PluginsArray.GetSize()>=512) return;
CString sStr;
CString sCurFullPath=sPlugInsPath;
sCurFullPath+=sPath;
sCurFullPath+="*";
WIN32_FIND_DATA FindData;
HANDLE hFindFiles=FindFirstFile(sCurFullPath,&FindData);
if (hFindFiles==INVALID_HANDLE_VALUE) return;
for(;;) {
if ((strcmp(FindData.cFileName,".")!=0) && (strcmp(FindData.cFileName,"..")!=0)) {
if (FindData.dwFileAttributes&FILE_ATTRIBUTE_DIRECTORY) {
sStr=sPath;
sStr+=FindData.cFileName;
sStr+="\\";
GetPlugInFiles(sStr);
}
else {
char *ptr=strrchr(FindData.cFileName,'.');
if (ptr) {
if (strlen(ptr)==4) {
if (ptr[1]=='x' && ptr[2]=='x' && ptr[3]=='x') {
CString sPath1=sPath;
sPath1+=FindData.cFileName;
PluginsArray.Add(sPath1);
}
}
}
}
}
if (!FindNextFile(hFindFiles,&FindData)) break;
}
FindClose(hFindFiles);
}


// так вызывается функция. После такого вызова массив
// PluginsArray будет заполнен

GetPlugInFiles("");
Покрашенко Александр