Sources.RU Magazine Поиск по журналу
 

TopList

Visual C++ 6/Visual Basic 6: Работа с плагинами

Автор: B.V.

Введение

Плагин – подключаемый модуль, который дополняет, расширяет функциональность программы без изменения её исходного кода. Например, придает программе новый внешний вид, увеличивает количество поддерживаемых форматов файлов, добавляет новые инструменты для работы с графикой или текстом... Ниже рассказывается о разработке плагинов в проектах на Visual C++ 6/Visual Basic 6. Предполагается, что читатель знаком с языками программирования С++ и/или Visual Basic, средой разработки Visual Studio 6 и WinAPI.

Как правило, плагины поставляются в виде Native DLL – обычной библиотеки, экспортирующей определенный набор функций. Список экспортируемых функций содержится в таблице экспорта (Export table), в которой названию функции соответствует её точка входа (Entry point) в библиотеке. Кроме того, каждой функции присваивается номер (Ordinal), по которому её можно вызвать точно так же, как и по имени. Для получения точки входа функции из таблицы экспорта используется функция GetProcAddress. VB6 не поддерживает компиляцию таких библиотек. Способы обхода подобных запретов существуют, но они требует отдельного разговора, поэтому пока не остается ничего, кроме использования разрешенных VB типов библиотек: ActiveX DLL. ActiveX DLL по своей сути тоже является Native DLL, но с COM-объектами. В таких библиотеках функции располагаются в COM-классах в виде методов и свойств. Доступ к ним осуществляется посредством COM-интерфейсов IUnknown и IDispatch. VB6 берет на себя работу с этими интерфейсами, хоть и в ущерб функциональности, но в VC++6 придется работать с ними явно.

COM-интерфейсы и работа с ними VB

Этот раздел может смело пропустить тот читатель, который собирается разрабатывать плагины только на VC++ или просто не желает вдаваться в подробности COM. Разработчику на VB вовсе не обязательно досконально знать механизм работы COM, достаточно иметь о нем общее представление.

  1. При подключении COM-объекта VB вызывает IUnknown посредством CoCreateInstance с IID_IUnknown.
  2. Метод CoCreateInstance, в свою очередь, вызывает CoGetClassObject с IID_IClassFactory. IClassFactory – это обязательный интерфейс описания COM-объектов.
  3. Вызвав затем pIClassFactory->CreateInstance с IID_IUnknown, VB получает указатель на интерфейс IUnknown (pIUnknown).
  4. Далее VB вызывает pIUnknown->QueryInterface с IID_IDispatch для получения указателя на IDispatch (pIDispatch).

Если pIDispatch будет равен нулю, VB откажется работать с таким COM-объектом. Впрочем, интерфейс IDispatch необязателен, его задача обеспечить работу OLE Automation контроллера (в качестве которого выступает, например, сам VB) с OLE Automation объектом. Работать с COM-объектом без IDispatch просто:

  • в начале каждого интерфейса (класса) COM-объекта компилятор записывает 4-байтный указатель virtual table pointer (или сокращенно vPointer) на массив 4-байтных указателей virtual method table (или сокращенно vTable);
  • в vTable хранятся указатели на методы унаследованных интерфейсов2, а также методы и свойства текущего интерфейса. Первые три указателя каждой vTable всегда указывают на методы IUnknown. IUnknown обязателен для любого интерфейса COM-объекта, так как он позволяет:
    1. получать указатели на прочие интерфейсы COM-объекта,
    2. вести учет количества ссылок на интерфейс COM-объекта
    3. и освобождать интерфейс COM-объекта.
    Указатели в vTable располагаются в том же порядке, что и методы/свойства в коде интерфейса. Таким образом, зная расположение методов/свойств (VC++ об этом знает из *.h-файлов, в которых определены интерфейсы), можно получить требуемый указатель.

Ниже приводится наглядный листинг, демонстрирующий формирование vTable у интерфейса COM-объекта:

//Создается абстрактный класс (интерфейс) для класса A...

//Определяется IID_IA для QueryInterface
extern "C" const GUID __declspec(selectany) IID_IA = {%IID_IA_GUID%};

//Этот класс должен иметь свой GUID, но не должен обладать vTable
interface struct __declspec(uuid("%IA_GUID%")) __declspec(novtable) IA : public IUnknown //Наследуется интерфейс IUnknown
{
public:
    //Объявляются методы интерфейса (т.н. 'pure virtual functions')
    virtual void __stdcall func1() = 0;
    virtual void __stdcall func2() = 0;
    virtual void __stdcall func3() = 0;
};

