Профессиональная разработка приложений с помощью Delphi 5

Часть 4. Создание и использование динамически загружаемых библиотек в Delphi

Сергей Трепалин

Создание простейшей DLL. Соглашения о вызовах методов

Статическая и динамическая загрузка DLL

Обмен данными с DLL

Вызов методов приложения в DLL

Модальные формы в DLL

Немодальные формы в DLL

Экспорт дочерних форм из DLL

Заключение

 

Динамически загружаемые библиотеки (dynamic-link libraries, DLL)  являются, пожалуй, одним из наиболее мощных средств создания приложений в Windows. По структуре данных DLL напоминает приложение — exe-файл, но в отличие от *.exe-приложения код в DLL не может выполняться самостоятельно. DLL (как и *.exe-файл) можно загрузить в память компьютера, и работающие приложения могут вызвать методы, экспонируемые в DLL. На основе DLL создаются также элементы управления ActiveX.

Рассмотрим преимущества использования DLL:

1. Методы, описанные в DLL, могут одновременно обслуживать несколько приложений. При этом сами методы хранятся в виде одной копии в ОЗУ. Если вызываемый код достаточно велик и имеется несколько приложений, которые вызывают данный код, то вследствие этого можно достичь существенной экономии системных ресурсов.

2. Возможность хранения общих ресурсов. Если несколько приложений работают с одними и теми же ресурсами (например, большие растровые картинки — *.bmp), то при сохранении их в DLL можно иметь эти ресурсы в одном экземпляре.

3. Поддержка новых версий приложений. Если программист  сделал какие-либо изменения в реализациях методов, определенных в DLL, то конечному потребителю достаточно передать новую версию DLL- и *.exe-файл можно сохранить прежним. Это особенно актуально сейчас, когда приложения можно обновлять с помощью Internet. В этом случае важно снизить количество информации, посылаемой по Сети. Естественно, что если часть кода реализована в DLL, то при загрузке с сервера только этой DLL трафик, связанный с обновлением версии приложения,  будет уменьшен.

4. Возможно использовать различные языки программирования для создания *.exe и *.dll. Например, *.ex-файл может компилироваться из кода, написанного на Delphi, а *.dll, которая им используется, компилируется из кода, написанного на Microsoft Visual C++. Если приложение использует несколько DLL, то они могут быть созданы на различных языках программирования. Это значительно упрощает перенос кода в другие приложения.

5. DLL можно загружать в память только тогда, когда они требуются для выполнения приложений, реализуя тем самым динамическую загрузку. При этом опять же достигается экономия системных ресурсов. Так, например, в DLL можно реализовать диалог, с помощью которого изменяются какие-либо параметры приложения. Пользователь может месяцами не обращаться к данному диалогу, и при этом DLL, в которой он реализован, не загружается в память и не потребляет системных ресурсов. Только в момент, когда пользователь обращается к этому диалогу, происходит загрузка DLL в память, но после выполнения диалога эта память освобождается. Используя динамическую загрузку, можно оформить динамический пользовательский интерфейс:соответствующие опции меню приложения появляются в том случае, если найдена данная DLL, и исчезают при ее отсутствии. Такой интерфейс удобен при поставке приложений, в которых пользователь может заказать дополнительные возможности за отдельную плату.

Создание простейшей DLL. Соглашения о вызовах методов

В состав Delphi входит эксперт для создания DLL, который вызывается при выборе команды File/New и пиктограммы DLL на странице  New репозитария объектов. При этом возникает заготовка для реализации DLL:

library FirstLib;  
uses  
  SysUtils,  
  Classes;  
   
begin  
end.  

В приведенном выше коде отсутствует текстовый комментарий, который генерируется экспертом. Заготовка отличается от заготовки для создания кода *.exe-файла тем, что используется служебное слово Library вместо Program. Кроме того, отсутствуют обращение к методам объекта TApplication (хотя экземпляр этого объекта в действительности создается в DLL!), а также модуль реализации главной формы. Создадим в данной заготовке метод, который будет симулировать выполнение каких-либо вычислений:

function AddOne(N:integer):integer; stdcall; export;  
begin  
  Result:=N+1;  
end;  

Как видно, код реализации метода AddOne включает два оператора, которые обычно не используются в реализации методов при создании приложения, — stdcall и export. Директива stdcall связана с соглашениями вызова методов. Рассмотрим их здесь подробнее.

Когда в приложении осуществляется вызов метода, его параметры (как и локальные переменные) помещаются в стек. Стек, представляющий собой зарезервированное место в ОЗУ компьютера, имеет указатель текущей позиции, который при старте приложения устанавливается на начало стека. При вызове метода в стек помещаются все локальные переменные и параметры метода, при этом указатель текущей позиции стека смещается вправо в соответствии с размером помещаемых в него данных. Если метод, в свою очередь, вызывает другой метод, то локальные переменные второго метода добавляются в стек, так же как и список параметров. После окончания работы второго метода происходит освобождение области памяти в стеке — для этого указатель текущей позиции стека смещается влево. И наконец, после окончания работы первого метода указатель текущей позиции стека смещается в первоначальное положение. Сказанное иллюстрируется рис. 1.

Ясно, что если приложение работает нормально, то после окончания выполнения цепочки методов указатель текущей позиции стека должен вернуться в первоначальное состояние, то есть  созданный стек должен быть очищен (stack cleanup). Если же указатель не возвращается, то происходит крах стека (stack crash) — этот термин не следует путать с очисткой стека. В этом случае приложение прекращает свою работу (никакие ловушки исключений не помогают) и, если оно выполняется под Windows 95 или Windows 98, чаще всего требуется перезагрузка операционной системы. Понятно, что возврат указателя стека в первоначальное состояние должен происходить по окончании работы метода. Но при этом существуют две возможности — возврат указателя на место может производить как вызываемый метод по окончании работы, так и вызывающий метод после завершения работы вызываемого метода. В принципе, в различных языках программирования реализуются обе указанные возможности — очищать стек могут и вызванный, и вызывающий методы. Поскольку модуль пишется на одном языке программирования, то эти проблемы скрыты от программиста: очистка стека производится по специфичному для данного языка протоколу. Но если используются различные модули, код для которых реализован на различных языках программирования, то возникают проблемы. Например, в C++ стек очищается в методе, который вызвал второй метод, после окончания его работы. В Delphi же стек очищается в том же самом методе, где он используется, перед окончанием его работы. Если *.exe-модуль, созданный на языке C++, вызывает метод из DLL, созданный на Delphi, то перед окончанием работы метода в DLL стек будет очищен. После этого управление передается модулю, реализованном на C++, который также попытается очистить стек, — такое действие приведет к краху стека.

Кроме этих возможностей, существует и другой способ, а именно: последовательность помещения в стек параметров метода. Предположим, имеется метод, который использует для работы два параметра:

procedure DoSomething(N:integer; D:TDateTime); 

