6 января 2009 г.

Создаём систему плагинов, часть 8

В последний раз мы полностью закончили рассмотрение загрузки плагинов. Для начала мы рассмотрели достаточно (надеюсь понятно, что тут так подробно всё расписывается не потому, что это так уж важно, а просто для информации). Теперь поговорим о самом вкусном - о функциональности плагинов.

Итак, плагин у нас загружен и на руках у нас есть ссылка на интерфейс IInit. У самого плагина на руках есть IInterface, который мы передали ему при инициализации (если передали). В принципе, это все точки соприкосновения модуля плагина и exe-шника программы.
Разумеется, единственная функциональность интерфейса IInit - это инициализация и финализация модуля плагина. А у IInterface и вовсе нет никакой полезной нагрузки. Так как же плагин будет реализовывать свою Ту Самую Функцию "Сделать Что-То"?

Ответ: через другой интерфейс! Т.е. если нам надо, чтобы плагин что-то делал, то мы вводим интерфейс, который предоставит главной программе возможность (интерфейс) для выполнения работы, например:
type
  IPluginType1 = interface
  ['{D244E439-8F17-49AA-BB91-C8D745FCA93E}']
    procedure DoSomething; safecall;
    function  DoSomethingElse(const Data1: Integer; const Data2: Pointer; const Data2Size: Cardinal; const Data3: IInterface): Integer; safecall;
    procedure InvokeAppocalypse(const World: IWorld); safecall;
  end;
Т.е. мы просто объявляем новый интерфейс (не забываем сгенерировать новый GUID и использовать safecall в методах), в котором перечисляем все те действия, которые плагин должен уметь делать. Далее, в плагине мы реализовываем этот интерфейс (можно тем же объектом, который реализовывал нам IInit). А в программе мы можем получить этот интерфейс из имеющегося у нас на руках экземпляра IInit:
var
  Intf: IInit;
  Plugin: IPluginType1;
...
  Plugin := (Intf as IPluginType1);
  Plugin.InvokeAppocalypse(TWorld.Create('You are doomed.'));
...
Кстати, работу оператора as в этом примере как раз и обеспечивает GUID интерфейса. У нас есть на руках IInit, как мы запросим другой интерфейс? У интерфейса нет имени, идентификатора и т.п. Но зато у него есть GUID. Вот его и использует оператор as, когда мы просим у него получить IPluginType1. Если бы у интерфейса не было GUID, то и получить его не получилось бы - пришлось бы вводить в IInit функцию, которая явно возвращает IPluginType1.

Итак, для реализации функциональности, мы должны объявить и реализовать в плагине интерфейс. Справедливо это и в обратную сторону - если плагину требуется что-то от программы-ядра, то программа должна реализовать подходящий интерфейс, а плагин сможет получить его из того самого IInterface, который передали ему при инициализации (или не передали - но теперь-то уж точно будете передавать).

Более того, вовсе не обязательно, чтобы плагин и программа реализовывали бы по одному интерфейсу. Их можно вводить столько, сколько нужно. Удобно группировать схожие по назначению функции в отдельный интерфейс. Кроме того, такое понятие как "тип плагина" может определяться тем, поддерживает ли плагин определённый интерфейс. Например, у Total Commander есть плагины следующих типов: архивов (wcx), листера (wlx), файловой системы (wfx) и колонок (wdx). Если бы мы реализовывали бы свой файловый менеджер и плагины делали бы на интерфейсах, то мы ввели бы четыре интерфейса (по одному на каждый тип плагина), а их методами бы стали сегодняшние экспортируемые функции. Тогда все файлы были бы однотипными - bpl (разумеется, ничто не мешает вам делать плагины в виде DLL, а также переименовывать файлы как душе угодно, в том числе давая им расширения wcx, wlx, wfx и wdx для удобства). А тип плагина определялся бы только после загрузки, смотря по тому, поддерживает ли его IInit интерфейсы (гипотетические) IPackerPlugin, IListerPlugin или IFileSystemPlugin).
Кстати, помните мы говорили, что имя точки входа должно быть уникальным? Так вот, вовсе не обязательно, чтобы оно было разным для каждой вашей программы. Пока вы не меняете базовую основу нашего протокола - вы можете везде использовать одно и то же имя. Да, это будет означать, что плагин от любой вашей программы будет подходить к любой другой вашей программе (и любой чужой программе, использующей те же соглашения). Просто он не будет ничего делать, поскольку будет реализовывать не те интерфейсы, что требуются другим программам. Если вам не нравится такая "универсальная подходимость" (если кто-то напутает и скинет программе плагины от другой, то в самой программе будут видны эти плагины, которые ровно ничего делать не будут), то вы можете при инициализации проверить, что переданный плагину IInterface поддерживает конкретный интерфейс, специфичный для вашей программы. А если нет - то отказаться грузиться (например, возбуждением исключения).

