Полезное для программистов:

Фриланс
Новости
Статьи
   
Рубрики:

Error. Page cannot be displayed. Please contact your service provider for more details. (14)


Использование 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;


// Функция возвращает информацию о модуле. В данном
// случае это цифровое отображение, путь и тип модуля.

// инициализация
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;


Желательно не забывать об освобождении модулей
Автор: DRKB
Сайт: http://delphiworld.narod.ru






Просмотров: 2399

 

 

Новые статьи:


Популярные:
  1. Как сделать цикличным проигрывание MIDI-файла?
  2. Создание AVI файла из рисунков
  3. Как устройство "отключить в данной конфигурации"?
  4. Kто в данный момент присоединен через Сеть?
  5. Как узнать количество доступной памяти?
  6. Как реализовать в RichEdit разноцветный текст?
  7. Как скрыть свое приложение от ProcessViewer
  8. Как программно нажать/скрыть/показ кнопку "Start"?
  9. Модуль работы с ресурсами в PE файлах
10. Функции вызова диалоговых окон выбора
11. Проверка граматики средствами Word'а из Delphi.
12. Модуль для упрощенного вызова сообщений
13. Функции для записи и чтение своих данных в, ЕХЕ- файле
14. Рекурсивный просмотр директорий
15. Network Traffic Monitor
16. Разные модули
17. Универсальная функция для обращения к любым экспортируем функциям DLL
18. Библиотека от VladS
19. Протектор для UPX'а
20. Еще об ICQ, сообщения по контакт листу?
21. Использование открытых интерфейсов
22. Теория и практика использования RTTI
23. Работа с TApplication
24. Примеры использования Drag and Drop для различных визуальных компонентов
25. Что такое порт? Правила для работы с портами
26. Симфония на клавиатуре
27. Загрузка DLL
28. Исправление автоинкремента
29. Взаимодействие с чужими окнами
30. Проверить дубляжи в столбце


 

 

 
 
На главную