    Программирование DLL: введение 

    ЭрикАбер 

    DLL означает Dynamic Link Library (динамически компонуемая библиотека) -— 
удачный термин, отражающий как принцип действия, так и способ реализации. DLL 
— это отдельно компилируемые, выполняемые библиотеки, подключаемые к программе 
или более крупной системе во время выполнения. Удобство их применения состоит 
в возможности сосредоточить в одном модуле фрагменты кода, данных и ресурсов, 
которые затем могут использоваться сразу несколькими программами. Применение 
DLL может не зависеть от языка, на котором она написана, что 
позволя^т"Йсполй"овать"средс-гва Delphi в программах, написанных, например, на C++. 
    Используя способность DLL предоставлять доступ ср_азу нескольким 
программам, программист может создать единый программный интерфейс 
(Application Programming Interface — API), уменьшив тем самым объем ЕХЕ-файла 
за счет размещения части кода в библиотеках, которые можно подключать и к 
другим программам. А поскольку эта часть кода используется программами в виде 
готовых подпрограмм (процедур и функций), модификация DLL (например, отладка) 
приводит к модификации всех программ, в которых она используется. 
    DLL можно загружать при необходимости и затем выгружать. Это дает 
программе возможность в любой момент регулировать наличие памяти и других 
ресурсов. Предположим, что в программе задействовано несколько форм с высокими 
требованиями к ресурсам. Такие формы удобно размещать в DLL, которая будет 
загружаться при необходимости воспользоваться этими формами, а затем выгружаться.	/-^ 
    Обратите внимание, что в Delphi входят блок поддержки элементов управления 
ActiveX и пакет библиотеки визуальных компонентов (Visual Component Library— 
VCL). И в основе этих фантастических средств лежат технологии программирования 
и архитектура DLL. 

    ГЛАВА 25 

    Программирование DLL и основные принципы Win32 

    Изучите основные принципы Win32 в программировании DLL 

    Программы на основе DLL 

    Обретите знания, необходимые для создания более быстрых и эффективных 
программ на основе DLL. 

    DLL на стадии проектирования 

    Научитесь добиваться наилучшего сочетания эффективности и компактности программ. 

    Внутренняя структура DLL 

    Разберитесь во внутреннем устройстве DLL, и это облегчит их отладку. 

591


    DLL и их роль в Win32 

    DLL составляют основу архитектуры всех версий Windows, в том числе WinNT и 
Win95. Модель программирования на основе DLL позволяет добиться модульности 
всех элементов системы, а модульность операционной системы — предоставить 
программисту более динамичный и открытый интерфейс. Рассмотрим области 
применения DLL. 
    в	CPL-файлы (компоненты панели управления) 
    •	Расширения оболочек Win95 и WinNT 
    •	Файлы ядра Win32, такие как KERNEL32 и GDI32 
    •	Драйверы устройств, такие как COMM.DRV 
    •	Создание и применение вызовов 16-битового кода из 32-битового и наоборот 
    •	Системные службы NT 
    •	Серверы OLE 
    Список этот, конечно, далеко не полный. С выпуском версий Win95 и NT 4.0 
не просто появилась новая операционная система, а произошел резкий поворот в 
технике программирования. 
    Системы Win95 и NT крайне СОА/-центричны, и к этому надо относиться с 
максимальной серьезностью, ибо СОМ представляет собой основу современных и 
будущих версий Windows. Элементы СОМ можно реализовать с помощью внутренних 
серверов, расположенных в DLL. А поскольку они располагаются внутри DLL, 
рассматриваемые в данной главе принципы послужат необходимым введением в СОМ. 
Более подробно о СОМ и внутренних серверах рассказывается в главе 20, 
"Автоматизация OLE". 

    Перенос из 16-разрядной Windows 

    Вероятно, у вас, как и у большинства разработчиков программ для Windows, 
установлена операционная система WinNT или Win95. Если вы некоторое время 
работаете с Delphi, у вас, надо полагать, уже есть версии 1.0 и 2.0. И вот 
теперь — Delphi 3.0. Обратная сторона медали — это то, что пока большая часть 
программ, которыми вы пользуетесь, рассчитана на систему Windows 3.x. В WinNT 
и Win95 16-разрядные программы работают вполне надежно, но важно иметь в виду, 
что они не могут воспользоваться всеми средствами, предоставляемыми модулем 
Win32. Исходя из этого для перехода в мир линейной модели памяти и программ, 
управляемых сообщениями, одной перекомпиляции иногда оказывается 
недостаточно.~"~ """ "" 
    Фирма Borland придерживается принципов обеспечения совместимости снизу 
вверх, однако эта совместимость имеет реальные пределы. Если в новой 
операционной системе некоторое средство просто отсутствует или работает 
по-другому, не поможет даже такая мощная система, как Delphi. 
    \ 
    I Совет 1 

    Библиотеки DLL в Win 16 

    В Win32 произошли изменения, существенно повлиявшие на программирование 
DLL. Эти изменения в архитектуре привели к тому, что программы, написанные в 
определенной модели, просто не могут выполняться. Перевести в Win32 
16-разрядную программу, использующую наряду с другими программами некоторую 
DLL, — задача не из простых, так что следует отнестись к материалу данной 
главы со всей серьезностью. 
    Изменение архитектуры Win32 — это следствие устранения модели 
сегментированной памяти, которая была заменена моделью линейной памяти. В 
16-разрядных версиях Windows библиотека DLL рассматривалась как глобально 
доступная, интегрируемая в операционную систему библиотека, что и обеспечивало 
ее совместное использование несколькими программами. 16-разрядные библиотеки 
DLL использовали стек загрузившей их программы, однако имели собственный 
сегмент данных объемом до 64 Кбайт. Кроме того, обращение к функции 
LoadLibrary позволяло загрузить библиотеку только один раз — повторный вызов 
этой функции лишь увеличивал на единицу значение счетчика загрузок, 
поддерживаемого Windows. После окончания работы с DLL вызывалась функция 
FreeLibrary, которая уменьшала значение счетчика на единицу, пока оно не 
становилось равным 0, и библиотека удалялась из памяти. 

    592	Часть VI. Сложный материал 


    Модель памяти Huge 

    Pascal никогда не поддерживал указателей Huge, на которые привыкли 
полагаться программисты C++. В Цвязи~с использованием модели сегментированной 
памяти, применявшейся в 16-разрядных версиях Windows, стандартный 4-байтовый 
указатель состоял фактически из двух частей. Старшее его слово представляло 
собой селектор, служивший адресом сегмента памяти объемом 64 Кбайт, младшее 
слово задавало смещение внутри этого сегмента. " '——— 
    Операционная система позволяла, запрашивать блоки объемом свыше 64 Кбайт, 
однако ответственность за обеспечение доступа к такий'многосегмёнтным блокам 
полностью перекладывалась на программиста. Для считывания значения из блока 
объемом 100 Кбайт, занимающего два сегмента, приходилось сначала считывать все 
данные вплоть до границы сегментов (зачастую по^эдному^байту), а затем 
добавлять значение селекторной части указателя. После переадресации 
селекторной части на второй сегмент можно было продолжить считывание данных. 
Считав все данные, следовало реорганизовать их во что-то более осмысленное. 
    8JUU032 подобная сегментация просто отсутствует. При переносе в Win32 
подпрограмм и классов, содержащих манипуляции селекторами и операции с 
многосегментными блоками памяти, подобные фрагменты программ приходится просто 
удалять. В Win32 указатели по-прежнему имеют длину 4 байт, но теперь все 
jaio—„ смещение. Если модифицировать ту часть указателя, которая прежде была 
селектором, произойдет изменение смещения с непредсказуемым результатом. 
    Программисты не замедлили воспользоваться преимуществами такой 
архитектуры, в которой можно было с помощью одной DLL осуществлять обмен 
данными между несколькими программами. Ввиду наличия у DLL собственного 
сегмента данных можно было использовать глобальные переменные и постоянные в 
качестве флажков при передаче данных между программами. Программа могла даже 
заставить DLL распределить блок в области глобальной динамической памяти, а 
затем передать указатель на этот блок другой программе для непосредственного использования. 
    Применение старых программ, следующих такой схеме, теперь затруднено (по 
крайней мере, они требуют дополнительных действий по переносу в новую 
систему). В Win32 как программа, так и загруженная ею DLL находятся в 
собст-венньдх^дресных пространствах. Образцы кода или даже целые программы, 
пользующиеся одной DLL, совершенно не зависят друг от другаГ 

    Библиотеки DLL в Win32 

    В Win32 каждая программа вполне самодостаточна. Получив команду загрузить 
