Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Паскаль / tp3 / tp3 / 10

.doc
Скачиваний:
17
Добавлен:
10.12.2013
Размер:
72.19 Кб
Скачать

Глава 10. Динамически компонуемые библиотеки

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

Что такое DLL?

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

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

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

Чтобы прикладная программа Турбо Паскаля могла использовать DLL, динамически компонуемую библиотеку не обязательно писать на Паскале. Более того, программы, написанные на других языках, могут использовать DLL, написанные на Турбо Паскале. Таким образом, DLL идеально подходят для разработки программ, использующих разные языки.

Использование DLL

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

function GlobalAlloc(Flag: Word; Bytes: LongInt);

THandle: far; external 'KERNEL' index 15;

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

Турбо Паскаль для Windows может импортировать процедуры тремя способами:

- по имени;

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

- по порядковому значению (индексу).

Формат директивы external для каждого из трех методов показывается в следующем примере.

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

procedure ImportByName; external 'TESTLIB';

Когда задается оператор name, процедура или функция импортируется с другим именем, отличным от имени идентификатора. Далее процедура ImportByNewName импортируется из 'TESTLIB' с использованием имени 'REALNAME'.

procedure ImportByNewName; external 'TESTLIB' name 'REALNAME';

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

procedure ImportByOrdinal; external 'TESTLIB' index 5;

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

const

TestLib = 'TESTLIB';

Ordinal = 5;

procedure ImportByName; external TestLib;

procedure ImportByNewName; external TestLib name 'REALNAME';

procedure ImportByOrdinal; external TestLib index Ordinal;

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

Модуль импорта

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

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

unit DateTime;

interface

type

TTimeRec = record

Second: Integer;

Minute: Integer;

Hour: Integer;

end;

type

TDateRec = record

Day: Integer;

Month: Integer;

Year: Integer;

end;

procedure SetTime(var Time; TTimeRec);

procedure GetTime(var Time; TTimeRec);

procedure SetDate(var Time; TDateRec);

procedure GetDate(var Time; TDateRec);

implementation

procedure SetTime; external 'DATETIME' index 1;

procedure GetTime; external 'DATETIME' index 1;

procedure SetDate; external 'DATETIME' index 1;

procedure GetDate; external 'DATETIME' index 1;

end.

Теперь любая программа, которая использует DATETIME.DDL, может просто задать DateTime в операторе uses.

program ShowTime;

uses WinCrt, DateTime;

var

Time: TTimeRec;

begin

GetTime(Time);

with Time do

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

end.

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

Статический и динамический импорт

Директива external обеспечивает возможность статического импорта процедур и функций из DLL. Статически импортируемая процедура или функция всегда ссылается на одну и ту же точку входа в той же библиотеке DLL. Windows поддерживает также динамический импорт, при котором имя 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

GetTime := TGetTTime(GetProcAddress(Handle, 'GETTIME'));

if @GetTime <> nil then

begin

GetTime(Time);

with Time do

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

end;

FreeLibrary(Handle);

end;

end.

Как писать библиотеки DLL

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

В качестве примера приведем реализацию очень простой библиотеки 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.

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

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

library Editors;

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.

Процедурная директива export

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

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

Оператор exports

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

оператор ╒═════════╕ ┌─────────────────┐ ╒══╕

exports ─────┤ exports ├──│ список экспорта ├──│ ;├──

╘═════════╛ └─────────────────┘ ╘══╛

┌─────────────────┐

список экспорта ──────────│ запись экспорта ├───┬────

‑ └─────────────────┘ │

│ ╒══╕ │

└─────────────┤ ,│──────────┘

╘══╛

┌─────────────────┐

запись экспорта ──────│ идентификатор ├───┬────────────────┐

└─────────────────┘ │ ‑ │

┌───────────────────────────┘ │ │

│ ╒═════════╕ ┌─────────────────┐ │ │

└──┤ index ├──│ целая константа ├──┘ │

╘═════════╛ └─────────────────┘ │

┌─────────────────────────────────────────────────────────────┘

└─┬────────────────────────────────────────────┬────────────────

│ ‑ │ ‑

│ ╒═════════╕ ┌───────────────────────┐ │ │ ╒══════════╕│

└─│ name ├──│ строковая контастанта ├─┘ └│ resident ├┘

╘═════════╛ └───────────────────────┘ ╘══════════╛

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

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

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

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

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

Код инициализации библиотеки и код выхода

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

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

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

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

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

library Test;

uses WinTypes, WinProcs;

var

SaveExit: Pointer;

procedure LibExit; far;

begin

if ExitCode = wep_System_Exit then

begin

{ выполняется останов системы }

.

.

end else

begin

.

.

{ библиотекa DLL разгружена }

.

.

end;

ExtProc := SaveExit;

end;

begin

.

.

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

.

.

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

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

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

LibExit }

end.

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

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

Замечания по программированию библиотек

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

Глобальные переменные в DLL

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

Глобальные переменные и файлы в DLL

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

Библиотеки DLL и модуль System

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

Чтобы обеспечить правильные операции подсистемы распределения динамической памяти, содержащейся в модуле System, код инициализации библиотеки устанавливает HeapLimit в значение 0. Это позволяет выделять уникальный блок глобальной памяти при каждом вызове New или GetMem, фактически отключая возможность локального распределения в подсистеме распределения динамической памяти. Поскольку каждый блок глобальной памяти имеет избыточный объем по крайней мере в 32 байта, в DLL следует избегать выделения большого числа блоков динамической памяти небольшого размера. Если вы можете можете гарантировать, что DLL используется каждый раз только одной прикладной программой, то записав в HeapLimit стандартное значение 1024, вы можете разрешить возможность локального распределения.

Примечание: Подробнее о подсистеме управления памятью рассказывается в Главе 16 "Системные ресурсы".

Ошибки этапа выполнения в DLL

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

DLL и сегменты стека

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

Соседние файлы в папке tp3