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

Итак, вчера мы разобрались, что представляет из себя интерфейс в Delphi, каким образом реализуются интерфейсы и рассмотрели парочку вариантов того как эти самые интерфейсы реализуются.
Сегодня перейдем к следующему моменту — реализации своего первого плагина (plug-in, подключаемый модуль — как угодно) для приложения.
Итак, представим, что у нас есть некое приложение, работающее с текстом, например, записная книжка. Сейчас для нас неважно как реализована работа с текстом внутри программы — получаем ли мы его из Блокнота Google или записываем в вручную в Memo. Наша задача — обратиться к ресурсам нашего приложения из DLL. При этом следует также учесть, что на работу нашего импровизированного плагина можно было накладывать ограничение работы, например, запретить записывать в переменную строку «Delphi Is Dead!».

Вспомним, что интерфейс — это то посредством чего клиент общается с сервером. В нашем случае сервер — приложение, клиент — плагин. Реализацией интерфейса в Delphi выступает класс.
Вначале разрабатываем модуль, который будет содержать интерфейс. Именно этот модуль мы будем раздавать всем желающим, чтобы они с помощью него писали плагины.
Я создал следующий модуль:

unit InterfaceUnit;
 
interface
 
uses
  Classes;
 
type
  IPluginInterface = interface
  ['{34381143-4B31-11D8-8903-0020ED19BE94}']
     procedure Hello();
     function GetLocalString(): widestring;
     procedure SetLocalString(aStr: widestring);
  end;
 
implementation
 
end.

Теперь реализуем этот интерфейс в нашем приложении, не забыв поставить небольшое ограничение. Главная форма приложения выглядит следующим образом:

Реализация интерфейса IPluginInterface в приложении:

uses
 ..., InterfaceUnit;
 
const
    StopString = 'Delphi Is Dead';
 
type
  TForm1 = class(TForm, IPluginInterface)
    LoadDLL: TButton;
    UnloadDLL: TButton;
    SetI: TButton;
    GetI: TButton;
    ShowDLLForm: TButton;
    Memo1: TMemo;
    Label1: TLabel;
    Label2: TLabel;
   [...]
  private
    { Private declarations }
  public
    { Public declarations }
    procedure Hello();
    function GetLocalString(): widestring;
    procedure SetLocalString(aStr: widestring);
  end;
 
var
  Form1: TForm1;
  [...]
implementation
 
{$R *.DFM}
 
var
 MyString: wideString; //строка которую будем изменять
 
procedure TForm1.Hello();
begin
 ShowMessage('Hello World!')
end;
 
function TForm1.GetLocalString(): widestring;
begin
 Result:=MyString;
end;
 
procedure TForm1.SetLocalString(aStr: widestring);
begin
 if pos(LowerCase(StopString),LowerCase(aStr))<=0 then
   MyString:=aStr
 else
   ShowMessage('Delphi живее всех живых!')
end;

Теперь немного отвлечемся от главного приложения и займемся непосредственно разработкой плагина. Плагин будет содержать форму:

С помощью которой мы будем получать и изменять значение переменной MyString:string главного приложения.
Итак, создаем новую DLL, создаем в проекте новую форму и в unit формы подключим наш модуль интерфейса:

uses ..., InterfaceUnit,

И объявим одну глобальную переменную:

var  Plg: IPluginInterface;

Сразу распишем все обработчики нажатия кнопок, чтобы больше не возвращаться к форме:

procedure TForm2.Button1Click(Sender: TObject);
begin
//изменяем переменную в главном приложении
  Plg.SetLocalString(Memo1.Text)
end;
 
procedure TForm2.Button2Click(Sender: TObject);
begin
//получаем значение MyString
  Memo1.Text:=Plg.GetLocalString
end;
 
procedure TForm2.Button3Click(Sender: TObject);
begin
//здороваемся с пользователем
  Plg.Hello;
end;

Теперь переходим к написанию основного кода DLL плагина. Нам необходимо реализовать в библиотеке как минимум три метода:
1. Инициализация плагина;
2. Показ формы плагина;
3. Завершить работу плагина.

Инициализация плагина

