Оглавление
Зачем это надо
Для начала хорошо бы пояснить: а зачем вообще такая возможность может понадобится? Да для совершенно разных целей.Вот, к примеру, во второй части у нас были плагины экспорта и импорта. И там я сказал такую вещь:
Здесь нужно обратить внимание на фундаментальное изменение в поведении программы. Раньше, когда плагинов в программе у нас не было, мы могли загрузить файл и нажать на кнопку "Save". И это действие сохранило бы файл в оригинальном формате. Но когда мы перевели загрузку/сохранение на плагины - мы этого сделать не можем. Потому что у нас сейчас нет информации о связи плагинов импорта и экспорта. И если у нас установлен плагин импорта, скажем, из HTML, то это не значит, что установлен и плагин экспорта в HTML. Иными словами, операция может быть односторонней. Поэтому в данном примере при открытии файла мы помечаем его как ещё не сохранённый - и тогда при сохранении мы покажем диалог выбора имени файла и плагина для экспорта.Как видите, независимые плагины не могли обеспечить "гладкое" поведение программы: при открытии RTF-файла нельзя было просто сохранить файл - нужно было заново указывать имя и тип файла (т.е. выбирать плагин для экспорта).
А чтобы реализовать исходное поведение, нам нужно приложить больше усилий - нужно придумать способ ассоциации плагинов импорта с плагинами экспорта. Делать это можно разными способами, но сейчас я не буду рассматривать этот вопрос. Пока нам хватит текущего материала.
Если бы мы дали возможность плагинам работать друг с другом, то плагин импорта мог бы подобрать свою обратную пару и сообщить о ней ядру. Тогда можно было бы реализовать исходное поведение программы: после открытия файла его можно просто сохранить и файл будет сохранён в исходном (оригинальном) формате.
Другой пример: всевозможная обработка данных, когда плагины выстраиваются цепочкой. Если использовать наши наработки к этому моменту, то обработка некоторого потока данных выглядела бы так: ядро формирует поток данных (входную очередь), затем вызывает первый плагин, тот как-то обрабатывает данные и выдаёт ядру выходной поток данных, ядро берёт этот поток и передаёт второму плагину в цепочке и так далее. Это не очень страшно, если речь идёт о небольших объёмах данных, небольшом числе плагинов в цепочке или некритичности скорости обработки. Однако в сценарии, к примеру, с потоковой обработкой звука нам было бы желательно, чтобы плагины общались друг с другом напрямую, минуя ядро.
Третий пример: плагины для плагинов. Как мы видели ранее, в нашей системе отсутствует понятие "тип плагина". Плагин характеризуется просто набором интерфейсов - т.е. услуг, которые он предоставляет ядру. В этом случае возможны ситуации, когда один плагин предоставляет несколько сервисов и даже не предоставляет их вовсе (не предоставляет ни одного поддерживаемого ядром сервиса). Всё это означает, что систему плагинов может использовать не только само ядро, но и его плагины! К примеру, в прошлый раз мы создали плагин для работы с текстовым редактором нашей программы. Конечно, в качестве примера мы использовали очень простые возможности. В реальной программе мы могли бы реализовать более полный и функциональный интерфейс (об этом - в следующий раз), но всё равно, его сервиса могло бы быть недостаточно. Тогда мы могли бы написать мета-плагин: который берёт существующую функциональность ядра (интерфейс редактора) и делает вокруг неё обёртку: предоставляя новые функции. К примеру, замены поиска и замены текста, использование регулярных выражений, работа со стилями и т.п. Использование этого функционала существенно упростило бы разработку других текстовых плагинов. Поэтому было бы целесообразно писать "плагины к плагину" - плагины, которые используют сервисы нашего мета-плагина для выполнения своей работы. И мы даже могли бы написать плагин, который позволяет писать макросы для текстового редактора.
Четвёртый пример: "дружественные плагины". Тут имеется в виду вот что: если мы разместим в одной DLL два плагина (вернее один плагин, но с двумя наборами услуг: например, плагин импорта и экспорта), то два таких "плагина" могут общаться друг с другом напрямую, минуя API ядра. Иногда это может быть крайне полезно - в случаях, когда нужно работать с данными, конвертация или подготовка которых для API ядра может быть нежелательна (быть медленной или происходить с потерей данных). В этом случае плагины могут общаться между собой напрямую, минуя официальный API. Понятно, что в отличие от предыдущих случаев, эти плагины являются тесно связанными - как правило, они разрабатываются в паре и одним разработчиком.
Короче говоря, как вы видите из этих примеров - хотя это и не очень типичная ситуация, но на практике вполне могут встретиться ситуации, когда вам нужно реализовать взаимодействие между плагинами. Но чем же полезен этот раздел для тех, кто не собирается делать меж-плагиновые связи в своих программах? Дело в том, что разработка механизма взаимодействия плагинов затрагивает такие вопросы как идентификация плагинов - и некоторые из этих приёмов могут быть полезны и в обычных программах, без меж-плагинного взаимодействия.
Базовая идентификация и услуги ядра
В самой первой части мы ввели интерфейсIPluginInfo
, где, помимо human-readable информации, мы ввели уникальный идентификатор каждого плагина на базе GUID. В последующих частях этот интерфейс был выпилен, поскольку ни плагины экспорта/импорта, ни плагины меню, ни плагины редактора никак не идентифицировали себя и не показывались в каком-либо "списке плагинов".Начиная с этой части, мы возвращаем этот интерфейс в наши плагины. Теперь каждый плагин обязан реализовывать его и обязательно возвращать уникальный ID. Вся наша система будет идентифицировать плагины по этому ID.
Я напомню его объявление:
type IPluginInfo = interface // private ['{631B96BB-1E7E-407D-83F1-5C673D2B5A15}'] function GetID: TGUID; safecall; function GetName: WideString; safecall; function GetVersion: WideString; safecall; // public property ID: TGUID read GetID; property Name: WideString read GetName; property Version: WideString read GetVersion; end;
Далее, раз мы говорим про общение плагинов друг с другом - то это значит, что ядро должно дать плагинам возможность узнавать друг о друге и общаться. Мне видится, что общаться плагины могут и напрямую, без посредника в виде ядра: раз у нас есть интерфейсы вроде
IPlugin
/IPluginInfo
, которые плагин выдаёт ядру, то нет никаких причин, почему бы ровно эти же интерфейсы не использовали бы сами плагины!Это значит, что ядро должно реализовать интерфейс вроде такого:
type IPlugins = interface ['{AB739898-6A48-4876-A9FF-FFE89B409A56}'] // private function GetCount: Integer; safecall; function GetPlugin(const AIndex: Integer): IPluginInfo; safecall; // public property Count: Integer read GetCount; property Plugins[const AIndex: Integer]: IPluginInfo read GetPlugin; end;Ядро должно реализовывать этот интерфейс и передавать его плагинам - это и есть тот минимум, что позволит плагинам общаться между собой. Теперь любой плагин может запросить у ядра
IPlugins
и использовать его для перечисления всех загруженных плагинов и выбора нужного (по ID).Как вы видите, этот интерфейс является укороченным вариантом внутреннего
IPluginManager
из модуля PluginManager.pas прошлых серий. Конечно же, вы вольны расширить его полезными функциями вроде function IndexOf(const AID: TGUID): IPluginInfo;
и другими - но это уже дополнительный обвес.Если вы попытаетесь реализовать
IPlugins
в менеджере плагинов, то столкнётесь с проблемой: поскольку менеджер плагинов уже реализует IPluginManager
, то у него уже есть методы с таким именем (GetCount
). Решение проблемы - использовать делегирование:
type TPluginManager = class(TInterfacedObject, IUnknown, IPluginManager, IPlugins, ICore) private ... protected ... // IPluginManager function GetItem(const AIndex: Integer): IPlugin; function GetCount: Integer; ... // IPlugins function Plugin_GetCount: Integer; safecall; function Plugin_GetPlugin(const AIndex: Integer): PluginAPI.IPlugin; safecall; function IPlugins.GetCount = Plugin_GetCount; function IPlugins.GetPlugin = Plugin_GetPlugin; ... end;Как видите, для
IPluginManager
мы используем имена методов "как есть" - это обычный способ реализации интерфейсов. А для IPlugins
мы используем переименованные методы (добавляя префикс "Plugin_"), а затем используем перенаправление: указывая, что метод Plugin_GetCount
- это реализация для IPlugins.GetCount
. Аналогично и с индексным свойством.Также обратите внимание, что у нас есть два
IPlugin
: один - для ядра (с дополнительными свойствами вроде Index
, Handle
, FileName
), а второй - для плагинов. Поэтому мы использовали полное имя во втором случае (PluginAPI.IPlugin
). Альтернатива, как я уже упоминал ранее, - просто использовать различные имена.После этого любой плагин может делать примерно следующее:
constructor TPlugin.Create(const ACore: ICore); var Plugins: IPlugins; X: Integer; begin inherited Create; FCore := ACore; Assert(FCore.Version >= 1); if Supports(FCore, IPlugins, Plugins) then begin for X := 0 to Plugins.Count - 1 do if IWantThatGuy(Plugins[X].ID) then begin // Мы нашли другой плагин - теперь работаем с ним, что-то делаем. DoSomething(Plugins[X]); Break; end; end; ... end;Как видите, в базовом виде тут всё достаточно просто. Единственный тонкий момент здесь - циклические ссылки. Например, если при инициализации плагин сохранит себе ссылку на другой плагин, а другой плагин - на первый, то получится ситуация, когда два плагина ссылаются друг на друга и поэтому не могут быть выгружены с использованием стандартного механизма подсчёта ссылок (такая связь может быть и опосредованной, не прямой, и включать более двух участников).
Мы уже встречались с похожей ситуацией ранее и привели решение этой проблемы: использование интерфейса
IDestroyNotify
. Если плагин хранит какие-то ссылки, то он обязан реализовать IDestroyNotify
и в его методе Delete
отпустить все ресурсы. Почти всегда вам нужно просто разместить в Delete
код деструктора (и в самом деструкторе просто вызывать Delete
). Общий шаблон плагина при этом:
type TPlugin = class(TInterfacedObject, IUnknown, IDestroyNotify, { ...тут - сервисы, которые предоставляет ваш плагин...}) private ... procedure Delete; safecall; public constructor Create(const ACore: ICore); destructor Destroy; override; end; ... procedure TPlugin.Delete; begin // код очистки end; destructor TPlugin.Destroy; begin Delete; inherited; end; ...Вот и всё. Достаточно просто.
Примечание: на самом деле вы, скорее всего, захотите также дополнительно сделать что-то вродеtype ILoadNotify = interface ['{16A8B203-DED1-479E-AEB9-7ACAB1734F37}'] procedure Loaded; safecall; end;МетодLoaded
будет вызываться ядром после загрузки всех плагинов:procedure TPluginManager.DoLoaded; var X: Integer; LoadNotify: ILoadNotify; begin for X := 0 to Count - 1 do if Supports(Plugins[X], ILoadNotify, LoadNotify) then LoadNotify.Loaded; end;procedure TMainForm.FormCreate(Sender: TObject); begin ... // Загрузка всех плагинов. Подразумевается, что они лежат в под-папке Plugins Plugins.LoadPlugins(ExtractFilePath(ParamStr(0)) + 'Plugins', SPluginExt); // ... тут может стоять код загрузки дополнительных плагинов // Теперь - уведомим плагины, что загрузка закончена Plugins.DoLoaded; ... end;Дело в том, что если плагин попытается получить ссылки на другие плагины в своём конструкторе - ему это может не удасться сделать: потому что он может быть загружен ранее плагина, который он ищет! Вот почему нам нужно пост-событие для загрузки. Как только все плагины загружены, ядро вызываетILoadNotify.Loaded
, и те плагины, которые в этом заинтересованы, могут в этот момент начать работать с другими плагинами (которые уже гарантированно будут загружены к этому моменту).
С учётом этих наработок давайте дополним наш пример с плагинами экспорта/импорта новыми возможностями: а именно - решим проблему, о которой я сказал в начале статьи.
Для этого добавим в интерфейс
IImportPlugin
новый метод:
type IImportPlugin = interface(IExportImportPluginInfo) ['{6C85B093-7AAF-4EF0-B98E-D9DBDE950718}'] ... function ExportPlugin: TGUID; safecall; end;Как несложно сообразить, этот метод должен вернуть нам плагин (вернее, его ID), который является обратным к текущему плагину импорта. Любой плагин импорта может реализовать этот метод, чтобы вы могли сделать File/Open, а затем File/Save без необходимости заново выбирать формат файла. Как именно плагин будет выбирать себе пару - зависит только от плагина. Именно он решает, что выбрать. Кто-то может вернуть любой плагин, поддерживающий тот же формат, что и он:
type TPlugin = class(TInterfacedObject, IUnknown, IPlugin, IExportImportPluginInfo, IImportPlugin) .... // IImportPlugin procedure ImportRTF(const AFileName: WideString; const ARTF: IStream); safecall; function ExportPlugin: TGUID; safecall; ... end; ... function TPlugin.GetMask: WideString; begin Result := '*.rtf'; end; ... function TPlugin.ExportPlugin: TGUID; var Plugins: IPlugins; ExportPlugin: IExportPlugin; X: Integer; begin Result := GUID_NULL; if Supports(FCore, IPlugins, Plugins) then begin for X := 0 to Plugins.Count - 1 do if Supports(Plugins[X], IExportPlugin, ExportPlugin) then begin if ExportPlugin.Mask = GetMask then begin Result := Plugins[X].ID; Break; end; end; end; end;А кто-то может предпочесть жёсткую привязку к конкретному плагину:
function TPlugin.ExportPlugin: TGUID; const ExportPluginID: TGUID = '{E9BF498C-0B6F-4EDC-94D9-941EAFE91CAE}'; begin Result := ExportPluginID; end;В любом случае, теперь ядро может использовать эту информацию следующим образом:
procedure TMainForm.FileOpen(Sender: TObject); var ... Ind: Integer; begin ... if OpenDialog.Execute then begin ... // Команда сохранения Ind := Plugins.IndexOf(Plugin.ExportPlugin); if Ind >= 0 then begin SetFileName(OpenDialog.FileName); SaveDialog.FileName := OpenDialog.FileName; SaveDialog.FilterIndex := Plugins[Ind].FilterIndex + 1; end; end; end;Вот и всё, собственно. Теперь, наконец-то наше "плагинизированное" приложение ведёт себя точно так же, как и исходное (при условии, конечно, что у вас будут установлены парные плагины).
Скачать пример к этому моменту можно тут.
Кроме вышеуказанного в примерах сделаны следующие изменения, которые я, однако, обсуждать не буду:
- Блокировка загрузки дублирующихся (по ID) плагинов
- Поиск плагина по ID в менеджере плагинов
- Некоторые интерфейсы я переименовал
- В меню создаётся список загруженных плагинов (для информации)
- Все плагины были пересобраны с учётом вернувшегося
IPluginInfo
(который теперь простоIPlugin
)
IPluginInfo
). Такое фундаментальное изменение делает старые и новые плагины несовместимыми. И, по-хорошему, нам нужно изменить значение константы SPluginInitFuncName
на новый GUID. А если это не сделать - ваша новая программа может попытаться загрузить плагин старого формата и весьма впечатляюще вылететь с Access Violation.Идентификация обратного вызова
Теперь, более интересный вопрос. Чтобы ввести вас в курс дела, я напомню такие слова с прошлого раза:Ещё замечу, что в указанной модели подразумевается, что пункты меню, созданные плагином, будут отслеживаться и удаляться при выгрузке плагина. Поскольку у меня не сделано отслеживание пунктов меню, то функциональность "забыть ссылку без удаления" сейчас работать не будет. Вернее, работать-то она будет, но пункты плагина останутся висеть в меню вечно. Я оставлю доработку на следующий раз, где поговорю о вопросах общения плагинов между собой (это тесно связанный с указанной темой вопрос), а пока пусть плагины за собой чистят явно.Итак, суть проблемы в том, что когда плагин вызывает метод ядра (в том примере это был
IMenuManager.CreateMenuItem
), то ядро понятия не имеет, а кто же это его дёргает (имеется в виду, какой именно плагин из всех загруженных). Иными словами, обратные вызовы в той реализации не позволяют нам идентифицировать плагины.Классическое решение этой проблемы - это т.н. "куки" (cookie) (или "user-параметры"). Суть проста - вызывающему передаётся непрозрачное значение, которое он затем обязан передать без изменений в функцию обратного вызова (слово "непрозрачное" означает, что вызывающий не знает, что с ним (значением) можно делать и не умеет его трактовать; он может лишь передать его в функцию; если вам доводилось программировать на Windows API, то это понятие должно быть вам хорошо знакомым - непрозрачными куками являются
THandle
, HWND
и вообще любые другие описатели/дескрипторы). Итак, в переложении к нашему случаю эта модель выглядела бы примерно так:
type TInitPluginFunc = function(const ACore: ICore; const AUserParam: Pointer): IPlugin; safecall; ... IMenuManager = interface ['{216082F8-8FE8-4B51-83E5-C8324452AD18}'] function CreateMenuItem(const AUserParam: Pointer): IMenuItem; safecall; procedure DeleteMenuItem(var AItem: IMenuItem; const AUserParam: Pointer); safecall; end;Короче, у любого метода, предоставляемого ядром плагину, появится один новый дополнительный параметр -
AUserParam
. Ядро укажет этот параметр при инициализации плагина (это может быть, скажем, индекс плагина или указатель на объект-обёртку), а плагин обязан будет потом передавать этот параметр во все функции. Сам плагин не знает, что это за значение, он его просто передаёт. Тогда при вызове любого своего метода ядро сможет идентифицировать плагин по этому дополнительному параметру.Я не стану использовать эту модель по понятным причинам - мы по сути перекладываем задачи управления на плечи плагинов. Это не слишком-то дружелюбно, не так ли? Но что же мы тогда можем тут сделать?
Ну, если вы присмотритесь внимательнее, то увидите, что "печенюшки" у нас уже есть. Что же это?
Это - интерфейс
ICore
.В самом деле, интерфейсный указатель вполне ведь может служить "волшебным" значением. Пусть даже и прозрачным. Конечно, сейчас мы передаём во все плагины один и тот же указатель
ICore
, но никто же не запрещает нам передавать разные указатели, верно?Если каждому плагину мы дадим его собственный
ICore
, то при вызове методов ядра мы сможем идентифицировать плагин - по указателю, через который был сделан вызов.Вроде идея отличная, но как такое реализовать? Давайте по шагам.
Изменения в менеджере плагинов
Во-первых, понятно, что теперь вместо одногоICore
у нас появляется много ICore
- по одному на каждый загруженный плагин. Понятно, что теперь менеджер плагинов не может реализовывать все интерфейсы - потому что он один, а ICore
много. Кто тогда возьмёт на себя эту ответственность? Ну, объект-обёртка плагина, конечно же!Примечание: хотя это подходит нам с точки зрения соответствия один-к-одному (обёртка-ICore
), но это не очень правильно с точки зрения разделения обязанностей. Ведь задача обёртки к плагину - управление плагином. А дублирование интерфейсов ядра - это уже другая задача. Поэтому более правильным решением было бы создание отдельного объекта, который реализует интерфейсы ядра для указанного плагина и сохранение ссылки на него в обёртке к плагину. Но поскольку мне лень плодить объекты - я совмещу эти две сущности в одном объекте. Но вы предупреждены, что это не самое красивое решение.
Также заметим, что при таком решении у нас возникает дополнительная циклическая ссылка: обёртка держит ссылку на плагин, плагину мы передаём ядро (ICore
), аICore
у нас... реализует обёртка. Вот и получается, что обёртка ссылается на плагин, а плагин - на обёртку. Разрывается эта циклическая ссылка уже знакомым нам образом - через использованиеIDestroyNotify
. Поэтому, любой плагин, которых сохраняет переданный емуICore
, обязан также реагировать и наIDestroyNotify.Delete
, где ему нужно будет обнулить сохранённую ссылку:
type TPlugin = class(TInterfacedObject, IDestroyNotify, ...) private FCore: ICore; protected ... // IDestroyNotify procedure Delete; safecall; public ... destructor Destroy; override; ... end; destructor TPlugin.Destroy; begin Delete; inherited; end; procedure TPlugin.Delete; begin FCore := nil; end;
Итак, соответствующее изменение в модуле менеджера плагинов будет выглядеть примерно так (с компенсирующими изменениями в остальном коде, конечно же):
type TPlugin = class(TInterfacedObject, IUnknown, IPlugin, ICore) private ... protected ... // IPlugin ... function GetVersion: String; ... // ICore function GetCoreVersion: Integer; safecall; function ICore.GetVersion = GetCoreVersion; public ... end; ... constructor TPlugin.Create(const APluginManger: TPluginManager; const AFileName: String); begin ... FPlugin := FInit(Self); // было: FPlugin := FInit(FManager); ... end; ... function TPlugin.GetCoreVersion: Integer; begin // Здесь мы точно знаем, какой именно плагин запрашивает версию ядра - это ровно мы и есть Result := FManager.GetVersion; end;Здесь мы снова воспользовались делегированием метода для разрешения конфликта имён методов GetVersion у
IPlugin
и ICore
.Конечно, непосредственно для
ICore
возможность идентифицировать вызывающий плагин не является ужасно полезной - кому какая разница, кто вызывает GetVersion
? (и именно поэтому, в реализации метода мы просто вызываем "общую" реализацию, которая как была, так и осталась у менеджера плагинов).Что гораздо более интересно - когда плагин запрашивает у
ICore
другие интерфейсы. Если вы вспомните, как мы это делали в прошлый раз, то вы будете смотреть на метод RegisterServiceProvider
и концепцию "программа регистрирует в ядре свои услуги". Теперь, в связи с введением идентификации плагинов, у нас больше нет единого объекта для всех плагинов, у нас теперь много объектов (как ICore
, так и любых других зарегистрированных интерфейсов), поэтому нам нужно изменить и концепцию регистрации "провайдеров".По сути, нам потребуется выдавать индивидуальный интерфейс "услуги программы" для каждого плагина по его запросу. Т.е. как минимум это должно быть как-то так:
type IServiceProvider = interface function CreateInterface(const APlugin: IPlugin; const AIID: TGUID; out Intf): Boolean; end;И тогда метод регистрации изменится на:
type IPluginManager = interface ... procedure RegisterServiceProvider(const AProvider: IServiceProvider); end;Иными словами, если раньше мы регистрировали сам объект-реализатор, то теперь мы регистрируем "создатель объекта-реализатора" - т.е. мы увеличили уровень косвенности на 1. Именно введение промежуточного объекта позволит нам хранить ссылку на плагин.
Конечно же, соответствующим образом изменится и получение интерфейсов для плагинов:
type TPluginManager = class(TInterfacedObject, IUnknown, IPluginManager, IPlugins) ... protected // Нам больше не нужен QueryInterface, потому что ICore отсюда уехал к TPlugin // Теперь всеми интерфейсами у нас заведует TPlugin, но общее хранилище всё равно находится тут // Новый метод вместо QueryInterface: function CreateInterface(const APlugin: PluginManager.IPlugin; const AIID: TGUID; out Intf): Boolean; ... end; ... function TPluginManager.CreateInterface(const APlugin: PluginManager.IPlugin; const AIID: TGUID; out Intf): Boolean; var Provider: IServiceProvider; X: Integer; begin Pointer(Intf) := nil; Result := False; for X := 0 to FProviders.Count - 1 do begin // В D2009+ тут можно использовать TList<IServiceProvider>, чтобы избежать преобразования типов Provider := IServiceProvider(FProviders[X]); if Provider.CreateInterface(APlugin, AIID, Intf) then begin Result := True; Exit; end; end; end;Примечание: поскольку менеджер плагинов не является никаким поставщиком, поэтому ему не нужно реализовывать
IServiceProvider
, даже хотя он имеет аналогичный метод CreateInterface
. Понятно, что это разные вещи? Менеджер плагинов перебирает все зарегистрированные "провайдеры" в списке, а каждый конкретный провайдер (который и реализует IServiceProvider
) отвечает только на свои интерфейсы.Короче говоря, сейчас мы заменили метод создания интерфейсов так, что теперь мы передаём и отслеживаем ссылку на плагин. Разумеется, нам теперь нужно задействовать этот механизм из объекта-обёртки (напомню, что
QueryInterface
из менеджера плагинов мы убрали).
function TPlugin.QueryInterface(const IID: TGUID; out Obj): HResult; begin ... if Failed(Result) then if FManager.CreateInterface(Self, IID, Obj) then Result := S_OK; end;
Кроме того, сейчас менеджер плагинов у нас (вообще-то) тоже предоставляет свой собственный сервис - это
IPlugins
. Конечно же, это тоже нужно изменить. Конкретно с ним мы поступаем так же, как и с ICore
: переносим его из менеджера плагинов в объект-обёртку:
type TPlugin = class(TInterfacedObject, IUnknown, IPlugin, IPlugins, ICore) private ... protected ... // IPlugins function GetCount: Integer; safecall; function GetPlugin(const AIndex: Integer): PluginAPI.IPlugin; safecall; public ... end; ... function TPlugin.GetCount: Integer; begin Result := FManager.Count; end; function TPlugin.GetPlugin(const AIndex: Integer): PluginAPI.IPlugin; begin Supports(FManager[AIndex], PluginAPI.IPlugin, Result); end;Вы можете подумать, что этот код выглядит как тривиальный переходник. Ну, так оно и есть. Но в отличие от предыдущего кода у нас теперь есть ма-аленькая деталь: теперь на каждом шаге нам известен вызывающий плагин - даже хотя нам это не интересно.
Я думаю, что у некоторых читателей к этому моменту в голове возникла каша из всех этих объектов. Поэтому давайте я ещё раз кратко прорезюмирую внесённые изменения.
Итак, основная идея в том, что вместо единых общих объектов ядра мы выдаём каждому плагину индивидуальные объекты. Именно так мы сможем выполнять идентификацию. Поэтому, мы внесли такие изменения:
- Во-первых, мы перенесли общий
ICore
в обёрткуTPlugin
- даже хотя эта новая реализация всего-лишь вызывает общую. - Далее, мы сделали аналогичную вещь с каждым общим сервисом в менеджере плагинов. Пока что у нас всего одна такая вещь - это
IPlugins
. Она переносится аналогично. Мы снова вызываем общую реализацию (в менеджере плагинов, её мы не трогали) - просто потому, что для всех этих вещей идентификация плагинов нам не интересна. - Кроме этого, мы изменили метод регистрации функциональности программы в ядре, введя параметр отслеживания плагина.
Тут может возникнуть вопрос: если идентификация не нужна для
ICore
и IPlugins
, то зачем нужно было их размножать? Потому что эти ссылки будут использоваться плагином для получения интерфейсов (регистрируемых программой), которым идентификация всё же нужна. Если оставить ICore
общим, то у нас не будет возможности выдать каждому плагину индивидуальный интерфейс для тех случаев, когда нам нужно идентифицировать плагины - ведь с общим интерфейсом непонятно, кто именно вызвал запрос интерфейса.А как же тогда это будет выглядеть? А вот как...
Изменения в программе
После внесённых нами изменений попытка компиляции программы закончится неудачей, потому что мы изменили сигнатуруRegisterServiceProvider
. Наша задача - исправить это.Здесь мы видим, что мы больше не можем передавать в
RegisterServiceProvider
форму, как мы делали это ранее - потому что теперь каждый провайдер оказывается завязан на плагин, а форма у нас всего одна. Поэтому в нашей программе нам нужно ввести вспомогательный объект, который будет реализовывать интерфейсы программы (IMenuManager
и IEditor
) и который будет создаваться индивидуально для каждого плагина - что и позволит отслеживать плагины.Для этого нам нужно создать класс-реализатор интерфейсов. Поскольку предполагается, что подобные классы мы будем создавать в программе на каждый чих (ну, на каждый набор её услуг), то было бы неплохо создать общий базовый класс, чтобы скрыть в нём рутину (повторяющийся код). Я предлагаю такой прототип:
type IProvider = interface(IServiceProvider) procedure Delete; end; TBasicProvider = class(TInterfacedObject, IUnknown, IServiceProvider, IProvider) private FManager: IPluginManager; FPlugin: PluginManager.IPlugin; protected // IUnknown function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall; // IServiceProvider function CreateInterface(const APlugin: IPlugin; const AIID: TGUID; out Intf): Boolean; virtual; // IProvider procedure Delete; public constructor Create(const AManager: IPluginManager; const APlugin: PluginManager.IPlugin); property Manager: IPluginManager read FManager; property Plugin: PluginManager.IPlugin read FPlugin; end;Как вы можете видеть, этот шаблон хранит ссылки на плагин и менеджер плагинов, а также предоставляет базовую реализацию
QueryInterface
(перенаправляя её на оболочку). Кроме того, поскольку у нас снова возникает циклическая ссылка, её нужно разорвать уже обычным способом. Я мог бы использовать IDeleteNotify
, но счёл более удобным продублировать метод Delete
в наследнике от IServiceProvider
. Это было сделано для удобства: чтобы можно было не запрашивать IDeleteNotify
, а вызывать уведомления сразу (как будет видно из кода ниже).Прежде чем рассмотреть реализацию класса
TBasicProvider
, я предлагаю рассмотреть ситуацию, когда плагин несколько раз запрашивает интерфейсы ядра. При этом несколько раз вызовется TPlugin.QueryInterface
и TPluginManager.CreateInterface
- что создаст нам несколько экземпляров TBasicProvider
(вернее, его наследника). Поскольку задача TBasicProvider
- отслеживать связь с плагином, то нам, вообще-то, не нужны копии TBasicProvider
-ов. Поэтому было бы неплохо, если бы при повторном запросе ядро возвращало уже созданный экземпляр класса.Для этого я предлагаю каждой обёртке (
TPlugin
) отслеживать созданные TBasicProvider
. Тогда базовый класс TBasicProvider
в своём конструкторе может регистрировать себя в обёртке. А чтобы он мог это сделать, обёртке нужно предоставить интерфейс:
type IProviderManager = interface ['{799F9F9F-9030-43B1-B184-0950962B09A5}'] procedure RegisterProvider(const AProvider: IProvider); end;Тогда обёртка будет выглядеть так:
type TPlugin = class(TInterfacedObject, IUnknown, IPlugin, IProviderManager, IDestroyNotify, IPlugins, ICore) private ... FProviders: array of IProvider; ... procedure ReleasePlugin; procedure ReleaseProviders; protected function CreateInterface(const AIID: TGUID; out Intf): Boolean; // IProviderManager procedure RegisterProvider(const AProvider: IProvider); // IDestroyNotify procedure Delete; safecall; ... end; destructor TPlugin.Destroy; begin // Отцепление циклических ссылок Delete; if Assigned(FDone) then FDone; if FHandle <> 0 then begin FreeLibrary(FHandle); FHandle := 0; end; inherited; end; procedure TPlugin.Delete; begin // Отцепили провайдеров ReleaseProviders; // Отцепили сам плагин ReleasePlugin; end; procedure TPlugin.ReleaseProviders; var X: Integer; begin // Благодаря введению IProvider с его Delete - нам не нужно вызывать // if Supports(FProviders[X], IDestroyNotify, Notify) then for X := High(FProviders) downto 0 do FProviders[X].Delete; // прямой вызов Finalize(FProviders); end; procedure TPlugin.ReleasePlugin; var Notify: IDestroyNotify; begin if Supports(FPlugin, IDestroyNotify, Notify) then Notify.Delete; FPlugin := nil; end; function TPlugin.CreateInterface(const AIID: TGUID; out Intf): Boolean; var X: Integer; begin Pointer(Intf) := nil; Result := False; // Поиск по уже созданным провайдерам for X := 0 to High(FProviders) do begin Result := FProviders[X].CreateInterface(Self, AIID, Intf); if Result then Break; end; end; function TPlugin.QueryInterface(const IID: TGUID; out Obj): HResult; begin // Оболочка TPlugin Result := inherited QueryInterface(IID, Obj); // Сам плагин if Failed(Result) and Assigned(FPlugin) then Result := FPlugin.QueryInterface(IID, Obj); // Уже созданные провайдеры if Failed(Result) then begin if CreateInterface(IID, Obj) then Result := S_OK; end; // Потенциальные провайдеры (создаются) if Failed(Result) then begin if FManager.CreateInterface(Self, IID, Obj) then Result := S_OK; end; end;И теперь мы можем реализовать
TBasicProvider
так:
constructor TBasicProvider.Create(const AManager: IPluginManager; const APlugin: PluginManager.IPlugin); var Manager: IProviderManager; begin inherited Create; FManager := AManager; FPlugin := APlugin; if Supports(APlugin, IProviderManager, Manager) then Manager.RegisterProvider(Self); end; function TBasicProvider.CreateInterface(const APlugin: IPlugin; const AIID: TGUID; out Intf): Boolean; begin Result := Succeeded(inherited QueryInterface(AIID, Intf)); end; function TBasicProvider.QueryInterface(const IID: TGUID; out Obj): HResult; begin Result := inherited QueryInterface(IID, Obj); if Failed(Result) then Result := FPlugin.QueryInterface(IID, Obj); end; procedure TBasicProvider.Delete; begin FPlugin := nil; FManager := nil; end;Итак, здесь сделаны три вещи:
- Регистрация объекта в обёртке
- Отслеживание
IPlugin
- Перенаправление запросов интерфейсов на плагин
Теперь, когда у нас есть базовый класс, создание провайдеров в основной программе становится тривиальным. В нашем примере у нас есть два интерфейса:
IMainMenu
и IEditor
. Вот давайте их и реализуем.
type TMenuProvider = class(TBasicProvider, IMenuManager) private FForm: TMainForm; procedure DeletePluginMenuItems; protected // IMenuManager function CreateMenuItem: IMenuItem; safecall; procedure DeleteMenuItem(var AItem: IMenuItem); safecall; public constructor Create(const AManager: IPluginManager; const APlugin: IPlugin; const AForm: TMainForm); reintroduce; property Form: TMainForm read FForm; destructor Destroy; override; end; TEditorProvider = class(TBasicProvider, IEditor) private FForm: TMainForm; function GetEditor: TRichEdit; protected // IEditor function GetText: WideString; safecall; procedure SetText(const AValue: WideString); safecall; function GetSelText: WideString; safecall; procedure SetSelText(const AValue: WideString); safecall; function GetSelStart: Integer; safecall; procedure SetSelStart(const AValue: Integer); safecall; function GetSelLength: Integer; safecall; procedure SetSelLength(const AValue: Integer); safecall; function GetModified: BOOL; safecall; function GetCanUndo: BOOL; safecall; function GetCaretPos: TPoint; safecall; procedure SetCaretPos(const AValue: TPoint); safecall; procedure SelectAll; safecall; procedure Undo; safecall; procedure ClearUndo; safecall; procedure Clear; safecall; procedure ClearSelection; safecall; procedure CopyToClipboard; safecall; procedure CutToClipboard; safecall; procedure PasteFromClipboard; safecall; function FindText(const SearchStr: WideString; const StartPos, Length: Integer; Options: TEditorSearchTypes): Integer; safecall; public constructor Create(const AManager: IPluginManager; const APlugin: IPlugin; const AForm: TMainForm); reintroduce; property Form: TMainForm read FForm; property Editor: TRichEdit read GetEditor; end; ... { TMenuProvider } constructor TMenuProvider.Create(const AManager: IPluginManager; const APlugin: IPlugin; const AForm: TMainForm); begin inherited Create(AManager, APlugin); FForm := AForm; end; ... { TEditorProvider } constructor TEditorProvider.Create(const AManager: IPluginManager; const APlugin: IPlugin; const AForm: TMainForm); begin inherited Create(AManager, APlugin); FForm := AForm; end;Я не буду показывать код методов - он тривиален. Исключение составляет лишь этот код, ради которого всё это и затевалось:
destructor TMenuProvider.Destroy; begin DeletePluginMenuItems; inherited; end; procedure TMenuProvider.DeletePluginMenuItems; var X: Integer; begin for X := Form.miPlugins.Count - 1 downto 0 do if (Form.miPlugins[X] is TPluginMenuItem) and (TPluginMenuItem(Form.miPlugins[X]).Provider = Self) then Form.miPlugins[X].Free; Form.SetPluginsMenuVisible; end;Смысл кода таков: когда плагин выгружается, происходит удаление ассоциированных с ним провайдеров, в том числе и
TMenuProvider
. При удалении провайдера мы просматриваем меню формы и удаляем все пункты меню, принадлежащие плагину.Теперь мы можем переписать плагин с учётом этой новой функциональности:
constructor TPlugin.Create(const ACore: ICore); var Manager: IMenuManager; MenuItem: IMenuItem; // новинка: теперь можно не хранить ссылки на свои пункты меню begin inherited Create; FCore := ACore; // но нам всё ещё нужна ссылка на IEditor, чтобы работать с ним Assert(FCore.Version >= 1); if Supports(FCore, IMenuManager, Manager) then // <- теперь в этом месте используется отдельный объект-провайдер begin MenuItem := Manager.CreateMenuItem; // и главная программа теперь знает, кто вызвал CreateMenuItem MenuItem.Caption := 'Вставить дату'; MenuItem.Hint := 'Вставляет в документ текущую дату и время в местном формате'; MenuItem.Enabled := True; MenuItem.Checked := False; MenuItem.RegisterExecuteHandler(Self); end; end;
Примечание: вовсе не обязательно создавать отдельные объекты-провайдеры на каждый интерфейс. Поскольку задача объекта провайдера всего лишь в хранении ссылки на плагин, то вполне можно было бы сделать так:type TMainFormProvider = class(TBasicProvider, IMenuManager, IEditor) private FForm: TMainForm; function GetEditor: TRichEdit; procedure DeletePluginMenuItems; protected // IMenuManager ... // IEditor ... public constructor Create(const AManager: IPluginManager; const APlugin: IPlugin; const AForm: TMainForm); reintroduce; property Form: TMainForm read FForm; property Editor: TRichEdit read GetEditor; destructor Destroy; override; end;Это вполне нормально и именно так вам стоит поступать в реальных программах. Я специально создал два объекта, чтобы показать насколько просто это сделать, когда у вас уже есть готовая база (TBasicProvider
).
Теперь нам осталось только собрать всё в кучу:
type TMainForm = class(TForm, IServiceProvider) ... protected // IServiceProvider function CreateInterface(const APlugin: IPlugin; const AIID: TGUID; out Intf): Boolean; end; ... function TMainForm.CreateInterface(const APlugin: IPlugin; const AIID: TGUID; out Intf): Boolean; var ID: TGUID; I1: IMenuManager; I2: IEditor; begin Pointer(Intf) := nil; ID := IMenuManager; if CompareMem(@ID, @AIID, SizeOf(TGUID)) then begin I1 := TMenuProvider.Create(Plugins, APlugin, Self); IMenuManager(Intf) := I1; Result := True; Exit; end; ID := IEditor; if CompareMem(@ID, @AIID, SizeOf(TGUID)) then begin I2 := TEditorProvider.Create(Plugins, APlugin, Self); IEditor(Intf) := I2; Result := True; Exit; end; Result := False; end;Вот, собственно, и всё. Скачать код к этому моменту вы можете тут.
В примерах поставляются все те же плагины: экспорт/импорт для RTF и два плагина для меню. Один из которых также работает с текстовым редактором. Вот как раз этот последний плагин (DatePlugin) иллюстрирует новые возможности системы: плагин создаёт пункт меню, но не хранит на него ссылки. Поскольку теперь основная программа отслеживает все созданные плагином ресурсы, то она сама удалит пункты меню при выгрузке плагина. Код я уже приводил выше.
Заключение
В конце статьи я хотел бы просуммировать изменения этой статьи, а также проиллюстрировать их рисунками для более наглядного представления.Итак, для простого отслеживания плагинов нам достаточно ввести ID: TGUID в плагин - пусть каждый плагин возвращает свой уникальный ID и тогда мы можем записывать этот ID в разные структуры данных внутри программы, чтобы ассоциировать их с плагинами. Я напомню, что мы не можем использовать для этого индекс плагина в менеджере плагинов, потому что он может меняться.
Далее, для продвинутой идентификации (идентификации плагина в обратных вызовах) нам необходимо реализовать индивидуальные интерфейсы ядра для каждого плагина. Тогда при обратном вызове плагин будет определяться по ссылке, через которую был сделан вызов.
Для реализации последней возможности мы размножили
ICore
, IPlugins
, а также все дополнительные интерфейсы программы, которые регистрируются в менеджере плагинов (IMenuManager
, IEditor
).Т.е., было (примерная картина, не показаны многие классы и связи):
Архитектура плагинов до этой статьи |
(картинки кликабельны)
На данном рисунке показана основная программа с тремя загруженными плагинами. Первый плагин пустой, а два других создали пункты меню в программе. Причём первый из них (второй по счёту) держит ссылку только на менеджер меню (да, это немного не имеет смысла, но для примера), а второй (третий) - на сам пункт меню, но без менеджера (это как раз более осмысленно).
Итак, как вы видите по рисунку, раньше у нас был "главнюк":
IPluginManager
/TPluginManager
. Он был один и на него ссылались все. Вся функциональность программы регистрировалась также в нём, и плагины получали её (функциональность) через запрос к менеджеру.(кстати, обратите внимание на два
IPlugin
: один в плагине, а второй - обёртка в менеджере)А стало:
Архитектура плагинов после этой статьи |
Теперь у нас имеется взаимно-однозначное соответствие между плагинами и их обёртками в менеджере плагинов.
ICore
и IPlugin
перекочевали из менеджера плагинов в обёртки. У нас появились циклические ссылки обёртка-плагин. Кроме того, теперь все получения интерфейсов плагинами также проходят через обёртки, а не менеджер плагинов (хотя последний и служит общей базой зарегистрированных интерфейсов). Также теперь форма уже не является провайдером, а лишь содержит метод "создания", который она и регистрирует в менеджере плагинов. Появляются новые объекты: провайдеры. Это та часть, что исчезла из формы. Теперь это отдельные объекты, которые создаются каждому плагину. Именно в них хранится ссылка на плагин.Надеюсь, что если у вас оставались какие-то неясные моменты, то теперь картина прояснилась. А если нет - то вы всегда можете заглянуть в исходный код программы.
В следующей части этого цикла статей я, пожалуй, расскажу об обработке ошибок в плагинах. Хотя некоторые читатели предпочли бы сначала увидеть что-то более захватывающее, чем такая скучная тема, но я считаю, что другие темы могут подождать. Потому что наша система становится всё более сложной. И если кто-то уже пробовал писать и/или расширять систему плагинов, то наверняка он столкнулся с проблемами циклических ссылок (утечек памяти), исключений (включая Access Violation) и т.п. Поэтому мне хотелось бы в следующий раз обсудить как диагностировать проблемы с плагинами, как их решать, типичные косяки, и т.п.
Я долго ждал эту статью! Сейчас начну разбираться =)
ОтветитьУдалитьВ прошлый раз вы написали:
ОтветитьУдалитьНа следующий раз у нас остаётся взаимодействие плагинов друг с другом, пользовательский интерфейс в плагинах, а также обработка ошибок и отладка.
Вы планируете раскрыть эту темы (пользовательский интерфейс в плагинах)? В частности в MDI приложениях (есть проблемы, которые пока не получается решить, а именно - если из плагина была загружена форма, при закрытии приложения стабильно вылетает Access violation).
Тоже ждал продолжения именно из-за обещанного "пользовательский интерфейс в плагинах". Планируется ли продолжение?
ОтветитьУдалитьЭэээ.... а вы последний абзац вообще читали? Вроде же я явно сказал, что вытаскиваю обработку ошибок вперёд других тем, потому что "есть проблемы, которые пока не получается решить, а именно - ... при закрытии приложения стабильно вылетает Access violation".
ОтветитьУдалить"если из плагина была загружена форма, при закрытии приложения стабильно вылетает Access violation"
ОтветитьУдалитьЭмм, я подключаю MDI форму след. образом:
var
DLLApp: TApplication;
DLLScr: TScreen;
IForms = interface
['{29281C9A-11F2-4F73-A5DE-1988554DACB2}']
function CreateForm(App, Scr: integer): BOOL; safecall;
end;
function TPlugin.CreateForm(App, Scr: integer): BOOL;
begin
result:=false;
try
DLLScr := Screen;
Screen := TScreen(Scr);
DLLApp := Application;
Application := TApplication(App);
TfrmTest.Create(Application);
finnaly
result:=true;
end;
end;
И немножко изменил:
procedure Done; safecall;
begin
Screen := DLLScr;
Application := DLLApp;
end;
В ядре:
procedure Button1Click();
var
frms: IForms;
begin
for X := 0 to Plugins.Count - 1 do
if Supports(Plugins[X], IForms, frms) then
frms.CreateForm1(integer(Application),integer(Screen));
end;
Слова "не передавать объекты" из самой первой статьи, вы, видимо, пропустили мимо ушей.
ОтветитьУдалить"Но я не передаю объекты, я передаю Integer"
Да, точно. На клетку льва повесили табличку "слон". Ага.
Не нужно копировать безграмотный код в интернете.
Предлагаю более красивый вариант с формами.
ОтветитьУдалитьПусть Интерфейс предоставляет доступ к созданию формы (модальной или MDI).
Формы создаваться будут в основном приложении, и подгружать сценарий компонентов из указанного ресурса в DLL.
За счет этого не нужно передавать Screen и Application. Контроль, за окнами в главном приложении. Плагины будут реализовывать только логику работы окна, получать события и т.д.
Формы создаваться будут в основном приложении, и подгружать сценарий компонентов из указанного ресурса в DLL.
ОтветитьУдалитьВариант неплохой, но не подходит, если плагин захочет создать окно с использование визуальных контролов-компонентов о которых основное приложение не знает.
<>
ОтветитьУдалитьПодскажите а как можно получить список этих объектов???
Теперь это отдельные объекты, которые создаются каждому плагину. Именно в них хранится ссылка на плагин.
ОтветитьУдалитьплиз, прошу подсказать как можно получить список этих объектов???
А зачем вам нужен список этих объектов? Это же чисто технические переходники к форме.
ОтветитьУдалитьНо если почему-то они вам так нужны - то заводите список для них. В конструкторе TMainFormProvider заносите Self в список, в деструкторе - удаляйте. Тогда в любой момент можно будет перечислить эти объекты - это будет ровно ваш список.
Теперь это отдельные объекты, которые создаются каждому плагину. Именно в них хранится ссылка на плагин.
ОтветитьУдалитьМожет глупый вопрос, но можно ли сохранить ссылку хранящуюся на плагин с помощью : файлы в стиле Pascal в текстовый файл, а потом восстановить ее и вызвать плагин по загруженной/восстановленой ссылке? Если в принципе да, то прошу натолкнуть на мысль :-)