Указанный способ заключается в том, что сначала в стек может быть помещена константа N, а затем D (слева направо) или вначале помещается константа D, а затем N (справа налево). Кроме того, некоторые языки программирования (в частности, Delphi) часть параметров метода вообще не помещают в стек, а передают их через регистры процессора. К тому же в разных языках программирования параметры могут помещаться в стек как слева направо, так и справа налево. Если они были помещены слева направо, а вызываемый метод будет читать справа налево, то получится путаница: в качестве значения константы N вызываемый метод будет считать значение правой половины константы D, а константу D он будет формировать из константы N и левой половины D.

По этой причине в любом языке программирования предусмотрена возможность объявить, какой из методов — вызываемый или вызывающий,  [U1] будет очищать стек и в какой последовательности параметры метода помещаются в стек. Такое объявление называется соглашением вызова (calling conversion); имеется ряд зарезервированных слов, которые помещаются после заголовка методов, как показано в таблице.

 

Директива Порядок следования параметров Очистка стека Использование регистров
register Слева направо Вызываемый метод +
pascal Слева направо Вызываемый метод -
cdecl Справа налево Вызывающий метод -
stdcall Справа налево Вызываемый метод -
safecall Справа налево Вызываемый метод -

 

Для методов, экспонируемых в DLL, рекомендуется (но не обязательно) использовать то же соглашение вызова, что и в Windows API. Для 32-разрядных приложений Windows API методы реализованы таким образом, что параметры помещаются в стек справа налево и стек очищает вызываемый метод; при этом не используются регистры процессора для передачи данных. Этим условиям удовлетворяет директива stdcall, которая в описанном выше примере помещается после заголовка метода AddOne. Если после заголовка метода отсутствует соглашение о вызове, то по умолчанию Delphi использует соглашение register.

Второе служебное слово в заголовке метода — export — информирует компилятор о том, что код для данного метода должен быть создан таким образом, чтобы его можно было вызывать из других модулей.  Эта директива требуется при реализации DLL в Delphi 3; в Delphi 4 и 5 ее можно опустить.

Однако написанного выше кода еще недостаточно для вызова метода AddOne из другого модуля. Одна DLL может предоставлять несколько методов внешнему модулю. Для того чтобы внешний модуль мог выбрать конкретный метод, в DLL должна присутствовать специальная секция, которая имеет заголовок exports (не путать с директивой export!). В нашем примере эту секцию можно объявить следующим образом:

exports 
  AddOne index 1 name ’CalculateSum’;  

Для экспонирования метода в секции exports просто приводится его название (AddOne), после которого следует либо служебное слово index с целочисленным идентификатором после него (идентификатор должен быть больше нуля), либо служебное слово name с текстовым идентификатором, либо оба вместе — как в данном случае. Внешний модуль может обращаться к конкретному методу как по индексу, так и по имени. Как это делается — будет рассказано в следующем разделе. На данном этапе изложения материала следует отметить, что название метода AddOne нигде и никогда не будет видно во внешних модулях — будет использоваться либо целочисленная константа 1, либо имя CalculateSum. Сразу же следует отметить, что имя чувствительно к регистру букв — метод не будет найден, если использовать, например, такие имена, как calculatesum или CALCULATESUM. Индексы, если они объявляются в секции exports, обязаны начинаться с 1 и принимать все целочисленные значения подряд (2, 3, 4…). Нельзя опускать какое-либо число в этой последовательности натуральных чисел — могут быть ошибки при импорте метода по индексу!

Недавно компания Microsoft объявила о том, что DLL должны экспортироваться по имени. Поэтому во вновь создаваемых DLL необходимо объявлять имя метода в секции exports, при этом индексы объявлять не следует (по крайней мере, не использовать их для определения адреса метода).

В начало В начало

Статическая и динамическая загрузка DLL

Модуль может вызывать методы другого модуля, а тот, в свою очередь, — следующего и т.д. Например, приложение вызывает DLL, а эта DLL вызывает методы другой DLL: так можно формировать длинные цепочки вызовов. Для вызова метода из другого модуля необходимо сначала загрузить его в память, а затем определить адрес метода. Существует два способа загрузки и определения адреса метода — статический и динамический.

При статической загрузке для вызова другого модуля следует в какой-либо из секций описать метод из DLL следующим образом:

function Add1(K:integer):integer; stdcall; external 'FirstLib.dll' name 'CalculateSum';  

или

function Add1(K:integer):integer; stdcall; external 'FirstLib.dll' index 1;  

Для тестирования необходимо описать в приложении внешний метод одним из вышеупомянутых способов и сделать, например, обработчик события OnClick кнопки, поставленной на форму вместе с компонентом TEdit:

procedure TForm1.Button1Click(Sender: TObject); 
var  
  N:integer;  
begin  
  N:=StrToInt(Edit1.Text);  
  N:=Add1(N);  
  Edit1.Text:=IntToStr(N);  
end;  

При нажатии кнопки будет вызываться метод из DLL. Обратите внимание на изменение имен метода: из обработчика события OnClick вызывается метод с именем Add1. Этот метод экспонируется в DLL под именем CalculateSum. В реализации DLL он имеет название AddOne.

При таком определении метода DLL будет загружена немедленно после старта приложения и выгружена вместе с его завершением. В приведенном выше примере следует обратить внимание на то, что после имени динамической библиотеки указано ее расширение (FirstLib.dll). Такая конструкция необходима для загрузки библиотеки в Windows NT, поскольку без расширения *.dll файл не будет найден! В Windows 95 расширение не обязательно.

При поиске DLL для загрузки первоначально определяется, была ли данная DLL уже загружена в память другим модулем. Если была — то извлекается адрес метода и передается приложению. Если же нет — то операционная система начинает ее поиск на диске. При этом, если путь при имени DLL не указан в явном виде, система ищет библиотеку в каталоге модуля, который старается загрузить DLL. Если не находит, то продолжает поиски в директориях WINDOWS и WINDOWS\SYSTEM (или WINNT, WINNT\SYSTEM, WINNT\SYSTEM32). После этого происходит поиск в каталогах, определенных в переменной среды Path. Если библиотека с заданным именем будет найдена, то она загрузится и приложение стартует. Если же нет — происходит исключение и приложение прекратит свою работу. Приложение прекращает работу также и в том случае, если не будет найден метод с данным именем (или индексом, если он импортируется по индексу).

Динамическая загрузка DLL позволяет загружать библиотеку только в тот момент, когда она требуется. Кроме того, если не будет найдена библиотека или метод, то это можно проанализировать и запустить приложение и в этом случае. Конечно, в такой ситуации следует информировать пользователя о невозможности вызвать метод из DLL — например, сделав невидимым элемент меню, который обращается к данному методу. Пример динамической загрузки DLL выглядит следующим образом:

type 
  TAddFunction=function(K:integer):integer; stdcall;  
   
