Динамически компонуемые  библиотеки (DLL) позволяют нескольким прикладным программа Windows или DOS защищенного режима  совместно использовать код и ресурсы. В Borland Pascal вы можете как  использовать существующие DLL,  так и написать  свои  собственные  DLL, которые можно применять в других программах.

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

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

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

Другое отличие модулей от DLL состоит в том,  что модули могут экспортировать  типы, константы,  данные и объекты,  а DLL -  только процедуры и функции.

Чтобы ее можно было использовать в программе Borland Pascal,  DLL не обязательно должна быть написана на Borland Pascal.  Кроме  того, программы,  написанные на других языках, могут использовать  DLL, написанные на Borland Pascal.  DLL,  таким образом, идеально  подходит при программных проектах, реализуемых на нескольких языках.

Чтобы модуль мог использовать процедуру или функцию  в DLL,  он должен  импортировать процедуру или функцию с помощью описания  external. Например,  в следующем описании из DLL и именем  KERNEL  (ядро Windows) импортируется функция с именем GlobalAlloc:

function GlobalAlloc(Glags: Word; Bytes: Longint): THandle;

far; external 'KERNEL' index 15;

В импортируемой процедуре или функции директива external занимает место описательной и операторной части, которые нужно было  бы включить в противном  случае. В  импортируемых  процедурах и  функциях должна  использоваться дальняя модель вызова,  выбранная  ключевым словом far или директивой компилятора {$F+}; во всем остальном их поведение не отличается от обычных процедур и функций.

Borland Pascal  импортирует процедуры и функции тремя способами:

- по имени;

- по новому имени;

- по порядковому номеру.

Формат директив external для каждого из трех методов показан  в приведенном ниже примере.

Когда оператор index или name не указан, процедура или функция экспортируются по имени.  Это имя совпадает с идентификатором  процедуры или  функции. В  данном примере процедура ImportByName  импортируется из библиотеки 'TESTLIB' по имени 'IMPORTBYNAME':

procedure ImportByName; external 'TESTLIB';

Когда задан оператор name,  процедура или функция импортируется под именем,  отличным от имени идентификатора.  В следующем  примере процедура  ImportByName   импортируется   из   библиотеки  'TESTLIB' по имени 'REALNAME':

procedure ImportByName; external 'TESTLIB'name 'REALNAME'

Наконец, при  наличии оператор  index процедура или функция  импортируется по порядковому значению. Такой вид импорта уменьшает время  загрузки модуля,  так как отпадает необходимость поиска  имени в  таблице  имен DLL.  В  следующем   примере   процедура  ImportByOrd импортируется из библиотеки 'TESTLIB':

procedure ImportByOrd; external 'TESTLIB' index 5;

Имя DLL  задается  после  ключевого слова external,  а новое  имя, заданное в операторе name,  не обязано  представлять  собой  строковые литералы.  Допускается любое строковое выражение-константа. Аналогично, порядковый  номер,  задаваемый в   операторе  index, может быть любым целочисленным выражением-константой.

const

TestLib = TestLib;

Ordinal = 5;

procedure ImportByName; external TestLib;

procedure ImportByName; external TestLibname 'REALNAME'

procedure ImportByOrd; external TestLib index Ordinal;

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

Описания импортируемых процедур и функций  могут  помещаться  непосредственно в программу, которая их импортирует. Однако обычно они объединяются в модуль импорта,  содержащий  описания всех процедур и функций в DLL, а также все типы и константы, необходимые для интерфейса с DLL. Примерами таких модулей импорта являются поставляемые  с Borland  Pascal  модули WinTypes,  WinProcs и  WinAPI. Модули импорта не обязательны для интерфейса  с  DLL, но  они значительно  упрощают обслуживание использующих множество DLL  проектов.

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

unit DateTime;

 

interface

 

type

   TTimeRec = record

Second: Integer;

Minute: Integer;