В этом методе нам необходимо получить ссылку на интерфейс, чтобы впоследствии работать с переменной MyString. Для этого подключаем в uses библиотеки модуль интерфеса и пишем следующую процедуру:

library Plugin;
 
uses
  ...
  InterfaceUnit in '..\InterfaceUnit.pas';
 
{$R *.RES}
 
var
  DA: TApplication;
 
procedure InitPlugin(App: Integer); StdCall;
begin
  DA:=Application;
  Application:=TApplication(App);
  if Application.MainForm.GetInterface(IPluginInterface,Plg) then
    ShowMessage('Ссылка на интерфейс получена')
  else
    ShowMessage('Не удалось получить ссылку на интерфейс!');
end;

В нашем случае указатель на интерфейс получается посредством вызова функции:

function GetInterface(const IID:   TGUID;   out Obj): Boolean;

Так как MainForm является свойством класса TApplication, то метод QueryInterface из секции protected класса TCustomForm для него не поддерживается. Однако ничто не мешает воспользоваться методом GetInterface, реализованным еще в классе TObject в секции published.

Показ формы плагина

Здесь все достаточно просто, т.к. после инициализации плагина мы можем использовать TApplication:

function DoForm(): TForm; stdcall;
begin
  if Form2=nil then
   Form2:=TForm2.Create(Application);
  Result:=Form2;
end;

И, наконец третий и последний этап реализации работы плагина — его выгрузка.

Завершение работы плагина

Здесь нам необходимо уничтожить форму, если она была создана и передать TApplication обратно:

procedure FinishPlugin(); stdcall;
begin
 if Form2<>nil then
   FreeAndNil(Form2);
 Application:=DA;
end;
 
exports
  InitPlugin,
  DoForm,
  FinishPlugin;
 
begin
end.

На этом разработку плагина можно считать законченной. Осталось немного доработать наше основное приложение, а именно — «научить» приложение загружать и выгружать плагин и использовать его (плагина) форму.

Организация работы с плагинами из основного приложения

Как вы знаете, существует два основных способа загрузки DLL — статическая загрузка и динамическая. В Delphi 2010 появился третий способ — отсроченная загрузка. Однако в работе с плагинами, как Вы понимаете, ни статическая ни отсроченная загрузка не подойдут, поэтому придётся немного поработать ручками и организовать динамическую загрузку/выгрузку DLL.
Вначале распишем процедуру загрузки DLL:

procedure TForm1.LoadDLLClick(Sender: TObject);
var
 s: string;
begin
  s:=ExtractFilePath(ParamStr(0))+'Plugin.dll';
  DllHandle:=LoadLibrary(PChar(s));
  if DllHandle<>0 then
   begin
     @InitPlugin:=GetProcAddress(DllHandle, 'InitPlugin');
     if @InitPlugin<>nil then
      begin
        InitPlugin(Integer(Application));
        @DoForm:=GetProcAddress(DllHandle, 'DoForm');
        @FinishPlugin:=GetProcAddress(DllHandle, 'FinishPlugin');
      end;
     MessageDlg('Plugin.dll загружен!', mtInformation, [mbOk], 0);
   end
  else
   begin
     MessageDlg('Библитека Plugin.dll не загружена!', mtError, [mbOk], 0);
   end;
end;

Здесь мы вначале получаем Handle библиотеки, запрашиваем адрес процедуры InitPlugin и, если такая процедура имеется в библиотеке, то считаем, что это плагин для нашей программы и инициализируем его. После чего запрашиваем адреса функции показа формы плагина и завершения его работы.
Соответственно обработчик кнопки «Показать форму» будет следующим:

procedure TForm1.ShowDLLFormClick(Sender: TObject);
var
 DLLForm: TForm;
begin
 if DllHandle<>0 then
  begin
    if @DoForm<>nil then
     begin
      DLLForm:=DoForm();
      DLLForm.Visible:=true;
     end;
  end;
end;

И, наконец, завершение работы плагина:

procedure TForm1.UnloadDLLClick(Sender: TObject);
begin
  if DllHandle<>0 then
   begin
     if @FinishPlugin<>nil then
      FinishPlugin;
      if FreeLibrary(DllHandle) then
       begin
         DllHandle:=0;
         MessageDlg('Плагин выгружен!', mtInformation, [mbOk], 0);
       end;
   end;
