Максим Мазитов дата публикации 14-01-2002 16:31 Использование DLL в качестве PlugIn-ов В темах для написания статей раздела "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 при закрытии программы висит, а затем "Access 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;
Функция возвращает информацию о модуле. В данном случае это цифровое отображение, путь и тип модуля.
// инициализация
procedure Initialize;stdcall;
begin
// запоминание старого Application
OldApp:=Application;
fmDigitalMain:=nil;
end;
Процедура запоминает переменную Application и делает нулевой ссылку на форму плагина.
// запуск
procedure Run;stdcall;
begin
// создание окна плагина
if fmDigitalMain=nil then
fmDigitalMain:=TfmDigitalMain.Create(Application);
end;
Процедура запуска плагина создаёт окно. Окно создаётся видимым.
// останов
procedure Terminate;stdcall;
begin
// освобождение окна
fmDigitalMain.Free;
fmDigitalMain:=nil;
// восстановление старого TApplication
Application:=OldApp;
end;
Процедура уничтожает окно и возвращает старый TApplication.
// приём данных
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;
Процедура получения данных. В зависимости от полученного типа данных с данные в переменной Buffer соответственно типизируются. Здесь происходит обращение к форме плагина, расписывать я его не буду, там всё просто, см. исходники. Типы, которые используются здесь, описаны в SharedTypes.pas
По плагинам это всё.
Прежде всего следует подумать об инкапсуляции функций подключённого плагина в класс. Этот класс реализован в 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;
Желательно не забывать об освобождении модулей. Это уже в самом конце (см. исходные тексты).
И вот как это всё выглядит.
В этом тексте я привёл костяк возможного решения построения модульной системы. Вопросы, наезды и пожелания принимаются.
Максим Мазитов
Специально для Королевства Delphi
[TObject] [TApplication] [Модель плагинов] [Экспорт/импорт функций]
Обсуждение материала [ 28-05-2007 11:40 ] 21 сообщение |