Hour: Integer;

   end;

 

type

   TDateRec

   TDateRec = record

  Day: Integer;

  Month: Integer;

  Year: Integer;

   end;

 

procedure SetTime(var Time: TTimeRec);

procedure GetTime(var Time: TTimeRec);

procedure SetDate(var Date: TDateRec);

procedure GetDate(var Date: TDateRec);

 

inplementation

 

procedure SetTime; external 'DATETIME' index 1;

procedure GetTime; external 'DATETIME' index 2;

procedure SetDate; external 'DATETIME' index 3;

procedure GetTime; external 'DATETIME' index 4;

 

end.

Любая программа,  использующая  DATETIME.DLL  может теперь  просто задать в своем операторе uses  модуль  DateTime.  Приведем  пример программы Windows:

program ShowTime;

 

uses WinCrt, DateTime;

 

    var

   Time: TTimeRec;

 

begin

   GetTime(Time);

   with Time do

  WriteLn('Текущее время: ', Hour, ':', Minute, ':',

  Second);

end.

Другим преимуществом  использования  модуля импорта,  такого  как DateTime, является то, что при модификации DATETIME.DLL обновить требуется только модуль импорта DateTime.

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

Если вы пишете собственные DLL, они не компилируются автоматически при компиляции использующей ее программы с помощью команды CompileіMake. DLL следует компилировать отдельно.

Директива external обеспечивает возможность статического импорта процедур и функций из DLL.  Статически импортируемая процедура и функция всегда ссылается на одну и ту  же  точку  входа  в   DLL. Расширения Windows и защищенного режима DOS Borland поддерживает также динамический импорт,  при котором имя DLL и имя  или  порядковый номер  импортируемой процедуры или функции задается во  время выполнения.  Приведенная ниже программа ShowTime использует  динамический импорт  для вызова процедуры GetTime в DATETIME.DLL.  Обратите внимание на использование переменной  процедурного  типа  для представления адреса процедуры GetTime.

program ShowTime;

 

uses WinProcs, WinTypes, WinCrt;

 

type

   TTimeRec = record

Second: Integer;

Minute: Integer;

Hour: Integer;

   end;

  TGetTime = procedure(var Time: TTimeRec);

 

var

   Time: TTimeRec;

   Handle: THAndle;

   GetTime: TGetTime;

 

begin

   Handle := LoadLibrary('DATETIME.DLL');

   if Handle >= 32 then

   begin

  @GetTie := GetProcAddress(Handle, 'GETTIME');

  if @GetTime <> nil then

   begin

  GetTime(Time);

  with Time do

WriteLn('Текущее время: ', Hour, ':', Minute, ':',

Second);

end;

FreeLibrary(Handle);

  end;

end;

Структура DLL Borland Pascal идентичная структуре программы,  но DLL начинается вместо заголовка program с  заголовка  program.  Заголовок library указывает Borland Pascal, что нужно создать выполняемый файл с расширением .DLL, а не с расширением .EXE, и выполняемый файл помечается как DLL.

   библиотека

   і

   і  ЪДДДДДДДДДДДДДї   ЪДДДї  ЪДДДДДДї

   АДД>і  заголовок  ГДД>і ; ГДВДДДДДДДДДДДДДДДДДДі блок ГДДДДДДД>

  і библиотеки  і  АДДДЩ і   ЪДДДДДДДДДДї ^ АДДДДДДЩ

  АДДДДДДДДДДДДДЩ   АДД>і оператор ГДЩ

  і   uses   і

  АДДДДДДДДДДЩ

 

   ЪДДДДДДДДДї   ЪДДДДДДДДДДДДДДДї

   заголовок ДДДД>і library ГДД>і идентификатор ГДДДДД>

   процедуры   АДДДДДДДДДЩ   АДДДДДДДДДДДДДДДЩ