end;
Также не следует забывать выгружать плагины при завершении работы программы. Для этого можно создать обработчик OnClose главной формы. Т.к. код обработчика OnClose будет практически на 100% идентичен предыдущему, то в посте я его приводить не буду, а лучше дам ссылку на скачивание исходников рассмотренного в посте проекта.
И теперь самое последнее и видимо главное. В настройках компиляции DLL и основного приложения следует указать опцию "Build with Runtime Packages" следующим образом:
<a href="http://webdelphi.ru/wp-content/uploads/2010/05/plugin12.png"><img class="aligncenter size-medium wp-image-3405" title="plugin delphi" src="http://webdelphi.ru/wp-content/uploads/2010/05/plugin12-300x199.png" alt="" width="300" height="199" /></a>

И только после этого компилировать библиотеку. Не знаю с чем это связано, но абсолютно такой же пример, написанный в Delphi 7 без проблем работал без этой опции компиляции. В Delphi 2010 постоянно происходит ошибка при выгрузке плагина «AccessViolation …».
На этом разработку нашего нехитрого приложения и плагина к нему можно считать выполненной. В этом примере мы научились не только передавать значения функций из DLL в основное приложение, но и выполнять обратное действие — из DLL получать значения переменных в основного приложения и все это оказалось возможным с использованием интерфейса.

0 0 голоса
Рейтинг статьи
уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.
Подписаться
Уведомить о
30 Комментарий
Межтекстовые Отзывы
Посмотреть все комментарии
Алексей Тимохин

Нельзя передавать строки из DLL-ки в основное приложение, у них же разные менеджеры памяти! Не знаю как сейчас, но по крайней в ранних версиях Delphi точно так делать было. Самое ужасное в том, что оно даже может работать, до поры.

У GunSmoker-a же была целая серия публикаций посвящённая плагинам. Там подробно рассматривались все нюансы. Почитай на досуге что ли. Там вся матчасть есть.

Алексей Тимохин

Проще использовать Widestring.

Казанцев Алексей

WideString можно, ими управляет система. Обычные нельзя даже при использовании ShareMem, если хочешь делать по настоящему правильно, т.к. формат заголовка строк в юникод-версиях изменился. Ты же не хочешь сказать, что плагины писать можно только с использованием конкретных версий Delphi. PChar использовать тоже нельзя, нужно конкретизировать — PAnsiChar или PWideChar, иначе при компиляции в другой (юникод/анси) версии Delphi жди беды. Соглашение о вызовах, опять же, декларировать следует всегда (касательно интерфейсов отдаваемых во вне).

Алексей Тимохин

Sharemem должен подключаться явно. Для старых версий Delphi ещё можно было использовать FastShareMem или как-то так, который не требовал DLL-ки. Не знаю, изменилась ли ситуация с тех пор как главным менеджером памяти стал FastMM. Тем не менее даже Delphi 2010 при создании библиотеки генерирует следующий комментарий: { Important note about DLL memory management: ShareMem must be the first unit in your library’s USES clause AND your project’s (select Project-View Source) USES clause if your DLL exports any procedures or functions that pass strings as parameters or function results. This applies to all strings passed to and from your DLL—even those… Подробнее »

Алексей (Тамбов)
Алексей (Тамбов)
12/05/2010 09:34

Я использую dll-ки подключаемые динамически к основной программе, в большом проекте. Так вот перед тем как все это дело затеять, много чего перечитал. Обязательным конечно прописывать FastShareMem в самое начало в список юнитов, после этого работа со строками проходит абсолютно нормально и в программу и обратно. Решил использовать именно dll. А те кто говорит, что не стоит использовать dll, просто плохо знают эту область. Не скрою, что много нюансов приходится учитывать при создании dll, но для этих целей я создал образец (каркас) dll, по которой создаются все другие. У меня сейчас в программе используется 52 dll-ки. Работают 2 человека именно… Подробнее »

Казанцев Алексей

>В том числе и причина, почему не стоит делать DLL-ки в Delphi.

Вот это да… Пойду вены вскрою :)))

Алексей Тимохин

>В том числе и причина, почему не стоит делать DLL-ки в Delphi.