Что касается самой функциональности, то я предлагаю сделать такую схему, в которой в одном бинарном модуле может располагаться несколько плагинов. Т.е., например, файлик MyPlugins.bpl может содержать в себе два плагина: MyPlugin1 и MyPlugin2.

Почему именно такая схема? Ну, во-первых, если вам нужна классическая "один файл - один плагин", то ничто не помешает вам ровно так и сделать - создайте модуль с одним плагином. Зато, если вдруг вам понадобится написать два плагина, которые должны взаимодействовать друг с другом и/или использовать какие-либо общие части - то проще всего будет поместить эти плагины в один модуль.

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

Другой пример - какой-нибудь сетевой чат. Пусть у вас будут плагины протоколов (как в Miranda, QIP Infium) и, например, плагины для окон чата. Тогда вы можете разместить в одном модуле два разнотипных плагина, обменивающихся информацией. К примеру, протокол плагинов может не предусматривать передачу объектов (смайликов, ссылок, рисунков в тексте и т.п.) - только голый текст. Тогда плагин протокола может передавать текст со специальными текстовыми вставками - ссылками на внутренние данные. А плагин окна чата может выцеплять из текста эти спец-части и вставлять вместо них в окно чата объекты. Сами данные для объектов плагины передают между собой напрямую - например, в глобальном массиве. Т.е. минуя протокол плагинов, навязанный программой.

Окей, что это всё значит с точки зрения реализации? Это значит, что нам нужна новая сущность: "плагин". Кроме того, при любой схеме у нас есть ещё сущность "программа" ("ядро"). Что общего есть у программы и плагина? Для этого я предлагаю ввести общую информацию (разумеется, вы вольны добавить/убрать необходимые/лишние свойства):
type
  IVersionInfo = interface
  ['{1D23BA15-6A23-4CFA-8EBE-AD033D2E1AE5}']
  // private
    function GetGUID: TGUID; safecall;
    function GetCaption: WideString; safecall;
    function GetDescription: WideString; safecall;
    function GetURL: WideString; safecall;
    function GetAuthor: WideString; safecall;
    function GetVersion: Longword; safecall;

  // public
    property GUID: TGUID read GetGUID;
    property Caption: WideString read GetCaption;           // (*)
    property Description: WideString read GetDescription;   // (*)
    property URL: WideString read GetURL;                   // (*)
    property Author: WideString read GetAuthor;             // (*)
    property Version: Longword read GetVersion;
  end;
Здесь мы определяем 6 свойств, которые реализованы 6-ю же методами. Сами методы напрямую мы вызывать не будем - будем использовать свойства. Итак, что тут у нас есть.

- GUID - это уникальный идентификатор плагина и программы. Вы должны будете создавать новый GUID на каждый новый плагин. Зачем он нужен? Хм, для идентификации. Как вы будете отличать один плагин от другого? У него нет имени, нет идентификатора. Имя бинарного модуля, в котором расположен плагин для идентификации непригодно (во-первых, его могут переименовать, а во-вторых, в одном пакете может быть два и более плагина). Вот для этого и вводится GUID. Когда одному плагину нужны услуги другого (конкретного) плагина - он сможет так и сказать: мне нужен плагин с GUID таким-то. Когда программа-ядро хранит настройки плагинов, она может хранить их по текстовому предствалению этого GUID и т.д. В свою очередь плагин может использовать GUID программы, чтобы проверить, что его загрузила именно та программа, для которой он предназначался. Альтернативный способ, как уже было сказано ранее, - проверять наличие какого-то конкретного интерфейса.

- Caption - это название плагина, для отображения его в интерфейсе пользователя (не имеет большого смысла для программы, если только вы не делаете две программы, которые разделяют общие плагины). Будет не очень хорошо, если название плагина будет отображаться как "4F38192D-4326-4479-8A8E-EF9CE164B69F", верно? Гораздо лучше для человека звучит "Мой плагин экспорта в формат txt". Разумеется, это поле не обязано быть уникальным.

- Description - это подробное описание плагина (также не слишком полезно у программы). Название (Caption) должно быть кратким, чтобы помещаться в список плагинов (т.е. на экране одновременно будет множество Caption от разных плагинов). Описание (Description) выводится только после того, как пользователб щёлкнет по списку - в каком нибудь отдельном Memo (т.е. на экране одновременно может быть только одно Description). Например: "Плагин экспортирует в текстовый формат txt (unicode, LE, BOM присутствует) следующую информацию: ...(перечисление информации/данных программы и способа их записи в текстовый вид)...".

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

- Author - авторы, копирайты и т.п. Чтобы пользователь знал, кого бить за плагин ;)