procedure TForm1.Button2Click(Sender: TObject);  
var  
  Add1:TAddFunction;  
  HLib:THandle;  
  N:integer;  
begin  
  HLib:=0;  
  try  
    HLib:=LoadLibrary('FirstLib.dll');  
    if HLib>HINSTANCE_ERROR then begin  
      Add1:=GetProcAddress(HLib,'CalculateSum');  
      if Assigned(Add1) then begin  
        N:=StrToInt(Edit1.Text);  
        N:=Add1(N);  
        Edit1.Text:=IntToStr(N);  
      end else ShowMessage('Method with name CalculateSum was not found');  
    end else ShowMessage('Can not load library FirstLib.dll');  
  finally  
    if HLib>HINSTANCE_ERROR then FreeLibrary(HLib);  
  end;  
end;  

Первоначально определяется новый процедурный тип, например TAddFunction, который имеет такой же список параметров и такие же договоренности вызова, что и метод в DLL. Delphi в процессе компиляции приложения никак не может проверить соответствие процедурного типа  методу, вызываемом из DLL. Проверка может быть осуществлена только во время выполнения, а при несоответствии формальных параметров или неврно указанной договоренности вызова происходит крах стека, что программист обнаружит очень быстро.

Далее в коде приложения вызывается метод LoadLibrary, который в качестве параметра использует имя библиотеки. После успешной отработки данного метода и загрузки библиотеки в память указатель на загруженную библиотеку помещается в переменную HLib. Если библиотеку не удается найти (или загрузить), то в эту же переменную помещается код ошибки. Чтобы определить, была ли загружена библиотека, переменную HLib следует сравнить с константой HINSTANCE_ERROR, которая определена в модуле Windows. Если библиотека была успешно загружена, то осуществляется попытка найти адрес метода в памяти компьютера при помощи вызова метода GetProcAddress. Указанный метод возвращает адрес того метода, имя которого указано во втором параметре GetProcAddress. Если же метод не был найден, то возвращается nil-адрес.

Соответственно вызов метода Add1 осуществляется только в случае, если он был успешно найден. Затем, поскольку при загрузке библиотеки были заняты системные ресурсы, их необходимо вновь вернуть операционной системе, выгрузив библиотеку из памяти. Для этого вызывается метод FreeLibrary. При загрузке DLL производится подсчет ссылок, а именно: при каждом успешном обращении к методу LoadLibrary в DLL счетчик ссылок увеличивается на единицу, а при каждом вызове метода FreeLibrary счетчик ссылок уменьшается на единицу. Как только счетчик ссылок станет равным нулю, библиотека выгружается из памяти компьютера. Следовательно, каждому успешному вызову LoadLibrary должно соответствовать обращение к FreeLibrary — иначе DLL не выгрузится из памяти компьютера даже после окончания работы приложения. Поэтому данные методы были помещены в защищенный блок try…finally..end; — это гарантирует вызов метода FreeLibrary, если происходит исключение.

Метод GetProcAddress для загрузки DLL может также использовать индексы. Для примера, представленного выше, в котором метод AddOne экспонируется с индексом 1, использование индексов в GetProcAddress выглядит следующим образом:

Add1:=GetProcAddress(HLib,pchar(1));  

Помните, что при использовании индексов следует соблюдать осторожность. Для корректного использования индексов необходимо, чтобы все методы в DLL были проиндексированы с значениями индексов от 1 до N (N — число методов, обьявленных в секции exports). Если какой-либо индекс опускается (в этом случае максимальное значение индекса будет больше числа методов), то GetProcAddress возвращает ненулевой адрес для несуществующего индекса! Ясно, что такой адрес является недействительным и при попытке обращения к нему генерируется исключение. По-видимому, по причине некорректной работы метода GetProcAddress Microsoft запрещает использовать индексы для импорта методов из DLL.

Теперь следует рассмотреть, каким образом загружаемая DLL размещается в ОЗУ компьютера. При загрузке DLL осуществляется резервирование памяти, необходимое для хранения кода методов. Кроме того, резервируется место для всех глобальных переменных и выполняются секции инициализации в модулях DLL. Если другой процесс также пытается загрузить DLL, то вновь происходит резервирование памяти для хранения глобальных переменных. Однако копирование методов DLL не осуществляется;  также не выполняется и секция инициализации. Другими словами, одна копия метода в ОЗУ обслуживает несколько приложений. Глобальные переменные являются уникальными для каждого приложения, и если одно приложение изменит их значение при помощи вызова какого-нибудь метода, то другое приложение этого не заметит. Поскольку секция инициализации выполняется только при первой загрузке DLL, ее нельзя использовать для установки начальных значений глобальных переменных. Вышесказанное необходимо  учитывать при разработке DLL.

В начало В начало

Обмен данными с DLL

DLL имеет общее адресное пространство с приложением, из которого вызываются его методы. Из этого следует, что указатель на какой-либо объект в памяти DLL является легальным внутри приложения, и наоборот. Это позволяет передать, например, адрес метода или адрес данных, чего нельзя сделать без использования маршрутизации при взаимодействии двух приложений. Имеется, однако, существенное отличие от передачи данных между двумя методами одного модуля: в различных модулях разными являются и диспетчеры памяти (memory manager). Это означает, что если в каком-то модуле (например, в DLL) был вызван метод GetMem, то освободить системные ресурсы вызовом метода FreeMem можно только в том же самом модуле. Если попытаться вызвать метод FreeMem в приложении (для примера выше), то происходит исключение. Поскольку при создании экземпляров класса также происходит обращение к диспетчеру памяти, то их деструкторы нельзя вызвать за пределами модуля. В частности, если в DLL происходит исключение, то создается объект в диспетчере памяти DLL. Если не использовать ловушки исключений, то этот объект попадает в приложение и после показа пользователю сообщения приложение попытается его разрушить. В результате вновь произойдет исключение. Поэтому все экспонируемые в DLL методы, в которых могут произойти исключения, должны иметь ловушку исключения:

try 
  {Main code should inserted here}  
  {Resource cleanup here}  
except  
  On E:exception do begin  
    ShowMessage(E.Message);  
    {Resource cleanup. Do not use RAISE directive here!}   
  end;  
end;  

Следует иметь в виду, что ни в коем случае нельзя использовать директиву Raise в секции except…end! В этой секции нужно просто показать пользователю сообщение о возникновении исключения (или не показывать его, если условия работы приложения позволяют сделать это).

По этой же причине (различные диспетчеры памяти) нельзя использовать строки Delphi для передачи данных между модулями. Строки Delphi — это объекты, и при изменении их содержимого происходит перераспределение памяти. Это вызовет исключение, если перераспределение памяти происходит в другом модуле. Поэтому следует использовать переменные типа PChar для обмена текстовой информации с DLL.