//Класс для реализации функций интерфейса
class A : public IA //Наследуется интерфейс IA
{
public:
    //Реализуется IUnknown
    virtual HRESULT __stdcall QueryInterface(REFIID riid, LPVOID FAR* ppvObj){/* реализация */};
    virtual ULONG __stdcall AddRef(){/* реализация */};
    virtual ULONG __stdcall Release(){/* реализация */};
    //Реализация IA
    void func0(){/* реализация */} //Не виртуальная функция, она не будет включена в vTable
    virtual void __stdcall func1(){/* реализация */};
    virtual void __stdcall func2(){/* реализация */};
    virtual void __stdcall func3(){/* реализация */};
};

//Создаётся экземпляр класса A...
A * a = new A;
Образ памяти созданного экземпляра a: 
+0: указатель на virtual method table класса A (vPointer)
virtual method table класса A:
    +0: A::QueryInterface(...)
    +4: A::AddRef()
    +8: A::Release()
    +12: A::func1()
    +16: A::func2()
    +20: A::func3()
    +24: A::~A()

Если расположение методов/свойств неизвестно (нет *.h-файла), может помочь декомпиляция TypeLibrary. Например, с помощью инструмента OLE View, входящего в дистрибутив Visual Studio 6 (Microsoft Visual Studio\Common\Tools\OLEVIEW.EXE). Для этого необходимо запустить OLE View, выбрать 'File->View TypeLib…' и выбрать нужный файл. В описании нужного интерфейса будут показаны прототипы методов/свойств в нужном порядке. При этом необходимо помнить о методах IUnknown и IDispatch, если последний присутствует:

interface _A : IUnknown {
    [id(0x00000001)]
    void func1();
    [id(0x00000002)]
    void func2();
    [id(0x00000003)]
    void func3();
};

Этого достаточно, чтобы определить интерфейс в каком-нибудь *.h-файле, автоматически преобразовав ODL/IDL файл в *.h-файл с помощью MKTYPLIB.EXE или директивы #import. Если же нужен только один из методов, то после того, как OLE View показал его порядковый номер метода и прототип, ничто не мешает его вызвать:

#define METHOD_NUM 4 //Указывается номер вызываемого метода (func2). 3 метода IUnknown + 1 метод IA

void Foo()
{
    __asm
    {
        mov ebx, [a]; //Сохранение адреса класса (COM-интерфейса, если угодно)
        mov esi, [ebx]; //Получение в esi vPointer
        push ebx; //Передается this
        call [esi + METHOD_NUM * 4]; //Вызывается метод...
    }
}

Если VB получит указатель на IDispatch, то вызов метода/свойства будет осуществлен через pIDispatch->Invoke, а перед этим будет вызван pIDispatch->GetIDsOfNames с именем вызываемой функции для получения указателя на эту функцию dispidMember.
Прототип pIDispatch->Invoke выглядит следующим образом:

HRESULT Invoke(DISPID dispidMember, REFIID riid, LCID lcid, unsigned short wFlags, DISPPARAMS FAR* pdispparams, VARIANT FAR* pvarResult, EXCEPINFO FAR* pexcepinfo, unsigned int FAR* puArgErr);

Здесь:

riid - зарезервированный на будущее аргумент, он всегда должен быть равен IID_NULL.
lcid - задает контекст локали, в который будут интерпретироваться аргументы. Если приложение не поддерживает множественные национальные языки, этот параметр можно проигнорировать.
wFlags - определяет режим вызова для Invoke и может принимать следующие значения:
DISPATCH_METHOD - член реализован как метод.
DISPATCH_PROPERTYGET - член должен вызываться как свойство (или член данных) для получения значения
DISPATCH_PROPERTYPUT - член должен изменить свое значение как свойство (или член данных).
DISPATCH_PROPERTYPUTREF - член должен изменить свое значение как свойство (или член данных) через присвоение ссылки, а не значения, как в предыдущем случае.
pdispparams - указатель на структуру DISPPARAMS, содержащую массив аргументов, массив argument dispatch IDs для именованных аргументов и числа, определяющие количество элементов в массивах.
pvarResult - указатель на то место, куда будет записан возвращенный результат. Если вызывается присвоение значения свойству или процедура, не возвращающая значения, этот указатель должен быть равен нулю.
pexcepinfo - указатель на структуру с описанием исключения. Структура будет заполнена только в том случае, если SCODE значение будет равно DISP_E_EXCEPTION.
puArgErr - возвращает индекс в массиве аргументов rgvarg первого элемента, вызвавшего ошибку. Аргументы в pdispparams->rgvarg запоминаются в обратном порядке, так что первый аргумент будет иметь самый старший индекс в массиве аргументов. Этот параметр возвращается только в том случае, если SCODE будет равен DISP_E_TYPEMISMATCH или DISP_E_PARAMNOTFOUND.

