25 декабря 2011 г.

Разработка системы плагинов в Delphi

Эта статья - продолжение старой серии.

Напомню, что те статьи, несмотря на некоторый практический выхлоп в конце, всё же не очень-то освещали тему плагинов, а представляли собой копание во внутренностях работы DLL и пакетов Delphi. Было решено эту серию закончить - именно как серию о плагинах.

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

В общем, все эти факторы в итоге перевесили мою лень и я решил написать нормальную статью про плагины.

Примечание: я был бы очень благодарен, если кто-нибудь со знанием Visual Studio (C++) просмотрел бы раздел 8 (и особенно - касательно генерации заголовочников для Visual Studio C++) на предмет моих ошибок.

Оглавление

  1. Что у нас есть
  2. Что хотим получить
  3. Как пойдём
  4. Основные понятия
  5. Базовый набор правил и соглашений
  6. Структура файлов и папок
  7. Менеджер плагинов
  8. API

Что у нас есть

Напомню основные способы реализации систем плагинов:

Что хотим получить

Мы хотим разработать систему плагинов - т.е. набор правил, по которым сторонние разработчики (т.е. не авторы программы) могли бы писать функциональность, встраивающуюся в основную программу и как-бы являющуюся её частью. При этом хочется, чтобы плагины можно было бы писать на любом (нативном) языке программирования, а не только 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.

Кроме этих правил есть ещё множество негласных общих правил программирования, которые не специфичны для системы плагинов, а должны выполняться всегда. Вот некоторые из них (составлены Реймондом Ченом):
  • Всё, что не определено - неопределённо. Это может звучать как тавтология, но, на самом деле, это очень полезная тавтология. Многие правила ниже являются просто частным случаем этого правила.
  • Все параметры должны быть допустимы. Контракт функции может применяться только если вызывающий выполнил все свои обязательства, и одно из них - все параметры должны быть тем, чем они заявлены. Это частный случай правила "Всё, что не определено - неопределённо":
    • Указатели должны быть не равны nil, если только противное не указано явно.
    • Указатели указывают на то, на что они должны указывать. Если функция принимает указатель на CRITICAL_SECTION, то вам нужно передавать указатель на корректную CRITICAL_SECTION.
    • Указатели корректно выровнены. Выравнивание указателей - это фундаментальное требование архитектуры, хотя многие люди и пренебрегают им из-за всепрощающей архитектуры x86.
    • Вызывающий имеет право использовать память, на которую ему указывают. Это означает отсутствие вещей вроде указателей на освобождённую память или на память, над которой у вызывающего нет контроля.
    • Все буфера должны быть доступны и корректны вплоть до задекларированного или подразумеваемого размера. Если вы передаёте указатель на буфер и говорите, что он 10 байтов в длину, то буфер действительно должен иметь размер в 10 байт.
    • Описатели должны ссылаться на корректные объекты, которые не были уничтожены. Если функция хочет описатель окна, то вам нужно дать ей описатель настоящего окна.
  • Все параметры стабильны:
    • Вы не можете менять параметр, пока работает функция, в которую он передан.
    • Если вы передали указатель, то память, на которую он указывает, не должна модифицироваться другим потоком в течение вызова функции.
    • Вы также не можете освобождать эту память во время вызова функции.
  • Передаётся правильное число параметров и в правильном соглашении вызова. Это ещё один особый случай правила "Всё, что не определено - неопределённо".
    • Слава богу, современные компиляторы отказываются передавать неверное число параметров, хотя вы будете удивлены, узнав, скольким людям удаётся обмануть компилятор, чтобы передать неверные параметры, обычно используя изощрённые приведения типов.
    • Когда вы вызываете метод объекта, то Self должен быть объектом. Опять-таки, это - то, что автоматически отслеживает современный компилятор, хотя людям, использующим COM из C (и, да, такие бывают), приходится передавать его вручную, так что иногда они лажают.
  • Время жизни параметров функций:
    • Вызываемая функция может использовать параметры во время своего выполнения.
    • Вызываемая функция не может использовать параметры после возврата управления. Конечно же, если вызывающий и вызываемый устанавливают отдельное соглашение о продлении времени жизни параметров, то применимы и такие правила:
      • Время жизни параметра, который является интерфейсом (указателем на COM объект), может быть увеличено вызовом метода IUnknown.AddRef.
      • Многие параметры передаются в функции с подразумеваемым контекстом, что они будут использованы после выхода из функции. Если это так, то гарантия продления времени жизни параметра на срок, который нужен вызываемой функции, является ответственностью вызывающего. К примеру, если вы регистрируете функцию обратного вызова (callback), то она должна быть допустима, пока вы не отмените её регистрацию.
  • Входные буфера:
      Функции разрешено читать буфер от начала до указанной или подразумеваемой границы буфера, даже если для определения результата выполнения функции требуется только часть информации из буфера.
  • Выходные буфера:
    • Выходной буфер не может пересекаться с входным буфером или любым другим выходным буфером.
    • Функции разрешено писать в выходной буфер в границах, указанными вызывающим, даже если для записи результата нужен не весь буфер.
    • Если функции нужна только часть буфера для хранения результата вызова, то содержимое неиспользованной части буфера не определено.
    • Если функция завершается неудачей и документация не указывает состояние выходных буферов при неудаче, то содержимое выходных буферов не определено. Это частный случай правила "Всё, что не определено - неопределённо".
    • Заметьте, что COM устанавливает дополнительные правила для выходных буферов. COM требует, чтобы все выходные буферы находились бы в маршаллируемом состоянии даже при завершении функции с ошибкой. Для объектов, которые требуют нетривиального маршаллинга (наиболее яркие примеры: интерфейсы и BSTR (WideString)), это означает, что выходные буфера должны быть равны nil при ошибке.