программу, операционная система создает объект главного процесса, именуемый 
обычно просто процессом. Процесс располагает 4 Гбайт виртуальной адресуемой 
памяти, в которой и работает программа. Для поддержания адресации 
распределяется участок памяти из системного страничного файла. После создания 
процесса создается еще один объект, известный как объект отображения файла, 
который отображает выполняемый файл в адресное пространство процесса. 
Выполняемый файл именно отображается с жесткого диска, минуя..загрузку^всегр 
образа в RAM или копированиев страничный файл, в любом случае находящийся на 
жестком диске.	' 

    ПРЕДОСТЕРЕЖЕНИЕ 
    Обычно DLL экспортируют функции для использования ЕХЕ-модулями или другими 
DLL. При перекомпиляции старых 16-разрядных DLL надо иметь в виду, что в 
32-разрядной системе действует по умолчанию совсем иная схема вызова (читайте 
раздел "Соглашения о вызовах" этой главы). 

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

    Глава 25. Программирование DLL: введение	593 

    В Win32 DLL также могут использоваться совместно разными программами и 
даже разными фрагментами кода одной программы. Каждая программа отображает DLL 
в собственное пространство процесса. DLL в Win32 по-прежнему имеют собственные 
области динамически распределяемой памяти, но распределение памяти и адресация 
в них происходят относительно загружающего процесса. Если в 32-разрядой DLL 
выполняется распределение памяти и осуществляется оно способом, который в 
16-разрядных версиях Windows именовался глобальным распределением динамической 
памяти, то и это распределение происходит относительно загружающего процесса. 
Имендо_й 31ож^р?тои-г_изм?нение модели совместного использования, вследствие 
которого может понадобиться некоторая переделка старых программ. Рассмотрим, 
как можно выполнить эти изменения. 

    Решение задач переноса 

    В Win32 ^совместное использование данных разными процессами может быть 
реализовано несколькими способами. Все они требуют некоторой модификации 
старых программ. Большинство этих способов рассмотрено в данной книге. 
    И   Генерирование сообщения wm_CopyData. С помощью этого сообщения 
некоторая структура данных передается от одной программы к другой, минуя 
промежуточную DLL. Подробнее об этом сообщении рассказывается в главе 32, 
"Катание на санках". 
    В   Реализация вызовов 16-разрядного кода из 32-разрядного. Можно 
применять старые DLL как часть программы, однако это может быть лишь временным 
решением. Более подробно об этом — в главе 32, "Катание на санках". 
    •   Объекты отображения файлов. Модификация DLL путем добавления в нее 
инициализации объекта FileMap-ping для каждого сообщения dll_Process_Attach. 
Более подробно об этом речь идет в главе 26. "Программирование DLL: сложный 
материал".и   Применение сервера OLE. Создание OLE-сервера для работы с одним 
экземпляром данных. Более подробно об этом рассказывается в главе 20, 
"Автоматизация OLE". 
    В настоящем разделе были рассмотрены, на мой взгляд, наиболее существенные 
изменения, которым придется подвергнуть программы, переводимые в Win32. 
Предстоит рассмотреть еще ряд вопросов. Причем многие из них будут возникать в 
самых неожиданных местах. По ходу изложения главы я буду выделять наиболее 
существенные моменты, что позволит читателю в процессе изучения делать 
необходимые заметки. 

    Создание проекта 

    В Delphi есть простое средство создания DLL-проекта. DLL-проект состоит из 
одного DPR-файла. Какая-либо фррма,_ .создаваемая по умолчанию, или 
дополнительные модули отсутствуют. Чтобы добавить в проект форму или модуль, 
выберите команду File^New. 

    Рис. 25.1. Диалоговое окно New Items позволяет создавать не только DLL-проекты 

    Можете попробовать создать новый DLL-проект. Выберите в главном меню 
Delphi команду File^New, и откроется диалоговое окно, показанное на рис. 25.1. 

    594 Часть VI. Сложный материал 

    В открывшемся окне выберите пиктограмму DLL, щелкните на кнопке ОК, и 
появится проект, показанный в листинге 25.1. Этот листинг находится в файле 
BAREBONES. DPR на дополнительно распространяемой дискете. 

    !  ЛИСТИНГ 25.1. \UDELPHI3\CHP25\BAREBONES. DPR. Проект BareBones 
    Library BareBones; { 

    Существенное замечание об управлении памятью в DLL. Если DLL экспортирует 
функции со строковыми параметрами или возвращающие строковые значения, модуль 
ShareMem надо указыват^ в разделе Uses библиотеки и__проекта _первым. Это 
касается любых строк, передаваемых как в DLL, так и из нее, даже если они 
размещаются внутри записей или объектов. Модуль ShareMem служит интерфейсом 
менеджера разделяемой памяти DELPHIMM.DLL, который должен разворачиваться 
одновременно с данной DLL..Чтобы избежать 
П?11М§и?й11Я,.,ЛЖ1*ЕН-1ММ-..13ЬЬ,..,,строкоВуЮ информацию можно пересдавать с 
помощью параметров типа PChar или ShortString.

    uses 
    SysUtils, 
    Classes; begin end. 

    Большая часть листинга 25.1 представляет собой комментарий, составленный 
экспертом DLL, в котором даются краткие рекомендации по применению в новой DLL 
библиотеки DELPHIMM.DLL. Delphi размещает функции интерфейса с библиотекой 
DELPHIMM в интерфейсном модуле ShareMem. Включение этого модуля 
в.лрое_кт__позврляет.,пе?едавать данные типа Ansi String, из DLL в ЕХЕ-мрдуль 
(или другую DLL) и обратно.	~* 

    Тип AnsiString 

    Во еторой версии DeiBhi введен новый тип данных Ansistring — длинная 
строка, отличающаяся OTj-радици-оннои строки языка Pascal тем, что не имеет 
фиксированной длины. По существу, это указатель^на стщку с '5ав€]ршающим 
нулевым символом, которой предшествует 12-байтовая струк;гура,_т.е. структура, 
начинаю-йЖяС|Г з^ JJ байт до начала собственно строки. Указатель адресуется на 
начало строки, так'Что"с"ёгБ"помо-1дь"кэ можно получЙТЁ доступ только к 
строковым данным. Поэтому компилятор, раэ.йещает_задавать строкам 
AnsiStringJ-ип PChar. 
    В 12-байтовой структуре, предшествующей строке Ansistring,_содержатся 
инфоом,ацйя„об о^бъеме^даспре-деленной для нее_ памяти, счетчик_^,0р,ащений и 
фактическая Ля!Ш1сшоЖ Эти данные используются *для автоматической' регистрации 
свободных областей памяти и оптимизации копирования и записи строк. В 
контексте программирования DLL вся^магия^обоабртки с трок и 
сов^естн^г^и^дрльзова^^^^ на в библиотеке DELPHIMM.DLL.     
    Созданный проект DLL можно сохранить и скомпилировать. В результате 
получится "скелет" DLL, не выполняющий никаких действий. При необходимости 
можно добавлять в проект код, ресурсы, данные и получать к ним доступ; все 
это, являясь внешним по отношению к DLL, находится в пределах данного 
процесса. А теперь рассмотрим точку входа (или код инициализации) DLL. 

    Точка входа DLLMain 

    В проекте Delphi точка входа в некоторую DLL может находиться где угодно 
между начальным begin и конечным end. Сразу после загрузки DLL выполняются 
операторы, расположенные в этой части DPR-файла. Здесь удобно размещать 
процедуры инициализации или распределения памяти. Если необходимо, чтобы DLL 
выполнила после загрузки некоторый пакет операций, вызов соответствующих 
процедур можно разместить здесь же. Эта особенность DLL дает_воз-можность 
пгдщШШ.их даже без.вьпова сс^держащи^хсд^ди^фщкц.ий. 
    Процедура DLLMain (которой мы посвятим несколько абзацев)— это разработка, 
выполненная конкретно в "а	Win32 и C++. Применение B^Delphi методов 
программирования, ориентированных на Win32, сдерживается не-заметку    
хваткой^документации по ту>дц>.^лмнрованню"т языке Pascal. В принципе, 
источников информации более чем достаточнбГЭто и сеть для разработчиков 
Microsoft (CTlcr6§bft Developers Network — MSDN), и множество журналов по 
соответствующей тематике, и группы новостей в Internet. Беда, однако, в том, 
что все это написано на С или C++. А это значит, что каждый пример приходится 
переводить на Pascal. При этом встречаются элементы, не поддающиеся переводу 
либо отсутствующие в Pascal, например множественное наследование. 

    Глава 25. Программирование DLL: введение	595 

    Рассмотрим процедуру DLLEntryPoint, именуемую еще DLLMain. Все описания в 
Win32 на эту тему ориентированы на язык C++. В целом, представлен вид 
операторов запуска для экспортирования функции DLLEntryPoint на языке С. Если 
эта функция существует, она вызывается при каждом подключении (и последующем 
отключении) нового процесса или подзадачи. В большинстве языков есть, кроме 
того, возможность указать имя данной функции на стадии компоновки. Назначение 
рассматриваемой функции — обеспечить для DLL единую точку входа-выхода, 
применяемую, как правило, для задач инициализации и сброса. В языке C++ 
объявление функции DLLEntryPoint выглядит следующим образом: 

    BOOL WINAPI   DLLEntryPoint(HINSTANCE  hlnst,   DWORD  dwReason,   LPVOID  
 IpvReserved); 

    В Delphi эта функция экспортируется (в синтаксисе языка Pascal), однако ее 
вызов в блоке запуска не производится. Вместо этого выполняется 
инициализационный блок, заключенный между начальным оператором begin и 
конечным end в DPR-файле. Этот блок выполняется всего один раз — при 
отображении модуля DLL в пространство первого процесса. 

    Реализация DLLMain в Delphi 

    В Delphi есть механизм запуска DLLMain подобный запуску в C++, но при этом 
приходится выполнить несколько дополнительных действий. Реализация этой 
функции в Delphi приведена в листинге 25.2. 

    | Листинг 25.2. DLLMAIN. Реализация DLLMain в Delphi 

    library DLLEntry; uses 
    ShareMem, Windows, SysUtils, Classes; 
    procedure DLLMain(dwReason: DWORD); begin 
    case dwReason of 
    dll_Process_Attach : {Операторы.}; dll_Process_Detach : {Операторы.}; 
dll_Thread_Attach  : {Операторы.}; dll_Thread_Detach  : {Операторы.}; end; 
end; begin 
    DLLProc := @DLLMain; DLLMain(dll_Process_Attach); end. 

    Прежде всего, в этом листинге отсутствует комментарий относительно модуля 
ShareMem и библиотеки DELPHIMM.DLL. В раздел uses добавлены модули ShareMem и 
Windows. Последний содержит определения необходимых dll_-KOHCT3HT. Сразу за 
разделом uses следует сама процедура DLLMain. 
    В Delphi процедура DLLMain имеет один параметр dwReason целого типа. При 
вызове функции этот параметр содержит значение от 0 до 3. В модуле Windows 
определены четыре константы с соответствующими значениями. Имена, значения 
й~смысл этих констант перечислены в табл. 25.1. 

    Константа	Значение      Причина вызова                         Назначение 

    dll_Process_Attach       \                      Процесс отобразил DLL в 
свое	Инициализация глобальных объектов адресное пространство. Один	или 
переменных при загрузке DLL либо вызов на процесс	выполнение пакета обработок 
    dll_Thread_Attach         2	Процесс создал дочернюю под-      Выполнение 
специальных операций при задачу (не применяется для	подключении очередной 
подзадачи главной подзадачи) 

    596 Часть VI. Сложный материал 


    Окончание табл. 25.1 ] 

    Константа	Значение       Причина вызова   ___ ___   Назначение 

    dll_Thread_Detach        3	Подзадача завершена                      Если 
