30 января 2012 г.

Разработка системы плагинов, часть 4: взаимодействие плагинов

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

Оглавление

  1. Зачем это надо
  2. Базовый сервис
  3. Обратные вызовы
  4. Заключение

Зачем это надо

Для начала хорошо бы пояснить: а зачем вообще такая возможность может понадобится? Да для совершенно разных целей.

Вот, к примеру, во второй части у нас были плагины экспорта и импорта. И там я сказал такую вещь:
Здесь нужно обратить внимание на фундаментальное изменение в поведении программы. Раньше, когда плагинов в программе у нас не было, мы могли загрузить файл и нажать на кнопку "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;
Вы можете подумать, что этот код выглядит как тривиальный переходник. Ну, так оно и есть. Но в отличие от предыдущего кода у нас теперь есть ма-аленькая деталь: теперь на каждом шаге нам известен вызывающий плагин - даже хотя нам это не интересно.

Я думаю, что у некоторых читателей к этому моменту в голове возникла каша из всех этих объектов. Поэтому давайте я ещё раз кратко прорезюмирую внесённые изменения.

Итак, основная идея в том, что вместо единых общих объектов ядра мы выдаём каждому плагину индивидуальные объекты. Именно так мы сможем выполнять идентификацию. Поэтому, мы внесли такие изменения:
  1. Во-первых, мы перенесли общий ICore в обёртку TPlugin - даже хотя эта новая реализация всего-лишь вызывает общую.
  2. Далее, мы сделали аналогичную вещь с каждым общим сервисом в менеджере плагинов. Пока что у нас всего одна такая вещь - это IPlugins. Она переносится аналогично. Мы снова вызываем общую реализацию (в менеджере плагинов, её мы не трогали) - просто потому, что для всех этих вещей идентификация плагинов нам не интересна.
  3. Кроме этого, мы изменили метод регистрации функциональности программы в ядре, введя параметр отслеживания плагина.
Я также дам поясняющие рисунки в конце статьи, когда мы сделаем вторую часть изменений.

Тут может возникнуть вопрос: если идентификация не нужна для 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;
Итак, здесь сделаны три вещи:
  1. Регистрация объекта в обёртке
  2. Отслеживание IPlugin
  3. Перенаправление запросов интерфейсов на плагин

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

12 комментариев :

  1. Я долго ждал эту статью! Сейчас начну разбираться =)

    ОтветитьУдалить
  2. В прошлый раз вы написали:
    На следующий раз у нас остаётся взаимодействие плагинов друг с другом, пользовательский интерфейс в плагинах, а также обработка ошибок и отладка.

    Вы планируете раскрыть эту темы (пользовательский интерфейс в плагинах)? В частности в MDI приложениях (есть проблемы, которые пока не получается решить, а именно - если из плагина была загружена форма, при закрытии приложения стабильно вылетает Access violation).

    ОтветитьУдалить
  3. Тоже ждал продолжения именно из-за обещанного "пользовательский интерфейс в плагинах". Планируется ли продолжение?

    ОтветитьУдалить
  4. Ээээ.... а вы последний абзац вообще читали? Вроде же я явно сказал, что вытаскиваю обработку ошибок вперёд других тем, потому что "есть проблемы, которые пока не получается решить, а именно - ... при закрытии приложения стабильно вылетает Access violation".

    ОтветитьУдалить
  5. "если из плагина была загружена форма, при закрытии приложения стабильно вылетает 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;

    ОтветитьУдалить
  6. Слова "не передавать объекты" из самой первой статьи, вы, видимо, пропустили мимо ушей.

    "Но я не передаю объекты, я передаю Integer"

    Да, точно. На клетку льва повесили табличку "слон". Ага.

    Не нужно копировать безграмотный код в интернете.

    ОтветитьУдалить
  7. Предлагаю более красивый вариант с формами.

    Пусть Интерфейс предоставляет доступ к созданию формы (модальной или MDI).
    Формы создаваться будут в основном приложении, и подгружать сценарий компонентов из указанного ресурса в DLL.

    За счет этого не нужно передавать Screen и Application. Контроль, за окнами в главном приложении. Плагины будут реализовывать только логику работы окна, получать события и т.д.

    ОтветитьУдалить
  8. Формы создаваться будут в основном приложении, и подгружать сценарий компонентов из указанного ресурса в DLL.

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

    ОтветитьУдалить
  9. <>

    Подскажите а как можно получить список этих объектов???

    ОтветитьУдалить
  10. Теперь это отдельные объекты, которые создаются каждому плагину. Именно в них хранится ссылка на плагин.

    плиз, прошу подсказать как можно получить список этих объектов???

    ОтветитьУдалить
  11. А зачем вам нужен список этих объектов? Это же чисто технические переходники к форме.

    Но если почему-то они вам так нужны - то заводите список для них. В конструкторе TMainFormProvider заносите Self в список, в деструкторе - удаляйте. Тогда в любой момент можно будет перечислить эти объекты - это будет ровно ваш список.

    ОтветитьУдалить
  12. Теперь это отдельные объекты, которые создаются каждому плагину. Именно в них хранится ссылка на плагин.
    Может глупый вопрос, но можно ли сохранить ссылку хранящуюся на плагин с помощью : файлы в стиле Pascal в текстовый файл, а потом восстановить ее и вызвать плагин по загруженной/восстановленой ссылке? Если в принципе да, то прошу натолкнуть на мысль :-)

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.

Примечание. Отправлять комментарии могут только участники этого блога.