(Запомните, каждый пункт здесь является базовым правилом, а не непреложной истиной. Считайте, что перед каждым пунктом написано "Если явно не указано обратное, то...". Если вызывающий и вызываемый соглашаются на исключении из правила, то это исключение будет работать.)

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

Удобное правило для оценки того, что вы можете и не можете делать: спросите себя, "Как бы мне понравилось, если бы кто-то сделал это со мной?" (это особый случай теста "представьте, если бы это было возможным").

Структура папок и файлов

Следующий вопрос - какие файлы нам понадобятся, что нам нужно создать. Для начала заметим, что когда мы говорим о системе плагинов, то мы можем говорить с двух сторон: со стороны ядра и со стороны плагина. И ядро и плагин должны следовать контракту, который мы для них определим, но делать это они будут с разных сторон. Это означает, что нам потребуются:
  1. Общие файлы - используются и ядром и плагинами
  2. Файлы ядра (то, к чему не имеют доступа плагины)
  3. Файлы плагинов (то, к чему не имеет доступа ядро)
  4. Файлы вне системы плагинов (функциональность ядра и плагинов)

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

Итак, что будет делать менеджер плагинов? Ну, наверное для начала плагины надо бы найти и загрузить. Как это обычно делают? Есть два способа:
  1. Пользователь указывает в настройках программы, какие плагины нужно включать.
    Плюсы:
    • Пользователь может использовать плагин в любом месте.
    • Пользователь может отключать плагин из интерфейса программы, не удаляя его.
    Минусы:
    • Пользователю нужно настраивать плагины вручную.

  2. Программа загружает как плагины все файлы из предопределённой папки.
    Плюсы:
    • Пользователю ничего не надо делать.
    Минусы:
    • Чтобы удалить или добавить плагин, его нужно скопировать в папку или удалить из неё.
    • Плагин нельзя "отключить", не удалив его.

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

Итак, что должен уметь делать менеджер плагинов, чтобы реализовать какую-то из схем выше?

Загрузка одного плагина

Какую-бы схему мы ни реализовали бы - нам обязательно в любом случае потребуется функция загрузки одного плагина. Вот давайте с неё и начнём:
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 - это и есть нужные нам файлы.

Выводы по ведению заголовочных файлов

Итак, подводя черту, у вас есть три способа ведения заголовочников:
  1. Просто писать .pas файл и при необходимости вручную перевести его на другие языки.
  2. Создать библиотеку типов и редактировать её в редакторе Delphi (вообще говоря, для этого можно использовать любой редактор TLB, а не только Delphi). В конце автоматически получить .tlb и комплект файлов для Delphi, C++ Builder, Visual Studio C++, а также текстовое описание (idl/ridl).
  3. Создать idl или ridl файл. Писать текст - это быстрее и удобнее, чем использовать редактор, но нужно знать язык описания интерфейсов (IDL). В конце скомпилировать файл в .tlb и получить комплект сопроводительных файлов.