при создании подзадачи DLL выполняет блок специальной обработки, то при 
завершении подзадачи распределенная память может быть освобождена, __          
                               а все флаги и счетчики сброшены 
    dll_Process_Detach      О	DLL отключена минимум от од-     Освобождение 
всех глобальных объек
    ного из процессов, в которые	тов, закрытие файлов и портов, откры-была 
отображена                              тых или инициализированных во время 
    dll_Process_Attach 

    Процедуру DLLMain можно вызывать многократно, однако каждый вызов должен 
определяться одной из четырех причин, перечисленных в табл. 25.1. При каждом 
первоначальном отображении программой DLL в свое адресное пространство 
вызывается процедура DLLMain с параметром dll_Process_Attach. Аналогично, если 
программа удаляет образ DLL, процедура DLLMain вызывается с параметром dll_Process_Detach. 

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

    Если процесс sarpjQicaeT J)LL ,трижды_ _в трех подзадачах, то ее образ не 
освобождается из адресного пространства процесса до третьей выгрузки (обычно 
основной подзадачей) или окончания процесса. И только при последней выгрузке 
(когда значение счетчика обращений равно 0) происходит вызов процедуры DLLMain 
с параметром dll_Process_Detach. 
    Если DLL загружается несколькими процессами, процедура DLLMain с 
параметром dll_Process_Attach вызывается один раз при первоначальной загрузке 
каждым процессом, затем еще раз — с параметром dll_Process_Detach — при 
выгрузке со значением 0 на счетчике обращений. 
    Процедура DLLMain с параметром dll_Process_Attach вызывается при каждом 
создании подзадачи в загружающем процессе. Если данная DLL используется 
несколькими процессами и один из них разрушает дочернюю подзадачу, процедура 
DLLMain с параметром dll_Process_Detach вызывается только при удалении 
процессом подзадачи. От других процессов DLL не получает уведомлений dll_Thread_Detach. 

    ПРЕДОСТЕРЕЖЕНИЕ 
    Не стоит ожидать, что каждому вызову процедуры DLLMain с параметром 
dil_Thread_Detach должен предшествовать ее же вызов с параметром 
dii_Thread_Attach. Процесс может заранее, до загрузки DLL, создать несколько подзадач. 

    Предположим, некоторый процесс создал две дочерние подзадачи, а затем 
загрузил DLL. При его завершении для каждой подзадачи будет вызвана процедура 
DLLMain с параметром dll_Thread_Detach, несмотря на то что не было ни одного 
ее вызова с параметром dii_Thread_Attach. 

    Присвоение DLLProc 

    В листинге 25.2 показаны изменения, которые необходимо внести в DLL, чтобы 
задействовать процедуру DLLMain. Мы уже рассмотрели изменения в разделе uses, 
а также объявление и определение самой процедуры. Было также сказано, что 
процедура DLLMain не вызывается автоматически, как следует из документации для 
языков С,и, C++, Как же убедить Delphi воспользоваться процедурой DLLMain? Для 
этого существует перемениая DLLProc, 
    ВёТрпУ автоматически подключает все средства, содержащиеся в модуле 
System. Среди этих средств— код запуска, используемый в Pascal. В модуле 
System объявлена и определена ассемблерная функция _StartLib, имеющая важное^ 
значение. С ее помощью выполняется код, присвоенный системной переменной 
DLLProc. DLLProc — это указатель, со

    Глава 25. Программирование DLL: введение 597 

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

    begin 
    DLLProc := §DLLMain; 
    DLLMain(dll_Process_Attach); end. 

    14шщиализационный блок выполняется один раз на каждый процесс. В 
приведенном выше примере_глобально доступной переменной DLLProc присваивается 
адрес определенной выше процедуры DLLMain. В Delphi вызов блока инициализации, 
определенного в DPR-файле проекта DLL, эквивалентен получению^сообщения: 
dll_Process_Attach. Исходя из этого, чтобы процедура D 
    LLMain обработала,данное сообщение, ее надо вызвать явно, как и показано в 
примере. Все последующие сообщения отсылаются автоматически, если задано 
значение переменной DLLProc. 

    Применение DLLMain в DLL 

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

    ЛИСТИНГ 25.3. DLLECXEPT. DPR. Инициализация С ПОМОЩЬЮ Процедуры DLLMain 

    library DLLExcept; uses 
    ShareMem, 
    Windows,	N SysUtils, Forms, Classes; type 
    TAppHandler = class 
    procedure HandlerProc(Sender: TObject; E: Exception); end; 
    procedure TAppHandler.HandlerProc(Sender: TObject; E: Exception); begin 
    {Код обработчика ошибок. } end; var 
    AppHandler: TAppHandler; procedure DLLMain(dwReason: DWORD); begin 
    case dwReason of 
    dll_Process_Attach : begin 
    AppHandler := TAppHandler.Create; 
    Application.OnException := AppHandler.HandlerProc; end; 
    dll_Process_Detach : begin 
    Application.OnException := nil; AppHandler.Free; end; 
    dll_Thread_Attach  : {Операторы.}; dll_Thread_Detach  : {Операторы.}; end; 