В приведенном ниже примере приведена  очень  простую DLL  с  двумя экспортируемыми функциями Min и Max, которые вычисляют наименьшее и наибольшее из двух целочисленных значений.

library MinMax;

 

function Min(X, Y: Integer): Integer; export;

begin

   if X < Y then Min := X else Min := Y;

end;

 

function Max(X, Y: Integer): Integer; export;

begin

   if X > Y then Max := X else Max := Y;

end;

 

exports

Min index 1,

Max index 2;

 

begin

end.

Обратите внимание на использование для подготовки Min и Max,  для экспорта ключевого слова export,  и на оператор exports,  используемый для фактического экспорта двух подпрограмм,  указывающий, для каждой из них, необязательный порядковый номер.

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

library Eritors;

 

uses EdInit, EdInOut, EdFormat, EdPrint;

 

exports

   InitEditors index 1,

   DoneEditors index 2,

   InsertText index 3,

   DeleteSelection index 4,

   FormatSelection index 5,

   PrintSelection index 6,

  .

  .

  .

   SetErrorHandler index 53;

 

begin

   InitLibrary;

end.

Если процедуры и функции должны  экспортироваться  DLL, они  должны компилироваться с директивой компилятора export. Директива  export принадлежит к тому же семейству процедурных директив,  что  и near,  far,  inline  и interrupt.  Это означает,  что директива  export, если она присутствует,  должна указываться  перед  первым  заданием процедуры или функции - она не может указываться в определяющем описании или в опережающем описании.

Директива export делает процедуру или  функцию  экспортируемой. Она принудительно  использует  для подпрограммы дальний тип  вызова и подготавливает ее для экспорта,  генерируя для процедуры  специальный код входа и выхода.  Заметим, однако, что фактический  экспорт процедуры или функции не происходит, пока подпрограмма не  перечисляется в операторе exports библиотеки.

Процедура или функция экспортируется DLL, когда она указывается в операторе exports библиотеки.

   оператор exports

   і  ЪДДДДДДДДДї  ЪДДДДДДДДДДДДДДДДї  ЪДДДї

   АДД>і exports ГДД>і список экспортаГДДДДДДДДДДД>і ; ГДДДДДДД>

    АДДДДДДДДДЩ  АДДДДДДДДДДДДДДДДЩ  АДДДЩ

 

  ЪДДДДДДДДДДДДДДДДї

   список экспорта ДДВД>і запись экcпортаГДДДДДДДДДДД>

   і  АДДДДДДДДДДДДДДДДЩ  ^

   і  ЪДДДї   і

   АДДДДДДД>і ; ГДДДДДДДДДЩ

   АДДДЩ

 

   оператор exports

  і ЪДДДДДДДДДДДДДДДї

  АДДД>і идентификатор ГДДВДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДї

АДДДДДДДДДДДДДДДЩ  і  ЪДДДДДДДї  ЪДДДДДДДДДДДДДДДДДї ^ і

  АДД>і index ГД>і целая константа ГДЩ і

  АДДДДДДДЩ АДДДДДДДДДДДДДДДДДЩ   і

  ЪДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДЩ

  АДВДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДДВДДДДДДДДДДДДДДДДДДДДДД>

  і ЪДДДДДДї   ЪДДДДДДДДДДДДДДДДДДДДДї ^і  ЪДДДДДДДДДДї  ^

  А>і name ГДД>і строковая константа ГДЩАД>і resident ГДДЩ

АДДДДДДЩ   АДДДДДДДДДДДДДДДДДДДДДЩ  АДДДДДДДДДДЩ

Оператор exports  может встречаться в любом месте описательной части программы или библиотеки и любое число раз.  Каждая запись в операторе exports задает идентификатор экспортируемой процедуры или функции. Однако, эта процедура или функция должна описываться до оператора exports, и ее описание должно содержать директиву export.  Перед идентификатором в операторе exports вы можете указать  идентификатор модуля с точкой; это называется полностью уточненным идентификатором.