- Version - версия плагина/программы. Может быть несколько плагинов с одним и тем же GUID, но разными версиями. В принципе, вместо Longword вы можете использовать любой другой формат, какой вам будет удобнее. Я у себя решил использовать Longword, который заполняется и читается так:
{ XX.YY.Z.IIII }
function  BuildVersion(const AMajor: Longword; const AMinor: Longword = 0; 
                       const ARev: Longword = 0; const ABuild: Longword = 0): Longword;
procedure ParseVersion(const AVersion: Longword; const AMajor: PLongword; 
                       const AMinor: PLongword = nil; const ARev: PLongword = nil;
                       const ABuild: PLongword = nil);

implementation

function BuildVersion(const AMajor, AMinor, ARev, ABuild: Longword): Longword;
begin
  Assert(AMinor < 100,   
    'Minor version number must be in range 0..99');   // Do Not Localize
  Assert(ARev < 10,      
    'Revision version number must be in range 0..9'); // Do Not Localize
  Assert(ABuild < 10000, 
    'Build version number must be in range 0..9999'); // Do Not Localize
  Result := AMajor * 1000000 + AMinor * 10000 + ARev * 1000 + ABuild;
end;

procedure ParseVersion(const AVersion: Longword; const AMajor: PLongword; const AMinor: PLongword; 
  const ARev: PLongword; const ABuild: PLongword);
var
  V: LongWord;
begin
  V := AVersion;
  if ABuild <> nil then
    ABuild^ := V mod 1000;
  V := V div 1000;
  if ARev <> nil then
    ARev^ := V mod 10;
  V := V div 10;
  if AMinor <> nil then
    AMinor^ := V mod 100;
  V := V div 100;
  if AMajor <> nil then
    AMajor^ := V;
end;
Удобно здесь то, что можно легко определить какая версия плагина старше: сравнив свойства Version как обычные числа. Чьё число больше - у того и версия выше.

Окей, значит каждая сущность "плагин", а также "программа", должны реализовывать этот интерфейс, чтобы предоставить информацию о себе. Если с программой всё понятно, то в схеме с несколькими плагинами на модуль возникает вопрос: как программа получит доступ к этим интерфейсам?

Для этого программа должна поддерживать дополнительный интерфейс, который позволит модулю плагина при своей инициализации зарегистрировать все свои плагины, например:
type
  IRegisterPlugin = interface
  ['{1A0D621E-01EA-49F5-B35E-8D0E2346D9BA}']
    procedure RegisterPlugin(const APlugin: IVersionInfo); safecall;
  end;
Тогда модуль может просто получить IRegisterPlugin из переданного ему IInterface, и для каждого реализуемого им плагина вызвать RegisterPlugin, передав информацию о плагине. При этом программа-ядро может занести этот IVersionInfo в массивчик - это будет список установленных плагинов.

Кстати, было бы также неплохо, если бы IInit также возвращал экземплярчик IVersionInfo - это будет информация о самом модуле. Т.е. например, информация о модуле может быть "Содержит плагины для экспорта в текстовый формат", а в нём может быть два плагина "Экспорт в unicode txt" и "Экспорт в ANSI txt". Первая информация получена из IVersionInfo, который получен из IInit, а последние две - из IVersionInfo, которые передали в RegisterPlugin.

В следующий раз мы попробуем написать пример "от и до" (разумеется, это будет просто пример, который не будет делать ничего полезного).

(*) Упражнение: объяснить почему используется тип WideString (а не, скажем, String или PChar), и как это работает.

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

  1. Большое спасибо тебе за твои статьи! Отлично структурированная информация.

    Ответ на упражнение: Потому что WideString проще передавать из программы в dll и обратно. =)

    ОтветитьУдалить
  2. >>> Отлично структурированная информация
    Только надо понимать, что это не статьи. Т.е. в них не рассказывается по плану заранее подготовленная информация.
    Как я уже сказал, мне просто захотелось "чё-нить пописать". А здесь я просто записываю процесс =))
    Поэтому что-то уже сказанное может быть позднее быть переделано.

    ОтветитьУдалить
  3. Да, я понимаю что это блог, в котором ты волен писать, что тебе захочется и как захочется. Но лично для себя не вижу причин почему твои посты нельзя назвать статьями. Для меня они выглядят намного более структурированными, чем многие публикации в других источниках, называющие себя статьями. =)

    ОтветитьУдалить
  4. Тут есть подводный камень.
    Если в программе полученные ссылки на интерфейсы будут размножены и сохранены в нескольких местах, то мы вполне можем забыть какую нибудь из них, и вызвать выгрузку плагина ранее, чем ссылка обнилится, что приведет к краху. Самое печальное тут то, что _Release ссылке компилятор вызовет автоматически, и найти это место может оказаться не так просто.

    ОтветитьУдалить
  5. Верно.

    Лично я в плагинах делаю глобальный счётчик созданных объектов. И если он не равен нулю - генерится ошибка.

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

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

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

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

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

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