end; begin 
    DLLProc := J_DLLMa.in; DLLMain (dll_P"roc'ess_Attach) ; ericf." 


    598	Часть VI. Сложный материал 

    По сравнению с листингом 25.2, здесь можно заметить некоторые изменения. 
Первое из них— подключение еще одного модуля (Forms). В этом модуле определен 
глобальный объект Application. Если модуль Forms включен в программу, этот 
объект инициализируется автоматически. Объект Application содержит обработчик 
сообщений ОпЕх-ception. Все исключительные ситуации, не обрабатываемые явно.,в 
блоке try. . . ecxept, передаются обработчику OnException объекта Application. 
Если обработчику OnException присвоен некоторый адрес, он выполняется и 
предоставляет возможность обрабатывать ошибки независимо от того, где они возникают. 
    Более подробно об объекте Application и обработчике OnException 
рассказывается в главе 5, "Обработка исключительных ситуаций". 
    В листинге 25.3 после раздела uses появился новый раздел, в котором 
определен класс TAppHandler с единственным методом HandlerProc. В данном 
примере этот метод не выполняет никаких действий, однако в него можно добавить 
операторы обработки исключительной ситуации. 
    За определением метода HandlerProc следует объявление глобальной 
переменной AppHandler типа TAppHandler. Инициализация и сброс переменной 
AppHandler происходят в процедуре DLLMain. 
    Обратите внимание на вариант оператора case, в котором по значению 
управляющей переменной dll_Process_Attach происходит присвоение значения 
переменной AppHandler. В следующей строке методу Application. OnException 
присваивается метод HandlerProc. Обратите также внимание на то, что в 
варианте, соответствующем значению dll_Process_Detach, методу Application. 
OnException присваивается значение nil, после чего объект AppHandler 
освобождается. Применение процедуры DLLMain позволяет программировать четкие, 
легко читае-MbieDLL с одной точкой входа и одной точкойвыхода. 

    Соглашения о вызовах 

    Соглашение о вызовах определяет протокол передачи переменных методу или 
функции. Программистам приходится быть очень точными при выборе соглашений, 
поскольку все они отличаются один от другого. Соглашение о вызовах определяет 
порядок, в_котором параметры передаются функции, применение (или не 
применение) для^этого регистров процессора и кто отвечает за освобождение 
стека — вызывающий блок или вызываемая функция. 
    В табл. 25.2 перечислены все соглашения о вызовах, поддерживаемые Delphi, 
порядок следования параметров в каждом из них и ответственность за выгрузку стека. 

    Таблица 25.2. Соглашения о вызовах, поддерживаемые Delphi 

    Соглашение	Порядок вызова	Выгрузка стека 

    Fast-Call    (регистровый)	Слева направо	Вызываемая подпрограмма 
    StdCall	Справа налево	Вызываемая подпрограмма 
    Pascal	Слева направо	Вызываемая подпрограмма 
    Cdecl	Справа налево	Вызывающая подпрограмма 
    Safecall	Справа налево	Вызываемая подпрограмма 

    становлелнае,по_умолчанию в 16-разрядной версии Delphi 1.0 соглашение 
Pascal во второй версии_былр заменено соглашением Fast-Call — самым 
эффективным из протоколов. В этом протоколе первые три параметра по 
возможности передаются через регистры процессора. Это обеспечивает скорость 
(что и отражено в названии Fast-Call — быстрый вызов). Остальные параметры, 
если таковые имеются, передаются по соглашению Pascal. 

    ПРЕДОСТЕРЕЖЕНИЕ 
    Вызов Fast-Call (или регистровый) поддерживается компиляторами Delphi и 
C++ фирмы Borland, а также некоторых ее конкурентов. Однако реализация 
Fast-call в^компиляторах^ BQrlanct^yHMJSQbtia, из-за чего его следует 
применять только"*в"случаях7 когда не требуется совместимость с другими 
языками и компиляторами. Поэтому, если возникает проблема совместимости 
компиляторов, не следует экспортировать из DLL функции с вызовом Fast-Call, 
как и функции косвенного вызова (callback). Лучше всего применять Fast-Call 
только в пределах кода, полностью принадлежащего Delphi. 

    В большинстве программных интерфейсов Windows 3.x, как и в Delphi 1.0, 
действует соглашение о вызовах Pascal. Это же соглашение применялось для 
функций, экспортируемых из DLL. В Win32 стандартным стало соглашение StdCall. 
В Delphi же по умолчанию действует соглашение Fast-Call, что в конечном счете 
приводит к немалой путанице. 

    Глава 25. Программирование DLL: введение 599 

    •    Как следует из табл. 25.2, во всех соглашениях о вызовах, за 
исключением Cdecl, очисткой стека занимает-ся вызываемая подпрограмма. Как же 
в таком случае вызывающая программа получает возвращаемые значе-ния? Если 
объем возвращаемого значения не больше 32 бит, оно помещается в регистр ЕАХ 
процессора и тем самым сохраняется во время очистки стека. Если же объем 
возвращаемого значения больше 32 бит. в памяти выделяется область для его 
временного хранения. Адрес этой области помещается вызывающей программой в 
стек перед передачей в него параметров. Вызванная подпрограмма, освобождая 
стек, уберет из него только параметры, оставив нетронутым результат. 
    Неверный выбор соглашения о вызовах — наиболее распространенная ошибка при 
программировании DLL. В большинстве 16-разрядных программ Delphi вообще не 
надо было указывать соглашения— по умолчанию принималось соглашение Pascal. 
Если такие программы перекомпилировать в Delphi 2.0 или 3, они^втоматически 
перейдут на соглашение Fast-Call. Если не вмешаться в эту ситуацию, в такой 
программе будут возникать ошибки при обращениях к функциям Обратного вызова, 
обращениях самой программы к DLL, написанным другими программистами, или даже 
при обращениях к собственным DLL, написанным на языке C++. 
    Отладку DLL всегда следует начинать с проверки соглашений о вызовах. 
Компилятор не фиксирует подобные несовместимости, поэтому следует заранее 
определять источники возможных ошибок. Если функция из DLL вызывается по 
соглашению StdCall, но в ней действует соглашение Pascal, то, по всей 
вероятности, вызов такой функции щюйдет успешно. Однако параметры она получит 
в обратном порядке и результаты работы с такими параметрами будут совершенно непредсказуемыми. 
    Если в вызывающей программе действует соглашение StdCall, а в вызываемой 
функции, написанной на языке C++, — Cdecl, может не, пройти сам вызов. 
Причиной может быть отсутствие директивы использования соглашений языка С, 
игнорирование регистра или даже переменное число параметров. Если же вызов 
пройдет, возникнут другие проблемы, связанные с освобождением вызываемой 
функцией параметров из стека. После возврата управления вызывающая программа 
попытается освободить стек еще раз. Результат нетрудно предсказать.elphi 
поддерживает соглашение о вызовах Cdecl, но не поддерживает передачу 
переменного числа парамет-ров. Однако и при фиксированном числе параметров 
вызываемой функции Delphi может не разрешить ссыл-ку, если не снять установку, 
позволяющую изменять имя функции. Более подробно о соглашении Cdecl и ,       
изменении имен рассказывается в главе 30, 

    "Содружество Delphi и C++". 

    Помнить о соглашениях о вызовах следует всегда. Обязательно сверяйте 
соглашения, действующие в вызывающем и вызываемом блоках. Для совместимости с 
Win32 следует применять соглашение StdCall, оставив Fast-Call для кода, 
полностью замкнутого на Delphi. 

    Экспортирование 

    Записывая DLL на диск, компоновщик записывает также имена всех 
экспортируемых подпрограмм вместе с их смещениями в образе файла. Когда во 
время выполнения программы DLL отображается в адресном пространстве 
загружающего процесса, загрузчик находит внутри образа файла DLL имена и 
адреса всех процедур, необходимых загружающей программе (или DLL). Если 
какую-то из подпрограмм не удается найти, загрузка DLL прекращается. 
    К экспортируемым DLL подпрограммам можно обращаться из операционной 
системы, прикладной программы или другой DLL. Сами эти подпрограммы могут 
служить интерфейсом связи с объектами внутри DLL либо независимо выполнять 
некоторые нужные операции. Простейшее применение DLL — объединение некоторого 
числа функций сходного назначения, которые затем могут использоваться разными 
программами. Пример такого подхода представлен в листинге 25.4. 
    Объявление экспортируемой функции в Delphi состоит в выполнении трех 
простых операций. Во-первых, функцию нужно объявить в разделе interface 
модуля. Во вторых, функцию надо указать* в разделе exports. Наконец, 
в-третьих, тело функции следует определить в разделе implementation. Все три 
этапа отражены в примере EXPORTU1. PAS, представленном в листинге 25.4. 

    ЛИСТИНГ 25.4. \UDELPHI3\CHP25\EXPORTSU1. PAS. ExportsUl ВХОДИТ В проект 
Exporting, dpr и предоставляет загружающей программе несколько функций 

    unit   EXPORTSU1;	•• 
    . interface                             (-4/V1 «     (*-#<-••«? 
    uses 
    SysUtils,   Windows; 
    procedure ExSetVolumeLabel(pcName:   PChar);   StdCall; exports 


    600	Часть VI. Сложный материал 


    ExSetVolumeLabel; implementation 
    procedure  ExSetVolumeLabel(pcName :   PChar); begin	~—'              "    
     ' 
    if not Assigned(pcName)   then 
    Raise Exception.Create('Name passed to  LabelDiskVolume  is  nil'); if not 
 SetVolumeLabel(nil,pcName)   then 
    Raise Exception.Create('Cant  set volume  label.   GetLastError  reports   '+ 
    IntToStr(GetLastError)); end; end. 

    В Win32 многие функции возвращают логическое значение. Как правило, они 
возвращают True после выполнения без ошибок, иначе — возвращают False. 
Предположим, необходимо создать библиотеку подпрограмм Delphi, каждая из 
которых включала бы такую функцию вместе с обработкой соответствующего 
исключения: если функция выполняется нормально, выполнение содержащей ее 
подпрограммы продолжается обычным образом; в противном случае инициируется 
исключение, обрабатываемое блоками try. . .except. Именно такая конструкция 
реализована в листинге 25.4, где функция_Зе^уо1итеЬаЬе1 из Win32 помещена в 
экспортируемую процедуру ExSetVolumeLabel. 
    Обратите внимание, что эта процедура объявлена в разделе interface модуля. 
Поскольку это экспортируемая процедура, в ней должно действовать стандартное 
соглашение о вызовах, т.е. StdCall. Именно это соглашение и задано директивой 
StdCall в конце объявления процедуры. Повторно задавать директиву StdCall в 
разделе implementation нет надобности. 
    При переходе из Delphi 1.0, казалось бы, всем подпрограммам следует 
задавать директивы far и export. 
    Однако ни одна из этих директив Delphi больше не нужна. Компилятор оставит 
их без внимания, поскольку 

    заметку 

    0Ли_2?тавлены исключительно:_для обеспечения совместимости_снизу вверх. 
Кроме того, применение директивы export может сделать текст более прозрачным, 
выделив подпрограммы, действительно экспортируемые в другие модули. 
    За объявлением процедуры ExSetVolumeLabel следует раздел exports, в 
котором указана процедура ExSetVolumeLabel. В списке exports указывать 
параметры или тип возвращаемого значения необязательно. 
    Список exports не обязательно помещать в раздел interface модуля, 
экспортирующего соответствующие подпрограммы. В Delphi 1.0 он должен был 
размещаться в DPR-файле. Однако помещать его в модуле сразу после объявлений 
подпрограмм^удофнее: список всегда перед глазами и напоминает о том, что в 
него надо включить объявленную процедуру или функцию, если она должна быть экспортируемой. 
    В разделе exports процедуре или функции можно присвоить порядковое 
значение, по которому она будет быстрее находиться при импортировании. В 
приведенном выше примере это должно выглядеть следующим образом. 

    exports 
    ExSetVolumeLabel index 1; 

    При импортировании данную подпрограмму можно вызывать из модуля ЕХЕ по 
заданному порядковому значению (в данном случае— 1). Такой способ поиска 
занимает меньше времени, однако в больших программах он может привести к 
путанице. Вызывать процедуры и функции по имени — значительно надежнее. К тому 
же в этом случае программу проще отлаживать. Многие полагают также, что фирма 
Microsoft не рекомендует применятьJJIJSOB импортируемых .функций по 
порядковому значению из-за того, что в будущих операционных системах такой" 
вызов может не поддерживаться или не работать~должным "образом. 
    При переносе программ из Delphi 1.0 можно столкнуться с директивой 
Resident. Если в Win32 указать эту директиву в списке exports, компилятор 
проигнорирует ее, поскольку она сохранена исключительно для обеспечения 
совместимости снизу вверх. 
    В Winl6,^^afifl?goMj^pjgiMaTe_NE^New Executable) есть таблица резидентных 
и нерезидентных имен. Имена экспортируемых подпрограмм, помеченных директивой 
Resident, фиксируются в памяти, вследствие чего их вызов происходит очень 
быстро, хотя и за счет небольшого дополнительного расхода памяти. _В_фЩ>мате 
файла_?Е (Portable Executable) таблица резидентных и нерезидентных имен 
отсутствует, поскольку в модели "продес9/подзаданд^^5на_не_нужна. Здесь каждая 
экспортируемая DLT^-функция записывается в раздел . edata образа файла. Более 
подробно о разделах файлового образа в формате РЕ рассказывается в главе 26, 
"Программирование DLL: сложный материал". 
    
    I Совет 1 


    Глава 25. Программирование DLL: введение	601 

    После директивы implementation следует определение процедуры 
ExSetVolumeLabel. Сама процедура немногое добавляет к действию функции 
SetVolumeLabel из Win32, но может служить основой более сложных конструкций. 
@*ней*выполняется проверка параметра, который должен указывать наГимя тома. 
Если параметр равен nil, инициируется исключительная ситуация и выполнение 
передается вызывающей процедуре. 
    |||  В Delphi 3 при переходе от 16-разрядной Delphi 1.0 следует иметь в 
виду, что теперь вызов подпрограммы из DLL в модуль ЕХЕ может заключаться в 
блок try. . . except или try. . . finally. Это стало возможным „     *^    еще 
в Delphi 2.0, поскольку Win32 содержит средства обработки исключительных 
ситуаций, доступные Delphi. В 16-разрядных версиях Windows этих средств нет. 
    Если параметр pcName не равен nil, вызывается функция SetVolumeLabel из 
Win32. В случае ее успешного выполнения происходит обычный возврат из 
процедуры ExSetVolumeLabel. В противном случае возникает исключительная 
ситуация с выводом сообщения об ошибке, в которое внесена часть текста функции GetLastError. 
    Если программа неправильно взаимодействует с DLL_HJUi^OBce__OTKa3biBaeTCfl 
с ней взаимодействовать, прежде всего следует сверить принятае соглашения о 
вызовах. Если соглашения, принятые в DLL и загружающем модуле, совпадают, 
следующим шагом должна быть проверка фактической экспортируемости функций. В 
главе 26, "Программирование DLL: сложный материал", будет рассмотрена утилита 
Turbo Dump, с помощью которой можно проверить экспортируемость функций в DLL и 
получить другую ценную информацию. 
    Более подробно об экспортировании функций из DLL речь идет в главе 26, 
"Программирование DLL: сложный материал". Обработка исключительных ситуаций 
подробно рассматривается в главе 5, "Обработка исключительных ситуаций". 
Описание утилиты Turbo Dump приводится в главе 26, "Программирование DLL: 
сложный материал". 

    Загрузка и выгрузка DLL 

    Чтобы воспользоваться DLL, ее надо отобразить в адресном пространстве 
процесса. Это делается с помощью функций LoadLibrary и LoadLibraryEx из 
программного интерфейса Win32. Функция LoadLibrary взята (с небольшими 
модификациями) из 16-разрядной версии Windows. Что же касается LoadLibraryEx, 
то она представляет собой расширение функции LoadLibrary и доступна только при 
32-разрядных вызовах. 
    То, что DLL загружаются вызовом одной из этих функций, вовсе не означает, 
что необходимо явно к ним обращаться. В Delphi поддерживается как явная, так и 
неявная загрузки. При неявной загрузке Delphi автоматически обращается в 
нужные моменты к функциям LoadLibrary и FreeLibrary (последнюю мы еще 
рассмотрим). Явная и неявная загрузки DLL рассматриваются далее в этой главе. 
    Delphi импортирует функции LoadLibrary и LoadLibraryEx из модуля Windows. 
Определение LoadLibrary выглядит следующим образом: 

    function  LoadLibrary(LibFileName:   PChar):   HMODULE;   stdcall; 

    Применять функцию LoadLibrary несложно. Ей задается один параметр типа 
PChar, указывающий на имя загружаемого файла DLL. Расширение DLL файла можно 
опускать, поскольку именно оно подразумевается по умолчанию. Если в параметре 
LibFileName не указывается полный маршрут к файлу, поиск DLL происходит в 
следующих каталогах. 
    1.   Каталог, из которого загружена программа. 
    2.   Текущий каталог. 
    3.   Каталог \WINDOWS\SYSTEM (или— в системе NT— \Windows\System32, за 
которым следует каталог 16-разрядных модулей \System). 
    4.   Каталог \WINDOWS. 
    5.   Все каталоги, указанные в команде PATH (файл autoexec. bat). 
    Если DLL не удается найти, функция возвращает 0. Чтобы определить причину 
неудачной загрузки, можно воспользоваться функцией GetLastError из Win32, 
возвращающей код ошибки. 
    Функция LoadLibraryEx работает подобно LoadLibrary, но с некоторыми 
дополнениями. В модуль Windows она импортируется в следующем виде. 

    function  LoadLibraryEx(LibFileName:   PChar;   hFile:   THandle; 
    dwFlags:   DWORD):   HMODULE;   stdcall; 


    602	Часть VI. Сложный материал 

    Функция GetLastError возвращает одно из заранее определенных значений. 
Обычно возможные коды ошибок конкретной функции перечислены в документации к 
содержащему ее пакету. Если какой-то код не удается найти, можно поискать файл 
Windows, pas в каталоге \\DELPHI3\SOURCE\RTL\WIN. В этом файле все коды ошибок 
представлены в форме соответствующих постоянных. Например, если вызов 
некоторой функции из Win32 привел к ошибке, а обращение к функции GetLastError 
дало код ошибки 15, можно задать поиск символов "15" в файле Windows . pas. 
Поиск даст следующий результат. (The system cannot find the drive specified}	"""" 

    ERROR_INVALID_DRIVE  =   15; 

    Природу ошибки иногда можно определить уже из имени постоянной; тем не 
менее определение постоянной обычно сопровождается комментарием. 
    ТГаконец,*в~модуле SyltJtiis"есть функция SysErrorMessage, возвращающая 
строку с описанием кода ошибки, указанного в качестве ее единственного 
параметра. Однако возвращаемая строка не превышает комментариев из файла 
Windows .pas. 
    Первый параметр, как и в функции LoadLibrary, указывает имя DLL-файла, в 
котором также по умолчанию предполагается расширение DLL. Имя файла можно 
задавать и с полным маршрутом, иначе поиск его будет выполняться по уже 
указанной схеме. Опять же, при неудачной загрузке возвращается значение 0 
(причину неудачи можно определить с помощью функции Get LastError). 
    В функции LoadLibraryEx определены два дополнительных параметра. О первом 
ее параметре, LibFileName, уже шла речь при рассмотрении функции LoadLibrary. 
Второму всегда присваивается значение 0 (зарезервирован на будущее). В третьем 
параметре типа DWORD устанавливается один из трех возможных флагов. 

    Флаг Dont_Resolve_DLL_References 

    Флаг Dont_Resolve_DLL_References может задаваться в качестве третьего 
параметра функции LoadLibraryEx. Этот флаг не следует применять в программе, 
предназначенной исключительно для операционных систем Win95 и WinNT. 
    Установка флага Dont_Resolve_DLL_Ref erences предотвращает следующие три 
ситуации при обычной загрузке DLL (или просто при обращении к функции LoadLibrary). 
    1.   Вызов процедуры DLLMain. 
    2.   Проверку DLL на зависимость от других DLL, что ведет к необходимости 
разрешать все эти зависимости и отображать все DLL в адресное пространство процесса. 
    3.   Разрешение ссылок из данной DLL на функции из других DLL (внешних ссылок). 
    В остальном DLL загружается обычным образом. Если не принимать во внимание 
способа установки системой атрибутов защиты страниц выполняемого кода 
(разбивка на страницы в DLL обычно имеет место), данный флаг мало чем 
отличается от флага Load_Library_As_DataFile, рассматриваемого в следующем 
разделе. Причем использование флага Load_Library_As_DataFil 
e|^редпрдтгительнее1доякйдьку он действует также в системах Win95 и WinNT. 

    Флаг Load_Library_As_DataFile 

    Флаг Load_Library_As_DataFile может задаваться в качестве третьего 
параметра функции LoadLibraryEx. Если этот флаг установлен, модуль 
отображается в адресное пространство процесса как обычный файл, с помощью 
объектов отображения файлов из ядра Windows. Коду не задаются какие бы то ни 
было атрибуты защиты страниц, процедура DLLMain не вызывается, 
внешние,ссвдкице^азреш_аются. Однако при установке этого флажка остается 
важным значение, возвращаемое функцией LoadLibraryEx. Им можно воспользоваться 
для загрузки ресурсов, например битовых образов или пиктограмм. 
    Этим флагом мож1^вр?П?л^з^щт^?я_^ля^13^Л?чения[.ресурсов (например, 
пиктограмм) из DLL- или ЕХЕ-файлов. Значение, возвращенное функцией 
LoadLibraryEx, можно указать в качестве параметра функции LoadResource, 
которая возвратит дескриптор пиктограммы. Этот дескриптор можно указать в 
качестве параметра функции LockResource, по возвращенному ею указателю 
скопировать данные и записать их в ICO-файл. При этом возможная ошибка 
выполнения функции LoadLibrary из-за зависимости от других модулей или 
неудачной инициализации DLLMain не приведет ни к каким последствиям. 

    Г Совет I 

    Глава 25. Программирование DLL: введение	603 

    При установленном флаге Load_Library_As_DataFile вместо DLL можно 
загружать ЕХЕ-файл. Сама по себе функция LoadLibrary не могла бы с этим 
справиться, поскольку пыталась бы вызвать процедуру _StartLib, а затем 
DLLMain, но ни та, ни другая не применяются в ЕХЕ-файле. Поскольку при 
установленном флаге Load_Library_As_DataFile не происходит обращения к коду^ 
точки входа модуля ЕХЕ, ЕХЕ-файл просто отображается в адресное пространство 
процесса, что обеспечивает" доступ' к его данным и ресурсам. 

    Флаг LoadJtfith_Altered_Search_Path 

    Флаг Load_With_Altered_Search_Path может задаваться в качестве третьего 
параметра функции LoadLi-braryEx. Он задает для функции LoadLibraryEx (в 
случаях, когда не указывается полный путь) схему поиска DLL-файла, отличную от 
задаваемой по умолчанию. Если этот флажок установлен, поиск файла, имя 
которого указывается параметром LibFileName, выполняется в следующих каталогах. 
    1.   Каталог, явно задаваемый параметром LibFileName. 
    2.   Текущий каталог. 
    3.   Каталог \WINDOWS\SYSTEM (или— в системе NT— \Windows\System32, за 
которым следует каталог 16-разрядных модулей \System). 
    4.   Каталог \WINDOWS. 
    5.   Все каталоги, указанные в команде PATH (файл autoexec. bat). 
    При загрузке одной DLL в адресное пространство процесса может отображаться 
еще несколько модулей, в том числе совершенно неизвестных. И чтобы отображение 
прошло успешно, все эти модули должны быть найдены. Если же некоторые из них 
связаны еще с какими-то, точно так же должны быть найдены и они. Флаг 
Load_With_Altered_Search_Path полезен для точной настройки поиска подобной 
системы связанных DLL, когда точное имя и расположение каждой из них указать 
невозможно. При установленном флаге Load_With_Altered_Search_Path следует 
заранее знать и указать полный маршрут к искомому файлу; в противном случае 
поиск выполняется по схеме, задаваемой по умолчанию. 

    Дескриптор модуля и функция FreeLibrary 

    Функции LoadLibrary и LoadLibraryEx возвращают значение типа HModule (по 
существу, это тот же тип THandle). В случае ошибки обе функции возвращают 0. 
Дескриптор модуля необходим многим функциям программного интерфейса Windows 
для взаимодействия с ним или извлечения данных, ресурсов и другой полезной 
информации. Вот лишь несколько примеров таких функций. 
    •	EnumResourceNames 
    •	LoadResource 
    •	EnumResTypeProc 
    •	FreeLibraryAndExitThread 
    •	FindResourceEx 
    •	GetProcAddress 
    •	FreeLibrary 
    Дескриптор модуля поддерживается системой. Если процесс отображает DLL в 
свое адресное пространство, функция Load-Library возвращает значение типа 
HMODUKE. Если дочерние подзадачи попытаются еще раз (с помощью функции 
LoadLibrary) отобразить данную DLL в процесс, они получат копию значения 
HMODULE, полученного при первоначальном отображении. Сама 
    DLL повторно в процесс не отображается. Вместо этого система при каждом 
последующем обращении к функции LoadLibrary или LoadLibraryEx увеличивает на 1 
значение счетчика обращений. Таким образом, реально библиотека загружается 
только один раз, повторные же попытки приводят лишь к приращению значения 
счетчика обращений. 
    Удаление DLL из процесса происходит только после того, как значение 
счетчика обращений снова станет равным 0. А это, как правило, происходит 
тогда, когда DLL не нужна. Для этого в Win32 есть функция FreeLibrary. При 
каждом вызове она уменьшает значения счетчика для указанной DLL — в процессе, 
в котором она используется — на I. Если в результате вызова функции 
FreeLibrary значение счетчика обращений достигает 0, образ файла удаляется из 
адресного пространства процесса. Объявление функции FreeLibrary, импортируемой 
модулем Windows, имеет следующий вид. 

    function  FreeLibrary(hLibModule:   HMODULE):   BOOL;   stdcall; 

    Функция FreeLibrary имеет один параметр типа HMODULE. Он, конечно же, 
должен быть равен значению, возвращенному функцией LoadLibrary или 
LoadLibraryEx при загрузке DLL. В случае успешного выполнения функция 
FreeLibrary возвращает True, иначе — False. В последнем случае причину неудачи 
можно найти с помощью функции GetLastError из Win32. 

    604	Часть VI. Сложный материал 

    Мы рассмотрели применение функций LoadLibrary и FreeLibrary соответственно 
для загрузки и выгрузки DLL. Однако для доступа к содержащемуся в DLL коду 
этого недостаточно. Следующий шаг— импортирование функций, экспортируемых DLL. Продолжим! 

    Импортирование 

    После создания DLL, экспортирующей некоторые функции, этими функциями 
можно воспользоваться, лишь загрузив DLL. После этого можно вызывать 
экспортируемые подпрограммы. Вот так просто. Почти просто... Дело в том, что, 
если в программе, использующей DLL, просто указать имя одной из экспортируемых 
подпрограмм, компилятор не опознает этого имени (поскольку не знает 
содержимого DLL) и выдаст ошибку. Поэтому все импортируемые из DLL процедуры и 
функции необходимо_объявить. 
    ОднакоГчтобы программу можно было компилировать, требуется еще кое-что. Во 
время компоновки компоновщик будет пытаться разрешить все объявления и, если 
это не удастся, выдаст сообщение об ошибке. Поэтому должен существовать способ 
объявления функции для компилятора с отметкой внешней ссылки на нее для 
компоновщика. В то же время ссылка должна разрешаться во время выполнения 
программы. Звучит несколько запутанно. Что ж, попробуем вникнуть в подробности 
этого процесса, именуемого импортированием. 
    В Delphi есть два способа загрузки и импортирования подпрограмм из DLL. 
    •   Неявный. DLL загружается статически одновременно с загрузкой программы 
по объявлениям функций, следуя определенным в Delphi правилам таких объявлений. 
    •   Явный. DLL при необходимости загружается динамически с помощью функции 
LoadLibrary, после чего адре-са экспортируемых функций получаются благодаря 
функции GetProcAddress.	~, 
    Большинство программистов предпочитают неявный метод ввиду его простоты. 
Однако за простоту приходится платить потерей управляемости процесса и, самое 
главное, динамичности. С другой стороны, неявная загрузка быстра, надежна и 
автоматически прекращает выполнение программы в случае ошибки. Рассмотрим оба 
метода подробнее, начав с более простого и популярного — неявного. 

    Неявная загрузка 

    В предыдущем разделе данной главы рассматривалось применение в Win32 
функций LoadLibrary и LoadLibraryEx для загрузки DLL, а также FreeLibrary — 
для выгрузки. Delphi поддерживает специальные синтаксические конструкции, 
позволяющие задать автоматический вызов функций:LoadLikrary и FreeLibrary,во 
время выполнения программы. 
    По существу, Delphi не осуществляет вызова данных функций — эту задачу 
берет на себя операционная система. По специальным объявлениям компоновщик 
Delphi записывает имена импортируемых функций и содержащих их DLL по 
определенному адресу внутри образа ЕХЕ-файла. Когда происходит запуск 
программы и ее ЕХЕ-файл отображается в адресном пространстве процесса, Windows 
обращается к этому адресу и извлекает имена всех DLL, используемых программой, 
вместе с именами импортируемых из них функций. 
    Как только Windows определяет, что программа использует некоторую DLL, она 
вызывает функцию LoadLibrary. Эта функция ищет по заданной схеме DLL-файл и, 
если не находит, выгружает программу. Если же DLL находится, ее файл 
отображается в адресное пространство процесса программы. После этого Windows 
находит определенный адрес внутри образа DLL-файла, откуда извлекает имена 
всех экспортируемых DLL функций. Если не удается найти хотя бы одной функции, 
которую программа предполагает импортировать, образ DLL удаляется из процесса 
и выполнение программы прекращается. 
    Файловая спецификация РЕ (Portable Executable) отличается 
дев^ят^ь^объектами^известными как разделы и применяемыми в 32-разрядных 
выполняемых (ЕХЕ или DLL) модулях. Эти разделы сущест1уют"внутри об-™    раза 
файла наряду с различными заголовками. Каждый раздел располагается по 
определенному адресу и содержит информацию, необходимую операционной системе 
для управления выполнением программы. В разделах помещается код, данные, 
информация для отладки и многое другое. 
    Один из разделов, определенных по умолчанию (обычно^jHMeHyeMbiH.-^data}, 
содержит имена, относительные адреса и порядковые значения всех функций, 
экспортируемых модулем. Второй подобный раздел (. idata) содержит имена всех 
используемых программой DLL вместе с именами всех содержащихся в DLL и 
импортируемых 'программой "функций. 
    Для нахождения разделов и содержащейся в них информации операционная 
система располагает в определенном месте образа файла модуля серию адресов. В 
конце концов нужный раздел находится и информация из него извлекается. 
    Более подробно о разделах и файловом формате РЕ рассказывается в главе 26, 
"Программирование DLL: сложный материал". 

    Глава 25. Программирование DLL: введение	605 

    Попросту говоря, имена всех экспортир^емьдх-функшш записаны по 
определенному адресу внутдиDLL-файла. Имена же всех функций, 
импортируемых"модулем ЕХЕ, записываются по определенному адр~ёсу 
тутри^ЕХЕ-файла вместе с именами DLL, в которых они располагаются. При 
загрузке ЕХЕ-файла Windows проверяет существование всех нужных ГДД^й.наличие в 
них соответствующих экспортируемых функций. Если не все удается найти, 
программа не запускается. 
    Импортирование происходит в обратном порядке. Модуль ЕХЕ может 
использовать совсем неизвестные DLL. Библиотека визуальных компонентов Delphi 
(VCL) постоянно обращается к программному интерфейсу Win32. А это означает, 
что в программу неявно включаются файлы ядра Windows: Kernel32.dll, 
User32.dll, GDI32.dll и др. Если программа использует пакеты, неявно 
подключаются и пакетные DLL. Каждый пакет сам по себе тоже использует файлы 
ядра Windows и другие пакеты. Опять же, многие используемые в программе DLL 
используют друг друга. Получается нечто вроде замкнутого круга. Эта ситуация 
отражена на рис. 25.2. 

    Процесс 


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

    Для неявного импортирования функции GetNetUserlD из DLL NETTOOLS.DLL надо 
поместить в программу следующее объявление. 
    function  GetNetUserlD(aUserName:   string):   longint;   StdCall; 
external   'NetTpols.^jdll'   Name   'GetNetUserlDA1; 
    Обратите внимание на то, что GetNetUserlD— несуществующая функция. 
Объявление начинается как любое другое — с ключевого слова procedure или 
function. Затем следует имя (в данном случае — GetNetUserlD). Функции 
мржнозадать любое имя, если затем указать ^щ^шстич^кое^после директивы Name в 
конце объявления). Вслед за именем идет~сгшсок"пара1иетров и тип возвращаемого 
значения (для функций). Далее можно указать соглашение о вызовах для данной 
функции. Если неизвестно, какое именно соглашение действует в импортируемой 
подпрограмме, выясните это. Задания неправильных соглашений о вызовах приведет 
к отрицательным последствиям, которые трудно будет устранить. Что касается 
стандартов"Wrri32;"TO йсё функцйиГйз DLL Win32 должны импортироваться по 
соглашению StdCall. После описания подпрограммы и соглашения о вызовах следует 
директива external. Включение этой директивы -Обязательно, ибо она указывает 
имя DLL, содержащей данную функцию. Для большей прозрачности текста программы 
имяШ! L следует указывать с расширением, однако по умолчанию предполагается 
расширение DLL. 
    После директивы external и имени модуля идет необязательная директива 
Name. Она необходима только в тех слу-„чаях, когда имя объявляемой 
подпрограммы (следующеё~сразу за ключевым словом function или procedure) не 
сов: дадаех.^именем, под которым она экспортируется из DLL. В приведенном 
примере функция объявляется с именем Get-Ые^зегщГфактическое же имя 
импортируемой функции— GetNetUserlDA. В модуле ЕХЕ все обращения к этой 
функции происходят по имени GetNetUserlD. 

    На      'Л заметку 

    Программный интерфейс Win32 многие из своих функций экспортирует в двух 
вариантах. Пример — функции MessageBoxA и MessageBoxW. Буква А в конце имени 
означает ANSI, W — Wide (Wide Char). В модуле Windows можно обнаружить 
следующие операторы импортирования. 

    function MessageBoxA;	external	'user32'   name	'MessageBoxA1; 
    function MessageBoxW;	external	'user32'   name	'MessageBoxW'; 
    function MessageBox;	external	'user32'   name	'MessageBoxA'; 

    В этом примере одна и та же функция импортируется под тремя разными 
именами. Обратите внимание, что документированное имя MessageBox предполагает 
стандарт ANSI. Для вызова версии функции, поддерживающей стандарт Wide Char, 
необходимо явно вызвать MessageBoxW. 

    606 Часть VI. Сложный материал 

    ^)бъявл&ния импортируемых функций следует, йазу?ща^ ^^^ЖЛ?_ЙН1НЕФ525? 
М°ДУЛЯ- Однако можно разместить объявление и таким образом, что часть его, 
удовлетвор^ющ^тре^оза1^м_дс?мг|1шят"6ра,' разместится в разделе интерфейса, 
вторая же, удовлетворяющаяТ-р^о^Жн^'ЖКошГонрвщика, — в разделе реализации, как 
в следующем примере. 

    var	• 
    Forml: TForml; 
    function GetNetUserlD(aUserName: string): longint; StdCall; implementation 
{$R *.DFM} function GetNetUserlD; external 'NetTools.dll' Name 'GetNetUserlDA'; 
    end. 

    Другой способ импортирования подпрограммы — по порядковому значению. 
Рассмотрим следующий вариант объявления функции GetNetUserlD: 

    function GetNetUserlD(aUserName:   string):   longint;   StdCall; 
    external   'NetTools.dll'   index 25; 

    Поиск функции по порядковому значению выполняется быстрее, однако 
применять его не рекомендуется, поскольку в больших программах он может 
привести к путанице. К тому же в этом случае усложняется отладка программы. Ко 
всему прочему, многие^ г^олагают, что фирма Microsoft не рекомендует применять 
поиск по порядковым значениям, ибо в будущих операционных системах такой поиск 
может не поддерживаться или не работать должным образом. 
    В последнем примере необязательная директива_пате опущена. Как уже 
указывалось, фактическое имя импортируемой функции здесь не GetNetUserlD, a 
GetNetUserlDA. Как же ее найти? При наличии директивы index поиск подпрограммы 
по имени не производится. Вместо этого в списке exports DLL ищется указанное 
по5эядковоё11ичёние. Й"1ШнПом~случае оно равно 25Г 
    Чтобы программа правильно выполнялась, DLL должна под номером 25 
экспортировать функцию GetNetUserlDA. Если это не так, будет выполняться та 
функция, которая экспортируется под этим номером. Нетрудно представить 
результат! Если не удастся найти функции с порядковым значением 25, произойдет ошибка. 
    Если использовать указанные выше объявления импортируемых функций, нет 
никакой необходимости явно обращаться к функции LoadLibrary, LoadLibraryEx или 
FreeLibrary. Объявленные подпрограммы и имена соответствующих DLL будут 
занесены в таблицы импортирования модуля ЕХЕ, об остальном позаботится 
операционная система. За такое автоматическое применение DLL приходится 
платить определенную цену. Огромным преимуществом применения в программе DLL 
является возможность ЙхТагрузки иТшгрузки в любой момент. В рассмотренном же 
методе неявной загрузки DLL загружается одновременно с загрузкой программы 
(замедляяi ее: запускай остается отображенной в пространстве процесса на 
протяжении всего выполнения даже после того, как становится ненужной. Это 
приводит к избыточным затратам памяти и системных ресурсов. 

    Явная загрузка 

    Импортирование подпрограммы можно задать явно, воспользовавшись ^}Щ1ЩИ?|;_ 
J^^jr??Addj:.&s s из программного интерфейса Win32 наряду с обращением к 
таким_^ункциямд KaK_l,oadLibrary, LoadLibraryEx и FreeLibrary. Явные'загрузка 
и импортирование более динамичны и программно управляемы. Программисты часто 
избегают этого подхода из-за большего объема кодирования. Начинающим 
программистам языка Pascal явные загрузка и импортирование кажутся сложными; 
еще больше склоняет их к неявному методу недостаток документации. И все же, 
рассмотрим пример, представленный в листинге 25.5. Файл EXPORTING. DLL можно 
найти на дополнительно распространяемой дискете. 

    ЛИСТИНГ 25.5. \UDELPHI3\CHP25\LOADERU1. PAS. Использование EXPORTING. DLL 
В LoaderEXE 

    unit LoaderUl; 
    interface 
    uses 
    Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; 


    Глава 25. Программирование DLL: введение	607 


    type 
    TExSetVolumeLabgl^= procedure(pcName: PChar); StdCall; TForml = 
class(TForm)'" Buttonl: TButton; 
    procedure ButtonlClick(Sender: TObject); private 
    { Private declarations } public 
    { Public declarations } end; var 
    Forml: TForml; implementation {$R *.DFM} 
    procedure TForml.ButtonlClick(Sender: TObject); var .ExSetVolumeLabel: TExSetVolumeLabel; 
    hDHInst: THandleT""'"" begin 
    hDHInst := LoadLibrary( 'Exporting.dll '); if hDHInst <="Tf"then 
    Raise Exception.Create('[LoadLibrary Fail] GetLastError reports: '+ 
    IntToStr(GetLastError)) ; try' 
    _?ExSetVolumeLabel := GetPrQ,cAddress (hDHInst, 'ExSetVolumeLabel' ) ; if 
not Assigned(ExSetVolumeLabel) then 
    Raise Exception.Create('[GetProcAddress Fail] GetLastError reports: '+ 
    IntToStr(GetLastError)); ExS^etVolvmeLabel (' BOOT DRIVE1) ; 
finally	••••-• -.. 
    FreeLibrary(hDHInst) ; end;  ~*~-~ • 
    end; end. 

    Почти весь показанный здесь раздел интерфейса модуля Delphi генерирует 
автоматически при создании новой программы. Единственное исключение — 
оператор, следующий сразу за директивой type: 

    TExSetVolumeLabel  = procedure(pcName:   PChar);   StdCall; 

    Это объявление нового типа с именем TExSetVolumeLabel, представляющего 
процедуру с соглашением StdCall и одним параметром типа PChar. Этот тип 
соответствует формату процедуры ExSetVolumeLabel, экспортируемой модулем 
Exporting. dll. Процедура меняет метку тома текущего каталога. При успешном 
выполнении тому будет задана новая метка, указанная параметром pcName. 
    В листинге 25.4 была показана DLL, экспортирующая функцию 
ExSetVolumeLabel. Функция 
    Чаметку	объявлялась и определялась в модуле ExportUl ^заз-Этот;модуль 
является_частью._лроекта Ехporting, после компиляции образующего модуль 
Exporting.dll. Исходный текст этого модуля, как и все листинги данной главы, 
находится на дополнительно распространяемой дискете. 
    В последнем примере главная нагрузка приходится на раздел реализации. 
Здесь в единственной процедуре TForml.. ButtonClick выполняются все 
необходимые действия по загрузке DLL, импортированию функции ExSetVolumeLabel 
из модуля Exporting. dll и выгрузки DLL. 
    Процедура начинается с объявления двух переменных: 

    var  ExSetVolumeLabel:   TExSetVolumeLabel; hDHInst:   THandle; 

    Обратите внимание, что переменная ExSetVolumeLabel имеет тип 
TExSetVolumeLabel. Она применяется для вызова функции из DLL. Во вторую 
переменную— hDHInst— записывается результат вызова функции LoadLibrary. 

    hDHInst   := LoadLibrary ('Exporting.dll1 ); if hDHInst =  0  then 
    Raise Exception.Create('[LoadLibrary Fail]   GetLastError  reports:   '   + 
    IntToStr(GetLastError)); 


    608	Часть VI. Сложный материал 

    Вызывается функция LoadLibrary. В качестве параметра указывается маршрут к 
файлу Exporting.dll. В Win32 функция LoadLibrary возвращает 0, если DLL не 
удается загрузить. Это значение проверяется во втором операторе. Если 
переменная hDHInst оказывается равной нулю, инициируется исключительная 
ситуация. В текст сообщения об ошибке включается код ошибки, полученный с 
помощью функции Win32 GetLastError. 
    Если DLL загружается успешно, вызывается функция GetProcAddress, 
пытающаяся получить указатель на адрес функции ExSetVolumeLabel, 
экспортируемой из модуля Exporting.dll. 

    @ExSetVolumeLabel   :=  GetProcAddress(hDHInst,    'ExSetVolumeLabel1); if 
 not  Assigned(ExSetVolumeLabel)   then 
    Raise  Exception.Create('[GetProcAddress   Fail]   GetLastError  reports:  
  '   + 
    IntToStr(GetLastError)); 

    Переменная ExSetVolumeLabel не является указателем, поэтому слева к ней 
добавляется символ^, чтобы пока-     '/ |   зать, что адрес, возвращаемый 
функцией GetProcAddress, становится адресом переменной ExSetVolumeLabel. В j   
качестве первого параметра функции GetProcAddress указывается дескриптор DLL, 
возвращаемый функцией Load-Library, в качестве второго— имя функции, 
экспортируемой DLL. Если при выполнении функции GetProcAddress происходит 
ошибка, переменной ExSetVolumeLabel присваивается значение nil. 
    Проверка равенства переменной ExSetVolumeLabel значению nil выполняется с 
помощью функции Assigned. Если переменная равна nil, инициируется исключение с 
добавлением в текст сообщения об ошибке кода ошибки, возвращаемого функцией 
Win32 GetLastError. Если переменная ExSetVolumeLabel не равна nil, вызывается 
функция ExSetVolumeLabel. В данном примере в качестве параметра ей передается 
текст "BOOT DRIVE": 

    ExSetVolumeLabel('BOOT   DRIVE'); 

    Сразу за вызовом процедуры ExSetVolumeLabel следует вызов функции 
FreeLibrary, освобождающий модуль 

    Exporting.dll: 
    FreeLibrary (hDHInst) ; 

    В листинге 25.5 показано действительно динамическое применение DLL. Однако 
данный метод, как и неявный, имеет свои недостатки. Предположим, метод TForml. 
ButtonlClick вызывался бы в цикле. Эффективность программы была бы просто на 
нуле. Однако программист сам может определять, какого рода эффективности 
добиваться: принять ли необходимость медленной первоначальной инициализации 
или смириться с небольшими потерями времени при каждом вызове функций 
LoadLibrary и FreeLibrary? Все зависит от конкретной задачи, но чаще всего 
оказывается удобным со-"чётать^оба подхода. 

    Резюме 

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

    Глава 25. Программирование DLL: введение	609 
    20 Использование Delphi 3. Специальное издание 

