Использование DLL в качестве Plug-in

=== Использование DLL в качестве Plug-in ===
В темах для написания статей раздела "Hello World" присутствует вопрос о динамических библиотеках и модуле ShareMem. Я хотел бы несколько расширить постановку вопроса: Пусть нам надо построить систему безболезненно расширяемую функционально. Напрашивающееся само собой решение - библиотеки динамической компоновки. И какие же грабельки разбросаны на этой тропинке? 
==== Грабли ====
В месте моей текущей трудовой деятельности вопрос о такой системе всплыл давно. И поскольку он не собирался тонуть, создать такую систему пришлось. Так что всё изложенное ниже - из собственного опыта. 
Первый вопрос возникающий при создании библиотеки (DLL): А что это тут написано в закомментированной части исходного кода библиотеки. А рассказывается там следующее - если вы используете динамические массивы, длинные строки (что и является динамическим массивом) как результат функции, то необходимо чтобы первым в секции uses стоял модуль ShareMem. Причём и в основном проекте! От себя добавлю, что это относится более широко к тем случаям, когда вы выделяете память в одной библиотеке, а освобождаете в другой, что и произойдёт когда вы создадите динамический массив в одной Dll-ке, а освободите его в другой. 
Использовать ли ShareMem - вопрос конкретной постановки задачи. Если можно обойтись без таких выделений памяти, то вперёд, с песней! Иначе придётся вместе с программой таскать borlndmm.dll, которая и реализует безболезненный обмен указателями между библиотеками. 
Можно задаться вопросом "А почему?". И получить ответ "Так надо!". По всей видимости, Delphi работает с Heap (кучей, откуда выделяется память) по-своему. Некоторое время назад мы на работе обсуждали этот вопрос, ползали по исходникам и к единому мнению та к и не пришли. Но есть предположение, что Delphi выделяет сразу большой кусок памяти в куче и уже потом по запросу отрезает от него требуемые кусочки, тем самым не доверяя системе выделять память. Возможно, это не так и если кто подправит меня, буду благодарен. Так или иначе - проблема существует, и решение имеется. 
Вопрос второй, он освещался уже на этом сайте - а вот хочется положить форму в нашу библиотеку. Нет проблем, кладём, запускаем. Форма создаёт свою копию на панели задач. Почему? Если вы создавали окно средствами WinAPI, то обращали внимание на то, что заголовок окна и текст соответствующей кнопки на панели задач совпадают и сделать их (тексты) различными невозможно. Т.е. когда процесс создаёт первое окно, у которого владелец - пустая ссылка (если точнее то Handle - дескриптор), то окно выводится на панель задач. А как же Delphi? В переменной Application:TApplication, которая имеется всегда, когда вы используете модуль Forms, при создании Application содаётся невидимое окно, которое становится владельцем для всех окон приложения. А поскольку у библиотеки н е происходит действий по инициализации окна переменной Application, то создаваемая форма не имеет окна владельца и как следствие - появление кнопки на панели задач. Решение уже описано, это передача ссылки на экземпляр объекта Application из вызывающей программы в вызываемый модуль и присвоение переменной Application переданного значения. Главное перед выгрузкой библиотеки не забыть вернуть старое значение Application. 
Совпадение свойств Name разных окон будет вызывать исключительную ситуацию. Избегнуть этого не сложно, а возникает ошибка видимо из-за того, что разные типы классов имеют одно имя в пределах одного контейнера. 
Достаточно важным является уничтожение окна перед выгрузкой библиотеки и завершением программы. Delphi расслабляет: за выделенными ресурсами следить не надо, окна сами создаются и уничтожаются и ещё много чего делается за программиста. Накидал компонентиков, установил связи и всё готово... Представим: библиотека выгружена, окно из библиотеки существует, система за библиотекой уже почистила дескрипторы, да остальные ресурсики и что получается? Секунд пять Delphi при закрытии программы висит, а затем "Acces s violation ..." далее вырезано цензурой... 
Больше граблей замечено не было. Да и упомянутые - серьёзной проблемы не представляют, единственное, что нужно, писАть аккуратно, текст вылизывать, да и думать почаще. 
==== Построение программы с Plug In-ами ====
 