Типичный обмен текстовой информацией с DLL выглядит следующим образом. Если необходимо передать какую-либо строку в метод DLL, то можно просто использовать в методе указатель на строку:

procedure SendString(P:pchar); stdcall; external 'FirstLib.dll' name 'SendString';  
   
procedure TForm1.Button5Click(Sender: TObject);  
var  
  S:string;  
begin  
  S:='This test string will be sended to DLL';  
  SendString(pchar(S));  
end;  
   
Метод SendString в DLL реализован следующим образом:  
   
procedure SendString(P:pchar); stdcall; export;  
var  
  S:string;  
begin  
  S:=StrPas(P);  
  ShowMessage(S);  
end;  

При запуске данного примера появится сообщение, которое показывает содержимое строки, созданной в *.exe-модуле. Для того чтобы получить текстовую информацию из DLL, в приложении обычно создается буфер, который заполняется в DLL. Естественно, размер буфера должен быть достаточным для хранения всей текстовой информации. Чтобы обезопасить буфер от переполнения, вместе с буфером в качестве параметра чаще всего посылается его размер. Типичный пример получения текстовой информации из DLL выглядит следующим образом:

procedure ReceiveString(P:pchar; Size:integer); stdcall; external 'FirstLib.dll'    name 'ReceiveString'; 
   
procedure TForm1.Button6Click(Sender: TObject);  
var  
  C:array[0..1000] of char;  
  S:string;  
begin  
  ReceiveString(C,1000);  
  S:=StrPas(C);  
  Caption:=S;  
end;  

Метод ReceiveString реализован в DLL:

procedure ReceiveString(P:pchar; Size:integer); stdcall; export; 
var  
  S:string;  
  N:integer;  
begin  
  FillMemory(P,Size,0);  
  if InputQuery('The string will be transferred to application','Type text',S) then begin  
    N:=length(S);  
    if N>Size then N:=Size;  
    if N>0 then System.Move(S[1],P[0],N);  
  end;  
end;  

Именно таким образом работает большинство методов Windows API, предоставляющих в приложение текстовую информацию.

Можно использовать и переменную типа PChar для получения указателя из DLL, но в этом случае в DLL должна быть глобальная переменная, куда помещают текстовую информацию:

procedure ReceiveBuffer(var P:pchar); stdcall; external 'FirstLib.dll' name    'ReceiveBuffer'; 
   
procedure TForm1.Button7Click(Sender: TObject);  
var  
  P:pchar;  
  S:string;  
begin  
  ReceiveBuffer(P);  
  S:=StrPas(P);  
  Caption:=S;  
end;  

В DLL данный метод реализован так:

var {The variable can not be local!} 
  Buffer:array[0..1000] of char;  
   
procedure ReceiveBuffer(var P:pchar); stdcall; export;  
var  
  S:string;  
  N:integer;  
begin  
  FillMemory(@Buffer[0],1001,0);  
  if InputQuery('The string will be transferred to application','Type text',S) then begin  
    N:=length(S);  
    if N>1000 then N:=1000;  
    if N>0 then System.Move(S[1],Buffer[0],N);  
  end;  
  P:=@Buffer[0];  
end;  

Буфер нельзя определять как локальную переменную — после отработки метода ReceiveBuffer в DLL тот стек, куда помещаются локальные переменные, будет разрушен и возвращаемый указатель будет указывать на недоступную область памяти.

Аналогично, с использованием буфера, можно передавать любые двоичные данные между приложением и DLL. Но если размер двоичных данных варьируется в широких пределах, могут возникнуть проблемы, связанные с размером буфера. Например, размер OLE-документов может быть от нескольких десятков байт до нескольких десятков мегабайт. При использовании описанной выше технологии размер буфера должен быть равным максимально возможному размеру данных. Но если объявить буфер размером, скажем, 50 Мбайт, то большинство современных компьютеров начнет создавать временное хранилище на диске. При этом передача даже небольших документов будет занимать заметное время.

Выход из данной ситуации состоит в резервировании памяти для хранения объекта в DLL и освобождении системных ресурсов в приложении, но без использования диспетчеров памяти приложения и DLL! Пример приведен ниже:

procedure ReceiveWinAPI(var HMem:integer); stdcall; external 'FirstLib.dll'    name 'ReceiveWinAPI'; 
   
procedure TForm1.Button8Click(Sender: TObject);  
var  
  H:integer;  
  P:pchar;  
  S:string;  
begin  
  S:='';  
  ReceiveWinAPI(H);  
  if H>0 then begin  
    P:=GlobalLock(H);  
    S:=StrPas(P);  
    GlobalUnlock(H);  
    GlobalFree(H);  
  end;  
  Caption:=S;  
end;  

Реализация в DLL метода ReceiveWinAPI:

procedure ReceiveWinAPI(var HMem:integer); stdcall; export; 
var  
  S:string;  
  N:integer;  
  P:pchar;  
begin  
  HMem:=0;  
  if InputQuery('The string will be transferred to application','Type text',S) then begin  
    N:=length(S);  
    if N>0 then begin  
      HMem:=GlobalAlloc(GMEM_DDESHARE or GMEM_MOVEABLE or GMEM_ZEROINIT,N+1);  
      if HMem>0 then begin  
        P:=GlobalLock(HMem);  
        if Assigned(P) then System.Move(S[1],P[0],N);  
      end;  
    end;  
  end;  
end;  

Здесь память резервируется в DLL, а освобождается в приложении. Для резервирования и освобождения памяти используются методы Windows API GlobalAlloc и GlobalFree соответственно. Памяти резервируется ровно столько, сколько необходимо для хранения объекта. Для приведенного выше примера с OLE-документами это означает, что в большинстве случаев не будет происходить обращение к виртуальной памяти на диске и, следовательно, обмен данными будет совершаться быстро.

В начало В начало

Вызов методов приложения в DLL

Ранее мы рассматривали только такие варианты, когда методы DLL вызываются из приложения. Но часто требуется, чтобы DLL вызывала методы приложения, например для нотификационных сообщений.

Если необходимо вызвать из приложения метод, который при этом не является методом класса, то достаточно передать указатель на метод в DLL:

var 
  NSum:integer=0;  
   
function CalculateSum(ReturnCallback:pointer):integer; stdcall;  external 'FirstLib.dll' name 'Sum';  
   
function GetNextValue:integer; stdcall;  
begin  
  if NSum<200 then begin  
    Inc(NSum);  
    Result:=NSum;  
  end else Result:=-1;  
end;  
   
procedure TForm1.Button9Click(Sender: TObject);  
begin  
  Caption:=IntToStr(CalculateSum(@GetNextValue));  
end;  

В приложении создается метод (не метод класса!), адрес которого передается в DLL. Для того чтобы данный метод можно было вызывать из DLL, созданных на других языках программирования, желательно использовать соглашение stdcall вызова при реализации  указанных методов (так называемые callback-методы). Использование вызова метода в DLL можно проиллюстрировать на таком примере:

type 
  TReturnNextMethod=function:integer; stdcall;  
   
function CalculateSum(ReturnCallback:pointer):integer; stdcall; export;  
var  
  N:integer;  
  ReturnNext:TReturnNextMethod;  
begin  
  Result:=0;  
  if ReturnCallback=nil then Exit;  
  N:=0;  
  ReturnNext:=TReturnNextMethod(ReturnCallback);  
  while N>=0 do begin  
    N:=ReturnNext;  
    if N>=0 then Result:=Result+N;  
  end;  
end;  

При необходимости вызвать метод объекта следует учитывать, что данный метод объекта характеризуется двумя адресами — адресом метода и адресом данных. Следовательно, необходимо передавать два указателя. Но вместо передачи двух указателей можно воспользоваться структурой TMethod, определенной в модуле SysUtils.pas:

type 
  TMethod = record  
    Code, Data: Pointer;  
  end;  

Код в приложении для приведенного выше примера выглядит так:

type 
  TGetNextValueObject=function:integer of object; stdcall;  
   
function CalculateSumObject(Method:TMethod):integer; stdcall;  external 'FirstLib.dll' name 'SumObject';  
   
function TForm1.GetNextValueObject:integer; stdcall;  
begin  
  if NSum<10 then begin  
    Inc(NSum);  
    Result:=NSum;  
  end else Result:=-1;  
end;  
   
procedure TForm1.Button10Click(Sender: TObject);  
var  
  FGetNext:TGetNextValueObject;  
begin  
  NSum:=0;  
  FGetNext:=GetNextValueObject;  
  Caption:=IntToStr(CalculateSumObject(TMethod(FGetNext)));  
end;  

Код в DLL, осуществляющий вызов метода объекта, выглядит следующим образом:

type 
  TReturnNextMethodObject=function:integer of object; stdcall;  
   
function CalculateSumObject(Method:TMethod):integer; stdcall; export;  
var  
  N:integer;  
  ReturnNext:TReturnNextMethodObject;  
begin  
  Result:=0;  
  N:=0;  
  ReturnNext:=TReturnNextMethodObject(Method);  
  while N>=0 do begin  
    N:=ReturnNext;  
    if N>=0 then Result:=Result+N;  
  end;  
end;  

Следует учитывать, что методы объекта являются зависимыми от языка, то есть в разных языках программирования генерируются различные способы передачи данных в метод объекта. Поэтому данный пример следует использовать только в случае, если и приложение, и DLL написаны на Delphi (или на одном и том же языке программирования).

И наконец, само приложение может экспонировать методы таким же способом, что и DLL. В приложении можно создать секцию exports и объявить имена (и/или индексы) методов. После этого в DLL можно воспользоваться методом GetProcAddress для получения указателя на метод и вызвать его. Для описанного выше примера код приложения будет такой:

function GetNextValueExport:integer; stdcall; export; 
begin  
  if NSum<10 then begin  
    Inc(NSum);  
    Result:=NSum;  
  end else Result:=-1;  
end;  
   
function CalculateSumExport(HInstance:integer; MethodName:pchar):integer; stdcall;  external 'FirstLib.dll' name 'SumExport';  
   
procedure TForm1.Button11Click(Sender: TObject);  
var  
  N:integer;  
begin  
  NSum:=0;  
  N:=CalculateSumExport(HInstance,'GetNextValueExport');  
  Caption:=IntToStr(N);  
end;  
   
exports {!! The section is announced in Application !!}  
  GetNextValueExport index 1 name 'GetNextValueExport';  

Обратите внимание: теперь в приложении (в проекте, где будет генерироваться *.exe-файл[U2] [NE3] ) определена секция exports! Соответствующий код в DLL для тестирования данного метода выглядит следующим образом:

type 
  TReturnNextMethodExport=function:integer; stdcall;  
   
function CalculateSumExport(AppInstance:integer; MethodName:pchar):integer; stdcall; export;  
var  
  ReturnNextExport:TReturnNextMethodExport;  
  N:integer;  
begin  
  Result:=0;  
  N:=0;  
  ReturnNextExport:=GetProcAddress(AppInstance,MethodName);  
  while N>=0 do begin  
    N:=ReturnNextExport;  
    if N>=0 then Result:=Result+N;  
  end;  
end;  

При запуске этого проекта приложение автоматически загружает DLL и находит адрес метода в DLL CalculateNextExport (который экспортируется по имени SumExport). Загруженная DLL в качестве параметров получает заголовок модуля приложения и имя метода, под которым [U4] он экспортируется в приложении. Далее DLL использует метод GetProcAddress для нахождения адреса метода, экспортируемого приложением. Для того чтобы был возвращен адрес метода, в приложении объявляется секция exports, где описано внешнее имя метода. Этот пример показывает формально одинаковые структуру и способ формирования *.exe- и *.dll-файлов.

В начало В начало

Модальные формы в DLL

В DLL можно не только выполнять вычисления, но и показывать формы, например диалоги. Для показа диалога в DLL следует открыть проект реализации DLL,  создать модуль с формой и поместить на нее необходимые элементы управления вместе с обработчиками событий. Затем следует создать экспортируемый метод, который выполнит диалог:

function ExecDialog(AppHandle:THandle; var PictName:pchar):boolean; stdcall;    export; 
var  
  FDialog:TForm1;  
begin  
  FDialog:=nil;  
  PictName:=nil;  
  Result:=False;  
  Application.Handle:=AppHandle; {Two icons are arisen at taskbar without this operator. Warning while dynamic loading!}  
  try  
    FDialog:=TForm1.Create(Application);  
    if FDialog.ShowModal=mrOK then begin  
      FillMemory(@C[0],1000,0);  
      if length(FDialog.Edit1.Text)>0 then  
      StrPCopy(C,FDialog.Edit1.Text);  
      PictName:=@C[0];  
      Result:=True;  
    end;  
    FDialog.Release;  
    FDialog:=nil;  
    {!!! Case dynamic loading, one has to use method Free instead of Release!}  
  except  
    On E:exception do begin  
      ShowMessage(E.Message);  
      if Assigned(FDialog) then FDialog.Release;  
    end;  
  end;  
end;  

Данный код вызова диалога следует применять только при статической загрузке DLL. При выполнении диалога в DLL нужно учитывать, что формы в DLL не могут создаваться вместе с запуском DLL, как это возможно в приложении при установке опции Auto-Create Form в разделе Project/Options/Forms. Поэтому форму необходимо создавать динамически, вызывая ее конструктор Create из кода. Соответственно перед выходом из процедуры, вызывающей форму, необходимо вызвать ее деструктор (в нашем примере — FDialog.Release). Необходимо учитывать, что в DLL создается объект типа TApplication. Поскольку и само приложение имеет данный объект, то (если не принимать никаких мер) на экране в панели приложений появятся две пиктограммы: одна — для приложения, другая — для DLL, выполняющей диалог (рис. 2).