Запись экспорта может также включать в себя оператор  index,  который состоит из ключевого слова index,  за которым следует целочисленное значение в диапазоне от 1 до  32767.  Когда  задается  оператор index,  для экспортируемой процедуры или функции должно  использоваться специальное порядковое  значение.  Если в  записи  экспорта оператор index отсутствует, то порядковое значение присваивается автоматически.

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

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

Программа может содержать оператор exports,  но это встречается редко,  так как Windows не позволяет прикладным  программам  экспортировать функции,  используемые другие прикладными программами.

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

DLL хранится в памяти,  пока ее счетчик использования больше  нуля. Когда  счетчик использования становится нулевым,  указывая,  что все использующие DLL прикладные программы  завершили  работу,  она удаляется из памяти. При этом выполняется код процедуры выхода. Процедуры  выхода   регистрируются   с   помощью  переменной  ExitProc, которая описывается в Главе 22 "Вопросы управления".

Код инициализации  DLL обычно выполняет такие задачи как регистрация класса окна для содержащихся в DLL оконных  процедур  и  установка начальных значений для глобальных переменных DLL. Установив в нулевое значение переменную ExitCode,  код  инициализации  библиотеки может указать состояние ошибки (ExitCode описывается в  модуле System).  По умолчанию ExitCode равна 1,  что указывает на  успешную инициализацию. Если код инициализации устанавливает значение этой переменной в 0,  то DLL выгружается из системной памяти, и вызывающая прикладная программа уведомляется  о  неудачной  загрузке DLL.

Когда выполняется библиотечная процедура выхода,  переменная  ExitCode не  содержит  код  завершения процесса.  Вместо   этого  ExitCode содержит  одно из значений wep_System или wep_Free_DLL,  определенных в модуле WinTypes.  wep_System указывает на завершение работы Windows, а wep_Free_DLL указывает на то, что выгружена  данная DLL.

Приведем пример библиотеки с кодом инициализации и  процедурой выхода:

library Test;

 

{$S-}

 

uses WinTypes, WinProcs;

 

var

   SaveExit: Pointer;

 

procedure LibExit; far;

begin

if ExitCode = wep_System_Exit then

  begin

  { выполняется завершение работы системы }

.

.

.

end else

begin

   .

   .

   .

{ разгружается DLL }

   .

   .

  .

end;

ExitProcess : SaveExit;

end;

 

begin

   .

   .

   .

{ выполнить инициализацию DLL }

   .

   .

   .

SaveExit := ExitProc;   { сохранить старый указатель

  процедуры выхода }

ExitProc := @LibExit;   { установка процедуры выхода

   LibExit }

end.

В защищенном режиме DOS передаваемое  процедуре  выхода DLL  значение ExitCode всегда равно 0 и соответствует wep_FREE_DLL.

После разгрузки DLL экспортируемая функция вызывает процедуру WEP (процедура выхода Windows)  DLL, если  она  присутствует.  Библиотека Borland Pascal автоматически экспортирует функцию WEP,  которая продолжает вызывать записанный в переменной ExitProc  адрес, пока ExitProc не примет значения nil. Поскольку этот механизм процедур выхода соответствует работе с процедурами выхода  в  программах Borland Pascal, и в программах, и в библиотеках вы можете использовать одну и ту же логику процедур выхода.

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

В следующих разделах описаны некоторые важные моменты, которые следует иметь в виду при работе с DLL.

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

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

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

В Windows глобальные блоки памяти,  распределенные с атрибутом gmem_DDEShare (определенные в модуле  WinTypes),  принадлежат  DLL, а  не  вызывающим прикладным программам.  Такие блоки памяти  остаются распределенными, пока они явно не освобождаются DLL, или  пока DLL не выгружается.