MSDN предоставляет перечень значений, которые может вернуть Invoke в SCODE. Читатель может ознакомиться с ними самостоятельно, сейчас следует учитывать только то, что в случае успеха SCODE должен быть равен S_OK, а все остальные значения свидетельствуют об ошибке вызова.

Реализация этого метода интерфейса IDispatch обычно сводится к вызову DispInvoke:

class FAR CMyInterface : public IMyInterface
{
public:

//...

//Декларация метода IDispatch в классе
//virtual HRESULT __stdcall
STDMETHOD(Invoke)(DISPID dispidMember, 
                    REFIID riid, 
                    //... 
                    EXCEPINFO FAR* pexcepinfo, 
                    UINT FAR* puArgErr); 

//...

};

//Реализация метода. Вместо DispInvoke можно использовать класс ITypeInfo,
//или, в крайнем случае, CreateStdDispatch
STDMETHODIMP //HRESULT __export __stdcall
CMyInterface::Invoke(DISPID dispidMember, 
                  REFIID riid, 
                  //...
                  EXCEPINFO FAR* pexcepinfo, 
                  UINT FAR* puArgErr) 
{

    //...

    return DispInvoke(this, m_ptinfo, dispidMember, wFlags, pdispparams, 
        pvarResult, pexcepinfo, puArgErr); 
}

Будем считать, что общее представление о работе VB с интерфейсами и об интерфейсах в целом получено.

Организация системы плагинов

Система плагинов - это своего рода план, согласно которому будут взаимодействовать плагины и программа. К системе плагинов стоит отнестись серьезно, так как от способа её реализации сейчас зависит будущее плагинов вашего проекта. Предусмотреть все, конечно, невозможно, но вполне реально свести проблемы обратной совместимости к минимуму. Например, не следует передавать в ключевые функции плагина набор аргументов, лучше передать указатель на структуру с аргументами, что даст возможность расширения количества аргументов с сохранением обратной совместимости. Перед тем, как приступить к составлению системы плагинов, нужно определиться, какую задачу (или задачи) должен реализовывать каждый плагин и что ему нужно для реализации этой задачи (задач). Рассмотрим конкретный пример:

Пусть имеется текстовый редактор TextPad, представляющий собой окно с полем EDIT и умеющий открывать/сохранять файлы в формате Plain Text. Возникла потребность расширить функциональность TextPad, допустим, возможностями поиска текста и отображения статистики документа (количества символов, строк, слов...). Плагинам для выполнения этих задач потребуется возможность работы с полем EDIT. Значит, манипулятор этого поля нужно передать плагинам. Кроме того, окнам плагинов совсем не помешает возможность взаимодействия с основным окном программы. Значит, кроме манипулятора окна EDIT, полезно передать и манипулятор основного окна программы. Предоставить пользователю возможность работы с плагинами проще всего посредством пунктов меню. Но чтобы определиться с заголовками этих пунктов, потребуется возможность получения полного название каждого плагина. Полученная в итоге схема системы плагинов для TextPad показана на рис. 1.

Схема системы плагинов для TextPad
Рис. 1 – Схема системы плагинов для TextPad

При запуске программы, или в любой другой удобный момент времени, можно загрузить плагин и получить его имя, которое затем использовать для добавления пункта в меню плагинов. Как только пользователь выбрал некоторый пункт меню, соответствующему плагину передается манипулятор основного окна и манипулятор поля EDIT, фактически активируя выполнение плагина.
Ниже рассмотрен процесс обмена данными (передача и получение) между программой и плагинами.

Интерфейс плагинов