При этом после нажатия на пиктограмму приложения оно активируется и всплывает главная форма, но доступ к элементам управления главной формы получить нельзя. Ясно, что такое поведение является некорректным. Поэтому в качестве параметра метода в DLL, выполняющего диалог, необходимо использовать ссылку на объект TApplication (точнее, его свойство Handle) приложения. Посредством присвоения Application.Handle:=AppHandle; в DLL уничтожается объект TApplication и приложение начинает поддержку рассылки сообщений элементам управления, созданным в DLL. В этом случае на панели задач присутствует одна пиктограмма приложения — это корректно. Приведем типичный пример кода основного приложения, вызывающего диалог из DLL:

function ExecDialog(AppHandle:THandle; var PictName:pchar):boolean; stdcall;    external 'FirstLib.dll' name 'ExecDialog'; 
   
procedure TForm1.Button3Click(Sender: TObject);  
var  
  P:pchar;  
  S:string;  
begin  
  if ExecDialog(Application.Handle,P) then begin  
    S:=P;  
    Caption:=S;  
  end;  
end;  

При использовании динамической загрузки DLL, возникает ряд сложностей.

Первая заключается в том, что, как правило, загрузка и выгрузка библиотеки, в которой выполняется диалог, осуществяются в пределах одного метода:

HLib:=LoadLibrary('FirstLib.dll'); 
if HLib>HINSTANCE_ERROR then begin  
  ExecDialog:=GetProcAddress(HLib,'ExecDialog');  
  if Assigned(ExecDialog) then begin  
    if ExecDialog(Application.Handle,P) then {...};  
  end else ShowMessage('Method with name ExecuteDialog was not found');  
   FreeLibrary(HLib);  
end else ShowMessage('Can not load library FirstLib.dll');  

В этом случае библиотека выгружается немедленно после выполнения диалога, а метод Release, который рекомендуется использовать для вызова деструктора формы, посылает сообщения CM_RELEASE самой форме посредством вызова метода PostMessage. Этот метод ставит сообщение в конец очереди, приложение продолжает выполнять код — и выгружает DLL! Только после выполнения кода начинается обработка очереди сообщений, в конце концов достается сообщение CM_RELEASE, делается попытка выполнить деструктор формы — но методы-то уже выгружены! Если операционная система обладает значительным резервом ресурсов, то велика вероятность, что место в ОЗУ, где хранился код, сохранит свое содержимое и форма диалога будет разрушена успешно. Но при небольшом количестве ресурсов на освободившееся место в ОЗУ немедленно помещаются какие-либо данные из виртуальной дисковой памяти, а попытка выполнить деструктор кончается исключением. Поэтому при динамической загрузке обязательно нужно использовать метод Free вместо Release. Кроме того, перед выгрузкой DLL рекомендуется вызвать метод Application.ProcessMessages.

Вторая проблема состоит в следующем. Если использовать один и тот же объект TApplication в приложении и DLL посредством выполнения приведенного выше оператора —  Application.Handle:=AppHandle; — перед вызовом метода ShowModal в DLL, то после выгрузки DLL главная форма приложений минимизируется и ее необходимо восстанавливать вновь. Один из способов решения проблемы — вызвать метод ShowWindow(Handle,SW_RESTORE) сразу же после выполнения команды FreeLibrary в приложении. Однако при этом главная форма приложения будет мерцать. Другой способ — оставить разные объекты TApplication в приложении и DLL,— для этого не надо выполнять оператор Application.Handle:=AppHandle; в DLL. Но тогда на панели задач появляются две пиктограммы. Корректное решение проблемы — присвоить нулевое значение свойству Application.Handle в DLL перед выходом из метода. Ниже приведен рекомендуемый код в DLL, которая выполняет диалог при динамической загрузке:

function ExecDialog(AppHandle:THandle; var PictName:pchar):boolean; stdcall;    export; 
var  
  FDialog:TForm1;  
begin  
  FDialog:=nil;  
  PictName:=nil;  
  Result:=False;  
  Application.Handle:=AppHandle;  
  try  
    FDialog:=TForm1.Create(Application);  
    if FDialog.ShowModal=mrOK then begin  
      FillMemory(@C[0],1000,0);  
      if length(FDialog.Edit1.Text)>0 then  
      StrPCopy(C,FDialog.Edit1.Text);  
      PictName:=@C[0];  
      Result:=True;  
    end;  
    FDialog.Free;  
    FDialog:=nil;  
  except  
    On E:exception do begin  
      ShowMessage(E.Message);  
      if Assigned(FDialog) then FDialog.Free;  
    end;  
  end;  
  Application.ProcessMessages;  
  Application.Handle:=0;  
end;  

Приведем код приложения, динамически вызывающего данную DLL:

type 
  TExecDialog=function(AppHandle:THandle; var PictName:pchar):boolean; stdcall;  
   
procedure TForm1.Button4Click(Sender: TObject);  
var  
  ExecDialog:TExecDialog;  
  HLib:THandle;  
  P:pchar;  
  S:string;  
begin  
  HLib:=0;  
  try  
    HLib:=LoadLibrary('FirstLib.dll');  
    if HLib>HINSTANCE_ERROR then begin  
      ExecDialog:=GetProcAddress(HLib,'ExecDialog');  
      if Assigned(ExecDialog) then begin  
        if ExecDialog(Application.Handle,P) then begin  
          S:=P;  
          Caption:=S;  
        end;  
      end else ShowMessage('Method with name ExecuteDialog was not found');  
    end else ShowMessage('Can not load library FirstLib.dll');  
  finally  
    Application.ProcessMessages;  
    if HLib>HINSTANCE_ERROR then FreeLibrary(HLib);  
  end;  
end;  
В начало В начало

Немодальные формы в DLL

Показ немодальных форм традиционно осуществляется со статической загрузкой DLL. Вот типичный код для показа немодальной формы в DLL:

procedure ShowNonModalForm(AppHandle:THandle); stdcall; export;  
begin  
  Application.Handle:=AppHandle;  
  with TForm1.Create(Application) do Show;  
end;  

Как и при показе модальных форм, необходимо присвоить дескриптор окна главного приложения приложению, создаваемому в DLL, — иначе на панели задач будут показаны две иконки. Затем просто создается форма и вызывается ее метод Show. Такой способ показа немодальных форм приводит к тому, что из главного приложения можно неоднократно вызвать данный метод и тем самым создать несколько экземпляров форм. Зачастую это оправданно, поскольку одинаковые типы форм могут содержать, например, разные документы. Но при этом способе показа рекомендуется сделать обработчик события OnClose для TForm1 и присвоить параметру CloseAction значение caFree — в противном случае при закрытии форма будет спрятана [NE5][U6]  на экране без освобождения системных ресурсов.

Для показа единственного экземпляра немодальной формы следует немного изменить код:

procedure ShowSingleNonModalForm(AppHandle:THandle); stdcall; export;  
begin  
  Application.Handle:=AppHandle;  
  if Assigned(Form2) then Form2.Show else begin  
    Form2:=TForm2.Create(Application);  
    Form2.Show;  
  end;  
end;  

Первоначально необходимо проверить, была ли создана форма ранее. Если была — просто вызывается ее метод Show; если нет — вызывается тот же метод после отработки конструктора. Вызов метода Show для уже созданного экземпляра формы имеет смысл, поскольку пользователь может обратиться к команде показа формы в тех случаях, когда уже имеющийся экземпляр перекрыт другими окнами и незаметен на экране, — использование команды Show приводит к всплытию формы. Переменная Form2 является глобальной переменной.

Оба описанных выше способа вызова немодальных форм не требуют создания специальной процедуры для их разрушения. Ресурсы будут корректно освобождены при закрытии приложения, так как приложение является владельцем форм. Код приложения для тестирования этих методов выглядит следующим образом:

procedure ShowNonModalForm(AppHandle:THandle); stdcall; external 'NMStat.dll' name 'ShowNonModalForm';  
procedure ShowSingleNonModalForm(AppHandle:THandle); stdcall; external 'NMStat.dll' name 'ShowSingleNonModalForm';  
   
procedure TForm1.Button1Click(Sender: TObject);  
begin  
  ShowNonModalForm(Application.Handle);  
end;  
   
procedure TForm1.Button2Click(Sender: TObject);  
begin  
  ShowSingleNonModalForm(Application.Handle);  
end;  

Иногда возникает необходимость показа немодальных форм из динамически загружаемых DLL, например при редком использовании в приложении немодальных форм для экономии ресурсов. Если реализовать код так же, как и при показе модальных диалогов, то форма будет создана и, может быть, даже показана на экране. Но после этого произойдет выгрузка DLL, а затем немедленно последуют исключения, поскольку в памяти компьютера будет отсутствовать код для работы с элементами управления формы. Традиционное решение этой проблемы выглядит следующим образом: загружается динамическая библиотека, в качестве одного из параметров передается адрес метода главного приложения, который будет вызван при закрытии немодальной формы — обычно в обработчике события OnDestroy. Этот метод должен информировать главное приложение о необходимости выгрузки DLL из памяти компьютера, но DLL должна выгружаться после завершения его работы (и, следовательно, после завершения работы деструктора формы) — иначе возможно исключение из-за отсутствия кода в памяти компьютера. Выгрузка DLL после завершения работы приложения [U7] достигается с использованием асинхронной развязки — посылки сообщения методом PostMessage какому-либо окну приложения, обычно главной форме. Приведем код реализации данной технологии в DLL:

type  
  TNotifyClose=procedure; stdcall;  
   
  TForm1 = class(TForm)  
    Memo1: TMemo;  
    procedure FormDestroy(Sender: TObject);  
    procedure FormClose(Sender: TObject; var Action: TCloseAction);  
  private  
    { Private declarations }  
  public  
    { Public declarations }  
    FNC:TNotifyClose;  
  end;  
   
var  
  Form1: TForm1;  
   
implementation  
   
{$R *.DFM}  
   
procedure TForm1.FormDestroy(Sender: TObject);  
begin  
  if Assigned(FNC) then FNC;  
end;  
   
procedure DynNonmodal(AppHandle:THandle; NC:pointer); stdcall; export;  
begin  
  Application.Handle:=AppHandle;  
  if Assigned(Form1) then Form1.Show else begin  
    Form1:=TForm1.Create(Application);  
    Form1.FNC:=TNotifyClose(NC);  
    Form1.Show;  
  end;  
end;  

Приложение, использующее эту DLL, имеет следующий код (WM_DLLUNLOAD определена как константа в секции interface модуля):

type  
  TDynNonmodal=procedure(AppHandle:THandle; NC:pointer); stdcall;  
   
procedure ReceiveCloseNotify; stdcall;  
begin  
  Application.ProcessMessages;  
  PostMessage(Form1.Handle,WM_DLLUNLOAD,0,0);  
end;  
   
procedure TForm1.WMDLLUnload(var Message:TMessage);  
begin  
  Application.ProcessMessages;  
  if FHLib>HINSTANCE_ERROR then FreeLibrary(FHLIB);  
  FHLib:=0;  
  ShowMessage('Library unloaded');  
end;  
   
procedure TForm1.Button3Click(Sender: TObject);  
var  
  DM:TDynNonmodal;  
begin  
  if FHLIB<=HInstance_Error then FHLib:=LoadLibrary('NMDyn1.dll');  
  if FHLib>HInstance_Error then begin  
    DM:=GetProcAddress(FHLib,'DynNonmodal');  
    if Assigned(DM) then DM(Application.Handle,@ReceiveCloseNotify);  
  end;  
end;  

Очевидно, что код получается довольно громоздким: в главном приложении необходимо реализовывать три метода вместо одного. Альтернативный вариант можно предложить исходя из того, что в DLL имеется объект TApplication, который может поддерживать цикл выборки сообщений. Но в DLL нельзя создать форму, используя метод TApplication. CreateForm, так как соответствующая закладка диалога Project/Options/Forms отсутствует в проектах Delphi 4 и 5 и неактивна в Delphi 3. Однако можно вызвать все методы объекта Tapplication, вручную дописав соответствующий код в DLL:

procedure ShowNMApplication; stdcall; export;  
begin  
  if Assigned(Form1) then begin  
    Form1.Show;  
    Exit;  
  end else begin  
    Application.Initialize;  
    Application.CreateForm(TForm1, Form1);  
    Application.Run;  
    Form1.Free;  
    Form1:=nil;  
  end;  
end;  

Следует обратить внимание, что дескриптор главного приложения не присваивается в данном проекте дескриптору TApplication в DLL. Это реально приводит к появлению двух пиктограмм на панели приложений. Правда, в некоторых случаях это полезно — так легче добраться до перекрытых окон. Интересно отметить, что в Delphi 3 после написания данного кода становятся доступными элементы управления диалога Project/Options/Forms, где можно определить автоматически создаваемые формы и главную форму приложения. Код главного приложения, использующий данную DLL, таков:

type  
  TShowApp=procedure; stdcall;  
   
procedure TForm1.Button4Click(Sender: TObject);  
var  
  HLib:THandle;  
  ShowApp:TShowApp;  
begin  
  HLib:=LoadLibrary('NMDyn2.dll');  
  if HLib>HINSTANCE_ERROR then begin  
    ShowApp:=GetProcAddress(HLib,'ShowNMApplication');  
    if Assigned(ShowApp) then ShowApp;  
    Application.ProcessMessages;  
    FreeLibrary(HLib);  
  end;  
end;  

В отличие от предыдущего примера, динамическая загрузка DLL и ее выгрузка осуществляются в одном методе, да и объем написанного кода существенно меньше.