Как делать - на ваш выбор. В принципе, начать можно с п1, а затем перейти к п2 или п3, но надо понимать, что при этом вы будете дублировать свою работу (сначала вы пишете .pas, а потом ровно это же воспроизводите в библиотеке типов). Так что изначально начав работу с библиотекой типов вы избежите повторения. Тем не менее, именно по этой причине (поскольку одно тождественно второму), здесь и далее я буду говорить только про Паскаль и приводить код только Delphi модулей - подразумевая, что для других языков вы либо сделаете перевод, либо составите .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\Headers целиком (с подпапками) - это то, что вам нужно распространять среди разработчиков ваших плагинов.

Далее мы к этому возвращаться не будем.

Что касается файла 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, обратные вызовы (инициация действий плагином, а не ядром).

54 комментария :

  1. Спасибо. Очень познавательно.
    А когда можно ожидать продолжение?

    ОтветитьУдалить
  2. Вот, очень хорошая статья! Все чётко =) Жду продолжения! Только пустые отступы удалите из вставок кода(в конце обычно), пример "Отключение плагинов".

    ОтветитьУдалить
  3. В конце концов пришел к похожему решению.
    Здоров если она поможет кому-то избежать многих ошибок.

    ОтветитьУдалить
  4. >>> А когда можно ожидать продолжение?

    Ориентировочно - на следующих выходных. Возможно - позже.

    >>> Только пустые отступы удалите из вставок кода(в конце обычно), пример "Отключение плагинов".

    Отступов не увидел. Возможно, косяк браузера? Я смотрел в Chrome и IE.

    ОтветитьУдалить
  5. Может я чего не доглядел, но по-моему в TPlugin.Create вы передаете Self в функцию FInit, а интерфейс ICoreInfo у вас реализует TPluginManager.

    ОтветитьУдалить
  6. Да, изначально я передал туда Self - как просто "что-то", поскольку никакой реализации ещё не было. Потом, когда появилась функциональность (ICoreInfo) - я стал передавать FManager (т.к. интерфейс реализует именно он). Это видно в исходниках, но тут я, похоже, просто забыл отметить.

    Наверное, стоит просто изменить Self на FManager в тексте, чтобы не путать.

    ОтветитьУдалить
  7. Функция TPluginManager CanLoad скорее всего должна называться CantLoad

    ОтветитьУдалить
  8. Да, точно. Изначально я её назвал Banned, а потом добавил проверку на уже загруженные плагины. Перед самым выкладыванием решил переименовать - и ошибся.

    ОтветитьУдалить
  9. Ога, тоже пришел к этой же схеме, 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 и тп.
    Но это, все равно морока, было бы интересно узнать про замену оберток.

    ОтветитьУдалить
  10. Спасибо, очень интересно.
    С самого начала возник вопрос, почему вместо массива плагинов не использовать TInterfaceList?
    Тогда не придется писать добавление и удаление. TPluginManager.UnloadPlugin вообще выглядит очень подозрительно, в первый момент я даже подумал что он ошибочен.
    И второе, если все на базе COM, то почему бы сразу не взять все механизмы, ту же обработку ошибок, COM категории и так далее?

    ОтветитьУдалить
  11. >>> Но это, все равно морока, было бы интересно узнать про замену оберток

    Обёртки нужны в любом случае, вне зависимости от выбранной схемы реализации плагинов. Исключение не должно выйти за пределы возбудившего его модуля.

    Другое дело, кто эти обёртки делает - вы или среда.

    Я ещё поговорю про обработку ошибок.

    Кстати, мне кажется вы отчёт неверно трактуете. Я поясню логику в следующей статье.

    >>> С самого начала возник вопрос, почему вместо массива плагинов не использовать TInterfaceList?

    Да никаких причин нет, можно использовать.

    >>> если все на базе COM, то почему бы сразу не взять все механизмы, ту же обработку ошибок, COM категории и так далее?

    Я от COM взял только три вещи:
    - Интерфейсы
    - BSTR/WideSrtring
    - Библиотеку типов

    Это всё - достаточно легковесные вещи, которые от COM, считай, не зависят, а используются повсеместно. Поэтому их легко понять, с ними легко работать. И кривая для изучения будет плавной.

    Если же делать COM-объекты, то нужно учить именно COM: threading model, apartments, masrshaling, in-proc/out-proc, ... кучу вещей. Понятно, экспертом быть не нужно, но надо будет иметь обо всём этом какое-то представление, чтобы понимать что ты делаешь при создании и регистрации объектов. Согласись, что это существенно сложнее чем интерфейсы, строки и заголовочники.

    Само собой, если делать приложение размера MS Word, то COM тут практически без альтернатив. Но для меньших программ - какие плюсы компенсируют сложность COM для изучения?

    Это личное мнение - понятно, могу ошибаться.

    ОтветитьУдалить
  12. >>> С самого начала возник вопрос, почему вместо массива плагинов не использовать TInterfaceList?

    Да, почему я его не использовал: у меня всего два действия: добавить и удалить. Очень просто. Собственно мне показалось, что это немного и можно и написать. С учётом, что никаких других возможностей я не использую. Хотя с отдельным классом, конечно, правильнее.

    ОтветитьУдалить
  13. >>> Это всё - достаточно легковесные вещи, которые от COM, считай, не зависят, а используются повсеместно. Поэтому их легко понять, с ними легко работать. И кривая для изучения будет плавной.
    Не факт. В реализации того же COM в Delphi уже многое предусмотрено, та же обработка ошибок, к примеру. Да и статей куча, достаточно не описывать все досконально, а просто дать ссылки.
    А здесь все вручную придется делать. SafeCallException перекрываться будет?
    На мой взгляд как раз в этом варианте потребуется более глубокое понимание механизмов Delphi, что, впрочем, полезно.
    COM же недаром вводит вещи, которые требуют изучения. Вот планируется UI, а как будет решаться вопрос с тем, что вызов к этому UI может идти из разных приложений, из разных потоков? Взять хотя бы случай когда запущены два экземпляра приложения. То же самое касается и обратных вызовов. А если захочется плагин-синглетон? Например, для доступа к COM-портам, к оборудованию.
    Вообще с нетерпением жду следующей части :)

    ОтветитьУдалить
  14. У варианта системы плагинов с библиотекой типов есть еще то преимущество, что при необходимости плагин можно вынести в отдельный процесс, COM сможет выполнить маршалинг.
    Например, у меня возникла такая ситуация: по мере развития программы появилось множество плагинов от разных авторов, причем самого разного качества. Некоторые плагины портят память, в некоторых есть проблемы с загрузкой/выгрузкой в DllMain других DLL и т.п. В основном это все редко используемые плагины.
    Чтобы повысить стабильность работы приложения, менеджер плагинов может запускать некоторые плагины в отдельных процессах.

    ОтветитьУдалить
  15. 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 лет правила изменились. Вам не нужно думать об этом, этом или этом и сотне других вещей. Поэтому, эта статья - попытка что-то с этим сделать.

    ОтветитьУдалить
  16. Маленькая оптимизация:
    1. Пусть библиотека плагинов экспортирует одну функцию, которая возвращает одну интерфейсную ссылку, экспортирующую фабрику плагинов.
    2. Тогда функции для явной зачистки писать не надо - достаточно специфицировать для ядра удержание ссылки на фабрику до выгрузки, а финализацию либы выполнять в деструкторе фабрики.
    3. Появляется возможность размещать плагины буквально где угодно - загрузчику достаточно уметь получать ссылку на фабрику, а каким способом - всего остального кода не касается. Например, помещение плагинов в один исполняемый файл полезно для предотвращения излишних модификаций конечными пользователями и облегчения тестирования и отладки (утечки памяти много проще искать для одного файла и Delphi 2007 очень любит падать при отладке dll и bpl)

    ОтветитьУдалить
  17. Leo Bonart, вы стали на путь изобретения COM. Сейчас "оптимизация" на фабрику класса... потом независимое размещение в дериктории, и поиск по записи в реестре...

    На мой взгляд иногда можно обойтись и без такой оптимизации. Нужно просто определить какие возможности действительно могут понадобится. Эдак можно до абсурда дойти и изобрести DCOM, изобрести dot.net... И дело не в том что оптимизация плохая, да, она гибкая, но поддержка такой гибкой оптимизации (которая возможно никогда не понадобится) требует документации, требует дополнительной реализации, дополнительного тестирования.
    Взять предложенную вами же реализацию через фабрики. Представим ситуацию, мы хотим плагины обновлять по интернету без перезапуска приложения, прямо на лету, а между плагинами есть допустим связи. Нужно разработать систему по выгрузке, и восстановления связей (fixup), а в случае с фабрикой нужно обучать фабрику каждого класса делать такой "финт". Лишняя реализация в каждый плагин.

    Поэтому важно выбрать именно тот уровень гибкости архитектуры, который нужен именно в нашем случае и не переусердствовать.

    ОтветитьУдалить
  18. MrShoor,
    COM мне ни к чему: он для плагинов уровня системы с его регистрацией в глобальном реестре, внешней автоматизации с маршалингом между процессами, различными потоковыми моделями и т.п. и т.д.
    Мои задачи - архитектура конкретного приложения на уровне разработки и сопровождения. И здесь разделение физики (загрузчик) и логики (плагины) очень помогает, причем в первую очередь для целей тестирования.
    И да, я это реализовал и оно используется в практике коммерческой разработки моего работодателя.

    ОтветитьУдалить
  19. Leo Bonart, я ни в коем случае не говорю что ваш подход плох. Для ваших задач возможно так сделать удобнее и логичнее. Но если задача простая, то не обязательно её усложнять. У меня например была задача: простейшая система плагинов, но чтобы плагины могли обновлятся и перезагружаться "нагорячую", без перезапуска приложения. Мне не нужна была гибкость, и меня вполне устраивала и устраивает система 1 модуль = 1 плагин. С фабрикой же будет 1 модуль = 1 фабрика = N плагинов. В этом случае мне нужно было бы предусмотреть выгрузку модуля через фабрику, а значит обучить фабрику выгружать созданные ей плагины, а ведь ссылки на IPlugin будут существовать за пределами модулей. И в данном случае фабрика будет только мешать, давая мне гибкость которая абсолютно мне не нужна.
    Поэтому прежде чем вводить какой-либо механизм, нужно подумать нужен ли он вам конкретно, его ведь еще обслуживать надо будет, ну и не переуседствовать с гибкостью :)

    ОтветитьУдалить
  20. MrShoor ,
    "Горячая" выгрузка - это очень жестко.
    У меня важно было другое - чтобы плагины минимально зависели друг от друга и способа загрузки с одной стороны и могли неограниченно пользоваться функционалом остальных плагинов - с другой, т.е. чистый конструктор лего.
    В итоге дошло до выражения зависимостей через интерфейсы, а ядро определяло порядок активации и вычисляло циклические зависимости.
    Горячей выгрузке, кстати, предлагаемая мной оптимизация не противоречит - просто добавляет новые обязанности, причем не к загрузчику - он тупой, а к ядру - оно обязано уметь выдергивать логический плагин из работающей системы на лету, что в любом случае тяжко.

    ОтветитьУдалить
  21. На самом деле это только с виду тяжко. На практике все довольно просто реализуемо (добавлением пары методов в IPlugins), если интересно - могу рассказать. Код ядра по выдергиванию тоже довольно простой.

    ОтветитьУдалить
  22. Интересно.
    Для меня главной проблемой выгрузки было следующее: если функционалом плагина пользуются другие плагины, то как безболезненно выдернуть его из живой системы?

    ОтветитьУдалить
  23. Для успешной выгрузки удобнее всего отвественными за "перезагрузку" плагинов сделать сами плагины. Вообще всю ответственность за поддержание "связей" возлагать на сами плагины.
    Просто в плагин добавляем пару методов:
    IPlugin = interface
    //.... некоторые методы
    procedure Unload(plugin: IPlugin); safecall;
    procedure FixUp(plugin: IPlugin); safecall;
    end;

    Менеджер перед выгрузкой плагина вызывает Unload у каждого загруженного плагина. Загруженный плагин должен сам определить использует ли он этот плагин или нет, если использует - освободить все ссылки на плагин. После менеджер может выгрузить dll с плагином, заменить, подгрузить обновленный плагин и вызывать FixUp передав новый плагин всем загруженным плагинам. Кому надо - получает ссылки на плагин.

    Проблемы доступа к плагину в момент перезагрузки я решил через критические секции. Обернул перезагрузку плагина в коде менеджера в критическую секцию, в реализации плагинов обращаюсь к плагинам только экранированно. Таким образом никаких проблем с перезагрузкой не возникает.

    ОтветитьУдалить
  24. можно поступить проще: на каждый плагин создать объект-переходник с тем же интефейсом(IPlugin),
    он единственный будет общаться с плагином напрямую, а всем остальным модулям/плагинам отдавать ссылку на него
    тогда Unload/FixUp не нужны

    ОтветитьУдалить
  25. можно поступить проще: на каждый плагин создать объект-переходник с тем же интефейсом(IPlugin)
    При взаимодействии между плагинами как правило нужно больше чем просто интерфейс IPlugin. Например у меня есть IPopupPlugin через который осуществляется работа со всплывающими окошками, с кучей методов и свойств. На каждый плагин писать либо свой переходник, либо придумывать "хак" на ассемблерном уровне по маршалингу.

    ОтветитьУдалить
  26. >>Для успешной выгрузки удобнее всего ответственными за "перезагрузку" плагинов сделать сами плагины.
    Такое решение упрощает разработку ядра, но сильно затрудняет разработку самих плагинов. Код повсеместно становится жестко зависимым от архитектуры приложения, явное знание о плагинной структуре и сильные зависимости от конкретных версий конкретных плагинов (причем с возможностью исчезновения этих плагинов в любой момент) затрудняют как добавление функционала, так и его модификацию, и даже рефакторинг.
    Передо мной стояла задача с прямо противоположными приоритетами - превратить несколько монолитных приложений в модульные с общим ядром и пересекающимся составом плагинов с минимальными модификация существующего кода. Позднее были добавлены требования ослабления зависимости между плагинами, дабы имел значение только реализованный функционал, а сам плагин без ущерба можно было заменить аналогичным (или несколькими аналогичными).

    ОтветитьУдалить
  27. >>Такое решение упрощает разработку ядра, но сильно затрудняет разработку самих плагинов.

    Честно говоря не сильно усложняет. Если плагин работает в контексте основного потока то вообще никакой сложности. Всю реализацию загрузки/выгрузки можно впихнуть в сдк.
    Если же плагин работает с несколькими потоками, то можно использовать Lock/Unlock в том же сдк при обращении к другим плагинам.

    ОтветитьУдалить
  28. >>> можно поступить проще: на каждый плагин создать объект-переходник с тем же интефейсом(IPlugin)

    На словах мне нравится, но писанины будет... ого-го.

    А вообще в большинстве программ не перезапуск - это не проблема.

    ОтветитьУдалить
  29. Хотя и тут радужно не получается.

    Плагин не может сделать:
    AnotherPlugin.DoSomething(Self)
    а вынужден делать:
    AnotherPlugin.DoSomething(ICore.GetStub(Self))

    А в первом варианте получим трудноуловимые проблемы.

    ОтветитьУдалить
  30. Я может чего-то не понял, но вот этот код меня смущает.

    if Length(FItems) >= FCount then // "Capacity"
    SetLength(FItems, Length(FItems) + 64);


    Не перепутан ли тут знак > со знаком < ?

    ОтветитьУдалить
  31. ЗЫ: Посмотрел исходники там тоже. :)

    ОтветитьУдалить
  32. Лениво править 7 демо-примеров... Исправление вставлю в 8-й.

    Кстати, насколько я помню, это не единственный найденный и поправленный косяк - так что для практики лучше смотреть последний вариант примеров.

    ОтветитьУдалить
  33. Молодец, Тверь.
    Такие отличные статьи редко встретишь.

    ОтветитьУдалить
  34. Александр, подскажите пожалуйста такую вещь. У меня есть приложение работающее с БД. Хочу сделать 2 модуля - будут добавлять свой пункт в главное меню, и свою вкладку на TabControl, где полностью интерфейс берется из плагина (т.е. не форма отдельно, а на закладке). Кроме того, интерфейс плагина и соотв. сам плагин работает с данными из БД, с которыми работает основное приложение. Как можно такое реализовать?

    ОтветитьУдалить
  35. Я не эксперт в БД.

    Если смысл вопроса в том, что вам нужно соединение с БД и/или данные разделять между ядром и плагином, то есть несколько вариантов.

    В зависимости от используемого механизма доступа к БД - вы можете передавать в плагин описатель (Handle) соеднинения. Это низко-уровневая информаци, но зато это относительно просто (хотя и не всегда возможно).

    Если такой вариант не устраивает, то нужно будет создавать интерфейс для... чего-нибудь. Я не уверен - возможно, для набора данных или, быть может, источника данных.

    Тогда вам нужно будет сделать два наследника от набора или источника данных - один для ядра, второй - для плагина (или наоборот). Один должен наследоваться от максимально дочернего класса реализовывать интерфейс, второй - наследоваться от максимально возможного предка (TCustomDataSet?) и использовать интерфейс для реализации набора/источника данных.

    Я попробую коснуться этого вопроса позже.

    ОтветитьУдалить
  36. Александр, а как быть, если экспортируемые функции используют свои интерфейсы (например, плагин должен реализовать IMyInterface2 и вернуть ядру). Такие интерфейсы мы также суем в TLB или как?

    ОтветитьУдалить
  37. Не очень понял вопрос. Что понимается под "свои"?

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

    Если же плагин выставляет наружу интерфейс (т.е. с этим интерфейсом будет работать не только сам плагин, но и другой код), то интерфейс определённо должен быть где-то описан в общем файле. Будете ли вы ручками создавать .pas/.h/кто-то-там-ещё или переложите эту задачу на библиотеку типов - это уже по вкусу.

    ОтветитьУдалить
  38. Один из шагов алгоритма такой: плагин загружается и вызывается его функция MakeReport (это как Init и Done), принимающая IMyInterface2 и возвращающая IMyReport, с которым дальше работает основная программа. То есть, плагин должен отвечать и за создание класса, реализующего IMyReport - а затем его возвратить.

    Вопрос в том, что если их не включать в библиотеку, то тот, кто реализует плагин будет не понимать, что есть IMyInterface2 и IMyReport -> соответственно либо их нужно будет включить в tlb, либо переводить вручную. Наверное это 2 вариант, так?

    ОтветитьУдалить
  39. У меня такой вопрос для кросс-платформенной разработки

    - насколько тут подходит интерфейсный подход?
    - использование BSTR заточено под Windows, что делать со строками?
    - tlb файлы видимо тоже идут мимо

    ОтветитьУдалить
  40. Интерфейс - это запись с указателями. Т.е. это весьма кросс-платформено. К примеру, интерфейсы - основа фреймворка CFPlugin в Mac OS.

    Даже если на какой-то платформе нет IUnknown, то ничто не помешает его определить вручную, поскольку, как я уже сказал, это просто три указателя. Мы не используем COM, так что в этом не будет ничего страшного.

    BSTR - это обычный PWideChar с явной длиной и с требованием выделения памяти через центральный менеджер памяти (под Windows ипользуется SysAllocString / SysFreeString). Разумеется, готового типа BSTR нет в других ОС. Но аналогичный тип можно сделать и вручную. К примеру, WideString существует и в Delphi/Mac и в FreePascal. Я только не знаю, как чисто технически он там реализуется.

    С TLB - я без понятия. Скорее всего, только Windows. Но вспомните, зачем нам нужен TLB. TLB - это просто способ упростить генерацию заголовочных файлов. TLB не нужен для разработки, он используется единственный раз для генерации заголовочников.

    ОтветитьУдалить
  41. Если не ошибаюсь, то в OS X, Delphi работает с WideString как с UnicodeString.


    Ещё заметил не совсем ясную вещь:

    из

    function f(a,b: widestring): widestring; stdcall;

    компилятор Delphi делает

    function f(result,a,b: widestring); stdcall;

    хотя result вроде вполне влезает в eax.

    safecall работает как положено.

    В итоге, я думаю, в своём ядре сделать функции для:
    - выделения/удаления/копирования строк
    - выделения/удаления памяти для передачи буферов

    ОтветитьУдалить
  42. Почему нельзя использовать array of? Т.е. вообще нельзя даже внутри класса для внутренних операций или чисто по поводу того, что нет вот в си такого?

    Допустим есть S: array of TStringlist
    Ну и пусть будет property GetS[Index,Index2:integer]:WideString read GetFromSArray;
    ...
    Result := WideString(S[Index][Index2]);

    Нет?

    ОтветитьУдалить
  43. Здравствуйте, у меня такая проблема...
    Компилятор Delphi 7 ругается на FManager, в модуле "PluginManager", в функции function TPluginManager.LoadPlugin(const AFileName: String): IPlugin;

    Неужели у меня одного такая проблема? Раз никто больше не отписал...

    ОтветитьУдалить
  44. >>> safecall работает как положено

    Преимущество safecall, да. У компилятора Delphi есть баги в реализации stdcall - он расходится во мнении с MS C++ в том, как возвращать сложные результаты (например, и это не единственный пример, поищите по QC).

    В целом, для любой модели вызова, кроме safecall, лучше руководствоваться правилом: не возвращать через Result сложные типы данных (более 4-х байт, либо с автоматическим управлением). Возвращайте их через последний var/out параметр.

    >>> Почему нельзя использовать array of?

    Вы упускаете контекст. Речь идёт про заголовочники - т.е. вы описываете то, как и чем будет обмениваться код на разных языках. Array of нельзя по той простой причине, что это - динамический, либо открытый массив, т.е. конструкция, специфичная для Delphi и не существующая в других языках.

    Внутри же, в реализации, вы вольны использовать что угодно, без ограничений.

    >>> Компилятор Delphi 7 ругается на FManager

    Как ругается? И в последнем варианте исходников тоже?

    ОтветитьУдалить
  45. >>> Как ругается?
    [Error] PluginManager.pas(72): Undeclared identifier: 'FManager'

    >>> И в последнем варианте исходников тоже?
    Не знаю, я сейчас застопорился на "Загрузка одного плагина" из-за этой ошибки.

    ОтветитьУдалить
  46. Но если добавить переменную FManager в секцию private "TPluginManager", то используя след. пример загрузки библиотек (с введением имени в TEdit) - вылезает AV.

    ОтветитьУдалить
  47. Заметил такую не очевидную для меня вещь; в исходниках вы описываете свойство и метод так:
    IPluginInfo = interface
    function GetID:TGUID; safecall;
    property ID:TGUID read GetID;
    end;

    в библиотеке типов же определяете IPluginInfo, создаете свойство ID, генерируете PAS из TLB и используете этот _TLB.pas в проекте dll.

    и фокус в том, что итог из TLB описывает методы set/get иначе чем вы...
    IPluginInfo = interface
    function Get_ID:TGUID; safecall;
    property ID:TGUID read Get_ID;
    end;

    Как это "лечить"? Подстраиваться под формат геттеров/сеттеров от генератора библиотеки типов в ядре плагинов?

    ОтветитьУдалить
  48. Не имеет значения, какое именно имя имеет геттер и сеттер.

    Поскольку я пишу пример, а не систему для реального использования, то TLB мне (обычно) не нужен. Поэтому все интерфейсы я описываю руками. Соответственно, при этом я обычно использую синтаксис GetСвойство и SetСвойство.

    Если же вы не пишете объявления интерфейсов сами, а используете TLB, то код будет сгенерирован автоматически, а имена будут Get_Свойство и Set_Свойство.

    Иными словами, какое имя будут иметь геттеры и сеттеры - будет зависеть от того, начинаете ли вы описывать интерфейсы с кода или с TLB.

    Если вы начинаете писать с кода, а потом создаёте TLB (что лучше не делать: не надо смотреть на меня - мне просто лениво), то вы можете сразу использовать синтаксис Get_Свойство и Set_Свойство. А если вы это не сделаете, а потом захотите использовать TLB, то вы просто можете использовать алиасы:

    type
    TSomeClass = class(TSomeParentClass, ISomeInterface)
    protected
    function GetID: Integer; safecall;
    function ISomeInterface.Get_ID = GetID;
    end;

    ...или переименовать методы.

    ОтветитьУдалить
  49. Эх, вот такой мануал бы, но по JvPlugin...

    ОтветитьУдалить
  50. Роман, сомнительно. JvPlugin заточен сугубо под Delphi.

    ОтветитьУдалить
  51. 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;

    //Если загружен всего один плагин :(((

    procedure TPluginManager.UnloadPlugin(const AIndex: Integer);
    var
    X: Integer;
    begin
    // Выгрузить плагин
    FItems[AIndex {0}] := nil;
    // Сдвинуть плагины в списке, чтобы закрыть "дырку"
    for X := AIndex{0} to FCount - 1 {1-1=0} do
    FItems[X {0}] := FItems[X + 1{0+1=1}]; //НЕ СУЩЕСТВУЕТ
    // Не забыть учесть последний
    FItems[FCount - 1 ] := nil;
    Dec(FCount);
    end;

    ОтветитьУдалить
    Ответы
    1. Цикл должен быть таким:
      for X := AIndex to FCount - 2 do

      С оригинальным кодом, пока у вас меньше 64-х плагинов, ничего плохого не случится.

      Удалить

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

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

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

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

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