Напомню, что те статьи, несмотря на некоторый практический выхлоп в конце, всё же не очень-то освещали тему плагинов, а представляли собой копание во внутренностях работы DLL и пакетов Delphi. Было решено эту серию закончить - именно как серию о плагинах.
Ну, во-первых, причиной для новой статьи стали просьбы её продолжить/закончить. За три года они поступали регулярно. Во-вторых, я заметил, что многие люди ссылаются на эту статью как на руководство по разработке плагинов, хотя, как я уже сказал, оно таким не является. Сам я всегда ссылался на него со словами "вот, это не читай, но в первой части тут неплохая подборка ссылок по интересующей тебя теме".
В общем, все эти факторы в итоге перевесили мою лень и я решил написать нормальную статью про плагины.
Примечание: я был бы очень благодарен, если кто-нибудь со знанием Visual Studio (C++) просмотрел бы раздел 8 (и особенно - касательно генерации заголовочников для Visual Studio C++) на предмет моих ошибок.
Оглавление
- Что у нас есть
- Что хотим получить
- Как пойдём
- Основные понятия
- Базовый набор правил и соглашений
- Структура файлов и папок
- Менеджер плагинов
- API
Что у нас есть
Напомню основные способы реализации систем плагинов:- COM-автоматизация.
- Плагины в виде COM-объектов.
- Плагины в виде пакетов + интерфейсы.
- Плагины в виде пакетов (bpl - packages).
- Плагины в виде DLL + интерфейсы.
- Плагины в виде DLL.
- Скрипты.
Что хотим получить
Мы хотим разработать систему плагинов - т.е. набор правил, по которым сторонние разработчики (т.е. не авторы программы) могли бы писать функциональность, встраивающуюся в основную программу и как-бы являющуюся её частью. При этом хочется, чтобы плагины можно было бы писать на любом (нативном) языке программирования, а не только Delphi и C++ Builder.При этом хочется, чтобы и работать было удобно, и чтобы у системы были бы мощные возможности. Как дополнительное пожелание - строить более-менее современную систему, а не по техникам 1995-го года.
Система должна по возможности допускать развитие со временем (добавление новых возможностей в будущем) с сохранением обратной совместимости (старые плагины работают в новой системе).
При этом предполагается, что плагины будут "тяжеловесные". Т.е. предполагающие существенный объём кода для реализации их функциональности. Ну, скажем, вроде как плагины звукового вывода в WinAmp, архиваторные, Lister-ые или FS-плагины в Total Commander или протокольные плагины в QIP.
Некоторые задачи более удачно ложатся на "легковесные плагины" - скрипты. К примеру, как макросы в MS Word. Вам нужно выбирать систему на скриптах, если ваши плагины должны много обращаться к интерфейсу (и, в частном случае, - внутренним объектам) программы, но при этом сами они относительно просты. Так, что их можно выразить на скриптовом языке. И в этом случае эта статья - не для вас (в этом случае могу ткнуть в направлении статьи про TMS Scripter Studio Pro в 6-м номере журнала Blaise Pascal Magazine).
В целом, в одном приложении вполне может быть реализовано две системы плагинов - для разных потребностей. К примеру, тот же MS Word (помимо макросов и VBA) поддерживает плагины в виде COM-объектов.
Как пойдём
Сразу замечу, что в этой статье я не буду говорить "почему". Я просто озвучу вариант, который выбрал я. Ответы на вопросы "почему" можно почерпнуть в старой серии статей.Что это значит? Реализации одной и той же задачи может быть несколько. Все из них имеют плюсы и минусы. Я не могу описать все возможные варианты - у меня не хватит ни времени, ни терпения это сделать. Поэтому я буду описывать лишь один вариант. Этот вариант я выбрал, исходя из требований к системе плагинов, изложенных в предыдущем пункте.
По тексту я кратко могу пояснять причины выбора того или иного решения, но подробное объяснение и альтернативы будут за кадром.
Если вы не начинающий (а зачем вы это читаете?) - вы можете сделать такой выбор сами. Если же вы начинающий и озабочены тем, что не можете сделать выбор, вот мой совет - просто следуйте этой статье. Когда у вас накопится опыт, вы уже более чётко будете представлять себе, что вы хотите получить, какие есть варианты достижения этого, какие у них плюсы и минусы. Так что вы сможете сделать свой выбор. Потом, не сейчас.
Основные понятия
Плаги́н (от англ. plug-in) — независимо компилируемый программный модуль, динамически подключаемый к основной программе, предназначенный для расширения и/или использования её возможностей. Плагин - это маленькая программка, которая встраивается в основную (большую) программу и расширяет её возможности.Основная программа при этом называется "ядро" (core).
Чаще всего основной программой является .exe файл, а плагины - это .dll файлы.
Ядро предоставляет сервисы, которые плагин может использовать. К ним относится предоставляемая плагину возможность зарегистрировать себя в ядре, а также протокол обмена данными с другими плагинами. Плагины являются зависимыми от сервисов, предоставляемых ядром и отдельно (сами по себе) не используются.
Протокол (также называемый API - Application Programming Interface) - набор правил, контракт, которому соглашаются следовать ядро и плагины, чтобы понять друг друга и успешно взаимодействовать. Все сервисы ядра могут предоставляться только в рамках этого соглашения.
Заголовочники, заголовочные файлы (headers) - набор исходных файлов, которые содержат объявления структур, использующихся в протоколе плагинов. Как правило, не содержат реализации. Заголовочные файлы предоставляются на нескольких языках - как правило, это язык, на котором написана программа (в нашем случае - Delphi), C++ (как стандарт) и некоторыми дополнительными (Basic и т.п.). Все эти файлы эквивалентны и просто представляют собой перевод из одного языка программирования на другой. Чем больше языков будет в комплекте - тем лучше. Если вы не предоставите заголовочные файлы для какого-то языка, то программисты на этом языке не смогут писать плагины для вашей программы, пока они сами не переведут файлы с предоставляемого языка (Delphi или C++) на их язык. Т.е. отсутствие заголовочников на каком-то языке - это не красный "стоп", но достаточное препятствие. В этом смысле очень удачно выглядит COM (где описание хранится в универсальном формате библиотеки типов - TLB). Вам не нужно ничего делать, кроме как распространять .tlb файл (который также может быть встроен в .dll). Если язык умеет работать с COM - он может импортировать информацию из TLB и создать заголовочник самостоятельно. TLB файл - это двоичный файл. Его создают и редактируют в каком-нибудь редакторе, либо он генерируется средой разработки. Его также можно "скомпилировать" из текстового описания - IDL файла (.idl или .ridl).
Документация - представляет собой словесное описание протокола плагинов. Она пишется разработчиком программы для разработчиков плагинов (т.е. она односторонняя). Конечно, вы можете вести документацию и для себя лично, но сейчас речь не про неё. Итак, в этой документации как минимум должно быть формальное описание API - перечисление всех функций, методов, интерфейсов и типов данных с объяснениями "как" и "зачем" (т.н. Reference). Дополнительно, документация может содержать неформальное описание процесса разработки плагинов (guide, how-to и т.п.). В простейших случаях документация пишется прямо в заголовочниках (комментариях), но чаще всего это файл (или файлы) в формате chm, html или pdf.
SDK (Software Development Kit) - набор из заголовочников и документации. SDK - это то, что необходимо стороннему разработчику для написания плагинов к вашей программе. SDK - это то, что вы должны создать и публично распространять для всех желающих писать плагины к вашей программе.
Базовый набор правил и соглашений
Итак, для системы плагинов я выбрал схему DLL + интерфейсы. Замечу, что мы не можем использовать пакеты по соображениям межязыковой совместимости, а COM мне не нравится отложенной выгрузкой :) Также замечу, что предлагаемая схема вполне будет способна работать с плагинами в виде пакетов, если вы захотите это сделать, но при этом не будет никаких плюшек пакетов (управление памятью, разделение классов), окромя бонуса с DllMain (как описано в оригинальной серии статей).Правило номер два - в системе плагинов обязательно должны быть явные функции инициализации и финализации. Это значит, что каждая DLL должна экспортировать минимум 2 функции, которые будут вызываться непосредственно (первой) после загрузки плагина ядром и перед самой выгрузкой (последней).
Правило номер три - модель вызова любых функций и методов в системе плагинов должна быть
safecall. Напомню, что safecall - это, на самом деле, stdcall с неявным HRESULT. Например, вот три примера эквивалентных объявлений (т.е. они написаны по-разному, но представляют собой одно и то же):
procedure DoSomething; safecall; function DoSomething: HRESULT; stdcall; procedure DoSomethingElse(Param: Integer); safecall; function DoSomethingElse(Param: Integer): HRESULT; stdcall; function DoSomethingMore: Integer; safecall; function DoSomethingMore(out AResult: Integer): HRESULT; stdcall;
Правило номер четыре - обработка ошибок в стиле COM (safecall/HRESULT). Т.е. в Delphi это прозрачно будут исключения.
Правило номер пять - все строки должны иметь тип
WideString.Правило номер шесть - вы не должны использовать типы данных Delphi, потому что они не имеют аналога в других языках. Например,
string, array of, TObject, TForm (и вообще любые объекты и уж тем более компоненты) и т.п. Что можно использовать - целочисленные типы (Integer, Cardinal, Int64, UInt64, NativeInt, NativeUInt, Byte, Word и т.п.; я бы не рекомендовал использовать Currency, если только он вам действительно нужен), вещественные (Single и Double; я бы рекомендовал избегать типов Extended и Comp, если только они действительно вам нужны и иначе никак), перечислимые и subrange-типы (с некоторыми оговорками), символьные типы (AnsiChar и WideChar, но не Char), строки (только в виде WideString), логический тип (BOOL, но не Boolean), интерфейсы (interface), в методах которых используются допустимые типы, записи (record) из вышеуказанных типов, а также указатели на них (в том числе указатели на массивы из вышеуказанных типов, но не динамические массивы).Как узнать, какой тип можно использовать, а какой - нет? Относительно простое правило - если вы не видите тип в этом списке, и типа нет в модуле
Windows (модуле Winapi.Windows, начиная с Delphi XE2), то этот тип использовать нельзя. Если же тип перечислен мною выше или находится в модуле Windows/Winapi.Windows - используйте его. Это достаточно грубое правило, но для начала - сойдёт.Правило номер семь - как только вы опубликовали какой-то тип (интерфейс), вы не должны его изменять. Если вам нужно его расширить или изменить - вы вводите новый интерфейс (новую версию интерфейса), но не меняете старый.
Правило номер восемь - вы не используете разделяемый менеджер памяти (не подключаете модуля вроде
ShareMem, SimpleShareMem и т.п.).Правило номер девять - все интерфейсы API должны иметь GUID. Все интерфейсы вне API (используемые только ядром) могут не иметь GUID.
Структура папок и файлов
Следующий вопрос - какие файлы нам понадобятся, что нам нужно создать. Для начала заметим, что когда мы говорим о системе плагинов, то мы можем говорить с двух сторон: со стороны ядра и со стороны плагина. И ядро и плагин должны следовать контракту, который мы для них определим, но делать это они будут с разных сторон. Это означает, что нам потребуются:- Общие файлы - используются и ядром и плагинами
- Файлы ядра (то, к чему не имеют доступа плагины)
- Файлы плагинов (то, к чему не имеет доступа ядро)
- Файлы вне системы плагинов (функциональность ядра и плагинов)
Поэтому, для начала, чтобы отделить п4 от пп1-3, давайте создадим новую папку (в любом месте), скажем - PluginAPI. В эту папку мы будем помещать всё то, что имеет отношение к системе плагинов, вне этой папки будем хранить всё остальное.
Далее, в этой папке создадим три подпапки: Headers (для общих файлов), Core (для файлов ядра) и Plugins (для файлов плагинов). Headers - это то, что вы должны распространять публично для всех желающих писать плагины для вашей программы (часть SDK, заголовочники). Core будет использоваться только вами (как разработчиком программы), а Plugins часто может оказаться пустой, но если это не так, то там лежат файлы, которые вы должны будете прикладывать к Headers, когда вы распространяете SDK. Это - дополнительная функциональность, обёртка вокруг заголовочников.
Далее, вы создаёте основную программу. Поскольку у меня программы нет, я возьму уже готовую - это демка RichEdit из комплекта поставки Delphi. Программа-пример представляет собой простой текстовый редактор. Файлы этой демки (remain.pas, remain.dfm и т.п.) относятся к пункту 4: файлы вне системы плагинов. Поэтому вы сохраняете основную программу в любом месте, а затем добавляете нужные файлы через Project/Add to project (либо прописываете пути поиска в проекте или IDE, но я не буду это показывать).
Какие файлы? Вроде у нас ещё ничего нет? Вот-вот. Так что давайте (наконец-то!) что-то попишем...
Менеджер плагинов
Откройте свою программу (в примере - скопированную демку RichEdit) и сделайте File/New unit. Сохраните этот модуль в папке PluginAPI\Core под именем PluginManager.pas и введите в него такую заготовку:unit PluginManager; interface type IPluginManager = interface end; function Plugins: IPluginManager; implementation uses SysUtils, Classes; type TPluginManager = class(TInterfacedObject, IPluginManager) end; //________________________________________________________________ var FPluginManager: IPluginManager; function Plugins: IPluginManager; begin Result := FPluginManager; end; initialization FPluginManager := TPluginManager.Create; finalization FPluginManager := nil; end.Этот код объявляет и создаёт менеджер плагинов. Менеджер плагинов - это вспомогательный код в вашей программе, который служит для управления плагинами. Он выполняет всю черновую работу с плагинами, так что вам не нужно засорять код своей основной программы. Менеджер создаётся автоматически при старте программы и автоматически удаляется при выходе из программы.
Вы видите тут три части. Секция
interface перечисляет то, с чем будет работать основная программа. Пока это глобальная функция Plugins для доступа к менеджеру и сам менеджер - интерфейс IPluginManager: пока пустой.Вторая часть (до подчёркивания) содержит код менеджера плагинов - тоже пока пустой. Это черновая работа, которую мы скрываем "под капотом" модуля, чтобы она не засоряла код основной программы.
Часть три (после черты) представляет собой код инициализации и удаления менеджера плагинов. Поскольку мы работаем с ним через интерфейс, то для его удаления мы просто очищаем ссылку. Обратите внимание, что поскольку менеджер плагинов в данном случае является глобальным объектом, то мы используем для его хранения глобальную переменную. Однако заметьте, что при этом глобальная переменная объявлена последней и лежит практически в самом низу текста (новый код будет добавляться выше черты, но не ниже). Плюс, она закрыта от внешнего доступа секцией
implementation. Т.е. она максимально изолирована от внешних воздействий.Итак, что будет делать менеджер плагинов? Ну, наверное для начала плагины надо бы найти и загрузить. Как это обычно делают? Есть два способа:
- Пользователь указывает в настройках программы, какие плагины нужно включать.
Плюсы:- Пользователь может использовать плагин в любом месте.
- Пользователь может отключать плагин из интерфейса программы, не удаляя его.
- Пользователю нужно настраивать плагины вручную.
- Программа загружает как плагины все файлы из предопределённой папки.
Плюсы:- Пользователю ничего не надо делать.
- Чтобы удалить или добавить плагин, его нужно скопировать в папку или удалить из неё.
- Плагин нельзя "отключить", не удалив его.
Итак, что должен уметь делать менеджер плагинов, чтобы реализовать какую-то из схем выше?
Загрузка одного плагина
Какую-бы схему мы ни реализовали бы - нам обязательно в любом случае потребуется функция загрузки одного плагина. Вот давайте с неё и начнём:unit PluginManager;
interface
uses
Windows;
type
IPlugin = interface
// protected
function GetIndex: Integer;
function GetHandle: HMODULE;
function GetFileName: String;
// public
property Index: Integer read GetIndex;
property Handle: HMODULE read GetHandle;
property FileName: String read GetFileName;
end;
IPluginManager = interface
// protected
function GetItem(const AIndex: Integer): IPlugin;
function GetCount: Integer;
// public
function LoadPlugin(const AFileName: String): IPlugin;
procedure UnloadPlugin(const AIndex: Integer);
property Items[const AIndex: Integer]: IPlugin read GetItem; default;
property Count: Integer read GetCount;
end;
function Plugins: IPluginManager;
implementation
uses
SysUtils,
Classes;
type
TPluginManager = class(TInterfacedObject, IPluginManager)
private
FItems: array of IPlugin;
FCount: Integer;
protected
function GetItem(const AIndex: Integer): IPlugin;
function GetCount: Integer;
public
function LoadPlugin(const AFileName: String): IPlugin;
procedure UnloadPlugin(const AIndex: Integer);
function IndexOf(const APlugin: IPlugin): Integer;
end;
TPlugin = class(TInterfacedObject, IPlugin)
private
FManager: TPluginManager;
FFileName: String;
FHandle: HMODULE;
protected
function GetIndex: Integer;
function GetHandle: HMODULE;
function GetFileName: String;
public
constructor Create(const APluginManger: TPluginManager; const AFileName: String); virtual;
destructor Destroy; override;
end;
{ TPluginManager }
function TPluginManager.LoadPlugin(const AFileName: String): IPlugin;
begin
// Загружаем плагин
Result := TPlugin.Create(FManager, AFileName);
// Заносим в список
if Length(FItems) <= FCount then // "Capacity"
SetLength(FItems, Length(FItems) + 64);
FItems[FCount] := Result;
Inc(FCount);
end;
procedure TPluginManager.UnloadPlugin(const AIndex: Integer);
var
X: Integer;
begin
// Выгрузить плагин
FItems[AIndex] := nil;
// Сдвинуть плагины в списке, чтобы закрыть "дырку"
for X := AIndex to FCount - 1 do
FItems[X] := FItems[X + 1];
// Не забыть учесть последний
FItems[FCount - 1] := nil;
Dec(FCount);
end;
function TPluginManager.IndexOf(const APlugin: IPlugin): Integer;
var
X: Integer;
begin
Result := -1;
for X := 0 to FCount - 1 do
if FItems[X] = APlugin then
begin
Result := X;
Break;
end;
end;
function TPluginManager.GetCount: Integer;
begin
Result := FCount;
end;
function TPluginManager.GetItem(const AIndex: Integer): IPlugin;
begin
Result := FItems[AIndex];
end;
{ TPlugin }
constructor TPlugin.Create(const APluginManger: TPluginManager;
const AFileName: String);
begin
inherited Create;
FManager := APluginManger;
FFileName := AFileName;
FHandle := SafeLoadLibrary(AFileName);
Win32Check(FHandle <> 0);
end;
destructor TPlugin.Destroy;
begin
if FHandle <> 0 then
begin
FreeLibrary(FHandle);
FHandle := 0;
end;
inherited;
end;
function TPlugin.GetFileName: String;
begin
Result := FFileName;
end;
function TPlugin.GetHandle: HMODULE;
begin
Result := FHandle;
end;
function TPlugin.GetIndex: Integer;
begin
Result := FManager.IndexOf(Self);
end;
//________________________________________________________________
...
end.
Ух, что-то тут много всего появилось. Давайте по порядку. Начнём с изменений в IPluginManager.Собственно функция загрузки плагина - это
LoadPlugin:
function LoadPlugin(const AFileName: String): IPlugin;Как видите, она принимает имя файла плагина для загрузки. Очевидно, что она должна возвращать загруженный плагин. Нам надо его как-то представить - вот тут появляется новая сущность: "плагин". Он у нас представлен новым интерфейсом
IPlugin.Лирическое отступление. Сразу заметим такую вещь: сейчас мы говорим про внутреннюю кухню ядра, мы ещё не начали формировать API (напомню: исходный файл менеджера плагинов лежит в PluginAPI\Core, а не в PluginAPI\Headers). Именно поэтому тут совершенно нормально использовать
string, соглашение вызова register и другие вещи, специфичные для Delphi: потому что их будет использовать только наша программа и никто иной. Окей, возвращаясь к коду. Функция
LoadPlugin устроена просто (если вы посмотрите на её реализацию в классе TPluginManager):
function TPluginManager.LoadPlugin(const AFileName: String): IPlugin;
begin
// Загружаем плагин
Result := TPlugin.Create(FManager, AFileName);
// Заносим в список
if Length(FItems) >= FCount then // "Capacity"
SetLength(FItems, Length(FItems) + 64);
FItems[FCount] := Result;
Inc(FCount);
end;
Она создаёт наш плагин (в виде объекта TPlugin, реализующего интерфейс IPlugin) и регистрирует его в своём "списке всех плагинов" - FItems. Как вы видите, функция LoadPlugin на самом деле не выполняет непосредственно загрузку плагина, а делегирует (передаёт) эту работу классу-оболочке для плагина TPlugin. Это - правильно. Загрузка плагина - забота плагина и его класса. Управление плагинами - забота менеджера плагинов. Правильное разделение обязанностей, короче говоря.Раз уж у нас появился "список плагинов", то неплохо бы его выставить наружу - так в
IPluginManager появляются свойства Items и Count:
IPluginManager = interface
// protected
function GetItem(const AIndex: Integer): IPlugin;
function GetCount: Integer;
// public
...
property Items[const AIndex: Integer]: IPlugin read GetItem; default;
property Count: Integer read GetCount;
end;
Они дают ядру доступ к списку плагинов, позволяя их перебрать.Ну и если плагин загружен, то надо ведь и обратное действие уметь выполнять: выгрузку плагинов. Вот у нас появляется ещё один метод:
UnloadPlugin.
procedure UnloadPlugin(const AIndex: Integer);Метод принимает номер плагина для его выгрузки. Из его реализации (в классе
TPluginManager):
procedure TPluginManager.UnloadPlugin(const AIndex: Integer);
var
X: Integer;
begin
// Выгрузить плагин
FItems[AIndex] := nil;
// Сдвинуть плагины в списке, чтобы закрыть "дырку"
for X := AIndex to FCount - 1 do
FItems[X] := FItems[X + 1];
// Не забыть учесть последний
FItems[FCount - 1] := nil;
Dec(FCount);
end;
видно, что он просто удаляет плагин из списка, уничтожая его.Пока мы ещё говорим про класс менеджера плагинов, заметим такую вещь: мы работаем с интерфейсами и для
FItems у нас используется динамический массив. Это значит, что все типы данных являются управляемыми и нам не нужно их освобождать руками. Т.е. если ядро вызовет три раза LoadPlugin для загрузки трёх плагинов, а потом просто выйдет, то у нас не будет никакой утечки памяти. Менеджер плагинов начнёт удаляться в finalization (при условии, что ссылку на него никто больше не держит), при этом автоматически очистится FItems (как авто-управляемый динамический массив), а все его элементы будут автоматически освобождены (как автоуправляемые интерфейсы) - опять же при условии, что на них больше нет ссылок. В этот момент и произойдёт выгрузка каждого плагина.Итак, с
IPluginManager и TPluginManager мы закончили, давайте посмотрим теперь на IPlugin/TPlugin.Тут пока совсем всё просто: наружу интерфейс выставляет несколько свойств, которые могут быть интересны ядру: индекс плагина, имя файла и описатель загруженного файла.
IPlugin = interface
// protected
function GetIndex: Integer;
function GetHandle: HMODULE;
function GetFileName: String;
// public
property Index: Integer read GetIndex;
property Handle: HMODULE read GetHandle;
property FileName: String read GetFileName;
end;
Реализация методов в классе TPlugin тривиальна. Единственный момент - обратите внимание на реализацию свойства Index.
function TPlugin.GetIndex: Integer; begin Result := FManager.IndexOf(Self); end;В данном случае оно реализовано через поиск (см.
TPluginManager.IndexOf).
function TPluginManager.IndexOf(const APlugin: IPlugin): Integer;
var
X: Integer;
begin
Result := -1;
for X := 0 to FCount - 1 do
if FItems[X] = APlugin then
begin
Result := X;
Break;
end;
end;
Можно было бы сделать иначе: хранить индекс плагина в поле класса TPlugin. Оба решения имеют как плюсы, так и минусы, но в целом разницы нет никакой (у вас будет загружено более 1000 плагинов? Навряд ли). Более интересно выглядят (скрытые) конструктор и деструктор плагина. Помимо тривиальностей по инициализации и заполнению свойств плагина, они выполняют собственно работу, ради которой всё и затевалось: загрузку и выгрузку DLL.
constructor TPlugin.Create(const APluginManger: TPluginManager;
const AFileName: String);
begin
inherited Create;
FManager := APluginManger;
FFileName := AFileName;
FHandle := SafeLoadLibrary(AFileName); // работа: загрузка
Win32Check(FHandle <> 0);
end;
destructor TPlugin.Destroy;
begin
if FHandle <> 0 then // работа: выгрузка
begin
FreeLibrary(FHandle);
FHandle := 0;
end;
inherited;
end;
Уже в этот момент этот код можно потестировать. К примеру, если вы бросите на форму основной программы
TButton, TEdit и TListbox, то вы можете написать такой тестовый код:
type
TMainForm = class(TForm)
...
private
...
procedure UpdatePluginsList;
end;
...
implementation
uses
REAbout, RichEdit, ShellAPI, ReInit,
PluginManager; // <- новый модуль
...
procedure TMainForm.Button1Click(Sender: TObject);
begin
Plugins.LoadPlugin(Edit1.Text);
UpdatePluginsList;
end;
procedure TMainForm.UpdatePluginsList;
var
X: Integer;
begin
ListBox1.Items.BeginUpdate;
try
ListBox1.Items.Clear;
for X := 0 to Plugins.Count - 1 do
ListBox1.Items.Add(IntToStr(Plugins[X].Index) + ': ' + Plugins[X].FileName);
finally
ListBox1.Items.EndUpdate;
end;
end;
...
end.
Запустите программу, вы можете ввести в Edit имя файла любой DLL и нажать на кнопку для загрузки её как плагина. При этом она тут же появится в списке загруженных плагинов.Конечно, сейчас это может показаться несколько странным: как это так получается, что любая DLL может быть загружена как плагин? И что вообще делает этот плагин? Собственно, ничего странного нет: ведь мы пока не написали ни одной строчки API, у нас нет никакого контракта для плагинов, пока мы писали лишь код поддержки. Именно поэтому для плагина (пока!) подходит любая DLL и она просто ничего не делает.
По этой же причине, уже написанный код - универсален и может быть использован как база для любой новой системы плагинов.
Загрузка папки с плагинами
Следующая задача - загрузить целую папку с плагинами. Раз у нас уже есть функция загрузки одного плагина, то теперь мы можем вызвать её в цикле по всем плагинам в папке (пусть даже пока функция загрузки просто грузит DLL, а сами плагины пока что ничего не делают - не беда).Я не буду приводить весь код целиком, а приведу лишь добавленный и изменённый код. Код в многоточиях оставлен без изменений.
interface
uses
Windows,
SysUtils,
Classes;
type
EPluginManagerError = class(Exception);
EPluginLoadError = class(EPluginManagerError);
EPluginsLoadError = class(EPluginLoadError)
private
FItems: TStrings;
public
constructor Create(const AText: String; const AFailedPlugins: TStrings);
destructor Destroy; override;
property FailedPluginFileNames: TStrings read FItems;
end;
...
IPluginManager = interface
...
procedure LoadPlugins(const AFolder: String; const AFileExt: String = '');
...
end;
...
implementation
resourcestring
rsPluginsLoadError = 'One or more plugins has failed to load:' + sLineBreak + '%s';
type
TPluginManager = class(TInterfacedObject, IPluginManager)
...
procedure LoadPlugins(const AFolder, AFileExt: String);
...
end;
...
procedure TPluginManager.LoadPlugins(const AFolder, AFileExt: String);
function PluginOK(const APluginName, AFileExt: String): Boolean;
begin
Result := (AFileExt = '');
if Result then
Exit;
Result := SameFileName(ExtractFileExt(APluginName), AFileExt);
end;
var
Path: String;
SR: TSearchRec;
Failures: TStringList;
FailedPlugins: TStringList;
begin
Path := IncludeTrailingPathDelimiter(AFolder);
Failures := TStringList.Create;
FailedPlugins := TStringList.Create;
try
if FindFirst(Path + '*.*', faNormal, SR) = 0 then
try
repeat
if ((SR.Attr and faDirectory) = 0) and
PluginOK(SR.Name, AFileExt) then
try
LoadPlugin(Path + SR.Name);
except
on E: Exception do
begin
FailedPlugins.Add(SR.Name);
Failures.Add(Format('%s: %s', [SR.Name, E.Message]));
end;
end;
until FindNext(SR) <> 0;
finally
FindClose(SR);
end;
if Failures.Count > 0 then
raise EPluginsLoadError.Create(Format(rsPluginsLoadError, [Failures.Text]), FailedPlugins);
finally
FreeAndNil(FailedPlugins);
FreeAndNil(Failures);
end;
end;
...
function TPluginManager.LoadPlugin(const AFileName: String): IPlugin;
begin
// Загружаем плагин
try
Result := TPlugin.Create(FManager, AFileName);
except
on E: Exception do
raise EPluginLoadError.Create(Format('[%s] %s', [E.ClassName, E.Message]));
end;
...
end;
...
{ EPluginsLoadError }
constructor EPluginsLoadError.Create(const AText: String;
const AFailedPlugins: TStrings);
begin
inherited Create(AText);
FItems := TStringList.Create;
FItems.Assign(AFailedPlugins);
end;
destructor EPluginsLoadError.Destroy;
begin
FreeAndNil(FItems);
inherited;
end;
//________________________________________________________________
Опять у нас получилось больше кода, чем изначально планировалось. И вот почему.Как видите, основной новый метод тут -
LoadPlugins. Он принимает имя папки и необязательное расширение файлов. Реализация метода ищет в указанной папке все файлы и загружает те из них, которые подходят под указанное расширение. Если расширение не указывать, то будут загружены вообще все файлы.К примеру:
procedure TMainForm.Button2Click(Sender: TObject);
begin
try
Plugins.LoadPlugins('C:\Windows', '.dll'); // загрузить все DLL из папки Windows
finally
UpdatePluginsList;
end;
end;
или:
procedure TMainForm.Button2Click(Sender: TObject);
begin
try
Plugins.LoadPlugins(Edit1.Text); // загрузить вообще все файлы из указанной папки
finally
UpdatePluginsList;
end;
end;
В реализации метода LoadPlugins нет ничего сложного, за исключением одного момента: обработка ошибок. Предположим, один из плагинов не смог загрузится. Что тогда делать? Есть два варианта: первый - остановиться и сообщить об ошибке. Это самый простой вариант и он мне не нравится. Зато можно было бы ничего больше не писать. Вариант два - продолжить загрузку плагинов. Именно этот вариант я и показал. Тут возникает вопрос, что делать с ошибкой загрузки плагина и как о ней сообщить. Ведь если мы грузим целую папку с плагинами, то отказать в загрузке может не один плагин, а много.Ответ заключается в том, что мы запоминаем плагины, которые не удалось загрузить. Но при этом продолжаем их загружать. В самом конце операции загрузки мы возбуждаем одну-единственную ошибку (одну - даже если несколько плагинов отказало), в которой суммируем информацию.
Тут возникает два момента: во-первых, раз уж мы возбуждаем свою ошибку, то нам надобен для этого класс. Использовать
Exception я вам категорически запрещаю. Поэтому мы вводим свои классы ошибок:
type
EPluginManagerError = class(Exception); // для всех ошибок менеджера плагинов
EPluginLoadError = class(EPluginManagerError); // для ошибок загрузки плагинов
EPluginsLoadError = class(EPluginLoadError); // для ошибок загрузки плагинов в папке
Кроме того, для последней ситуации кроме сообщения нам требуется хранить дополнительную информацию: список плагинов, которые отказались грузится. Зачем это нужно? Ну, скажем, ядро может отключить сбойнувшие плагины, чтобы не грузить их во второй раз в будущем при следующей загрузке. Плагин сбойнул? Отключили. Если пользователь исправит ошибку, препятствующую загрузке плагина - он включит плагин обратно.Итак, для этого к исключению мы пристыковываем список файловых имён:
EPluginsLoadError = class(EPluginLoadError) private FItems: TStrings; public constructor Create(const AText: String; const AFailedPlugins: TStrings); destructor Destroy; override; property FailedPluginFileNames: TStrings read FItems; end;А метод
LoadPlugins теперь может собирать информацию о сбойнувших плагинах:
try
LoadPlugin(Path + SR.Name);
except
on E: Exception do
begin
// Запомнили имя файла сбойнувшего плагина
FailedPlugins.Add(SR.Name);
// И вписали возникшую проблему в общий список ошибок
Failures.Add(Format('%s: %s', [SR.Name, E.Message]));
end;
end;
и возбуждать единую ошибку в конце работы:
if Failures.Count > 0 then // есть хотя бы один сбойнувший плагин? raise EPluginsLoadError.Create(Format(rsPluginsLoadError, [Failures.Text]), FailedPlugins);Ну и раз уж мы определили свои типы ошибок, неплохо бы их соблюдать. Поэтому изменяем метод
LoadPlugin, чтобы при исключении он возбуждал бы именно наш тип ошибки:
function TPluginManager.LoadPlugin(const AFileName: String): IPlugin;
begin
// Загружаем плагин
try
Result := TPlugin.Create(FManager, AFileName);
except
on E: Exception do
raise EPluginLoadError.Create(Format('[%s] %s', [E.ClassName, E.Message]));
end;
...
end;
Ну, вроде и всё с этим. Как обычно, вы можете проверить код в своём приложении - пусть оно грузит содержимое подпапки Plugins. Пусть даже сейчас это ничего не будет делать.Отключение плагинов
Итак, у нас есть функция загрузки одного плагина по команде пользователя, есть автозагрузка плагинов из папки, а теперь осталось лишь сделать отключение загрузки плагина без его удаления.К этой задаче можно подступиться по разному. Я предлагаю такое решение: завести "чёрный список" плагинов. Любой плагин можно добавить в этот список. Любой плагин можно удалить из списка. Функция
LoadPlugin (а следовательно и функция LoadPlugins) не станет грузить плагин, если он находится в чёрном списке. Соотвественно, чтобы отключить плагин - его надо занести в чёрный список (рассматривайте его как список отключенных плагинов), а чтобы включить - удалить из этого списка.Плагины можно идентифицировать по разному, но поскольку речь идёт о стадии загрузки, то наиболее естественно будет идентификация по полному имени файла - то, что передаётся в функцию
LoadPlugin.Итого получаем (и снова, я привожу лишь изменения, а не весь код):
type
...
IPluginManager = interface
...
procedure Ban(const AFileName: String);
procedure Unban(const AFileName: String);
procedure SaveSettings(const ARegPath: String);
procedure LoadSettings(const ARegPath: String);
...
end;
...
implementation
...
type
TPluginManager = class(TInterfacedObject, IPluginManager)
private
...
FBanned: TStringList;
protected
...
function CanLoad(const AFileName: String): Boolean;
public
constructor Create;
destructor Destroy; override;
...
procedure Ban(const AFileName: String);
procedure Unban(const AFileName: String);
procedure SaveSettings(const ARegPath: String);
procedure LoadSettings(const ARegPath: String);
end;
...
{ TPluginManager }
constructor TPluginManager.Create;
begin
inherited Create;
FBanned := TStringList.Create;
end;
destructor TPluginManager.Destroy;
begin
FreeAndNil(FBanned);
inherited;
end;
function TPluginManager.LoadPlugin(const AFileName: String): IPlugin;
begin
if not CanLoad(AFileName) then
begin
Result := nil;
Exit;
end;
// Загружаем плагин
...
end;
...
procedure TPluginManager.Ban(const AFileName: String);
begin
Unban(AFileName);
FBanned.Add(AFileName);
end;
procedure TPluginManager.Unban(const AFileName: String);
var
X: Integer;
begin
for X := 0 to FBanned.Count - 1 do
if SameFileName(FBanned[X], AFileName) then
begin
FBanned.Delete(X);
Break;
end;
end;
function TPluginManager.CanLoad(const AFileName: String): Boolean;
var
X: Integer;
begin
// Не грузить отключенные
for X := 0 to FBanned.Count - 1 do
if SameFileName(FBanned[X], AFileName) then
begin
Result := False;
Exit;
end;
// Не грузить уже загруженные
for X := 0 to FCount - 1 do
if SameFileName(FItems[X].FileName, AFileName) then
begin
Result := False;
Exit;
end;
Result := True;
end;
const
SRegDisabledPlugins = 'Disabled plugins';
SRegPluginX = 'Plugin%d';
procedure TPluginManager.SaveSettings(const ARegPath: String);
var
Reg: TRegIniFile;
Path: String;
X: Integer;
begin
Reg := TRegIniFile.Create(ARegPath, KEY_ALL_ACCESS);
try
// Удаляем старые
Reg.EraseSection(SRegDisabledPlugins);
Path := ARegPath + '\' + SRegDisabledPlugins;
if not Reg.OpenKey(Path, True) then
Exit;
// Сохраняем новые
for X := 0 to FBanned.Count - 1 do
Reg.WriteString(Path, Format(SRegPluginX, [X]), FBanned[X]);
finally
FreeAndNil(Reg);
end;
end;
procedure TPluginManager.LoadSettings(const ARegPath: String);
var
Reg: TRegIniFile;
Path: String;
X: Integer;
begin
Reg := TRegIniFile.Create(ARegPath, KEY_READ);
try
FBanned.BeginUpdate;
try
FBanned.Clear;
// Читаем
Path := ARegPath + '\' + SRegDisabledPlugins;
if not Reg.OpenKey(Path, True) then
Exit;
Reg.ReadSectionValues(Path, FBanned);
// Убираем "Plugin5=" из строк
for X := 0 to FBanned.Count - 1 do
FBanned[X] := FBanned.ValueFromIndex[X];
finally
FBanned.EndUpdate;
end;
finally
FreeAndNil(Reg);
end;
end;
Тут тоже ничего сложного: чёрный список плагинов хранится в FBanned, куда плагин можно добавить или из которого удалить через функции Ban и Unban соответственно. Функция CanLoad используется для определения статуса плагина: нужно его грузить или нет. Помимо собственно чёрного списка, функция дополнительно проверяет уже загруженные плагины, блокируя их повторную загрузку.Вспомогательные функции
Save- и LoadSettings используются для сохранения и загрузки списка заблокированных плагинов в реестр, следуя правилам хорошего тона. Таким образом, тестовый код может выглядеть так:
procedure TMainForm.Button3Click(Sender: TObject);
begin
Plugins.Ban(Edit1.Text);
end;
procedure TMainForm.Button4Click(Sender: TObject);
begin
Plugins.Unban(Edit1.Text);
end;
procedure TMainForm.FormCreate(Sender: TObject);
begin
Plugins.LoadSettings('\SOFTWARE\MyCompany\MyProduct');
...
end;
procedure TMainForm.FormDestroy(Sender: TObject);
begin
Plugins.SaveSettings('\SOFTWARE\MyCompany\MyProduct');
end;
Результат работы функции SaveSettings в реестре выглядит так:Разумеется, ничто не мешает вам реализовать функции сохранения/загрузки как-то иначе, или вовсе вынести их за пределы менеджера плагинов: пусть конфигурацию сохраняет и восстанавливает код основной программы.
Итак, мы написали менеджер плагинов в минимальном варианте. Вы можете расширять его функциями-обёртками для вашего удобства (например, ввести функцию, возвращающую загруженный плагин по имени файла), но это уже будет дополнительные удобства. Сейчас же самое время перейти к следующему шагу - собственно плагинам.
Скачать исходный код к этому моменту можно тут.
API
Теперь, когда у нас готова основа архитектуры плагинов со стороны ядра, можно начать прорабатывать контракт плагинов: API. Начнём с их инициализации. Как уже говорилось, у DLL должно быть минимум две функции: инициализации и финализации, которые будут вызываться первыми и последними. Вот давайте их сейчас и напишем. Откройте вашу программу и сделайте File/New unit. Сохраните новый модуль в папке PluginAPI\Headers под именем PluginAPI.pas:unit PluginAPI; interface type TInitPluginFunc = function(const ACore: IUnknown): IUnknown; safecall; TDonePluginFunc = procedure; safecall; const SPluginInitFuncName = '87D4EDFB420343F2976EB3CF4DB7C224'; SPluginDoneFuncName = SPluginInitFuncName + '_done'; SPluginExt = '.MyAppPlugin'; implementation end.Здесь мы определяем прототипы двух функций (Init и Done). Обе они имеют соглашение вызова safecall, а функция инициализации к тому же принимает параметры от ядра и возвращает плагин (т.е. самого себя). Пока у нас нет функциональности, поэтому я вписал самый базовый интерфейс, который только может быть. Заметьте, что мы сейчас уже говорим про API, поэтому должны придерживаться упомянутых в начале статье правил - в частности, касаемо соглашения вызова и типов данных.
Также в модуле определены имена, которые должны иметь функции инициализации и финализации. Имена выбраны как случайные - это сделано специально, чтобы в произвольной DLL таких имён точно бы не оказалось. Таким образом, если загружать произвольную DLL как плагин, то эта операция будет неудачной, потому что в произвольно взятой DLL (а не специально разработанном плагине) уж точно нет функции с именем '87D4EDFB420343F2976EB3CF4DB7C224'. Для получения такого имени я нажал в редакторе кода Delphi комбинацию Ctrl + Shift + G и получил:
['{87D4EDFB-4203-43F2-976E-B3CF4DB7C224}']
После чего я удалил [], {} и -, оставив только
'87D4EDFB420343F2976EB3CF4DB7C224'И таким образом я получил имя, которое гарантировано уникально.
Если вы будете использовать мой код в своих проектах, то вы должны для каждого своего проекта сгенерировать свою константу так же, как это сделал я. Это необходимо, чтобы плагины от одной программы нельзя было бы загрузить в другой.
Наконец, тут определена константа для расширений плагинов. К примеру, загрузка плагинов (возвращаясь к примеру с менеджером плагинов) тогда будет выглядеть так:
Plugins.LoadFolder(ExtractFilePath(ParamStr(0))) + 'Plugins', SPluginExt);
Т.е. мы говорим, что плагин для нашей программы представляет собой переименованную в .MyAppPlugin-файл обыкновенную DLL, которая экспортирует функции 87D4EDFB420343F2976EB3CF4DB7C224 и 87D4EDFB420343F2976EB3CF4DB7C224_done с прототипами, указанными выше, и расположенная в подпапке Plugins основной программы.
Возьмите на заметку - это слова для вашей (будущей) документации к плагинам.
В любом случае, давайте во-первых научим работать с этим наш менеджер плагинов, а потом напишем пустой плагин (ибо после внесения изменений в программу уже нельзя будет загрузить произвольную DLL):
unit PluginManager;
...
implementation
uses
Registry,
PluginAPI;
...
type
TPlugin = class(TInterfacedObject, IPlugin)
private
...
FInit: TInitPluginFunc;
FDone: TDonePluginFunc;
FPlugin: IInterface;
protected
...
end;
...
{ TPlugin }
constructor TPlugin.Create(const APluginManger: TPluginManager;
const AFileName: String);
begin
...
FDone := GetProcAddress(FHandle, SPluginDoneFuncName);
FInit := GetProcAddress(FHandle, SPluginInitFuncName);
Win32Check(Assigned(FInit));
FPlugin := FInit(FManager);
end;
destructor TPlugin.Destroy;
begin
FPlugin := nil;
if Assigned(FDone) then
FDone;
...
end;
...
end.
Мне кажется, тут всё очевидно. Хочу только обратить внимание, что Init-функцию мы подразумеваем обязательной, а Done функцию - опциональной (т.е. она может отсутствовать, если она не нужна плагину).Теперь давайте создадим плагин. Для этого сделайте File/New/Delphi Projects/Dynamic-Link Library и сохраните куда-нибудь проект (вне папки PluginAPI). Затем сделайте File/Add to project и укажите файл PluginAPI.pas. После чего напишите минимальный код:
library Plugin1;
uses
PluginAPI in 'PluginAPI\Headers\PluginAPI.pas';
{$R *.res}
function Init(const ACore: IInterface): IInterface; safecall;
begin
Result := nil;
end;
procedure Done; safecall;
begin
// ничего не делает
end;
exports
Init name SPluginInitFuncName,
Done name SPluginDoneFuncName;
end.
Мы просто вставили пустые функции инициализации и финализации и экспортировали их.Пока мы ничего сделать не можем, потому что ещё не определили функциональность. Но в этот момент мы можем проверить, как плагин и приложение работают вместе. Попробуйте как и ранее загрузить произвольную DLL - это не сработает. А теперь попробуйте загрузить наш плагин - операция пройдёт успешно.
Управление заголовочниками
Начнём понемногу строить функциональность. Давайте начнём с самого простого - добавим возможность ядру узнать версию плагина, а плагину - версию ядра.Очевидно, что для этого нам нужно объявить два интерфейса: один для ядра и один для плагина. Или один интерфейс для обоих сразу. В любом случае, интерфейс будет иметь метод (и свойство) для получения версии.
Тут сразу же я хотел бы рассмотреть вот какой момент. Предположим, сейчас мы напишем (слово "предположим" означает что это писать не надо):
unit PluginAPI;
interface
...
type
IPluginInfo = interface
['{F96EFCD5-17F6-4C17-956C-219F23C51AF9}']
// private
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;
ICoreInfo = interface
['{525797CF-89EB-4226-8BFE-4E3DD2123E13}']
// private
function GetVersion: Integer; safecall;
// public
property Version: Integer read GetVersion;
end;
implementation
end.
Мы можем использовать эти объявления и включить заголовочный файл PluginAPI.pas в SDK (вместе с его описанием в документации). Хорошо, но что будут делать программисты, работающие на других языках? Один из вариантов - перевести все .pas файлы на C++. К примеру, наш файл PluginAPI.pas, будучи переведённым на C++, станет выглядеть как-то так:
namespace Pluginapi
{
//-- type declarations -------------------------------------------------------
typedef HRESULT __stdcall (*TInitPluginFunc)(IUnknown *ACore, IUnknown **Result);
typedef HRESULT __stdcall (*TDonePluginFunc)(void);
MIDL_INTERFACE("F96EFCD5-17F6-4C17-956C-219F23C51AF9")
IPluginInfo : public IUnknown
{
public:
virtual HRESULT __stdcall GetID(GUID *Result) = 0;
virtual HRESULT __stdcall GetName(BSTR *Result) = 0;
virtual HRESULT __stdcall GetVersion(BSTR *Result) = 0;
};
MIDL_INTERFACE("525797CF-89EB-4226-8BFE-4E3DD2123E13")
ICoreInfo : public IUnknown
{
public:
virtual HRESULT __stdcall GetVersion(int *Result) = 0;
};
//-- var, const, procedure ---------------------------------------------------
#define SPluginInitFuncName L"87D4EDFB420343F2976EB3CF4DB7C224"
#define SPluginDoneFuncName L"87D4EDFB420343F2976EB3CF4DB7C224_done"
#define SPluginExt L".MyAppPlugin"
} /* namespace Pluginapi */
Примечание: чёрт, я мало что понимаю в C++, так что с переводом я мог наврать. Буду благодарен, если мне ткнут носом в правильный перевод.Как видите, текст дословно дублирует паскалевский код, но на другом языке (C++). Т.е. это просто копия.
Суть такого телодвижения в том, что C++ - это де-факто стандарт. Если кто-то не программирует на Delphi, то он программирует либо на C++ (и тогда он может воспользоваться указанным заголовочником), либо программирует на другом языке. В последнем случае ему придётся переводить заголовочники (с Delphi или C++ на свой язык) самостоятельно.
В любом случае, если вы в основном работаете на Delphi и слабо знакомы с другими языками, либо же вы хотите как-то автоматизировать работу (перевод заголовочников - не самая приятная работа, к тому же тут легко ошибиться: забыть перевести внесённые изменения), то вам захочется подыскать другой способ. Хотя, повторюсь, вы можете просто писать .pas файлы и переводить их на другие языки - это отлично будет работать.
Но сейчас я предлагаю воспользоваться ещё одним заимствованием из COM: библиотекой типов.
Библиотеки типов
Я кратко уже описал смысл выше: библиотека типов - это универсальное хранилище информации о сборке. Она хранится в двоичном формате (TLB) либо как отдельный файл (.tlb), либо как ресурс внутри .dll. С библиотеками типов умеют работать почти все современные компиляторы под Windows. Если кто-то умеет работать с COM, то он умеет работать и с библиотекой типов.Самое важное для нас тут то, что библиотека типов может быть импортирована сторонней средой разработки, и при этом среда разработки сама (и автоматически) сгенерирует все необходимые исходные файлы. К примеру, если импортировать TLB в Delphi - Delphi сама создаст .pas заголовочник. Если импортировать TLB в Visual Studio C++ - она сама создаст .hpp файлы. Аналогичное справедливо и для других сред разработки (импортировать TLB можно даже в PHP!).
Короче говоря, с точки зрения хранимых сведений, библиотека является более продвинутым аналогом заголовочных файлов - поскольку хранит в себе гораздо больше полезной информации, компактнее и быстрее (не нужно делать парсинг заголовочных файлов) и, главное, может использоваться в любой среде разработки и любом языке программирования, которые поддерживает COM, а не только в Delphi.
Давайте создадим описание наших интерфейсов в виде библиотеки типов. Я буду описывать процесс, используя Delphi XE2, но это же должно быть верно и для более старых версий Delphi, включая Delphi 7 - правда, с некоторыми оговорками. Итак, выберите File/New/ActiveX/Type Library. Появится редактор библиотеки типов:
![]() |
| Редактор в Delphi 7 |
![]() |
| Редактор в Delphi XE2 |
Сохраните этот файл с именем PluginAPI в папке PluginAPI\Headers. В Delphi XE2 это будет файл PluginAPI.ridl, а в Delphi 7 - PluginAPI.tlb. .ridl-файл - это текстовый формат, .tlb - двоичный.
Тут есть один не совсем очевидный момент: в Delphi XE2 на экране вы видите 2 кнопки сохранения: во-первых, это стандартная кнопка в панели инструментов, а, во-вторых, это кнопка сохранения в редакторе библиотеки типов (она видна на снимке экрана выше - это самая правая кнопка в панели инструментов редактора библиотеки типов). Так вот, чтобы сохранить изменения в библиотеки типов, нужно нажать на стандартную кнопку сохранения, а не на кнопку сохранения в редакторе библиотеки типов. Это ещё более неочевидно, потому что изменения в библиотеки типов не приводят к немедленному включению стандартной кнопки сохранения Save all. Чтобы она включилась, нужно переключить вкладки - уйти с редактора библиотеки типов на, скажем, главный модуль программы или страницу Welcome и вернуться обратно. После этого кнопка сохранения будет активной.
Если же вы щёлкните на кнопке сохранения в редакторе библиотеки типов, то это будет не сохранение, а экспорт в .tlb-файл (напомню, что Delphi XE2 хранит библиотеку типов в текстовом формате в .ridl файле). В Delphi 7 это немного попроще, потому что кнопка экспорта так и называется - экспорт. И выглядит она по другому. Соответственно, в Delphi 7 эта кнопка экспортирует двоичный файл в текстовый.
В общем, Delphi 7 и Delphi XE2 ведут себя с точностью до наоборот. Нам это, впрочем никак не мешает, поскольку одно в другое преобразовать не проблема.
В любом случае, при создании библиотеки типов ей автоматически был присвоен уникальный GUID и установлена версия 1.0. А после сохранения её в файл она стала иметь имя "PluginAPI". На первой вкладке вам ничего больше менять не нужно. Теперь переключитесь на вкладку "Uses" и сбросьте галочку с "Borland standard VCL type library", оставив только "OLE Automation". Это отвяжет библиотеку типов от Delphi (при этом среда покажет предупреждение, что теперь с этой библиотекой типов не будут работать некоторые Delphi-вые фишки - соглашайтесь).
Теперь создадим в библиотеке типов наши интерфейсы
IPluginInfo и ICoreInfo. Для этого щёлкните по первой (красной) кнопке в панели инструментов редактора библиотеки типов, либо же щёлкните по ней правой кнопкой мыши и выберите New\Interface. Это создаст новый интерфейс с автоматически сгенерированным GUID. Установите ему имя в "IPluginInfo", версию - в 1.0, а предка - в IUnknown:На вкладке "Flags" сбросьте опции "Dual" и "OLE Automation". Это отключит дополнительные возможности COM, объявив интерфейс в чистом виде.
Далее щёлкните правой кнопкой по интерфейсу в дереве и выберите New\Property - это создаст два метода-акцесора Get и Set. Поскольку свойства у нас только для чтения, то удалите метод Set, оставив только Get (Get - первый, Set - второй; ещё их можно опознать по свойству Invoke kind: Put - это Set, ну а Get - это Get). Установите имя в ID, а тип данных - в GUID. Больше ничего менять не нужно.
Аналогично создайте свойства
Name и Version, имеющие тип BSTR, а также интерфейс ICoreInfo со свойством Version, имеющим тип long:Готово.
Тут надо сделать замечание по поводу типов данных. Типы данных указываются в "стиле C++", а не Delphi. К примеру,
GUID вместо TGUID, BSTR вместо WideString, int вместо Integer, long вместо LongInt, * вместо ^ (указатель) и так далее. Список соответствия можно посмотреть в этой статье. Кроме того, поскольку интерфейсы в C++ являются просто абстрактными классами, а экземпляры классов в C++ не являются по умолчанию указателями (как в Delphi), то с именами интерфейсов необходимо явно указывать указатель, например:
IUnknown*Когда вы закончите формировать библиотеку типов, сохраните её. Также сделайте экспорт в альтернативный формат (в Delphi 7 - в текстовый, в Delphi XE2 - в двоичный). В текстовом виде наша библиотека сейчас выглядит примерно так:
[
uuid(F156F71A-758A-40E2-A34E-50187B8ED7B9),
version(1.0)
]
library PluginAPI
{
importlib("stdole2.tlb");
interface IPluginInfo;
interface ICoreInfo;
[
uuid(631B96BB-1E7E-407D-83F1-5C673D2B5A15),
version(1.0)
]
interface IPluginInfo: IUnknown
{
[propget, id(0x00000065)]
HRESULT _stdcall ID([out, retval] struct GUID* Value);
[propget, id(0x00000066)]
HRESULT _stdcall Name([out, retval] BSTR* Value);
[propget, id(0x00000067)]
HRESULT _stdcall Version([out, retval] BSTR* Value);
};
[
uuid(3BAA3534-5422-42B9-BDEA-1CE1037295B3),
version(1.0)
]
interface ICoreInfo: IUnknown
{
[propget, id(0x00000065)]
HRESULT _stdcall Version([out, retval] long* Value);
};
};
Файлы PluginAPI.tlb и PluginAPI.ridl (или .idl) - это то, что необходимо распространять в составе SDK. Имея на руках эти файлы любой сможет их импортировать в свой любимый язык программирования и получить заголовочные файлы. Вам не нужно ничего переводить самому.Генерация заголовочников для Delphi и C++ Builder
К примеру, если говорить про Delphi и C++ Builder, то получение нужных файлов (да, это немного бессмысленно, т.к. эти файлы у нас в любом случае есть, но чисто ради примера) удобно делать через утилиту GenTLB.exe в папке bin - вы можете указать ей .ridl файл и на выходе получить комплект из .pas, .hpp и .cpp - все необходимые заголовочники для Delphi и C++. Есть утилита tlibimp.exe, которая работает по .tlb файлу. К примеру, если у вас на руках есть .tlb файл, то получить комплект файлов вы можете такой командой:tlibimp.exe Source.tlb -C -P -I "-DD:\Output\" -Pt+Здесь Source.tlb - это исходный файл, а D:\Output\ - папка, куда нужно поместить результаты. Опции -C, -P и -I отвечают за генерацию .hpp/.cpp, .pas и .ridl файлов соответственно, а опция -Pt+ включает красивое "схлопывание" методов-акцессоров в свойства для Delphi.
Кроме того, если вы создадите библиотеку типов в рамках проекта (т.е. вы выберите File\New\ActiveX\Type Library в то время, когда у вас открыт проект основной программы), то необходимые .pas файлы будут сгенерированы (и будут обновляться в дальнейшем) автоматически.
Автогенерируемые файлы получают суффикс "_TLB". Например, библиотека "PluginAPI.tlb" создаст файл "PluginAPI_TLB.pas". Сам файл при этом выглядит примерно так:
unit PluginAPI_TLB;
// ************************************************************************ //
// WARNING
// -------
// The types declared in this file were generated from data read from a
// Type Library. If this type library is explicitly or indirectly (via
// another type library referring to this type library) re-imported, or the
// 'Refresh' command of the Type Library Editor activated while editing the
// Type Library, the contents of this file will be regenerated and all
// manual modifications will be lost.
// ************************************************************************ //
// $Rev: 41960 $
// File generated on 2011.12.25 18:49:13 from Type Library described below.
// ************************************************************************ //
// Type Lib: c:\Users\Александр\Documents\RAD Studio\Projects\Plugins\Example2\PluginAPI\Headers\PluginAPI.tlb (1)
// LIBID: {F156F71A-758A-40E2-A34E-50187B8ED7B9}
// LCID: 0
// Helpfile:
// HelpString:
// DepndLst:
// (1) v2.0 stdole, (C:\Windows\SysWOW64\stdole2.tlb)
// Cmdline:
// tlibimp "c:\Users\Александр\Documents\RAD Studio\Projects\Plugins\Example2\PluginAPI\Headers\PluginAPI.tlb" -C -P -I "-Dc:\Users\Александр\Documents\RAD Studio\Projects\Plugins\Example2\PluginAPI\Headers\Test\" -Pt+
// ************************************************************************ //
{$TYPEDADDRESS OFF} // Unit must be compiled without type-checked pointers.
{$WARN SYMBOL_PLATFORM OFF}
{$WRITEABLECONST ON}
{$VARPROPSETTER ON}
{$ALIGN 4}
interface
uses Windows, ActiveX, Classes, Graphics, StdVCL, Variants;
// *********************************************************************//
// GUIDS declared in the TypeLibrary. Following prefixes are used:
// Type Libraries : LIBID_xxxx
// CoClasses : CLASS_xxxx
// DISPInterfaces : DIID_xxxx
// Non-DISP interfaces: IID_xxxx
// *********************************************************************//
const
// TypeLibrary Major and minor versions
PluginAPIMajorVersion = 1;
PluginAPIMinorVersion = 0;
LIBID_PluginAPI: TGUID = '{F156F71A-758A-40E2-A34E-50187B8ED7B9}';
IID_IPluginInfo: TGUID = '{631B96BB-1E7E-407D-83F1-5C673D2B5A15}';
IID_ICoreInfo: TGUID = '{3BAA3534-5422-42B9-BDEA-1CE1037295B3}';
type
// *********************************************************************//
// Forward declaration of types defined in TypeLibrary
// *********************************************************************//
IPluginInfo = interface;
ICoreInfo = interface;
// *********************************************************************//
// Interface: IPluginInfo
// Flags: (0)
// GUID: {631B96BB-1E7E-407D-83F1-5C673D2B5A15}
// *********************************************************************//
IPluginInfo = interface(IUnknown)
['{631B96BB-1E7E-407D-83F1-5C673D2B5A15}']
function Get_ID: TGUID; safecall;
function Get_Name: WideString; safecall;
function Get_Version: WideString; safecall;
property ID: TGUID read Get_ID;
property Name: WideString read Get_Name;
property Version: WideString read Get_Version;
end;
// *********************************************************************//
// Interface: ICoreInfo
// Flags: (0)
// GUID: {3BAA3534-5422-42B9-BDEA-1CE1037295B3}
// *********************************************************************//
ICoreInfo = interface(IUnknown)
['{3BAA3534-5422-42B9-BDEA-1CE1037295B3}']
function Get_Version: Integer; safecall;
property Version: Integer read Get_Version;
end;
implementation
uses ComObj;
end.
Тут довольно много воды (к примеру, можно подчистить списки uses и удалить к чертям комментарии), но сам код практически дословно повторяет наш исходный вариант. Что и требовалось получить.Генерация заголовочников для Visual Studio C++
Итак, это был вариант для Delphi и C++ Builder. Чуть более интересно - Visual Studio C++. Во-первых, вам понадобится сама студия. У Microsoft есть обрезанный бесплатный вариант Visual Studio Express - его будет достаточно. Взять его можно тут. Только убедитесь, что берёте именно C++, а не Phone, Basic или C#. Обратите внимание, что по умолчанию при загрузке сайт предлагает установить Trial версии Professional - убедитесь, что вы щёлкните по второму варианту (установка версии Express). Скачается небольшой web-установщик - запускайте его и ставьте. Далее ничего необычного нет.Я думаю, что импортировать TLB можно и в самой студии, но т.к. я в ней не силён, то мне было проще работать через утилиты командной строки. Если вы знаете другой способ генерации заголовочников по TBL в Visual Studio - ради бога, используйте его. Я же установил Platform SDK (сейчас он называется Windows SDK), который взял тут. Это тоже веб-установщик и устанавливается он как обычно. После этого в SDK нас интересуют утилиты. Во-первых, там есть утилитка OleView.exe (C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\OleView.Exe). Она может открыть .tlb-файл и показать, что там внутри - т.е. это просмотрщик (примечание: если вдруг он почему-то не работает, то скорее всего надо зарегистрировать библиотеку IViewers.Dll в той же папке). Плюс, он умеет экспортировать библиотеку типов в .idl, .h и .c. Вот этим и нужно воспользоваться. Открываете свою библиотеку типов и экспортируете её в заголовочники.
По правде сказать, в .idl он у меня сконвертировал, а вот в .h и .c - нет. Мелькнувшее окошко консоли ругнулось на то, что он не может найти cl.exe - это препроцессор C, который идёт в комплекте с Visual Studio. Я не уверен, почему это происходит у меня, заставить его работать я так и не смог (ладно, особо упорно я и не старался). Поэтому я пошёл другим путём: я создал .bat файл с таким содержанием:
@echo off call "c:\Program Files\Microsoft Visual Studio 10.0\VC\bin\vcvars32.bat" "c:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\Midl.Exe" "c:\Test\PluginAPI.IDL" /out "C:\Test\Out" /h "PluginAPI.hpp"(где IDL файл - это текстовое представление библиотеки типов; я не пробовал с файлами, генерируемыми Delphi; этот IDL файл - тот, что мне выдал OLEView).
MIDL компилятор по IDL файлу сгенерировал мне PluginAPI.hpp и PluginAPI_i.c - это и есть нужные нам файлы.
Выводы по ведению заголовочных файлов
Итак, подводя черту, у вас есть три способа ведения заголовочников:- Просто писать .pas файл и при необходимости вручную перевести его на другие языки.
- Создать библиотеку типов и редактировать её в редакторе Delphi (вообще говоря, для этого можно использовать любой редактор TLB, а не только Delphi). В конце автоматически получить .tlb и комплект файлов для Delphi, C++ Builder, Visual Studio C++, а также текстовое описание (idl/ridl).
- Создать idl или ridl файл. Писать текст - это быстрее и удобнее, чем использовать редактор, но нужно знать язык описания интерфейсов (IDL). В конце скомпилировать файл в .tlb и получить комплект сопроводительных файлов.
Давайте я ещё просуммирую действия, которые вам необходимо делать при работе с TLB:
- Помещайте .tlb файлы в PluginsAPI\Headers. .tlb файл - это главный и основной файл. Его нужно распространять обязательно. Все остальные пункты ниже - необязательные. Их можно не делать. Но если вы их сделаете, то это будет дополнительное удобство.
- В папку PluginsAPI\Headers положите файлы .idl или .ridl.
- Получите комплект заголовочников для Delphi, C++ Builder и Visual Studio C++, как указано выше (tlibimp для Delphi и C++ Builder и oleview/midl для Visual Studio).
- В папке PluginAPI\Headers создайте подпапки Delphi, Builder и VC.
- В папку Delphi положите XYZ_TLB.pas
- В папку Builder положите XYZ_TLB.h и XYZ_TLB.cpp
- В папку VC положите XYZ.hpp и XYZ_i.c
Далее мы к этому возвращаться не будем.
Что касается файла PluginAPI.pas с двумя типами данных и двумя константами - вы можете описать его словами в документации, плюс приложить вот это определение на C:
//-- type declarations ------------------------------------------------------- typedef HRESULT __stdcall (*TInitPluginFunc)(IUnknown *ACore, IUnknown **Result); typedef HRESULT __stdcall (*TDonePluginFunc)(void); //-- var, const, procedure --------------------------------------------------- #define SPluginInitFuncName L"87D4EDFB420343F2976EB3CF4DB7C224" #define SPluginDoneFuncName L"87D4EDFB420343F2976EB3CF4DB7C224_done" #define SPluginExt L".MyAppPlugin"Этого будет достаточно, чтобы разработчики плагинов поняли бы, о чём идёт речь, и что нужно делать. Конечно, разработчикам на других языках (не C++ и не Delphi) придётся перевести эти строки на свой язык самостоятельно, но перевод пары строк - это полный пустяк. Основную массу заголовочников транслировать не нужно - для этого есть импорт библиотеки типов.
Далее, в документации нужно особо уточнить, что ваши плагины и ядро - это не COM-объекты. Просто самое типичное использование библиотеки типов - это COM. Т.е. если кто-то видит библиотеку типов, он может автоматически потянуться её импортировать и вызывать
CoCreateInstance для создания объектов. Только это не будет работать в вашем случае. Потому что у вас нет никаких COM-объектов, и уж тем более их никто глобально не регистрировал, чтобы их можно было получить через CoCreateInstance. Поэтому нужно чётко указать: это не COM, библиотека типов служит лишь для авто-генерации заголовочных файлов для вашего любимого языка программирования, а чтобы сделать плагин - нужно создать DLL с двумя вот такими (см. C код выше) функциями. Первая из которых работает с интерфейсами. Какими интерфейсами? А вот они как раз описаны в библиотеке типов. Как-то так.
Начала реализации функциональности
Итак, вернёмся же к нашим баранам. Я напомню, что мы решили дать возможность плагину и ядру узнать информацию друг о друге и для этого мы ввели такие интерфейсы:type
IPluginInfo = interface
['{F96EFCD5-17F6-4C17-956C-219F23C51AF9}']
// private
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;
ICoreInfo = interface
['{525797CF-89EB-4226-8BFE-4E3DD2123E13}']
// private
function GetVersion: Integer; safecall;
// public
property Version: Integer read GetVersion;
end;
Я написал их от балды - вы можете использовать любую другую структуру, даже один интерфейс вместо двух (скажем, IVersionInfo). Тут ничего жёстко не определено, работает ваша фантазия - как захотите, так и сделаете. Я бы только рекомендовал предусмотреть идентификацию плагинов по уникальному ID. В качестве такого хорошо подходит GUID. А вот имя файла - не достаточно (плагин можно переименовать).В любом случае, теперь их надо использовать в коде. Начнём с плагина. Возьмём пустой плагин из предыдущего примера и дополним его новым кодом следующим образом:
library Plugin1;
uses
SysUtils,
Classes,
PluginAPI in 'PluginAPI\Headers\PluginAPI.pas',
PluginAPI_TLB in 'PluginAPI\Headers\Delphi\PluginAPI_TLB.pas';
{$R *.res}
const
SPluginID: TGUID = '{C147E26F-3933-4AE2-A4A8-2A55BC9DABD2}';
SPluginName = 'Example plugin';
SPluginVersion = '1.0.0.0';
type
TPlugin = class(TInterfacedObject, IUnknown, IPluginInfo)
private
FCore: ICoreInfo;
protected
function Get_ID: TGUID; safecall;
function Get_Name: WideString; safecall;
function Get_Version: WideString; safecall;
public
constructor Create(const ACore: IInterface);
end;
{ TPlugin }
constructor TPlugin.Create(const ACore: IInterface);
begin
inherited Create;
if not Supports(ACore, ICoreInfo, FCore) then
Assert(False);
Assert(FCore.Version >= 1);
end;
function TPlugin.Get_ID: TGUID;
begin
Result := SPluginID;
end;
function TPlugin.Get_Name: WideString;
begin
Result := SPluginName;
end;
function TPlugin.Get_Version: WideString;
begin
Result := SPluginVersion;
end;
// _________________________________________________________________
function Init(const ACore: IInterface): IInterface; safecall;
begin
Result := TPlugin.Create(ACore);
end;
procedure Done; safecall;
begin
end;
exports
Init name SPluginInitFuncName,
Done name SPluginDoneFuncName;
end.
Итак, плагин реализует интерфейс IPluginInfo - для этого нам понадобился объект. Реализация методов в данном случае тривиальна - мы просто возвращаем константы. Конструктор объекта получает из переданного нам интерфейса интерфейс ICoreInfo и сохраняет его в поле FCore. Это не нужно в этом примере (нигде ниже FCore не используется), но может быть полезно в общем случае. После получения интерфейса, мы проверяем, что версия ядра - 1 или выше. Конечно, это всегда будет так. Глупая проверка :)Кстати, я выбрал строку для версии плагина, подразумевал, что единственное, что с ней можно сделать - показать в интерфейсе пользователя. Поэтому строка. С другой стороны, версия ядра не показывается (плагином) в UI, но нужна ему для проверки, есть ли у ядра интересующие плагин возможности. Вот почему это число - для простой проверки. К примеру, если в будущем мы расширим наше ядро новыми возможностями, то плагин при загрузке может проверить, что версия ядра не ниже двух - и отказаться грузиться в старой версии программы (потому что там нет новых возможностей, которые нужны плагину). Ладно, в любом случае сейчас это никак не используется, потому что у нас всего одна версия. Но это пример на будущее.
Теперь, что касается ядра.
unit PluginManager;
interface
...
IPlugin = interface
// protected
...
function GetID: TGUID;
function GetName: String;
function GetVersion: String;
// public
...
property ID: TGUID read GetID;
property Name: String read GetName;
property Version: String read GetVersion;
end;
IPluginManager = interface
// protected
...
// public
...
procedure SetVersion(const AVersion: Integer);
end;
...
implementation
uses
Registry,
PluginAPI,
PluginAPI_TLB;
...
type
TPluginManager = class(TInterfacedObject, IUnknown, IPluginManager, ICoreInfo)
private
...
FVersion: Integer;
protected
...
procedure SetVersion(const AVersion: Integer);
function Get_Version: Integer; safecall;
public
...
end;
TPlugin = class(TInterfacedObject, IPlugin)
private
...
FInfoRetrieved: Boolean;
FID: TGUID;
FName: String;
FVersion: String;
procedure GetInfo;
protected
...
function GetID: TGUID;
function GetName: String;
function GetVersion: String;
public
...
end;
{ TPluginManager }
constructor TPluginManager.Create;
begin
...
SetVersion(1);
end;
...
procedure TPluginManager.SetVersion(const AVersion: Integer);
begin
FVersion := AVersion;
end;
function TPluginManager.Get_Version: Integer;
begin
Result := FVersion;
end;
...
procedure TPlugin.GetInfo;
var
Info: IPluginInfo;
begin
if FInfoRetrieved then
Exit;
if Supports(FPlugin, IPluginInfo, Info) then
begin
FID := Info.ID;
FName := Info.Name;
FVersion := Info.Version;
FInfoRetrieved := True;
end;
end;
function TPlugin.GetID: TGUID;
begin
GetInfo;
Result := FID;
end;
function TPlugin.GetName: String;
begin
GetInfo;
Result := FName;
end;
function TPlugin.GetVersion: String;
begin
GetInfo;
Result := FVersion;
end;
Изменения в основной программе (показываем имя плагина и его версию вместо имени файла):
procedure TMainForm.UpdatePluginsList;
var
X: Integer;
begin
ListBox1.Items.BeginUpdate;
try
ListBox1.Items.Clear;
for X := 0 to Plugins.Count - 1 do
ListBox1.Items.Add(IntToStr(Plugins[X].Index) + ': ' + Plugins[X].Name + ' (' + Plugins[X].Version + ')');
finally
ListBox1.Items.EndUpdate;
end;
end;
Теперь пояснения по коду. Здесь я практически продублировал интерфейс IPluginInfo из заголовочника в IPlugin. Плюсы: вы посмотрите, как красиво выглядит код в основной программе! Можно подумать мы с компонентами работаем, а не с плагинами. Минусы: нам пришлось писать кучу кода-обёртки, который мне сейчас придётся объяснять.Подобное дублирование делать необязательно и в следующий раз я покажу другой пример.
Сам код для информации о плагине состоит из трёх однотипных методов (
GetID, GetName, GetVersion). По сути, здесь показан пример получения информации по запросу: мы не вызываем интерфейс IPluginInfo до тех пор, пока кто-то не обратится к одному из свойств плагина (ID, Name, Version). Это можно было бы делать и по другому: запросить информацию в конструкторе TPlugin. Кроме того, этот код показывает пример кэширования информации: когда мы получили информацию, мы сохранили её в полях объекта (кэше). Если она потребуется нам повторно - нам не придётся дёргать плагин снова, чтобы её получить. Этот подход отлично подходит для статической информации (которая не меняется), но мало пригоден для динамической.Что касается интерфейса
ICoreInfo, то его реализует менеджер плагинов. Тут есть такой вопрос: вообще-то менеджер плагинов не в курсе, что за версия у ядра. Это знает ядро, а не менеджер плагинов. Поэтому мы по умолчанию ставим версию 1. А ядру даёт метод SetVersion, с помощью которого оно сможет установить (при необходимости) номер версии 2, 3 и так далее. Обратите внимание, что хотя методы SetVersion и Get_Version и выглядят парой (и, по сути, это методы-акцессоры Get и Set для свойства "Версия"), они имеют существенное отличие: метод Get предназначен для плагинов, а метод Set - для ядра (это видно по соглашению вызова: safecall и register). Метод SetVersion плагин вызвать не может!В связи с тем, что у нас появился интерфейс ядра, который мы передаём плагину, у нас возникла небольшая проблема: как вы помните из сказанного выше, плагины не выгружаются программой явно, они выгружаются при удалении менеджера плагинов. А наш менеджер плагинов удаляется тогда, когда все ссылки на него будут уничтожены. Последняя из которых (ссылка) обнуляется в finalization модуля менеджера. А проблема тут вот в чём: мы ведь передали интерфейс менеджера плагинам. Это означает, что обнуление ссылки в секции finalization не будет последним: ведь ссылку на менеджер держат плагины, которые... не выгружаются потому, что менеджер ещё жив. Вот и получается круговая ссылка: менеджер ссылается на плагины, а плагины - на менеджер. И никто из них выгружаться не хочет, потому что есть активные ссылки. Вот поэтому нам нужно внести изменение: в секции finalization нужно явно скомандовать менеджеру плагинов выгрузить все плагины. Сделать это можно просто финализировав массив
FItems - всё остальное за нас сделает механизм учёта ссылок. Финализация массива уничтожит каждый его элемент, каждый элемент - это плагин. Т.е. каждый плагин будет выгружен. Выгрузка плагина отпустит ссылку на менеджер плагинов, после чего обнуление ссылки на менеджер плагинов в секции finalization уничтожит его - и все будут довольны. Код я тут выписывать не буду - там всего несколько строк.На этом пора бы подвести черту основному введению в разработку системы плагинов на Delphi. Код к этому моменту можно скачать тут. Нужно отметить, что весь этот код не имеет специфики, он универсален. А потому он может использоваться как база для ваших программ. В следующей статье мы начнём делать уже конкретные вещи и реально полезные плагины. Поэтому код в следующей части не может быть использован в вашем коде, а служит только лишь примером.
Примерный список вопросов для рассмотрения в следующий раз: обработка ошибок, функциональность в плагине, обращение из плагина к внутренностям ядра, общение плагинов между собой, показ плагинами UI, обратные вызовы (инициация действий плагином, а не ядром).