В начало В начало

Экспорт дочерних форм из DLL

Дочерние формы (то есть формы,  которые в качестве родителей имеют другие формы, а не экран компьютера) достаточно широко используются при создании приложений. В принципе, такие формы могут поставляться и внутри DLL. При этом используется статическая загрузка DLL. Динамическая загрузка DLL возможна только в том случае, если она загружается сразу же вместе с созданием формы, на которой размещается дочерняя форма, и выгружается при закрытии этой формы.

Нужно принять во внимание тот факт, что дочерняя форма может быть использована в нескольких формах главного приложения, различающихся жизненным циклом. Поэтому помимо метода, который будет создавать дочернюю форму, необходимо иметь метод для ее разрушения и возврата используемых ресурсов системе. К тому же, также из-за возможности использования нескольких дочерних форм,  при создании формы главному приложению должен быть сообщен какой-либо идентификатор формы, который необходимо сохранить и использовать при вызове деструктора данной дочерней формы. На основе этих теоретических предпосылок можно приступить к созданию DLL, способной экспортировать дочерние формы:

function CreateCustomWindow(ParentHandle:integer; DataRect:TRect; var WinHandle:THandle):integer; stdcall; export;  
{The method returns descriptor of the form which must be passed to destructor (below)}  
var  
  FD:TForm1;  
begin  
  Result:=0;  
  WinHandle:=0;  
  try  
    FD:=TForm1.Create(nil);  
    Result:=integer(FD);  
    WinHandle:=FD.Handle;  
    if ParentHandle<>0 then begin  
      SetParent(WinHandle,ParentHandle);  
      with FD do begin  
        SetWindowPos(Handle, HWND_TOP, DataRect.Left, DataRect.Top, DataRect.Right-DataRect.Left, DataRect.Bottom-DataRect.Top, SWP_SHOWWINDOW);  
        Show;  
      end;  
    end;  
  except  
    On E:exception do MessageDlg(E.Message,mtError,[mbOK],0);  
  end;  
end;  
   
procedure DeleteCustomWindow(WinID:integer); stdcall; export;  
begin  
  try  
    if WinID<>0 then TForm1(WinID).Free;  
  except  
    On E:exception do MessageDlg(E.Message,mtError,[mbOK],0);  
  end;  
end;  

На форму TForm1 помещают элементы управления, которые необходимо показать в главном приложении. Обычно свойство BorderStyle дочерних форм устанавливают равным bsNone (рис. 3).

Далее, дочерним формам в качестве параметров метода следует передавать прямоугольную область родительского окна, в которой она размещается. Это означает, что дочерняя форма должна иметь хорошо отлаженные обработчики событий, которые связаны с изменением ее размеров. Как и во всех предыдущих примерах, показ дочерних форм следует сделать независимым от языка; следовательно, необходимо использовать методы Windows API для изменения родителей. Такой метод определен в модуле user32.dll — SetParent. Другой метод Windows API — SetWindowPos — используется для изменения границ формы. В качестве возвращаемого параметра возвращается указатель на объект. Но приложение не будет его использовать, поскольку оно должно его запоминать и использовать при вызове метода DeleteCustomWindow.  Поэтому сохраняется возможность использовать данный проект в приложениях, написанных не на Delphi, а на других языках.

Еще один полезный параметр — дескриптор окна формы — передается как var-параметр метода CreateCustomWindow. Его может использовать главное приложение для посылки сообщений форме, динамического изменения размеров, видимости и т.д. посредством вызова соответствующих методов WinAPI.

Код основного приложения для тестирования данной DLL выглядит следующим образом:

TForm2 = class(TForm)  
    procedure FormClose(Sender: TObject; var Action: TCloseAction);  
    procedure FormCreate(Sender: TObject);  
    procedure FormDestroy(Sender: TObject);  
  private  
    { Private declarations }  
    FHLib:THandle;  
    FChildID:integer;  
    FChildHandle:THandle;  
  public  
    { Public declarations }  
  end;  
   
var  
  Form2: TForm2;  
   
implementation  
   
{$R *.DFM}  
type  
  TCreateCustomWindow=function(ParentHandle:integer; DataRect:TRect; var WinHandle:THandle):integer; stdcall;  
  TDeleteCustomWindow=procedure(WinID:integer); stdcall;  
   
   
procedure TForm2.FormClose(Sender: TObject; var Action: TCloseAction);  
begin  
  Action:=caFree;  
end;  
   
procedure TForm2.FormCreate(Sender: TObject);  
var  
  CreateW:TCreateCustomWindow;  
begin  
  FHLib:=LoadLibrary('ChildDLL.dll');  
  if FHLib>HINSTANCE_ERROR then begin  
    CreateW:=GetProcAddress(FHLib,'CreateCustomWindow');  
    if Assigned(CreateW) then FChildID:=CreateW(Handle,ClientRect,FChildHandle);  
  end;  
end;  
   
procedure TForm2.FormDestroy(Sender: TObject);  
var  
  DeleteW:TDeleteCustomWindow;  
begin  
  if (FChildID>0) and (FHLib>HINSTANCE_ERROR) then begin  
    DeleteW:=GetProcAddress(FHLib,'DeleteCustomWindow');  
    if Assigned(DeleteW) then DeleteW(FChildID);  
  end;  
  if FHLib>HINSTANCE_ERROR then FreeLibrary(FHLib);  
end;  

В этом проекте на форму TForm2 никакие элементы управления не помещаются. Главная форма приложения содержит одну кнопку, при нажатии на которую выполняется немодальный показ TForm2:

with TForm2.Create(Self) do Show;

При создании второй формы происходит загрузка DLL, а форма, созданная в DLL, становится дочерней для TForm2. Можно создать несколько экземпляров TForm2. При разрушении конкретного экземпляра разрушается и дочернее окно на нем, для чего используется сохраненный ранее параметр FchildID (рис. 4).

Казалось бы, аналогичную методику можно использовать для экспорта из DLL других элементов управления, среди предков которых не содержится класс TForm. Однако при попытке использовать метод SetParent непосредственно для элементов управления происходит генерация исключения об отсутствии у элемента управления родительского окна, и этот элемент [U8] не показывается на форме.

В начало В начало

Заключение

Итак, с помощью динамически загружаемых библиотек можно оптимизировать ресурсы, необходимые для выполнения приложений; использовать в проектах модули, написанные на различных языках программирования; создавать проекты, которые могут иметь необязательные функции и пункты меню. Вызов методов из DLL не представляет трудностей, за исключением того, что следует обращать особое внимание на исключительные ситуации: не допускать попадания экземпляра — потомка Exception в главный модуль, обязательно вызывать команду FreeLibrary при наличии исключений. Анализу исключительных ситуаций и отладке приложений будет посвящена следующая статья данного цикла.

КомпьютерПресс 4'2001