Интерфейс плагинов – это механизм взаимодействия плагинов с основной программой, который может представлять собой набор методов COM-класса, набор сообщений для окна программы, общий контейнер данных (FileMapping или Named Pipes) и т.д., а также комбинацию всего перечисленного.
Выбор подходящего интерфейса – дело вкуса. Кому-то нравится набор функций под каждое действие, кто-то, как автор, предпочитает компактность, кто-то комбинирует одно с другим. Система плагинов в примере с TextPad достаточно проста, поэтому нет нужды создавать сложный интерфейс из нескольких функций и/или ряда сообщений. Остановимся на одной функции Plugin Main с единственным аргументом - указателем на общую для плагина и TextPad структуру TPPLUGIN [TextPad Plugin]. Она состоит из

  1. манипулятора основного окна;
  2. манипулятора окна EDIT;
  3. строки для имени плагина.

Для различения режимов вызова основной функции можно, конечно, проверять значение манипуляторов, и, если оно будет равно нулю, считать вызов запросом имени. Но такой способ не является практичным и наглядным. Лучше добавить флаги, определяющие режим вызова основной функции плагина. Не помешает также флаг, сигнализирующий о завершении работы плагина, например, когда плагин отображает немодальное диалоговое окно через CreateDialog[Param] и выходит из основной функции:

typedef struct tagTPPLUGIN
{
    DWORD dwFlags;          //Флаги, определяющие режим вызова основной функции плагина и т.п.
    LPSTR lpDisplayName;        //Имя плагина, отображающееся в меню TextPad
    HWND hMainWnd;          //Манипулятор основного окна
    HWND hEditWnd;          //Манипулятор поля EDIT
} TPPLUGIN, * LPTPPLUGIN;

//Флаги:
#define TPPF_GETDATA    0x20        //Функция вызывается для получения каких-либо данных, например, lpDisplayName.
#define TPPF_ACTION 0x40        //Функция вызывается для выполнения задачи плагина

#define TPPF_ACTIVE 0x400       //Флаг активности плагина. TextPad не будет выгружать плагин до тех пор, пока
//этот флаг установлен. За снятие флага отвечает плагин.

//Основная функция плагина TextPad
//      pTPPlug: указатель на структуру TPPLUGIN
//      Возвращаемое значение: 0 в случае успеха, код ошибки иначе
extern "C" DWORD WINAPI TextPadPluginMain(LPTPPLUGIN pTPPlug)
{
    //Вызов на выполнение
    if ((pTPPlug->dwFlags & TPPF_ACTION) == TPPF_ACTION)
    {
        //Работа
        pTPPlug->dwFlags = (pTPPlug->dwFlags ^ TPPF_ACTIVE);
        return 0;
    }
    else //Вызов для получения информации
    {
        pTPPlug->lpDisplayName = "Plugin Name";
        pTPPlug->dwFlags = (pTPPlug->dwFlags ^ TPPF_ACTIVE);
        return 0;
    }
}

Но для VB этот способ в общем случае неприемлем. Дело в том, что передача User Defined Type (UDT) по ссылке в метод класса возможна только в том случае, если переменная была объявлена как UDT, определенный в данном классе. То есть, при попытке вызова метода объекта, полученного от CreateObject (позднее связывание, используемое, в том числе и для организации работы плагинов) компилятор выведет сообщение следующего содержания:

Compile error: 
Only user-defined types defined in public object modules can be coerced to or from a variant or passed to late-bound functions

А при раннем связывании:

Compile error:
ByRef argument type mismatch

Можно передать указатель на UDT как ByVal VarPtr(UDT) As Long, но работать со структурой в плагине станет, мягко говоря, неудобно. Придется обходиться методами и свойствами:

Option ExplicitPublic Function PluginMain() As Long
    'Работа
    'По завершению необходимо позаботиться о том, что бы свойство IsActive возвращало False
    PluginMain = 0
End FunctionPublic Property Get DisplayName() As String
    'Здесь возвращается имя плагина
    DisplayName = "Plugin Name"
End PropertyPublic Property Let MainWnd(ByVal hValue As Long)
    '...
End PropertyPublic Property Let EditWnd(ByVal hValue As Long)
    '...
End PropertyPublic Property Get IsActive() As Boolean
    'Здесь возвращается флаг активности плагина. TextPad не будет выгружать плагин до тех пор,
    'пока этот флаг установлен.
End PropertyPublic Property Let IsActive(ByVal bValue As Boolean)
    'Это свойство TextPad устанавливает в True перед вызовом PluginMain
End Property

Впрочем, можно было и не создавать собственные плагины на VB, а использовать плагины VC++ из проекта на VB точно так же, как и в проекте на VC++. Обратное тоже верно. Но тогда была бы нарушена чистота эксперимента.

Использование плагинов

