ЭЛЕКТРОННАЯ БИБЛИОТЕКА КОАПП |
Сборники Художественной, Технической, Справочной, Английской, Нормативной, Исторической, и др. литературы. |
Часть 12 Глава 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 не обязательно удаляется из памяти, поскольку в этот момент ее могут использовать другие прикладные программы. Так как D LL не имеет возможности определить, вызывалась она из прикладной программы Турбо Паскаля, или из прикладной программы, написанной на другом языке программирования, для DLL нет возможности вызвать перед завершением работы прикладной программы процедуры вы хода. Прикладная программа просто прерывается и выгружается из памяти. По этой причине убедитесь в наличии в DLL необходимых проверок, благодаря которым такие ошибки происходить не будут. DLL и сегменты стека В отличие от прикладной программмы, DLL не имеет собственного сегмента стека. Вместо этого она использует сегмент стека прикладной программы, которая вызывает библиотеку DLL. Это может вызывать в DLL проблемы, в частности в подпрограммах DLL, которы е предполагают, что регистры DS и SS ссылаются на один и тот же сегмент, что имеет место в прикладном модуле. Компилятор Турбо Паскаля никогда не генерирует код, в котором предполагается равенство DS и SS, в подпрограммах библиотеки исполняющей системы Т урбо Паскаля это предположение также не используется. Однако, если вы пишете код на языке Ассемблера, убедитесь, что вы не предполагаете, что регистры DS и SS содержат одно и то же значение. |