Модераторы: Poseidon, Snowy, bems, MetalFan
  

Поиск:

Ответ в темуСоздание новой темы Создание опроса
> Использование DLL в качестве Plug-in 
:(
    Опции темы
Poseidon
Дата 19.5.2005, 04:17 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Delphi developer
****


Профиль
Группа: Комодератор
Сообщений: 5273
Регистрация: 4.2.2005
Где: Гомель, Беларусь

Репутация: 53
Всего: 133



В темах для написания статей раздела "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. 

Немного остановлюсь на передаче данных. Паскаль при всей своей жёсткой типизации предоставляет приятное средство передачи в функцию нетипизированных данных. Если программа знает о том, какие именно данные пришли, оттипизировать smile их достаточно просто. Эт от способ передачи используется в 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; 


Мне кажется, что я достаточно подробно описал в комментариях производимые действия smile Ну и последнее — засылка данных по таймеру: 

Код

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; 


Желательно не забывать об освобождении модулей 


--------------------
Если хочешь, что бы что-то работало - используй написанное, 
если хочешь что-то понять - пиши сам...
PM MAIL ICQ   Вверх
  
Ответ в темуСоздание новой темы Создание опроса
Правила форума "Delphi: Общие вопросы"
SnowyMetalFan
bemsPoseidon
Rrader

Запрещается!

1. Публиковать ссылки на вскрытые компоненты

2. Обсуждать взлом компонентов и делиться вскрытыми компонентами

  • Литературу по Дельфи обсуждаем здесь
  • Действия модераторов можно обсудить здесь
  • С просьбами о написании курсовой, реферата и т.п. обращаться сюда
  • Вопросы по реализации алгоритмов рассматриваются здесь
  • 90% ответов на свои вопросы можно найти в DRKB (Delphi Russian Knowledge Base) - крупнейшем в рунете сборнике материалов по Дельфи


Если Вам понравилась атмосфера форума, заходите к нам чаще! С уважением, Snowy, MetalFan, bems, Poseidon, Rrader.

 
1 Пользователей читают эту тему (1 Гостей и 0 Скрытых Пользователей)
0 Пользователей:
« Предыдущая тема | Delphi: Общие вопросы | Следующая тема »


 




[ Время генерации скрипта: 0.1653 ]   [ Использовано запросов: 22 ]   [ GZIP включён ]


Реклама на сайте     Информационное спонсорство

 
По вопросам размещения рекламы пишите на vladimir(sobaka)vingrad.ru
Отказ от ответственности     Powered by Invision Power Board(R) 1.3 © 2003  IPS, Inc.