Система плагинов составлена, интерфейс плагинов создан, осталось только всем этим воспользоваться. Так как VB вынуждает использовать COM, алгоритмы работы с плагинами на VC++ и VB будут немного отличаться (листинги, приведенные ниже, сокращены, полный код см. в примерах к статье).

Сначала рассмотрим VC++. Каждый плагин должен иметь свою собственную копию структуры TPPLUGIN, в которой будет храниться его описание, флаги и еще один член, необходимый для работы с загруженным плагином - манипулятор плагина. Так как этот член полезен только в TextPad, он не был указан в структуре, определенной выше. Для работы с плагинами в программе потребуется создать массив указателей на TPPLUGIN достаточной размерности, например, на 500 плагинов. По мере загрузки новых плагинов в массив будут записываться и инициализироваться указатели на новые структуры:

#define MAX_PLUGINS     500

typedef struct tagTPPLUGIN
{
    DWORD dwFlags;
    LPSTR lpDisplayName;
    HWND hMainWnd;
    HWND hEditWnd;
    //--------------------
    HMODULE hPlugin;
    //--------------------
} TPPLUGIN, * LPTPPLUGIN;

//...

LPTPPLUGIN pTPP[MAX_PLUGINS];

void InitTPPArr(DWORD dwIndex)
{
    pTPP[dwIndex] = new TPPLUGIN;
    pTPP[dwIndex]->dwFlags = 0;
    pTPP[dwIndex]->lpDisplayName = "None";
    pTPP[dwIndex]->hMainWnd = 0;
    pTPP[dwIndex]->hEditWnd = 0;
    //--------------------
    pTPP[dwIndex]->hPlugin = 0;
    //--------------------
}

Непосредственно для реализации функции загрузки плагинов (LoadPlugins) стоит обратить внимание на функции FindFirstFile/FindNextFile, LoadLibrary и GetProcAddress. Первые две потребуются для получения списка плагинов, вторая для загрузки плагинов в память, последняя для получения адреса основной функции плагина TextPadPluginMain. Также потребуется функция AppendMenu, так как система плагинов предусматривает отображение плагинов в пунктах меню. Для AppendMenu нужен манипулятор меню плагинов, и наиболее наглядным способом будет передача манипулятора в качестве аргумента функции LoadPlugins:

#define ID_PLGBASE      110
#define ID_PLGUBOUND        1110

BOOL LoadPlugins(HMENU hPlugMenu)
{

    //...
    
    //Начинается поиск файлов в директории плагинов по маске '*.dll'
    hSearch = FindFirstFile(lpFindMask, &WFD);
    if (hSearch != INVALID_HANDLE_VALUE)
    {
        bNext = TRUE;
        while (bNext && dwCnt != MAX_PLUGINS)
        {
            if (strcmp(WFD.cFileName, ".") != 0 && strcmp(WFD.cFileName, "..") != 0)
            {
                InitTPPArr(dwCnt); //Инициализируется новую структуру TPPLUGIN
                
                //...

                //Манипулятор плагина запоминается в структуре
                pTPP[dwCnt]->hPlugin = LoadLibrary(lpPlug);
                if (!pTPP[dwCnt]->hPlugin)
                {
                    FindClose(hSearch);
                    return FALSE;
                }
                //Адрес основной функции плагина
                (FARPROC &)TextPadPluginMain = GetProcAddress(pTPP[dwCnt]->hPlugin, "TextPadPluginMain");
                //Флаги для получения информации
                pTPP[dwCnt]->dwFlags = TPPF_GETDATA | TPPF_ACTIVE;
                if (TextPadPluginMain(pTPP[dwCnt]) != 0)
                {
                    //Ошибка плагина
                }
                //Добавление нового пункта в меню. Его ID представляет собой ID_PLGBASE + порядковый
                //индекс плагина. Это поможет при активации плагина из ID меню получить исходный
                //порядковый индекс.
                if ((ID_PLGBASE + dwCnt) < ID_PLGUBOUND)
                    AppendMenu(hPlugMenu, MF_STRING, ID_PLGBASE + dwCnt, pTPP[dwCnt]->lpDisplayName);
            }
            bNext = FindNextFile(hSearch, &WFD);
            dwCnt++;
        }
        FindClose(hSearch);
        return TRUE;
    }
    else
    {
        return FALSE;
    }
}