Возможно 2 подхода к построению такой программы 
* плагины получают информацию от ядра программы, но сами к ядру не обращаются. Назовём такой подход пассивным. 
* в активном подходе плагины инициируют некоторые события и заставляют ядро их слушаться. 
Второй подход требует более сложного алгоритмического построения, поэтому я рассмотрю только первый. 
Пусть у нас есть программа, умеющая подключать динамические библиотеки (любые). Но для эффективной работы необходимо, чтобы эти библиотеки предоставляли стандартный интерфейс по передаче данных. По-русски, должен существовать стандартный набор экспортируемых функций библиотеки, через которые программа будет с ними общаться. 
В процессе работы выяснилось, что для пассивной модели достаточно 6 функций: 
Получение внутренней информации о плагине (в программе function GetModuleInfo: TModuleInfo). При наличии в библиотеке такой функции и правильном её вызове, мы будем знать что эта DLL - наш плагин. Сама функция может возвращать что угодно, например название и тип плагина. 
Формирование начальных значений (в программе procedure Initialize). Плагин приводит себя в порядок после загрузки, т.е. заполняет переменные значениями по умолчанию. Передача данных в плагин (в программе procedure SetData(Kind: TDataKind; const Buffer; Size: Integer)). Позволяет передавать данные в плагин. Получение данных - в программе не реализована, но делается по типу SetData. Запуск плагина (в программе Run). Запускается плагин. Действия могут быть различными: показ окна, модальный показ окна, расчёт какого-либо параметра и т.д. И есесьно останов плагина. Здесь действия обратные пункту 2. 
Немного остановлюсь на передаче данных. Паскаль при всей своей жёсткой типизации предоставляет приятное средство передачи в функцию нетипизированных данных. Если программа знает о том, какие именно данные пришли, оттипизировать :) их достаточно просто. Этот способ передачи используется в SetData. В модуле SharedTypes.pas, используемом всеми тремя проектами описаны соответствующие константы TDataKind для типов передаваемых данных. 
==== Теперь о реализации ====
Пусть ядро, т.е. exe-файл, ищет плагины, запускает их и по таймеру передаёт в них два цифровых значения, которые один плагин будет изображать в текстовом виде, а второй в виде диаграмм. Реализация плагинов отличается минимально, поэтому расскажу об одном - Digital.dll. Начнём перечисление функций: 
// получение информации о плагине
// Функция возвращает информацию о модуле. В данном
// случае это цифровое отображение, путь и тип модуля.
function GetModuleInfo: TModuleInfo; stdcall;
var
 Buffer: array [Byte] of Char;
begin
 with Result do
 begin
 Name := 'Отображение цифровых данных';
 Kind := mkDigital;
 if GetModuleFileName(hInstance, @Buffer, SizeOf(Buffer)-1) > 0 then
 Path := ExtractFilePath(StrPas(Buffer));
 end;
end;
// инициализация
// Процедура запоминает переменную Application
// и делает нулевой ссылку на форму плагина.
procedure Initialize; stdcall;
begin
 // запоминание старого Application
 OldApp := Application;
 fmDigitalMain := nil;
end;
// запуск
// Процедура запуска плагина создаёт окно.
// Окно создаётся видимым.
procedure Run; stdcall;
begin
 // создание окна плагина
 if fmDigitalMain = nil then
 fmDigitalMain := TfmDigitalMain.Create(Application);
end;
// останов
// Процедура уничтожает окно и возвращает старый TApplication.
procedure Terminate; stdcall;
begin
 // освобождение окна
 fmDigitalMain.Free;
 fmDigitalMain := nil;
 // восстановление старого TApplication
 Application := OldApp;
end;
// приём данных
// Процедура получения данных. В зависимости от полученного
// типа данных с данные в переменной Buffer соответственно
// типизируются. Здесь происходит обращение к форме плагина,
// расписывать я его не буду, там всё просто, см. исходники.
// Типы, которые используются здесь, описаны в SharedTypes.pas
procedure SetData(Kind: TDataKind; const Buffer; Size: Integer); stdcall;
begin
 case Kind of
 // передача TApplication
 dkApplication: if Size = SizeOf(TApplication) then
 Application := TApplication(Buffer);
 // передача данных во время работы
 dkInputData: if fmDigitalMain <> nil then
 begin
 fmDigitalMain.SetData(Buffer, Size);
 end;
 end;