Сорри, я страшно наврал. Я имел в виду проблему с DllMain. Она описана здесь а также обсуждается в комментариях к первой статье о плагинах gunsmoker-a.

Казанцев Алексей

Алексей, никакой проблемы с DllMain нет. Есть определенное поведение DLL’ей обусловленое системными соглашениями. Это не проблема Delphi. Это вообще не проблема. Это нужно просто знать. Есть прекрасная литература описывающая работу системных механизмов, например «Windows для профессионалов» от Джефри Рихтера или «Системное программирование в Windows» от Ала Вильямса(уверяю, узнаете больше чем из переводов блогеров). А без подобных знаний любой собственный косяк можно считать проблемой Delphi. Без обид.

deksden
deksden
12/05/2010 20:10

советую скачать tms plugin framework trial и изучить методологию построения такого софта — все таки коммерческое решение! 75 евров за него просят, существует оно давно и работает вроде бы достаточно успешно.

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

irwin
irwin
13/05/2010 20:48

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

Казанцев Алексей

2irwin: Если еще и версия Delphi одинакова у приложения и плагинов. Однако, лично я считаю идею использования пакетов в качестве плагинов не самой лучшей.

Алексей Тимохин

Алексей Казанцев, я готов согласится с тем, что при использовании только стандартных юнитов проблем нет.
Но при использовании чужих юнитов, которые могут пихать в initializtion секцию всё что угодно, проблемы всё-таки могут появится. Причём не зная откуда растут ноги, найти источник такой проблемы будет не так просто. Посему, насколько я понимаю, вместо Dll-ок в Delphi рекомендуется создавать BPL-ки (которые не имеют такой проблемы), и уже их использовать как DLL.

Gunsmoker здесь говорит примерно то же самое.

Рихтера я обязательно почитаю, спасибо.

Казанцев Алексей

> Но при использовании чужих юнитов, которые могут пихать в initializtion секцию всё что угодно, проблемы всё-таки могут появится.

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

Можно ли называть это проблемами Delphi? Нет. Это проблема (не)грамотного использования.

p.s. По поводу пакетов ты заблуждаешься.

Казанцев Алексей

А есть еще конструкторы классов (тоже, кстати, отрабатывают неявно), а есть еще циклические ссылки… Как страшно жить ;)

Александр
20/05/2010 00:05

>>> Однако, лично я считаю идею использования пакетов в качестве плагинов не самой лучшей. Лично я тоже так считаю, но только по той причине, что это 1). сложно 2). отключается smart-linking (по определению пакета). Использование пакетов само по себе ещё не означает привязку к Delphi, если вопрос про это. По поводу же DllMain — хотя я не вижу смысла в споре чей это баг, но факт остаётся фактом. Проблема есть и про неё нужно хотя бы знать. Почему я считаю, что это проблема Delphi (окей, проблема Delphi, диктуемая системой)? Да по той простой причине, что initialization/finalization преподносится в Delphi как… Подробнее »

Казанцев Алексей

Любому адекватному разработчику ясно, что код инициализирующих секций отрабатывающий до основного блока Begin End будет одинаково отрабатывать до него, как в случае обычного приложения, так и в случае dll. В справке отдельно указаны моменты важные для написания dll. Вот о какой проблеме тут идет речь? Что разработчик был не в курсе, как происходит загрузка библиотек и как они инициализируются (без технических дебрей, достаточно ключевых фаз)? В таком случае, это проблема того разработчика. >Внимание, вопрос: как я могу “просто знать”, что здесь что-то не так? Все так. Проблем нет. Нужно просто знать среду которую используешь и под которую пишешь. Все. Других… Подробнее »

Александр
20/05/2010 17:22

Алексей, давайте вы просто взгляните правде в глаза: разработчики Delphi не слишком-то осведомлены о проблемах DllMain. Отсюда идёт и справка, которая молчит об этом, и кривые стандартные модуля (вы же как «адекватный разработчик» не собираетесь это отрицать? Это баг в явном виде), и даже сам дизайн дельфёвых DLL.
> т.е. это был твой код?
Нет, конечно. Стал бы я тогда об этом упоминать.

Казанцев Алексей