В функции загрузки можно предусмотреть дополнительную обработку ошибок. Например, можно проверять манипулятор плагина и адрес функции TextPadPluginMain, так как нельзя исключать возможность повреждения плагина или плагина, который не является Win32 DLL. Можно не экспортировать функции с подобным именем, можно предусматривать пропуск цикла, если основная функция вернула код ошибки.

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

HWND hPDMainWindow;
HWND hPDEditWindow;

BOOL ActivatePlugin(USHORT uPlugID)
{

    //...

    (FARPROC &)TextPadPluginMain = GetProcAddress(pTPP[uPlugID - ID_PLGBASE]->hPlugin, 
        "TextPadPluginMain");
    pTPP[uPlugID - ID_PLGBASE]->hMainWnd = hPDMainWindow;
    pTPP[uPlugID - ID_PLGBASE]->hEditWnd = hPDEditWindow;
    //Флаги для активации плагина
    pTPP[uPlugID - ID_PLGBASE]->dwFlags = TPPF_ACTION | TPPF_ACTIVE;
    if (TextPadPluginMain(pTPP[uPlugID - ID_PLGBASE]) != 0)
    {
        //Ошибка плагина
    }
    return TRUE;
}

Для выгрузки плагинов поребуются две функции: FreeLibrary и RemoveMenu. Алгоритм довольно прост, нужно пройти в цикле по всем элементам массива плагинов и методично выгрузить один плагин за другим, одновременно удаляя соответствующие пункты меню:

BOOL UnloadPlugins(HMENU hPlugMenu)
{
    for (int i = 0; i < MAX_PLUGINS; i++)
    {
        if (pTPP[i])
        {
            //Надо проверить, не продолжает ли плагин свою работу. Если продолжает – отменить
            //дальнейшую выгрузку, так как выгрузка работающего плагина чревата падением этого плагина,
            //а вместе с ним и всей программы. 
            if ((pTPP[i]->dwFlags & TPPF_ACTIVE) == TPPF_ACTIVE) return FALSE;
            FreeLibrary(pTPP[i]->hPlugin); //Выгрузка плагин
            RemoveMenu(hPlugMenu, ID_PLGBASE + i, MF_BYCOMMAND); //Удаление соответствующего пункта меню
        }
    }
    return TRUE;
}

Вместо проверки на TPPF_ACTIVE можно было бы предусмотреть в интерфейсе функцию ClosePlugin и оставить продолжение работы на совести плагина. Кстати, в этом случае вовсе не обязательно экспортировать еще одну функцию, достаточно предусмотреть в структуре TPPLUGIN новый член pClosePlugin, хранящий указатель на данную функцию в плагине.

Рассмотренные выше функции полностью поддерживают работу с плагинами на VC. Они объединены в классе CPlugins (см. файл TextPad.h). В примерах к статье можно найти плагин FindText, обеспечивающий поддержку поиска текста в поле EDIT, там же есть пустой тестовый плагин, с которого можно начать разработку своего собственного плагина для TextPad на VC++.



Теперь рассмотрим VB. Алгоритм будет несколько отличаться, на VB предстоит целиком и полностью работать с COM-объектами, о методе загрузки плагинов на VC++ можно забыть. Вместо массива указателей на TPPLUGIN будет использоваться массив ссылок на COM-объекты – плагины. Размерность пусть останется той же – 500. Массив будет заполняться непосредственно в функции загрузки плагинов ссылками, возвращаемыми VB-функцией CreateObject. Эта функция вызывает CLSIDFromProgID для конвертации ProgID в CLSID, затем CoCreateInstance с IID_IUnknown и pIUnknown->QueryInterface с IID_IDispatch. Поиск файлов будет осуществляться с помощью встроенной VB-функции Dir. Но LoadLibrary/GetProcAddress/FreeLibrary, а также CallWindowProc все равно потребуются, так как у ActiveX библиотек есть один весьма неприятный недостаток: для работы их необходимо зарегистрировать, пусть и единожды. Регистрация представляет собой запись в HKEY_CLASSES_ROOT\ таких данных, как CLSID всех определенных в библиотеке COM-объектов, их ProgID, ID интерфейсов и т.д. Эту непростую задачу решает функция DLLRegisterServer, которую должна экспортировать любая библиотека, претендующая на поддержку OLE Automation. VB создает эту функцию автоматически, в VC пришлось бы реализовывать её самостоятельно.
Ниже показана регистрация плагинов с помощью вызова экспортируемой ими функции DLLRegisterServer:

Private Function RegisterLib(ByVal strFileName As String) As Boolean
    On Error Resume Next    '...

    hLib = LoadLibrary(strFileName)
    If hLib = 0 Then Exit Function
    hProc = GetProcAddress(hLib, "DllRegisterServer")
    If hProc = 0 Then GoTo StopRegister
    'Вызов DLLRegisterServer для регистрации плагина в системном реестре
    'Нужно обойти невозможность в VB вызывать функции по указателю:
    If CallWindowProc(hProc, 0, ByVal 0&, ByVal 0&, ByVal 0&) _
        <> 0 Then GoTo StopRegister
    RegisterLib = True
StopRegister:
    Call FreeLibrary(hLib)
End Function

Регистрацию стоит проводить только в том случае, если плагин еще не зарегистрирован, или если его регистрация повреждена. Проще всего это сделать при обработке VB-ошибки 429, которую генерирует CreateObject в том случае, если попытка создать объект с заданным ProgID была неудачной:

Private Const MAX_PLUGINS As Long = 500Private hTPPlugArr(MAX_PLUGINS) As ObjectPublic Function LoadPlugins() As Boolean
    On Error GoTo Error    '...

    strFileList = Dir(strFolder & strPattern, vbNormal)
    Do While Not strFileList = vbNullString
        lFilesCnt = lFilesCnt + 1
        'Создание объекта из найденной ActiveX DLL
        Set hTPPlugArr(lFilesCnt) = CreateObject(GetTitle(strFileList) & ".clsMain")
        'Создание пункта меню для плагина...
        If frmMain.mnuPlugin.UBound < lFilesCnt Then
            Load frmMain.mnuPlugin(lFilesCnt)
            With frmMain.mnuPlugin(lFilesCnt)
                .Visible = True
                .Caption = hTPPlugArr(lFilesCnt).DisplayName
            End With
        Else
            frmMain.mnuPlugin(lFilesCnt).Caption = hTPPlugArr(lFilesCnt).DisplayName
        End If
        strFileList = Dir
        DoEvents
    Loop
    LoadPlugins = True
Error:
    If Err.Number <> 0 Then
        Select Case Err.Number
            Case 429 'ActiveX компоненту не удалось создать объект. Попытка регистрации
                If RegisterLib(strFolder & strFileList) Then
                    Err.Clear
                    Resume
                End If
            Case Else
                LoadPlugins = False
                Exit Function
        End Select
    End If
End Function

Функция ActivatePlugin получает индекс плагина в массиве, инициализирует его свойства и вызываем PluginMain:

Public Function ActivatePlugin(ByVal lPlugID As Long) As Boolean    '...

    hTPPlugArr(lPlugID).MainWnd = hPDMainWindow
    hTPPlugArr(lPlugID).EditWnd = hPDEditWindow
    hTPPlugArr(lPlugID).IsActive = True
    If hTPPlugArr(lPlugID).PluginMain <> 0 Then
        'Ошибка плагина
    End If
    ActivatePlugin = True
End Function

Функция выгрузки плагинов похожа на рассмотренную для VC++. Осуществляется проход по массиву в цикле и методично удаляются ссылки на объекты после проверки их на активность:

Public Function UnloadPlugins() As Boolean
    Dim i As Long
    For i = 0 To UBound(hTPPlugArr)
        If Not hTPPlugArr(i) Is Nothing Then
            If hTPPlugArr(i).IsActive Then
                UnloadPlugins = False
                Exit Function
            End If
            'Удаление ссылки на класс 
            Set hTPPlugArr(i) = Nothing
            'Если меню основное – изменяется заголовок, его копии удаляются
            If i = 0 Then frmMain.mnuPlugin(i).Caption = "None" Else _
                Unload frmMain.mnuPlugin(i)
        End If
    Next i
    UnloadPlugins = True
End Function

Вместо проверки IsActive можно было бы создать метод PluginClose, где плагин принудительно завершал бы свою работу.
Функции, поддерживающие работу с плагинами на VB, объединены в классе clsPlugins (см. файл clsPlugins.cls). В примерах можно найти плагин Statistics, отображающий количество слов, символов и строк в поле EDIT, а также тестовый плагин, с которого можно начать разработку собственного плагина для TextPad на VB.
VB обладает широкими возможностями. Например, можно создать AxtiveX EXE, добавить в проект глобальный класс и определить в нем интерфейс плагина:

Option ExplicitPublic Function MyFunctionOne(ByVal lData As Long) As Boolean
    '