end;
По плагинам это всё. 
==== Ядро ====
Прежде всего следует подумать об инкапсуляции функций подключённого плагина в класс. Этот класс реализован в Modules.pas. При создании экземпляра класса происходит поиск и запоминание всех адресов функций плагина. Последующие вызовы функций происходят в одноимённых методах класса в том случае, если они не равны. Я приведу только описание типа класса: 
type
 // описания типов функций модуля
 TGetModuleInfo = function: TModuleInfo; stdcall;
 TInitialize = procedure; stdcall;
 TRun = procedure; stdcall;
 TTerminate = procedure; stdcall;
 TSetData = procedure(Kind: TDataKind; const Buffer; Size: Integer); stdcall;
 // непосредственно сам класс
 TModule = class
 private
 FFileName: string; // имя файла
 FHandle: THandle; // дескриптор библиотеки
 FModuleInfo: TModuleInfo; // информация о модуле
 // адреса функций плагина
 FGetModuleInfo: TGetModuleInfo; // функция получения информации о модуле
 FInitialize: TInitialize; // процедура инициализации 
 FRun: TRun; // процедура запуска
 FTerminate: TTerminate; // процедура останова
 FSetData: TSetData; // процедура передачи данных
 public
 constructor Create(AFileName: string; var IsValidModule: Boolean);
 destructor Destroy; override;
 // вызов функций плагина
 function GetModuleInfo: TModuleInfo;
 procedure Initialize;
 procedure Run;
 procedure Terminate;
 procedure SetData(Kind: TDataKind; const Buffer; Size: Integer);
 // свойства плагина
 property FileName: string read FFileName;
 property Handle: THandle read FHandle;
 property ModuleInfo: TModuleInfo read FModuleInfo;
 end;
Как видно из текста, это простая надстройка над плагином, не добавляющая функциональности, но позволяющая хранить всё в одном объекте. 
Теперь осталось только собрать плагины и запустить. Сбор информации и запуск происходит по нажатию одноимённой кнопки на главной форме. Как собирать плагины - дело вкуса. В этом примере я сканирую заданный каталог, можно хранить в INI-файле, реестре, можно придумать свой формат хранения. Сбор плагинов: 
// нажатие кнопки запуска
procedure TfmMain.btStartClick(Sender: TObject);
 // добавление плагинов в список
 procedure AppendModulesList(FileName: string);
 var
 Module: TModule;
 IsValid: Boolean;
 begin
 // создание экземпляра плагина
 Module := TModule.Create(FileName, IsValid);
 // если создан некорректно
 if not IsValid then
 // удаление
 Module.Free
 else
 begin
 // добавление
 SetLength(ModulesList, Length(ModulesList)+1);
 ModulesList[Length(ModulesList)-1] := Module;
 end;
 end;
var
 sr: TSearchRec;
 i: Integer;
begin
 // построение списка модулей
 SetLength(ModulesList, 0);
 // поиск файлов *.dll
 if FindFirst(edPath.Text + '*.dll', faAnyFile and not faDirectory, sr) = 0 then
 begin
 AppendModulesList(edPath.Text + sr.Name);
 while FindNext(sr) = 0 do
 AppendModulesList(edPath.Text + sr.Name);
 end;
 // запуск найденных модулей
 if Length(ModulesList) > 0 then
 begin
 for i := 0 to Length(ModulesList)-1 do
 begin
 // инициализация
 ModulesList[i].Initialize;
 // передача Application
 ModulesList[i].SetData(dkApplication, Application, SizeOf(Application));
 // запуск плагина
 ModulesList[i].Run;
 end;
 // старт таймера
 Events.Enabled := True;
 end;
end;
Мне кажется, что я достаточно подробно описал в комментариях производимые действия :) Ну и последнее - засылка данных по таймеру: 
procedure TfmMain.EventsTimer(Sender: TObject);
var
 Values: array [0..1] of Word;
 i: Integer;
begin
 // формирование случайных значений
 Values[0] := Random($ffff);
 Values[1] := Random($ffff);
 // передача данных
 if Length(ModulesList) > 0 then
 for i := 0 to Length(ModulesList)-1 do
 begin
 ModulesList[i].SetData(dkInputData, Values, SizeOf(Values));
 end;
end;
Желательно не забывать об освобождении модулей
Source: Взято с http://delphiworld.narod.ru
ID: 01998
Category:Статьи