>разработчики Delphi не слишком-то осведомлены о проблемах DllMain. Ты прав. Разработчики Delphi не осведомлены о проблемах DllMain. Потому, что проблем DllMain нет. Есть проблема разработчика не знающего/не понимающего происходящих процессов (в том числе, разработчика пишущего компоненты). >Отсюда идёт и справка, которая молчит об этом О чем еще должна говорить справка? О том, когда отрабатывают init/fin секции написано. Когда отрабатывает основной блок Begin End написано. Дальше идут системные механизмы, описывать которые нет смысла т.к. для этой информации существуют другие источники. Более того, это информация относящаяся уже к системе, а следовательно и литературу нужно читать соответствующую (ссылка на MSDN, где запрещается регистрировать… Подробнее »

Александр
21/05/2010 11:18

> в том числе, разработчика пишущего компоненты …забыли добавить: в том числе, разработчиков Delphi.   > О том, когда отрабатывают init/fin секции написано Ткните пальцем. Только не надо тыкать в «These statements are executed once every time the library is loaded». У пакетов тоже такое написано. Что не означает, что их инициализация как-то связана с DllMain.   > ссылка на MSDN, где запрещается регистрировать классы из DllMain будет? Простой вывод из следующих кусков: 1. RegisterClass принадлежит User32. 2. В описании DllMain написано «Calling functions that require DLLs other than Kernel32.dll may result in problems that are difficult to diagnose». Прямым текстом… Подробнее »

Казанцев Алексей

>Только не надо тыкать в «These statements are executed once every time the library is loaded». У пакетов тоже такое написано. Что не означает, что их инициализация как-то связана с DllMain. Именно в это и надо. Ну еще: «The initialization section contains statements that are executed, in the order in which they appear, on program start-up.». Адекватному (подчеркиваю!) разработчику достаточно знания того, что инициализирующий код будет выполнен до основного блока Begin End. Дальше только поиск информации о том, как это будет выглядеть в случае с dll, и погружение в дела системные. А чего ожидалось? Инструкции для «чайников»? >2. В описании… Подробнее »

Александр
02/07/2010 20:32

Показательный пост. Allen Bayer глаголет: а используйте-ка вы пакеты, ребята, и не парьтесь.

Казанцев Алексей

А как писать плагины к другим, не Delphi, приложениям? А как писать расширения оболочки (Explorer)? Как писать system wide hooks без умения писать и использовать dll ? Вопросы риторические. Все нежелающие париться давно уже свалили на C#.

Александр
13/07/2010 18:58

> А как писать плагины к другим, не Delphi, приложениям? Нормальные не-Delphi приложения предусматриваю в своей системе плагинов функции Init и Done, а не полагаются на DllMain плагина. Никто не запрещает вам использовать пакеты в качестве DLL-плагинов к не-Delphi программам. То, что вы собираете пакет — ещё не означает какой-то привязки к Delphi. Вы делаете пакет, выставляете наружу его Init/Done, которые вызываются системой плагинов — и, пожалуйста, никаких проблем в принципе не может возникнуть. Было бы неплохо, если бы обычная DLL позволяла бы такой трюк, без необходимости использовать такие черезжопные решения. > А как писать расширения оболочки (Explorer)? А в… Подробнее »

Казанцев Алексей

>Никто не запрещает вам использовать пакеты в качестве DLL-плагинов к не-Delphi программам.
Конечно никто, кроме здавого смысла. Мне, например, не улыбается перспектива иметь маленький плагин и 4х-метровый рантайм для него. А так конечно, пакет суть обычная dll.

GS
GS
30/12/2011 23:27

Я тоже в свое время прошел через данную проблему с плугинами, и в качестве изначального решения использовались пакеты. Все было круто, но только до того момента, пока Borland не выпустил Update Pack 3 к Delphi 4 (мы до этого разрабатывали на Delphi 4 с Update pack 2). Суть проблемы была в несовместимости пакетов между 2 и 3 апдейтами одной версии делфи! Перекомпиляция же всех плугинов и внедрение было невозможно из за большой удаленности от цивилизации рабочих мест с нашим софтом (в некоторые места можно только вертолетом добраться) и серьезного количества разных заказчиков. Поэтому на собственной шкуре понял, что приложение и плугины должны… Подробнее »