End FunctionPublic Sub MySubOne(ByVal strData As String)
    ' 
End SubPublic Property Get MyProperty() As Byte
    '
End PropertyPublic Property Let MyProperty(ByVal bValue As Byte)
    '
End Property

После этого в проекте плагина подключается EXE (Project->References), и в глобальном классе "наследуется" интерфейс класса EXE:

Option ExplicitImplements SomeClassPrivate Function SomeClass_MyFunctionOne(ByVal lData As Long) As Boolean
    '
End FunctionPrivate Property Let SomeClass_MyProperty(ByVal RHS As Byte)
    '
End Property'...

Теперь ошибка реализации интерфейса плагина исключается. Можно объявить класс EXE в плагине как:

Public WithEvents MyClass As SomeClass

А в проекте проинициализировать переменную MyClass:

Set hPlugin.MyClass = CSomeCls

Теперь из плагина обеспечивается доступ ко всем событиям, генерируемым классом. Механизм событий позволяет управлять всеми плагинами одновременно.

Примечание

В рассмотренных примерах и для VC++, и для VB все плагины загружаются в память и не выгружаются до тех пор, пока не будет вызвана функция UnloadPlugins, что происходит только при завершении работы программы. Это удобно, но не экономично по отношению к памяти. Решением может стать динамическая загрузка/выгрузка плагинов по мере необходимости. Например, для VC++ вместо сохранения манипуляторов библиотек можно запоминать имена файлов, и при необходимости использовать их для вызова LoadLibrary:

char lpTmp[MAX_PATH] = { 0 };
pTPP[dwCnt]->lpPlugin = new char[128];
strcpy(pTPP[dwCnt]->lpPlugin, WFD.cFileName);

//...

strcpy(lpTmp, lpPlugPath);
strcat(lpTmp, "\");
strcat(lpTmp, pTPP[dwCnt]->lpPlugin);
hVariable = LoadLibrary(lpTmp);

//...

FreeLibrary(hVariable)

Аналогично для VB, достаточно заменить массив ссылок на объекты строковым массивом, хранящим ProgID имеющихся плагинов, а при необходимости использовать сохраненные ProgID для вызова CreateObject:

strTPPlugArr(lFilesCnt) = GetTitle(strFileList) & ".clsMain"
Set hVariable = CreateObject(strTPPlugArr(lFilesCnt))'...

Set hVariable = Nothing

Заключение

Приведенные примеры демонстрируют только два из огромного множества способов реализации системы плагинов. Они просты для понимания, но не претендуют на звание самых эффективных и уж тем более универсальных методов. Поэтому читателю рекомендуется поэкспериментировать, попробовать создать свои методы на основе уже имеющихся и проверенных моделей. Так, например, для мультимедиа рекомендуется взять за основу Winamp SDK или foobar2000 SDK, для графики - GIMP SDK, для текста Notepad++ SDK или AbiWord SDK.

Автор: BV (borisbox@mail.ru)




Слово "плагин" (от англ. Plug-in [Plugin] – подключаемый, съемный, сменный) отсутствует в русском языке, но в последнее время используется как синоним и сокращение словосочетания «подключаемый модуль»

У проекта типа ActiveX DLL, определен как минимум один COM-объект, у которого есть несколько базовых интерфейсов:

  • IClassFactory - интерфейс для создания экземпляра этого COM-объекта
  • %DefaultInterface% - основной интерфейс COM-объекта. Именно его метод QueryInterface должен вызываться IClassFactory->CreateInstance
  • %DispInterface% - необязательный интерфейс, через который OLE Automation работает с событиями (Events)
  • %OtherUDInterfaces% - прочие дополнительные интерфейсы, например, ISupportErrorInfo, IProvideClassInfo или пользовательские интерфейсы.
Все эти интерфейсы обязаны наследовать и реализовывать интерфейс IUnknown, опционально - IDispatch (для %DispInterface% IDispatch обязателен).
COM-объект определяется в TypeLibrary директивой coclass (язык ODL - Object Description Language):
[
  uuid(00000000-0000-0000-0000-000000000000), //CLSID COM-объекта MyObject
  helpstring("MyObject")
]
coclass MyObject {
    [default] interface _IMyObject; //Основной интерфейс объекта
    [default, source] dispinterface _IMyObjectEvents; //Основной интерфейс событий объекта
    interface ... //Прочие интерфейсы объекта 
};



 Design by Шишкин Алексей (Лёха)  ©2004-2008 by sources.ru