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

`Покрашенко Александр bestx@mail.ru
Оригинал: http://www.sources.ru/cpp/cpp_create_simple_plugins.shtml`

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

Динамически подключаемые модули (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 && 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);
    }

`GetPlugInFiles(""); // так вызывается функция.`

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