Дата публикации статьи: 29.07.2008 18:09

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

Скачать примеры к статье

Введение

Многие из вас, наверно, задумывались о таком элементе программы как плагин (вообще говоря, слово "плагин" отсутствует в русском языке, есть "подключаемый [модуль]", но из-за неудобности произношения последнего я все-таки остановился на использовании слова "плагин") от англ. Plug-in [Plugin] - подключаемый; съемный; сменный. Для чего нужны плагины? Как и следует из названия, для того, что бы дополнить, расширить функциональность программы без изменения её самой. Например, придать программе новый внешний вид, увеличить количество поддерживаемых форматов файлов, добавить новые инструменты для работы с графикой или текстом... В этой статье я расскажу о разработке плагинов в проектах на Visual C++ 6/Visual Basic 6 (предполагается, что вы знакомы с языками программирования С++ и/или Visual Basic, средой разработки Visual Studio 6 и WinAPI).

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

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

Если вы не желаете вдаваться в подробности COM или вас интересует разработка плагинов только на VC++ (без использования COM), то можете пропустить эту часть статьи и сразу перейти к организации системы плагинов. На самом деле, разработчику на VB вовсе не обязательно знать механизм работы COM, достаточно иметь о нем общее представление.
Все еще здесь? Тогда продолжим. И так, при подключении COM-объекта VB вызывает IUnknown: посредством CoCreateInstance с IID_IUnknown (она в свою очередь вызывает CoGetClassObject с IID_IClassFactory (обязательный интерфейс описания COM-объектов), после pIClassFactory->CreateInstance с IID_IUnknown) VB получает указатель на интерфейс IUnknown (pIUnknown). Далее 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 всегда указывают на методы IUnknown (IUnknown обязателен для любого интерфейса COM-объекта, он позволяет получить указатели на прочие интерфейсы COM-объекта, вести учет количества ссылок на интерфейс COM-объекта и освободить интерфейс 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). А если нужен всего один метод? У нас есть порядковый номер метода и его прототип. Ничто не мешает нам его вызвать:

#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]; //Вызываем метод...
    }
}

Вернемся к получению указателя на IDispatch - если VB получит указатель, то вызов метода/свойства будет осуществлен через 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);

Что передается в dispidMember, вы уже знаете, теперь пройдемся по остальным аргументам:

riid
зарезервированный на будущее аргумент, он всегда должен быть равен IID_NULL.

lcid
задает контекст локали, в который будут интерпретироваться аргументы. Если приложение не поддерживает множественные национальные языки - этот параметр можно игнорировать.

wFlags
определяет режим вызова для Invoke, и может принимать следующие значения:
DISPATCH_METHOD - член реализован как метод.
DISPATCH_PROPERTYGET - член должен вызываться как свойство (или член данных) для получения значения
DISPATCH_PROPERTYPUT - член должен изменить свое значение как свойство (или член данных).
DISPATCH_PROPERTYPUTREF - член должен изменить свое значение как свойство (или член данных) через присвоение ссылки, а не значения, как в случае с DISPATCH_PROPERTYPUT.

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. TextPad представляет собой окно с полем EDIT, и умеет открывать/сохранять файлы в формате Plain Text. Мы захотели расширить функциональность TextPad, скажем, для внедрения таких возможностей, как поиск текста и отображение статистики документа (количество символов, строк, слов...). Что потребуется плагинам для выполнения этих задач? Конечно же, возможность работы с полем EDIT. Значит, передавать плагинам мы будем манипулятор нашего поля. Кроме того, окнам плагинов совсем не помешает возможность взаимодействия с основным окном программы. Значит, кроме манипулятора окна EDIT будем передавать и манипулятор основного окна программы. С тем, что будем передавать - определились. Теперь надо определиться с тем, что мы будем получать. Как предоставить пользователю возможность работы с плагинами? Проще всего это сделать посредством пунктов меню. Но что писать в заголовках этих пунктов? Названия файлов на диске? Нет, хотя бы потому, что это неинформативно. Значит, нам потребуется получить полное название плагина для заголовка пункта. Получилась вот такая схема системы плагинов для TextPad:

scheme

Все предельно просто: при запуске программы (или в любой другой удобный момент времени) мы загружаем плагин и получаем его имя, которое используем для добавления пункта меню в меню плагинов. Как только пользователь щелкнул по данному пункту меню, мы передаем плагину манипулятор основного окна и манипулятор поля 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, а из проекта на VB использовать плагины на VC++ точно так же, как и в проекте на VC++ (обратное тоже верно). Но тогда бы мы нарушили чистоту эксперимента. И так, интерфейсы плагинов на VC++ и VB готовы, осталось ими воспользоваться из TextPad. Плавно переходим к заключительной и самой важной части статьи - использование плагинов программой.

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

Система плагинов составлена, интерфейс плагинов создан, осталось только всем этим воспользоваться. Так как в случае 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) или не экспортировать функции с подобным именем. Можно так же предусмотреть пропуск цикла в случае, если основная функция вернула код ошибки.
И так, у нас есть массив плагинов, есть пункты меню, все готово для функции активации плагина ActivatePlugin. А что нам нужно для активации плагина? Конечно же, информация о том, какой именно плагин следует активировать Ну, и, собственно, манипуляторы основного окна и окна 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). Теперь можно смело заявить - TextPad на VC++ поддерживает плагины (по умолчанию, в директории %AppPath%\\Plugins\\) В примерах к статье можно найти плагин 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++, конечно же, придется реализовывать её самому, разве лишь за тем исключением, что регистрацию TypeLibrary можно будет оставить на LoadTypeLib/RegisterTypeLib. И так, для регистрации плагинов нам потребуется вызывать экспортируемую ими функцию DLLRegisterServer:

Private Function RegisterLib(ByVal strFileName As StringAs 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 LongAs 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

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

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 LongAs 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 LongAs 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 (borisvorontsov@gmail.com)


1 У проекта типа 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 ... //Тут указываются прочие интерфейсы объекта
};