Rambler's Top100
"Knowledge itself is power"
F.Bacon
Поиск | Карта сайта | Помощь | О проекте | ТТХ  
 Hello, World!
  
 

Фильтр по датам

 
 К н и г и
 
Книжная полка
 
 
Библиотека
 
  
  
 


Поиск
 
Поиск по КС
Поиск в статьях
Яndex© + Google©
Поиск книг

 
  
Тематический каталог
Все манускрипты

 
  
Карта VCL
ОШИБКИ
Сообщения системы

 
Форумы
 
Круглый стол
Новые вопросы

 
  
Базарная площадь
Городская площадь

 
   
С Л С

 
Летопись
 
Королевские Хроники
Рыцарский Зал
Глас народа!

 
  
ТТХ
Конкурсы
Королевская клюква

 
Разделы
 
Hello, World!
Лицей

Квинтана

 
  
Сокровищница
Подземелье Магов
Подводные камни
Свитки

 
  
Школа ОБЕРОНА

 
  
Арсенальная башня
Фолианты
Полигон

 
  
Книга Песка
Дальние земли

 
  
АРХИВЫ

 
 

Сейчас на сайте присутствуют:
 
  
 
Во Флориде и в Королевстве сейчас  06:46[Войти] | [Зарегистрироваться]

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

Максим Мазитов
дата публикации 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 функций:

  1. Получение внутренней информации о плагине (в программе function GetModuleInfo:TModuleInfo). При наличии в библиотеке такой функции и правильном её вызове, мы будем знать что эта DLL — наш плагин. Сама функция может возвращать что угодно, например название и тип плагина.
  2. Формирование начальных значений (в программе procedure Initialize). Плагин приводит себя в порядок после загрузки, т.е. заполняет переменные значениями по умолчанию.
  3. Передача данных в плагин (в программе procedure SetData(Kind:TDataKind;const Buffer;Size:Integer)). Позволяет передавать данные в плагин.
  4. Получение данных — в программе не реализована, но делается по типу SetData.
  5. Запуск плагина (в программе Run). Запускается плагин. Действия могут быть различными: показ окна, модальный показ окна, расчёт какого-либо параметра и т.д.
  6. И есесьно останов плагина. Здесь действия обратные пункту 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 сообщение
  
Время на сайте: GMT минус 5 часов

Если вы заметили орфографическую ошибку на этой странице, просто выделите ошибку мышью и нажмите Ctrl+Enter.
Функция может не работать в некоторых версиях броузеров.

Web hosting for this web site provided by DotNetPark (ASP.NET, SharePoint, MS SQL hosting)  
Software for IIS, Hyper-V, MS SQL. Tools for Windows server administrators. Server migration utilities  

 
© При использовании любых материалов «Королевства Delphi» необходимо указывать источник информации. Перепечатка авторских статей возможна только при согласии всех авторов и администрации сайта.
Все используемые на сайте торговые марки являются собственностью их производителей.

Яндекс цитирования