34 комментарий(ев):
Спасибо. Очень познавательно.
А когда можно ожидать продолжение?
Вот, очень хорошая статья! Все чётко =) Жду продолжения! Только пустые отступы удалите из вставок кода(в конце обычно), пример "Отключение плагинов".
В конце концов пришел к похожему решению.
Здоров если она поможет кому-то избежать многих ошибок.
>>> А когда можно ожидать продолжение?
Ориентировочно - на следующих выходных. Возможно - позже.
>>> Только пустые отступы удалите из вставок кода(в конце обычно), пример "Отключение плагинов".
Отступов не увидел. Возможно, косяк браузера? Я смотрел в Chrome и IE.
Может я чего не доглядел, но по-моему в TPlugin.Create вы передаете Self в функцию FInit, а интерфейс ICoreInfo у вас реализует TPluginManager.
Да, изначально я передал туда Self - как просто "что-то", поскольку никакой реализации ещё не было. Потом, когда появилась функциональность (ICoreInfo) - я стал передавать FManager (т.к. интерфейс реализует именно он). Это видно в исходниках, но тут я, похоже, просто забыл отметить.
Наверное, стоит просто изменить Self на FManager в тексте, чтобы не путать.
Функция TPluginManager CanLoad скорее всего должна называться CantLoad
Да, точно. Изначально я её назвал Banned, а потом добавил проверку на уже загруженные плагины. Перед самым выкладыванием решил переименовать - и ошибся.
Ога, тоже пришел к этой же схеме, ddl+interface, однако, использую вместо safecall, ибо использую свои коды ошибок, а safecall не всегда дает мне это сделать. На Королевстве Delphi как то писал про это.. Вот даже багрепорт автора блога нашел :) http://qc.embarcadero.com/wc/qcmain.aspx?d=81725
Так как применяю вариант с stdcall, то использовать интерфейсы неудобно, приходится использовать обертки, но есть и положительный момент, в обертках проверяются коды ошибок и вызываются нормальные исключения с разделением по типу ошибки, а не безликие ESafeCallException или EOleException. Да, кода много получается, пришлось сделать инструмент по генерации года, на входе имя (Test) и желаемые процедуры/функции, на выходе: интерфейс (ITest), клас реализации (TTest), клас обертки (TITest), ну и реализации классов begin-end try-except-end и тп.
Но это, все равно морока, было бы интересно узнать про замену оберток.
Спасибо, очень интересно.
С самого начала возник вопрос, почему вместо массива плагинов не использовать TInterfaceList?
Тогда не придется писать добавление и удаление. TPluginManager.UnloadPlugin вообще выглядит очень подозрительно, в первый момент я даже подумал что он ошибочен.
И второе, если все на базе COM, то почему бы сразу не взять все механизмы, ту же обработку ошибок, COM категории и так далее?
>>> Но это, все равно морока, было бы интересно узнать про замену оберток
Обёртки нужны в любом случае, вне зависимости от выбранной схемы реализации плагинов. Исключение не должно выйти за пределы возбудившего его модуля.
Другое дело, кто эти обёртки делает - вы или среда.
Я ещё поговорю про обработку ошибок.
Кстати, мне кажется вы отчёт неверно трактуете. Я поясню логику в следующей статье.
>>> С самого начала возник вопрос, почему вместо массива плагинов не использовать TInterfaceList?
Да никаких причин нет, можно использовать.
>>> если все на базе COM, то почему бы сразу не взять все механизмы, ту же обработку ошибок, COM категории и так далее?
Я от COM взял только три вещи:
- Интерфейсы
- BSTR/WideSrtring
- Библиотеку типов
Это всё - достаточно легковесные вещи, которые от COM, считай, не зависят, а используются повсеместно. Поэтому их легко понять, с ними легко работать. И кривая для изучения будет плавной.
Если же делать COM-объекты, то нужно учить именно COM: threading model, apartments, masrshaling, in-proc/out-proc, ... кучу вещей. Понятно, экспертом быть не нужно, но надо будет иметь обо всём этом какое-то представление, чтобы понимать что ты делаешь при создании и регистрации объектов. Согласись, что это существенно сложнее чем интерфейсы, строки и заголовочники.
Само собой, если делать приложение размера MS Word, то COM тут практически без альтернатив. Но для меньших программ - какие плюсы компенсируют сложность COM для изучения?
Это личное мнение - понятно, могу ошибаться.
>>> С самого начала возник вопрос, почему вместо массива плагинов не использовать TInterfaceList?
Да, почему я его не использовал: у меня всего два действия: добавить и удалить. Очень просто. Собственно мне показалось, что это немного и можно и написать. С учётом, что никаких других возможностей я не использую. Хотя с отдельным классом, конечно, правильнее.
>>> Это всё - достаточно легковесные вещи, которые от COM, считай, не зависят, а используются повсеместно. Поэтому их легко понять, с ними легко работать. И кривая для изучения будет плавной.
Не факт. В реализации того же COM в Delphi уже многое предусмотрено, та же обработка ошибок, к примеру. Да и статей куча, достаточно не описывать все досконально, а просто дать ссылки.
А здесь все вручную придется делать. SafeCallException перекрываться будет?
На мой взгляд как раз в этом варианте потребуется более глубокое понимание механизмов Delphi, что, впрочем, полезно.
COM же недаром вводит вещи, которые требуют изучения. Вот планируется UI, а как будет решаться вопрос с тем, что вызов к этому UI может идти из разных приложений, из разных потоков? Взять хотя бы случай когда запущены два экземпляра приложения. То же самое касается и обратных вызовов. А если захочется плагин-синглетон? Например, для доступа к COM-портам, к оборудованию.
Вообще с нетерпением жду следующей части :)
У варианта системы плагинов с библиотекой типов есть еще то преимущество, что при необходимости плагин можно вынести в отдельный процесс, COM сможет выполнить маршалинг.
Например, у меня возникла такая ситуация: по мере развития программы появилось множество плагинов от разных авторов, причем самого разного качества. Некоторые плагины портят память, в некоторых есть проблемы с загрузкой/выгрузкой в DllMain других DLL и т.п. В основном это все редко используемые плагины.
Чтобы повысить стабильность работы приложения, менеджер плагинов может запускать некоторые плагины в отдельных процессах.
Romkin, почему-то в Delphi интерфейсы используются повсеместно совершенно без ссылок на COM. К чему-бы это? Потому что интерфейс - это просто запись (record) с указателями на методы.
BSTR (который в WideString) аналогично совершенно спокойно используется в WinAPI (который новый), снова без всяких посылок к COM (в функциях).
HRESULT (который safecall) - это практически де-факто (современный) стандарт обработки ошибок на кодах ошибок. Используется, опять же, повсеместно - не только в WinAPI (и снова - в функциях), но и API сторонних разработчиков.
Понятно, что вместе с COM это всё безусловно тоже используется, но я привёл примеры того, что мой подход - это не что-то из рамок вон выходящее, а вполне себе обычная ситуация.
(Насчёт библиотеки типов примеров привести не могу.)
Почему в таких случаях не выбирают COM? Потому что это больше, чем требуется в данном конкретном случае. Вот почему не выбрал его здесь и я.
Разумеется, сколько людей - столько и мнений. И кто-то другой вполне может выбрать для своей реализации COM - ради бога. Я совершенно согласен, что если ты попробуешь предусмотреть всё-всё-всё, то просто переизобретёшь COM (и тогда тебе изначально надо было бы его использовать). Но эта статья - про другой подход. Возможно, когда-нибудь много позже я напишу и про другие подходы (но - навряд ли). А кому интересно сейчас - я ссылочки в начале статьи привёл.
Основная причина, как я уже сказал, - кривая обучения. Если ты делаешь систему на базе COM, тебе мало того, что придётся объяснять всё то, что я уже тут сказал, так ещё и объяснять новые вещи, связанные с COM, про которые я не говорил. И мне кажется, что не все из них тривиальны.
В моей же статье из нетривиальностей разве что библиотека типов. WideString ведёт себя как обычная строка. Интерфейсы - это вообще родной механизм под Delphi (к примеру, интерфейсы не существуют в C++, там они представляются абстрактными классами). Вроде у меня ничего особо сложного нет.
Почему это имеет значение? Эта статья предполагается для начинающих. Основная её цель - дать пинок в нужную сторону. Потому что лично меня задолбало видеть доморощенные API в системах, построенные с использованием PChar, самоделкиных функций управления памятью и самоизобретёнными же способами обработки ошибок. И это в 2011-м году!
Эти люди не понимают, что те вещи, которые они обычно замечают в WinAPI (PChar-ы и т.п.), находятся там только потому, что этот API был сделан более 20 лет назад - он тянется из 16-битных Windows, а некоторый - ещё из OS\2! Определённо, за 20 лет правила изменились. Вам не нужно думать об этом, этом или этом и сотне других вещей. Поэтому, эта статья - попытка что-то с этим сделать.
Маленькая оптимизация:
1. Пусть библиотека плагинов экспортирует одну функцию, которая возвращает одну интерфейсную ссылку, экспортирующую фабрику плагинов.
2. Тогда функции для явной зачистки писать не надо - достаточно специфицировать для ядра удержание ссылки на фабрику до выгрузки, а финализацию либы выполнять в деструкторе фабрики.
3. Появляется возможность размещать плагины буквально где угодно - загрузчику достаточно уметь получать ссылку на фабрику, а каким способом - всего остального кода не касается. Например, помещение плагинов в один исполняемый файл полезно для предотвращения излишних модификаций конечными пользователями и облегчения тестирования и отладки (утечки памяти много проще искать для одного файла и Delphi 2007 очень любит падать при отладке dll и bpl)
Leo Bonart, вы стали на путь изобретения COM. Сейчас "оптимизация" на фабрику класса... потом независимое размещение в дериктории, и поиск по записи в реестре...
На мой взгляд иногда можно обойтись и без такой оптимизации. Нужно просто определить какие возможности действительно могут понадобится. Эдак можно до абсурда дойти и изобрести DCOM, изобрести dot.net... И дело не в том что оптимизация плохая, да, она гибкая, но поддержка такой гибкой оптимизации (которая возможно никогда не понадобится) требует документации, требует дополнительной реализации, дополнительного тестирования.
Взять предложенную вами же реализацию через фабрики. Представим ситуацию, мы хотим плагины обновлять по интернету без перезапуска приложения, прямо на лету, а между плагинами есть допустим связи. Нужно разработать систему по выгрузке, и восстановления связей (fixup), а в случае с фабрикой нужно обучать фабрику каждого класса делать такой "финт". Лишняя реализация в каждый плагин.
Поэтому важно выбрать именно тот уровень гибкости архитектуры, который нужен именно в нашем случае и не переусердствовать.
MrShoor,
COM мне ни к чему: он для плагинов уровня системы с его регистрацией в глобальном реестре, внешней автоматизации с маршалингом между процессами, различными потоковыми моделями и т.п. и т.д.
Мои задачи - архитектура конкретного приложения на уровне разработки и сопровождения. И здесь разделение физики (загрузчик) и логики (плагины) очень помогает, причем в первую очередь для целей тестирования.
И да, я это реализовал и оно используется в практике коммерческой разработки моего работодателя.
Leo Bonart, я ни в коем случае не говорю что ваш подход плох. Для ваших задач возможно так сделать удобнее и логичнее. Но если задача простая, то не обязательно её усложнять. У меня например была задача: простейшая система плагинов, но чтобы плагины могли обновлятся и перезагружаться "нагорячую", без перезапуска приложения. Мне не нужна была гибкость, и меня вполне устраивала и устраивает система 1 модуль = 1 плагин. С фабрикой же будет 1 модуль = 1 фабрика = N плагинов. В этом случае мне нужно было бы предусмотреть выгрузку модуля через фабрику, а значит обучить фабрику выгружать созданные ей плагины, а ведь ссылки на IPlugin будут существовать за пределами модулей. И в данном случае фабрика будет только мешать, давая мне гибкость которая абсолютно мне не нужна.
Поэтому прежде чем вводить какой-либо механизм, нужно подумать нужен ли он вам конкретно, его ведь еще обслуживать надо будет, ну и не переуседствовать с гибкостью :)
MrShoor ,
"Горячая" выгрузка - это очень жестко.
У меня важно было другое - чтобы плагины минимально зависели друг от друга и способа загрузки с одной стороны и могли неограниченно пользоваться функционалом остальных плагинов - с другой, т.е. чистый конструктор лего.
В итоге дошло до выражения зависимостей через интерфейсы, а ядро определяло порядок активации и вычисляло циклические зависимости.
Горячей выгрузке, кстати, предлагаемая мной оптимизация не противоречит - просто добавляет новые обязанности, причем не к загрузчику - он тупой, а к ядру - оно обязано уметь выдергивать логический плагин из работающей системы на лету, что в любом случае тяжко.
На самом деле это только с виду тяжко. На практике все довольно просто реализуемо (добавлением пары методов в IPlugins), если интересно - могу рассказать. Код ядра по выдергиванию тоже довольно простой.
Интересно.
Для меня главной проблемой выгрузки было следующее: если функционалом плагина пользуются другие плагины, то как безболезненно выдернуть его из живой системы?
Для успешной выгрузки удобнее всего отвественными за "перезагрузку" плагинов сделать сами плагины. Вообще всю ответственность за поддержание "связей" возлагать на сами плагины.
Просто в плагин добавляем пару методов:
IPlugin = interface
//.... некоторые методы
procedure Unload(plugin: IPlugin); safecall;
procedure FixUp(plugin: IPlugin); safecall;
end;
Менеджер перед выгрузкой плагина вызывает Unload у каждого загруженного плагина. Загруженный плагин должен сам определить использует ли он этот плагин или нет, если использует - освободить все ссылки на плагин. После менеджер может выгрузить dll с плагином, заменить, подгрузить обновленный плагин и вызывать FixUp передав новый плагин всем загруженным плагинам. Кому надо - получает ссылки на плагин.
Проблемы доступа к плагину в момент перезагрузки я решил через критические секции. Обернул перезагрузку плагина в коде менеджера в критическую секцию, в реализации плагинов обращаюсь к плагинам только экранированно. Таким образом никаких проблем с перезагрузкой не возникает.
можно поступить проще: на каждый плагин создать объект-переходник с тем же интефейсом(IPlugin),
он единственный будет общаться с плагином напрямую, а всем остальным модулям/плагинам отдавать ссылку на него
тогда Unload/FixUp не нужны
можно поступить проще: на каждый плагин создать объект-переходник с тем же интефейсом(IPlugin)
При взаимодействии между плагинами как правило нужно больше чем просто интерфейс IPlugin. Например у меня есть IPopupPlugin через который осуществляется работа со всплывающими окошками, с кучей методов и свойств. На каждый плагин писать либо свой переходник, либо придумывать "хак" на ассемблерном уровне по маршалингу.
>>Для успешной выгрузки удобнее всего ответственными за "перезагрузку" плагинов сделать сами плагины.
Такое решение упрощает разработку ядра, но сильно затрудняет разработку самих плагинов. Код повсеместно становится жестко зависимым от архитектуры приложения, явное знание о плагинной структуре и сильные зависимости от конкретных версий конкретных плагинов (причем с возможностью исчезновения этих плагинов в любой момент) затрудняют как добавление функционала, так и его модификацию, и даже рефакторинг.
Передо мной стояла задача с прямо противоположными приоритетами - превратить несколько монолитных приложений в модульные с общим ядром и пересекающимся составом плагинов с минимальными модификация существующего кода. Позднее были добавлены требования ослабления зависимости между плагинами, дабы имел значение только реализованный функционал, а сам плагин без ущерба можно было заменить аналогичным (или несколькими аналогичными).
>>Такое решение упрощает разработку ядра, но сильно затрудняет разработку самих плагинов.
Честно говоря не сильно усложняет. Если плагин работает в контексте основного потока то вообще никакой сложности. Всю реализацию загрузки/выгрузки можно впихнуть в сдк.
Если же плагин работает с несколькими потоками, то можно использовать Lock/Unlock в том же сдк при обращении к другим плагинам.
>>> можно поступить проще: на каждый плагин создать объект-переходник с тем же интефейсом(IPlugin)
На словах мне нравится, но писанины будет... ого-го.
А вообще в большинстве программ не перезапуск - это не проблема.
Хотя и тут радужно не получается.
Плагин не может сделать:
AnotherPlugin.DoSomething(Self)
а вынужден делать:
AnotherPlugin.DoSomething(ICore.GetStub(Self))
А в первом варианте получим трудноуловимые проблемы.
Я может чего-то не понял, но вот этот код меня смущает.
if Length(FItems) >= FCount then // "Capacity"
SetLength(FItems, Length(FItems) + 64);
Не перепутан ли тут знак > со знаком < ?
Ага, перепутан.
Поправил.
ЗЫ: Посмотрел исходники там тоже. :)
Лениво править 7 демо-примеров... Исправление вставлю в 8-й.
Кстати, насколько я помню, это не единственный найденный и поправленный косяк - так что для практики лучше смотреть последний вариант примеров.
Молодец, Тверь.
Такие отличные статьи редко встретишь.
Отправить комментарий
Можно использовать некоторые HTML-теги, например:
<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>
Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).
Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.
Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.