Использование компилятора Delphi (dcc32.exe) в прикладных программах

4. Исполнение кода Как я уже говорил, возможны различные варианты того, в какой вид будет скомпилирована задача, сформулированная технологом. Если технолог передает результаты своей работы конечному пользователю, то удобный вариант - exe-файл. Если технолог решает некоторую задачу и сразу же пользуется результатами решения, то сам факт компиляции должен быть для него полностью прозрачен (или максимально незаметен). Технолог работает в программе, которая сделана разработчиком и, по большому счету, ему совершенно безразлично, каким конкретно способом разработчик предоставляет возможность изменять функциональность программы. Существует несколько технологий построения гибко подгружаемых модулей, и они описаны в литературе. Я остановлюсь только на одной технологии - динамическая загрузка и выгрузка DLL. Если результирующий проект, который нужен технологу, содержит визуальные формы (а их можно генерировать как dfm-файлы), то вероятно, более предпочтительными будут пакеты. Возможен также вариант, когда исполняемый код делится на две составляющие - исполняемое ядро (exe-файл) и подгружаемый модуль. Ядро создается разработчиком и динамически подключает модули, создаваемые технологом. Достоинство такого подхода в том, что работу с визуальными компонентами можно сосредоточить в ядре, а в DLL формировать только алгоритмическую часть задачи. Другое достоинство такого подхода - технолог может работать в мощной интегрированной среде, а конечному пользователю он передает только ядро и нужный модуль, скрывая от пользователя все технологические детали. Для работы с DLL, в библиотеку DccUsing добавлен класс TDllWrap - простая оболочка, инкапсулирующая дескриптор загруженной DLL. Основные методы класса:

public
 constructor Create(const aDllPath: String);
 destructor Destroy; override;
 function  Execute(const aFunctionName: String;
  const aInterface: Pointer): Pointer;
Конструктор Create просто сохраняет путь к файлу DLL и больше ничего не делает, деструктор Destroy выгружает DLL из памяти, если она была загружена. Основную работу делает метод Execute - он вызывает экспортируемую функцию DLL по имени и передает ей указатель на интерфейс вызывающей части. Экспортируемая функция возвращает интерфейс вызываемой части. Более подробно о взаимодействии вызывающей и вызываемой частей поговорим в следующем разделе, а пока рассмотрим реализацию метода Execute.
function TDllWrap.Execute(const aFunctionName: String;
 const aInterface: Pointer): Pointer;
var
 f: TDllFunction;
begin
 if FDllInst = 0 then begin
  if not FileExists(FDllPath) then
  raise Exception.Create(SFileNotFound + FDllPath);
  FDllInst := LoadLibrary(PChar(FDllPath));
  if FDllInst = 0 then
  raise Exception.Create(SCantLoadDll +
  SysErrorMessage(GetLastError));
 end;
 f := TDllFunction(GetProcAddress(FDllInst,
  PChar(aFunctionName)));
 if not Assigned(f) then
  raise Exception.Create(SCantFindFunction + aFunctionName);
 result := f(aInterface);
end;

Вначале метод Execute контролирует - загружена ли DLL? и, если DLL еще не загружена, то она загружается. Если загрузка была успешной, то с помощью функции GetProcAddress получаем адрес экспортируемой функции по ее символическому имени (можно также использовать индекс). Если адрес функции успешно получен, то вызываем ее и передаем ей аргумент - указатель на вызывающий интерфейс. Функция возвращает указатель на вызываемый интерфейс. Из этой реализации видно, что вызывающая часть может обратиться с помощью метода Execute к нескольким различным функциям DLL или многократно к одной и той же функции - DLL будет загружена только один раз.
5. Взаимодействие с DLL
С самых общих позиций можно считать, что вызывающая (Master) и вызываемая (Slave) части обладают своими интерфейсами. Экспортируемая функция конструирует Slave-интерфейс и возвращает его. Экспортируемая функция играет в этом случае роль фабрики класса. Сигнатура экспортируемой функции выглядит так:

TDllFunction = function(aInterface: Pointer): Pointer; StdCall;

После вызова этой функции Master и Slave части взаимодействуют друг с другом через свои интерфейсы. В качестве интерфейса наиболее удобно использовать чистый абстрактный класс, например:

IMaster = class

public

 procedure Method1; virtual; abstract;

 .............

end;

Виртуальный абстрактный класс не содержит переменных, а все его методы - виртуальные и абстрактные. Декларация интерфейса включается в обе взаимодействующие части. Для реализации интерфейса создается класс, наследуемый от абстрактного интерфейса и переписывающий все его виртуальные методы. Интерфейсный объект Master-части конструируется и удаляется в основной программе. Интерфейсный объект Slave-части конструируется в экспортируемой функции DLL, а уничтожается в блоке finalization при выгрузке DLL или с помощью другой экспортируемой функции. Например:

uses

 UnitIMaster, UnitISlave;

type

 TSlaveObject = class(ISlave)

 private

  FMain: IMain;

 public

  constructor Create(aMain: IMain);

  destructor Destroy; override;

  procedure  Method1; override;

  ............

 end;

 function CreateSlave(aInterface: Pointer): Pointer; stdcall;

 function DestroySlave(aInterface: Pointer): Pointer; stdcall;

implementation

var

 SlaveObject: TSlaveObject;

// Реализация TSlaveObject

............

function CreateSlave(aInterface: Pointer): Pointer;

begin

 SlaveObject := TSlaveObject.Create(IMaster(aInterface));

 result := SlaveObject;

end;

function DestroySlave(aInterface: Pointer): Pointer;

begin

 SlaveObject.Free;

 SlaveObject := nil;

 result := nil;

end;

initialization

 SlaveObject := nil;

finalization

 SlaveObject.Free;

end.

Отправить комментарий

Проверка
Антиспам проверка
Image CAPTCHA
...