Администратор памяти  защищенного режима DOS не поддерживает  совместно используемых   блоков  памяти   и   игнорирует флаг  gmem_DDEShare. В  защищенном режиме DOS распределяемые DLL блоки  памяти всегда принадлежат вызывающей библиотеку DLL программе.

В продолжении  существования DLL переменная HInstance содержит описатель экземпляра DLL.  Переменные FPrevInst и  CmdShow в  DLL всегда равны 0 (как и переменная PrefixSeg), поскольку DLL не  имеет префикса программного сегмента (PSP). В прикладной программе PrefixSeg никогда не равна 0,  поэтому проверка PrefixSeg <> 0  возвращает True,  если текущем модулем является прикладная  программа, и False, если текущим модулем является DLL.

Чтобы обеспечить правильную работу администратора динамически распределяемой области, содержащегося в модуле System, код запуска библиотеки устанавливает переменную HeapAllocFlags в значение gmem_Moveable + gmem_DDEShare. В Windows это приводит к тому,  что все  блоки  памяти,  распределенные  через  процедуры  New и  GetMem, будут принадлежать DLL,  а не вызывающей  ее  прикладной  программе.

Примечание: Подробности  об  администраторе  памяти вы  можете найти в Главе 21.

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

Поскольку DLL не может знать,  вызывается ли она из прикладной программы  Borland Pascal или из прикладной программы,  написанной на другом языке программирования, то DLL не может вызывать  процедуры выхода  прикладной программы  до завершения прикладной  программы. Прикладная программа просто прерывается и  выгружается  из памяти.  По  этой причине,  чтобы таких ошибок не происходило,  нужно обеспечить в DLL достаточное количество проверок.

Если в DLL под Windows происходит ошибка  этапа  выполнения,  то надежнее всего полностью выйти в Windows. Если вы просто пытаетесь модифицировать и перестроить сбойный код DLL, а затем снова  выполнить прикладную программу,  Windows не будет загружать новую  версию,  если ошибочная версия уже находится в память. Выйдите из  Windows  и перезапустите ее,  а Borland Pascal обеспечит загрузку  корректной версии DLL.

В отличие  от прикладной программы DLL не имеет своего собственного сегмента стека. Вместо этого она использует сегмент стека вызывающей DLL прикладной программы. Это может создать проблемы в подпрограмме DLL,  которые полагают,  что регистры DS  и  SS  ссылаются на один и тот же сегмент,  как это имеет место в модуле  прикладной программы Windows.

Borland Pascal никогда не  генерирует код,  подразумевающий  равенство DS  =  SS,  и  в библиотеке исполняющей системы Borland  Pascal таких предположений не делается.  Если вы  пишете код  на  языке ассемблера,  то не полагайтесь на то,  что регистры DS и SS  содержат одно и то же значение.

Borland Pascal поддерживает DLL, которые могут совместно использоваться в защищенном режиме DOS и в Windows.  Совместно  используемые DLL совместимы на уровне двоичного кода. Это означает,  что один и тот же файл .DLL  может использоваться  в  прикладной  программе защищенного  режима DOS  или  в прикладной  программе  Windows.

При компиляции совместно используемой DLL в качестве целевой  платформы нужно выбирать Windows:

* В  IDE выберите команду CompileіTarget и в диалоговом окне  Target (Целевая платформа) укажите Windows.

* При использовании компилятора,  работающего в  режиме командной строки,  для выбора  в качестве целевой платформы   Windows используйте переключатель /CW.

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

Совместно используемая  DLL может взаимодействовать с операционной системой (DOS защищенного режиме или Windows) только  через модуль  WinAPI.  Этот модуль представляет функции,  общие для  защищенного режима DOS  и Windows.  Другие  интерфейсные  модули  Windows, такие  как WinTypes и WinProcs,  описывают большое число подпрограмм API, не поддерживаемых в защищенном режиме DOS.

Примечание: О модуле WinAPI рассказывается в Главе  17  "Программирование в защищенном режиме DOS".

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