Программирование для Windows NT© Александр Фролов, Григорий ФроловТом 27, часть 2, М.: Диалог-МИФИ, 1996, 272 стр. |
Библиотеки динамической компоновки DLL(Dynamic Link Libraries) являются стержневым компонентом операционной системы Windows NT и многих приложений Windows. Без преувеличения можно сказать, что вся операционная система Windows, все ее драйверы, а также другие расширения есть ни что иное, как набор библиотек динамической компоновки. Редкое крупное приложение Windows не имеет собственных библиотек динамической компоновки, и ни одно приложение не может обойтись без вызова функций, расположенных в таких библиотеках. В частности, все функции программного интерфейса Windows NT находятся именно в библиотеках динамической компоновки DLL.
В 13 томе “Библиотеки системного программиста”, который называется “Операционная система Microsoft Windows 3.1 для программиста. Часть третья” мы уже рассказывали о создании и использовании библиотек DLL в среде операционной системы Microsoft Windows версии 3.1. Что же касается операционных систем Microsoft Windows NT и Microsoft Windows 95, то в них библиотеки DLL создаются и работают по-другому.
Прежде чем приступить к изучению особенностей использования библиотек динамической компоновки в операционной системе Microsoft Windows NT, напомним кратко, чем отличаются друг от друга статическая и динамическая компоновка.
При использовании статической компоновки вы готовили исходный текст приложения, затем транслировали его для получения объектного модуля. После этого редактор связей компоновал объектные модули, полученные в результате трансляции исходных текстов и модули из библиотек объектных модулей, в один исполнимый exe-файл. В процессе запуска файл программы загружался полностью в оперативную память и ему передавалось управление.
Таким образом, при использовании статической компоновки редактор связей записывает в файл программы все модули, необходимые для работы. В любой момент времени в оперативной памяти компьютера находится весь код, необходимый для работы запущенной программы.
В среде мультизадачной операционной системы статическая компоновка неэффективна, так как приводит к неэкономному использованию очень дефицитного ресурса - оперативной памяти. Представьте себе, что в системе одновременно работают 5 приложений, и все они вызывают такие функции, как sprintf, memcpy, strcmp и т. д. Если приложения были собраны с использованием статической компоновки, в памяти будут находится одновременно 5 копий функции sprintf, 5 копий функции memcpy, и т. д (рис. 3.1).
Рис. 3.1. Вызов функций при использовании статической компоновки
Очевидно, использование оперативной памяти было бы намного эффективнее, если бы в памяти находилось только по одной копии функций, а все работающие параллельно программы могли бы их вызывать.
Практически в любой многозадачной операционной системе для любого компьютера используется именно такой способ обращения к функциям, нужным одновременно большому количеству работающих параллельно программ.
При использовании динамической компоновки загрузочный код нескольких (или нескольких десятков) функций объединяется в отдельные файлы, загружаемые в оперативную память в единственном экземпляре. Программы, работающие параллельно, вызывают функции, загруженные в память из файлов библиотек динамической компоновки, а не из файлов программ.
Таким образом, используя механизм динамической компоновки, в загрузочном файле программы можно расположить только те функции, которые являются специфическими для данной программы. Те же функции, которые нужны всем (или многим) программам, работающим параллельно, можно вынести в отдельные файлы - библиотеки динамической компоновки, и хранить в памяти в единственном экземпляре (рис. 3.2). Эти файлы можно загружать в память только при необходимости, например, когда какая-нибудь программа захочет вызвать функцию, код которой расположен в библиотеке.
Рис. 3.2. Вызов функции при использовании динамической компоновки
В операционной системе Windows NT файлы библиотек динамической компоновки имеют расширение имени dll, хотя можно использовать любое другое, например, exe. В первых версиях Windows DLL-библиотеки располагались в файлах с расширением имени exe. Возможно поэтому файлы krnl286.exe, krnl386.exe, gdi.exe и user.exe имели расширение имени exe, а не dll, несмотря на то, что перечисленные выше файлы, составляющие ядро операционной системы Windows версии 3.1, есть ни что иное, как DLL-библиотеки. Наиболее важные компоненты операционной системы Microsoft Windows NT расположены в библиотеках с именами kernel32.dll (ядро операционной системы), user32.dll (функции пользовательского интерфейса), gdi32.dll XE "gdi32.dll" (функции для рисования изображений и текста).
Механизм динамической компоновки был изобретен задолго до появления операционных систем Windows и OS/2 (которая также активно использует механизм динамической компоновки). Например, в мультизадачных многопользовательских операционных системах VS1, VS2, MVS, VM, созданных для компьютеров IBM-370 и аналогичных, код функций, нужных параллельно работающим программам, располагается в отдельных библиотеках и может загружаться при необходимости в специально выделенную общую область памяти.
В операционной системе Microsoft Windows версии 3.1 после загрузки DLL-библиотека становилась как бы частью операционной системы. DLL-библиотека является модулем и находится в памяти в единственном экземпляре, содержит сегменты кода и ресурсы, а так же один сегмент данных (рис. 3.3). Можно сказать, что для DLL-библиотеки создается одна копия (instance), состоящая только из сегмента данных, и один модуль, состоящий из кода и ресурсов.
Рис. 3.3. Структура DLL-библиотеки в памяти
DLL-библиотека, в отличие от приложения, не имеет стека и очереди сообщения. Функции, расположенные в модуле DLL-библиотеки, выполняются в контексте вызвавшей их задачи. При этом они пользуются стеком копии приложения, так как собственного стека в DLL-библиотеке не предусмотрено. Тем не менее, в среде операционной системы Microsoft Windows версии 3.1 функции, расположенные в 16-разрядной DLL-библиотеке, пользуются сегментом данных, принадлежащей этой библиотеке, а не копии приложения.
Создавая приложения для операционной системы Microsoft Windows версии 3.1, вы делали DLL-библиотеки для коллективного использования ресурсов или данных, расположенных в сегменте данных библиотеки. Функции, входящие в состав 16-разрядной DLL-библиотеки, могут заказывать блоки памяти с атрибутом GMEM_SHARE. Такой блок памяти не принадлежит ни одному приложению и поэтому не освобождается автоматически при завершении работы приложения. Так как в Windows версии 3.1 все приложения используют общую глобальную память, блоки памяти с атрибутом GMEM_SHARE можно использовать для обмена данными между приложениями. Управлять таким обменом могут, например, функции, расположенные в соответствующей DLL-библиотеке.
Когда разные приложения, запущенные в среде операционной системы Microsoft Windows версии 3.1, обращались к одной и той же функции DLL-библиотеки, то они все использовали для этого один и тот же адрес. Это и понятно - так как все приложения работали на одной виртуальной машине, то все они находились в одном адресном пространстве.
В операционной системе Microsoft Windows NT каждое приложение работает в рамках отдельного адресного пространства. Поэтому для того чтобы приложение могло вызывать функции из DLL-библиотеки, эта библиотека должна находиться в адресном пространстве приложения.
Здесь мы обращаем ваше внимание на первое отличие механизма динамической компоновки в среде Microsoft Windows NT от аналогичного механизма для Microsoft Windows версии 3.1.
В среде Microsoft Windows NT DLL-библиотека загружается в страницы виртуальной памяти, которые отображаются в адресные пространства всех “заинтересованных” приложений, которым нужны функции из этой библиотеки. При этом используется механизм, аналогичный отображению файлов на память, рассмотренный в первой главе нашей книги.
На рис. 3.4 схематически показано отображение кода и данных DLL-библиотеки в адресные пространства двух приложений.
Рис. 3.4. Отображение DLL-библиотеки в адресные пространства двух процессов
На этом рисунке показано, что в глобальном пуле памяти находится один экземпляр кода DLL-библиотеки, который отображается в адресные пространства приложений (процессов). Что же касается данных DLL-библиотеки, то для каждого приложения в глобальном пуле создается отдельная область. Таким образом, различные приложения не могут в этом случае передавать друг другу данные через общую область данных DLL-библиотеки, как это можно было делать в среде операционной системы Microsoft Windows версии 3.1.
Тем не менее, принципиальная возможность создания глобальных областей памяти DLL-библиотеки, доступных разным процессам, существует. Для этого необходимо при редактировании описать область данных DLL-библиотеки как SHARED.
Заметим, что одна и та же функция DLL-библиотеки может отображаться на разные адреса в различные адресные пространства приложений. Это же относится к глобальным и статическим переменным DLL-библиотеки - они будут отображаться на разные адреса для различных приложений.
Когда первое приложение загрузит DLL-библиотеку в память (явно или неявно), эта библиотека (точнее говоря, страницы памяти, в которые она загружена) будет отображена в адресное прстранство этого приложения. Если теперь другое приложение попытается загрузить ту же самую библиотеку еще раз, то для него будет создано новое отображение тех же самых страниц. На этот раз страницы могут быть отображены уже на другие адреса.
Кроме того, для каждой DLL‑библиотеки система ведет счетчик использования (usage count). Содержимое этого счетчика увеличивается при очередной загрузке библиотеки в память и уменьшается при освобождении библиотеки.
Когда содержимое счетчика использования DLL-библиотеки станет равным нулю, библиотека будет выгружена из памяти.
В среде операционной системы Microsoft Windows версии 3.1 существовала возможность обмена данными между различными работающими параллельно приложениями через область локальной кучи (local heap) DLL-библиотеки. Эта область находилась в сегменте данных DLL-библиотеки и потому была доступна всем приложениям.
Что же касается операционной системы Microsoft Windows NT, то в ее среде DLL-библиотеки не имеют собственных областей данных, а отображаются в адресные пространства приложений, загружающих эти библиотеки. Как результат, приложения не могут получать адреса статических и глобальных переменных и использовать эти переменные для обмена данными - ведь адрес, верный в контексте одного приложения, не будет иметь никакого смысла для другого приложения.
Приложение также может сделать попытку изменить содержимое статической или глобальной переменной, с тем чтобы другие приложения могли прочитать новое значение. Однако этот способ передачи данных между приложениями не будет работать. Попытка изменения данных будет зафиксирована операционной сиситемой, которая создаст для этого приложения копию страницы памяти, в которой находится изменившиеся данные, с использованием механизма копирования при записи (copy-on-write). Мы рассказывали об этом механизме в предыдущем томе “Библиотеки системного программиста”.
Если по какой-либо причине вы не можете использовать для обмена данными между приложениями файлы, отображаемые на память, можно создать DLL-библиотеку с данными, имеющими атрибут SHARED.
Для этого прежде всего вы должны задать имя секции данных, которая будет использована для передачи данных между процессами. Это можно сделать с помощью прагмы транслятора data_seg:
#pragma data_seg (“.shar”)
После этого в файле определения модуля для DLL-библиотеки необходимо указать атрибуты секции данных, как это показано ниже:
SECTIONS .shar READ WRITE SHARED
Можно также указать ключ редактору связей:
-SECTION:.shar,RWS
Строка RWS определяет атрибуты секции: R - READ, W - WRITE, S - SHARED.
При обращении к глобальным переменным, расположенным в секции с атрибутом SHARED, процессы должны выполнять взаимную синхронизацию с использованием таких средств, как критические секции, объекты-события, семафоры.
В операционной системе Microsoft Windows версии 3.1 16-разрядная DLL-библиотека состоит из нескольких специфических функций и произвольного набора функций, выполняющих ту работу, для которой разрабатывалась данная библиотека. В заголовке загрузочного модуля DLL-библиотеки описаны экспортируемые точки входа, соответствующие всем или некоторым определенным в ней функциям. Приложения могут вызывать только те функции DLL-библиотеки, которые ей экспортируются.
В процессе инициализации после загрузки 16-разрядной DLL-библиотеки в память Windows версии 3.1 вызывает функцию LibEntry, которая должна быть определена в каждой DLL-библиотеке. Задачей функции LibEntry является инициализация локальной области памяти, если она определена для DLL-библиотеки.
Функция LibEntry должна быть дальней функцией, составленной на языке ассемблера, так как она получает параметры через регистры процессора. Мы подробно описали функцию LibEntry и ее параметры в 13 томе “Библиотеки системного программиста”. Заметим, что использование языка ассемблера затрудняет создание мультиплатформных приложенй, поэтому в мультиплатформной операционной системе Microsoft Windows NT используется другой способ инициализации.
Создавая 16-разрядную DLL-библиотеку, вам не надо определять функцию LibEntry самостоятельно, так как при создании файла DLL-библиотеки редактор связей, входящий в систему разработки, включит уже имеющийся в стандартной библиотеке модуль. Этот стандартный модуль выполняет всю необходимую работу по инициализации локальной области памяти DLL-библиотеки (с помощью функции LocalInit) и затем вызывает функцию LibMain.
Функция LibMain должна присутствовать в каждой 16-разрядной DLL-библиотеке. Эту функцию надо определить самостоятельно, причем вы можете воспользоваться языком программирования С.
По своему назначению функция LibMain напоминает функцию WinMain обычного приложения Windows. Функция WinMain получает управление при запуске приложения, а функция LibMain - при первой загрузке DLL-библиотеки в память. Так же как и функция WinMain, функция LibMain имеет параметры, которые можно использовать для инициализации библиотеки.
Прототип функции LibMain и ее параметры были описаны в 13 томе “Библиотеки системного программиста”.
Другая функция, которая присутствует в каждой 16-разрядной DLL-библиотеке, это функция WEP.
В среде Microsoft Windows версии 3.1 DLL-библиотека в любой момент времени может быть выгружена из памяти. В этом случае Windows перед выгрузкой вызывает функцию WEPXE. Эта функция, как и функция LibMain, вызывается только один раз. Она может быть использована для уничтожения структур данных и освобождения блоков памяти, заказанных при инициализации DLL-библиотеки.
Вам не обязательно самостоятельно определять функцию WEP. Так же как и функция LibEntry, функция WEP добавляется в 16-разрядную DLL-библиотеку транслятором.
Что же касается 32-разрядных DLL-библиотек операционной системы Microsoft Windows NT, то их инициализация и выгрузка из памяти происходит иначе.
В 32-разрядных DLL-библиотеках операционной системы Microsoft Windows NT вместо функций LibMain и WEP используется одна функция DLLEntryPoint, которая выполняет все необходимые задачи по инициализации библиотеки и при необходимости освобождает заказанные ранее ресурсы (имя функции инициализации может быть любым).
Функции LibMain и WEP вызываются только один раз при загрузке библиотеки в память и при ее выгрузке. В отличие от них, функция DLLEntryPoint вызывается всякий раз, когда выполняется инициализация процесса или задачи, обращающихся к функциям библиотеки, а также при явной загрузке и выгрузке библиотеки функциями LoadLibrary XE "LoadLibrary" и FreeLibrary XE "FreeLibrary" .
Ниже мы привели прототип функции DLLEntryPoint:
BOOL WINAPI DllEntryPoint( HINSTANCE hinstDLL, // идентификатор модуля DLL-библиотеки DWORD fdwReason, // код причины вызова функции LPVOID lpvReserved); // зарезервировано
Через параметр hinstDLL функции DLLEntryPoint передается идентификатор модуля DLL-библиотеки, который можно использовать при обращении к ресурсам, расположенным в файле этой библиотеки.
Что же касается параметра fdwReason, то он зависит от причины, по которой произошел вызов функции DLLEntryPoint. Этот параметр может принимать следующие значения:
Значение |
Описание |
DLL_PROCESS_ATTACH |
Библиотека отображается в адресное пространство процесса в результате запуска процесса или вызова функции LoadLibrary |
DLL_THREAD_ATTACH |
Текущий процесс создал новую задачу, после чего система вызывает функции DLLEntryPoint всех DLL-библиотек, подключенных к процессу |
DLL_THREAD_DETACH |
Этот код причины передается функции DLLEntryPoint, когда задача завершает свою работу нормальным (не аварийным) способом |
DLL_PROCESS_DETACH |
Отображение DLL‑библиотеки в адресное пространство отменяется в результате нормального завершения процесса или вызова функции FreeLibrary |
Параметр lpvReserved зарезервирован. В SDK, тем не менее, сказано, что значение параметра lpvReserved равно NULL во всех случаях, кроме двух следующих:
· когда параметр fdwReason равен DLL_PROCESS_ATTACH и используется статическая загрузка DLL-библиотеки;
· когда параметр fdwReason равен DLL_PROCESS_DETACH и функция DLLEntryPoint вызвана в результате завершения процесса, а не вызова функции FreeLibrary
В процессе инициализации функция DLLEntryPoint может отменить загрузку DLL-библиотеки. Если код причины вызова равен DLL_PROCESS_ATTACH, функция DLLEntryPoint отменяет загрузку библиотеки, возвращая значение FALSE. Если же инициализация выполнена успешно, функция должна возвратить значение TRUE.
В том случае, когда приложение пыталось загрузить DLL-библиотеку функцией LoadLibrary, а функция DLLEntryPoint отменила загрузку, функция LoadLibrary возвратит значение NULL. Если же приложение выполняет инициализацию DLL-библиотеки неявно, при отмене загрузки библиотеки приложение также не будет загружено для выполнения.
Приведем пример функции инициализации DLL-библиотеки:
BOOL WINAPI DLLEntryPoint( HMODULE hModule, // идентификатор модуля DWORD fdwReason, // причина вызова функции DLLEntryPoint LPVOID lpvReserved)// зарезервировано { switch(fdwReason) { // Подключение нового процесса case DLL_PROCESS_ATTACH: { // Обработка подключения процесса . . . break; } // Подключение новой задачи case DLL_THREAD_ATTACH: { // Обработка подключения новой задачи . . . break; } // Отключение процесса case DLL_PROCESS_DETACH: { // Обработка отключения процесса . . . break; } // Отключение задачи case DLL_THREAD_DETACH: { // Обработка отключения задачи . . . break; } } return TRUE; }
Хотя существуют DLL-библиотеки без исполнимого кода и предназначенные для хранения ресурсов, подавляющее большинство DLL-библиотек экспортируют функции для их совместного использования несколькими приложениям.
Кроме функций LibMain и WEP в 16-разрядных DLL-библиотеках операционной системы Microsoft Windows версии 3.1 и функции DLLEntryPoint в 32-разрядных библиотеках операционных систем Microsoft Windows NT и Microsoft Windows 95 могут быть определены экспортируемые и неэкспортируемые функции.
Экспортируемые функции доступны для вызова приложениям Windows. Неэкспортируемые являются локальными для DLL-библиотеки, они доступны только для функций библиотеки.
При необходимости вы можете экспортировать из 32-разрядных DLL-библиотек не только функции, но и глобальные переменные.
Самый простой способ сделать функцию экспортируемой - перечислить все экспортируемые функции в файле определения модуля при помощи оператора EXPORTSXE:
EXPORTS ИмяТочкиВхода [=ВнутрИмя] [@Номер] [NONAME] [CONSTANT] . . .
Здесь ИмяТочкиВхода задает имя, под которым экспортируемая из DLL-библиотеки функция будет доступна для вызова.
Внутри DLL-библиотеки эта функция может иметь другое имя. В этом случае необходимо указать ее внутреннее имя ВнутрИмя.
С помощью параметра @Номер вы можете задать порядковый номер экспортируемой функции.
Если вы не укажите порядковые номера экспортируемых функций, при компоновке загрузочного файла DLL-библиотеки редактор связи создаст свою собственную нумерацию, которая может изменяться при внесении изменений в исходные тексты функций и последующей повторной компоновке.
Заметим, что ссылка на экспортируемую функцию может выполняться двумя различными способами - по имени функции и по ее порядковому номеру. Если функция вызывается по имени, ее порядковый номер не имеет значения. Однако вызов функции по порядковому номеру выполняется быстрее, поэтому использование порядковых номеров предпочтительнее.
Если при помощи файла определения модуля DLL-библиотеки вы задаете фиксированное распределение порядковых номеров экспортируемых функций, при внесении изменений в исходные тексты DLL-библиотеки это распределение не изменится. В этом случае все приложения, ссылающиеся на функции из этой библиотеки по их порядковым номерам, будут работать правильно. Если же вы не определили порядковые номера функций, у приложений могут возникнуть проблемы с правильной адресацией функции из-за возможного изменения этих номеров.
Указав флаг NONAME и порядковый номер, вы сделаете имя экспортируемой функции невидимым. При этом экспортируемую функцию можно будет вызвать только по порядковому номеру, так как имя такой функции не попадет в таблицу экспортируемых имен DLL-библиотеки.
Флаг CONSTANT позволяет экспортировать из DLL-библиотеки не только функции, но и данные. При этом параметр ИмяТочкиВхода задает имя экспортируемой глобальной переменной, определенной в DLL-библиотеке.
Приведем пример экспортирования функций и глобальных переменных из DLL-библиотеки:
EXPORTS DrawBitmap=MyDraw @4 ShowAll HideAll MyPoolPtr @5 CONSTANT GetMyPool @8 NONAME FreeMyPool @9 NONAME
В приведенном выше примере в разделе EXPORTS перечислены имена нескольких экспортируемых функций DrawBitmap, ShowAll, HideAll, GetMyPool, FreeMyPool и глобальной переменной MyPoolPtr.
Функция MyDraw, определенная в DLL-библиотеке, экспортируется под именем DrawBitmap. Она также доступна под номером 4.
Функции ShowAll и HideAll экспортируются под своими “настоящими” именами, с которыми они определены в DLL-библиотеке. Для них не заданы порядковые номера.
Функции GetMyPool и FreeMyPool экспортируются с флагом NONAME, поэтому к ним можно обращаться только по их порядковым номерам, которые равны, соответственно, 8 и 9.
Имя MyPoolPtr экспортируется с флагом CONSTANT, поэтому оно является именем глобальной переменной, определенной в DLL-библиотеке, и доступной для приложений, загружающих эту библиотеку.
Когда вы используете статическую компоновку, то включаете в файл проекта приложения соответствующий lib-файл, содержащий нужную вам библиотеку объектных модулей. Такая библиотека содержит исполняемый код модулей, который на этапе статической компоновки включается в exe-файл загрузочного модуля.
Если используется динамическая компоновка, в загрузочный exe-файл приложения записывается не исполнимый код функций, а ссылка на соответствующую DLL-библиотеку и функцию внутри нее. Как мы уже говорили, эта ссылка может быть организована с использованием либо имени функции, либо ее порядкового номера в DLL-библиотеке.
Откуда при компоновке приложения редактор связей узнает имя DLL-библиотеки, имя или порядковый номер экспортируемой функции? Для динамической компоновки функции из DLL-библиотеки можно использовать различные способы.
Для того чтобы редактор связей мог создать ссылку, в файл проекта приложения вы должны включить так называемую библиотеку импорта (import library). Эта библиотека создается автоматически системой разработки Microsoft Visual C++.
Следует заметить, что стандартные библиотеки систем разработки приложений Windows содержат как обычные объектные модули, предназначенные для статической компоновки, так и ссылки на различные стандартные DLL-библиотеки, экспортирующие функции программного интерфейса операционной системы Windows.
В некоторых случаях невозможно выполнить динамическую компоновку на этапе редактирования. Вы можете, например, создать приложение, которое состоит из основного модуля и дополнительных, реализованных в виде DLL-библиотек. Состав этих дополнительных модулей и имена файлов, содержащих DLL-библиотеки, может изменяться, при этом в приложение могут добавляться новые возможности.
Если вы, например, разрабатываете систему распознавания речи, то можете сделать ее в виде основного приложения и набора DLL-библиотек, по одной библиотеке для каждого национального языка. В продажу система может поступить в комплекте с одной или двумя библиотеками, но в дальнейшем пользователь сможет купить дополнительные библиотеки и, просто переписав новые библиотеки на диск, получить возможность работы с другими языками. При этом основной модуль приложения не может "знать" заранее имена файлов дополнительных DLL-библиотек, поэтому статическая компоновка с использованием библиотеки импорта невозможна.
Однако приложение может в любой момент времени загрузить любую DLL-библиотеку, вызвав специально предназначенную для этого функцию программного интерфейса Windows с именем LoadLibrary. Приведем ее прототип:
HINSTANCE WINAPI LoadLibrary(LPCSTR lpszLibFileName);
Параметр функции является указателем на текстовую строку, закрытую двоичным нулем. В эту строку перед вызовом функции следует записать путь к файлу DLL-библиотеки или имя этого файла. Если путь к файлу не указан, при поиске выполняется последовательный просмотр следующих каталогов:
· каталог, из которого запущено приложение;
· текущий каталог;
· 32-разрядный системный каталог Microsoft Windows NT;
· 16-разрядный системный каталог;
· каталог в котором находится операционная система Windows NT;
· каталоги, перечисленные в переменной описания среды PATH
Если файл DLL-библиотеки найден, функция LoadLibrary возвращает идентификатор модуля библиотеки. В противном случае возвращается значение NULL. При этом код ошибки можно получить при помощи функции GetLastError.
Функция LoadLibrary может быть вызвана разными приложениями для одной и той же DLL-библиотеки несколько раз. В этом случае в среде операционной системы Microsoft Windows версии 3.1 загрузка DLL-библиотеки выполняется только один раз. Последующие вызовы функции LoadLibrary приводят только к увеличению счетчика использования DLL-библиотеки. Что же касается Microsoft Windows NT, то при многократном вызове функции LoadLibrary различными процессами функция инициализации DLL-библиотеки получает несколько раз управление с кодом причины вызова, равным значению DLL_PROCESS_ATTACH.
В качестве примера приведем фрагмент исходного текста приложения, загружающего DLL-библиотеку из файла DLLDEMO.DLL:
typedef HWND (WINAPI *MYDLLPROC)(LPSTR); MYDLLPROC GetAppWindow; HANDLE hDLL; hDLL = LoadLibrary("DLLDEMO.DLL"); if(hDLL != NULL) { GetAppWindow = (MYDLLPROC)GetProcAddress(hDLL, "FindApplicationWindow"); if(GetAppWindow != NULL) { if(GetAppWindow(szWindowTitle) != NULL) MessageBox(NULL, "Application window was found", szAppTitle, MB_OK | MB_ICONINFORMATION); else MessageBox(NULL, "Application window was not found", szAppTitle, MB_OK | MB_ICONINFORMATION); } FreeLibrary(hDLL); }
Здесь вначале с помощью функции LoadLibrary выполняется попытка загрузки DLL-библиотеки DLLDEMO.DLL. В случае успеха приложение получает адрес точки входа для функции с именем FindApplicationWindow, для чего используется функция GetProcAddress XE "GetProcAddress" . Этой функцией мы займемся немного позже.
Если точка входа получена, функция вызывается через указатель GetAppWindow.
После использования DLL-библиотека освобождается при помощи функции FreeLibrary, прототип который показан ниже:
void WINAPI FreeLibrary(HINSTANCE hLibrary);
В качестве параметра этой функции следует передать идентификатор освобождаемой библиотеки.
При освобождении DLL-библиотеки ее счетчик использования уменьшается. Если этот счетчик становится равным нулю (что происходит, когда все приложения, работавшие с библиотекой, освободили ее или завершили свою работу), DLL-библиотека выгружается из памяти.
Каждый раз при освобождении DLL-библиотеки вызывается функция DLLEntryPoint с параметрами DLL_PROCESS_DETACH или DLL_THREAD_DETACH, выполняющая все необходимые завершающие действия.
Теперь о функции GetProcAddress.
Для того чтобы вызвать функцию из библиотеки, зная ее идентификатор, необходимо получить значение дальнего указателя на эту функцию, вызвав функцию GetProcAddress:
FARPROC WINAPI GetProcAddress(HINSTANCE hLibrary, LPCSTR lpszProcName);
Через параметр hLibrary вы должны передать функции идентификатор DLL-библиотеки, полученный ранее от функции LoadLibrary.
Параметр lpszProcName является дальним указателем на строку, содержащую имя функции или ее порядковый номер, преобразованный макрокомандой MAKEINTRESOURCE.
Приведем фрагмент кода, в котором определяются адреса двух функций. В первом случае используется имя функции, а во втором - ее порядковый номер:
FARPROC lpMsg; FARPROC lpTellMe; lpMsg = GetProcAddress(hLib, "Msg"); lpTellMe = GetProcAddress(hLib, MAKEINTRESOURCE(8));
Перед тем как передать управление функции по полученному адресу, следует убедиться в том, что этот адрес не равен NULL:
if(lpMsg != (FARPROC)NULL) { (*lpMsg)((LPSTR)"My message"); }
Для того чтобы включить механизм проверки типов передаваемых параметров, вы можете определить свой тип - указатель на функцию, и затем использовать его для преобразования типа адреса, полученного от функции GetProcAddress:
typedef int (PASCAL *LPGETZ)(int x, int y); LPGETZ lpGetZ; lpGetZ = (LPGETZ)GetProcAddress(hLib, "GetZ");
А что произойдет, если приложение при помощи функции LoadLibrary попытается загрузить DLL-библиотеку, которой нет на диске?
В этом случае операционная система Microsoft Windows NT выведет на экран диалоговую панель с сообщением о том, что она не может найти нужную DLL-библиотеку. В некоторых случаях появление такого сообщения нежелательно, так как либо вас не устраивает внешний вид этой диалоговой панели, либо по логике работы вашего приложения описанная ситуация является нормальной.
Для того чтобы отключить режим вывода диалоговой панели с сообщением о невозможности загрузки DLL-библиотеки, вы можете использовать функцию SetErrorMode, передав ей в качестве параметра значение SEM_FAILCRITICALERRORS:
UINT nPrevErrorMode; nPrevErrorMode = SetErrorMode(SEM_FAILCRITICALERRORS); hDLL = LoadLibrary("DLLDEMO.DLL"); if(hDLL != NULL) { // Работа с DLL-библиотекой . . . } SetErrorMode(nPrevErrorMode);
Приведем прототип функции SetErrorMode:
UINT WINAPI SetErrorMode(UINT fuErrorMode);
Эта функция позволяет отключать встроенный в Windows обработчик критических ошибок. В качестве параметра этой функции можно указывать комбинацию следующих значений:
Значение |
Описание |
SEM_FAILCRITICALERRORS |
Операционная система Microsoft Windows NT не выводит на экран сообщения обработчика критических ошибок, возвращая приложению соответствующий код ошибки |
SEM_NOGPFAULTERRORBOX |
На экран не выводится сообщение об ошибке защиты памяти. Этот флаг может использоваться только при отладке приложений, если они имеют собственный обработчик такой ошибки |
SEM_NOOPENFILEERRORBOX |
Если Microsoft Windows NT не может открыть файл, на экран не выводится диалоговая панель с сообщением об ошибке |
Функция SetErrorMode возвращает предыдущий режим обработки ошибки.
Файл определения модуля для DLL-библиотекиотличается от соответствующего файла обычного приложения Windows. В качестве примера приведем образец такого файла:
LIBRARY DLLNAME DESCRIPTION 'DLL-библиотека DLLNAME' EXPORTS DrawBitmap=MyDraw @4 ShowAll HideAll MyPoolPtr @5 CONSTANT GetMyPool @8 NONAME FreeMyPool @9 NONAME
В файле определения модуля DLL-библиотеки вместо оператора NAME должен находиться оператор LIBRARYXE , определяющий имя модуля DLL-библиотеки, под которым она будет известна Windows. Однако формат строк файла описания модуля зависит от используемой системы разработки. Например, если вы создаете DLL-библиотеку при помощи Microsoft Visual C++ версии 4.0, оператор LIBRARY можно не указывать.
В комплекте системы разработки Microsoft Visual C++ входит программа dumpbin.exe, предназначенная для запуска из командной строки. С помощью этой утилиты вы сможете проанализировать содержимое любого загрузочного файла в формате COFF, в том числе DLL-библиотеки, определив имена экспортируемых функций, их порядковые номера, имена DLL-библиотек и номера функций, импортируемых из этих библиотек и т. д. Можно даже дизассемблировать секции кода с использованием таблицы символов, если такая имеется в файле.
Выберем для исследования DLL-библиотеку comdlg32.dll, в которой находятся функции для работы со стандартными диалоговыми панелями.
Вначале запустим программу dumpbin.exe, передав ей в качестве параметра имя DLL-библиотеки:
c:\msdev\bin>dumpbin comdg32.dll > lst.txt
Перед запуском программы dumpbin.exe мы скопировали файл comdg32.dll в каталог c:\msdev\bin.
Программа запишет в файл lst.txt информацию о типе файла (DLL-библиотека) и перечислит названия секций и их размер, как это показано ниже:
Microsoft (R) COFF Binary File Dumper Version 3.10.6038 Copyright (C) Microsoft Corp 1992-1996. All rights reserved. Dump of file comdlg32.dll File Type: DLL Summary 4000 .data 1000 .edata 2000 .rdata 2000 .reloc 9000 .rsrc 17000 .text
Ниже мы перечислили названия некоторых стандартных секций (полное описание вы найдете в документации, которая поставляется в составе использованного вами средства разработки приложений для Microsoft Windows NT):
Название |
Описание |
.data |
Секция инициализированных данных |
.text |
Секция кода |
.rdata |
Данные, которые можно только читать во время выполнения |
.edata |
Таблица экспортируемых имен |
.reloc |
Таблица перемещений |
.rsrc |
Ресурсы |
.bss |
Секция неинициализированных данных |
.xdata |
Таблица обработки исключений |
.CRT |
Данные библиотеки C, которые можно только читать во время выполнения |
.debug |
Отладочная информация |
.tls |
Локальная память задач |
Для просмотра более подробной информации о секции следует воспользоваться параметром /SECTION:
c:\msdev\bin>dumpbin comdg32.dll /SECTION:.data > lst.txt
Результат выполнения этой команды показан ниже:
Microsoft (R) COFF Binary File Dumper Version 3.10.6038 Copyright (C) Microsoft Corp 1992-1996. All rights reserved. Dump of file comdlg32.dll File Type: DLL SECTION HEADER #3 .data name 35E4 virtual size 1A000 virtual address E00 size of raw data 18C00 file pointer to raw data 0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers C8000040 flags Initialized Data Not Paged (no align specified) Read Write Summary 4000 .data
Аналогичная информация о секции .text приведена ниже:
Microsoft (R) COFF Binary File Dumper Version 3.10.6038 Copyright (C) Microsoft Corp 1992-1996. All rights reserved. Dump of file comdlg32.dll File Type: DLL SECTION HEADER #1 .text name 16FF9 virtual size 1000 virtual address 17000 size of raw data 400 file pointer to raw data 0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers 68000020 flags Code Not Paged (no align specified) Execute Read Summary 17000 .text
Для просмотра списка имен экспортируемых функций и глобальных переменных запустите программу dumpbin.exe с параметром /EXPORTS:
c:\msdev\bin>dumpbin comdg32.dll /EXPORTS > lst.txt
Список появится в следующем виде:
Microsoft (R) COFF Binary File Dumper Version 3.10.6038 Copyright (C) Microsoft Corp 1992-1996. All rights reserved. Dump of file comdlg32.dll File Type: DLL Section contains the following Exports for comdlg32.dll 0 characteristics 30D0D8ED time date stamp Fri Dec 15 05:09:49 1995 0.00 version 1 ordinal base 23 number of functions 23 number of names ordinal hint name 1 0 ChooseColorA (00012442) 2 1 ChooseColorW (0001164F) 3 2 ChooseFontA (00009235) 4 3 ChooseFontW (00014028) 5 4 CommDlgExtendedError (0000F92A) 6 5 FindTextA (0001373A) 7 6 FindTextW (0001372A) 8 7 GetFileTitleA (0000FB57) 9 8 GetFileTitleW (0000FAF1) 10 9 GetOpenFileNameA (00004952) 11 A GetOpenFileNameW (0000F9EE) 12 B GetSaveFileNameA (0000493B) 13 C GetSaveFileNameW (0000FA37) 14 D LoadAlterBitmap (0000A450) 15 E PageSetupDlgA (00014277) 16 F PageSetupDlgW (00014373) 17 10 PrintDlgA (0000738E) 18 11 PrintDlgW (00014238) 19 12 ReplaceTextA (0001375A) 20 13 ReplaceTextW (0001374A) 21 14 WantArrows (000122D5) 22 15 dwLBSubclass (000018F7) 23 16 dwOKSubclass (000018C4) Summary 4000 .data 1000 .edata 2000 .rdata 2000 .reloc 9000 .rsrc 17000 .text
Наряду с именами функций здесь отображаются порядковые номера функций и их адреса (в скобках).
Указав параметр /DISASM, вы можете дизассемблировать секцию кода, однако полученный в результате листинг может оказаться слишком большим и трудным для анализа. Вот фрагмент такого листинга:
Microsoft (R) COFF Binary File Dumper Version 3.10.6038 Copyright (C) Microsoft Corp 1992-1996. All rights reserved. Dump of file comdlg32.dll File Type: DLL 77DF1000: 83 3D EC C4 E0 77 cmp dword ptr ds:[77E0C4ECh],0 00 77DF1007: 56 push esi 77DF1008: 75 2D jne 77DF1037 77DF100A: 83 3D 8C C7 E0 77 cmp dword ptr ds:[77E0C78Ch],0 00 77DF1011: 8B 74 24 08 mov esi,dword ptr [esp+8] 77DF1015: 0F 84 93 97 00 00 je 77DFA7AE 77DF101B: 83 FE 01 cmp esi,1 77DF101E: 1B C0 sbb eax,eax 77DF1020: 24 FE and al,0FEh 77DF1022: 05 02 7F 00 00 add eax,7F02h 77DF1027: 50 push eax 77DF1028: 6A 00 push 0
В заключение этого раздела приведем список параметров программы dumpbin.exe.
Параметр |
Описание |
/ALL |
Просмотр всей доступной информации, исключая листинг дизассемблирования |
/ARCHIVEMEMBERS |
Просмотр минимальной информации об объектах библиотеки |
/DISASM |
Дизассемблирование секции кода |
/EXPORTS |
Просмотр экспортируемых имен |
/FPO |
Просмотр записи FPO (Frame Pointer Optimization) |
/HEADERS |
Просмотр заголовка файла и заголовков каждой секции, либо заголовка каждого объекта, расположенного в библиотеке |
/IMPORTS |
Просмотр импортированных имен |
/LINENUMBERS |
Просмотр номеров строк формата COFF |
/LINKERMEMBER |
Просмотр доступных символов, опередленных в библиотеке как public |
/OUT:ИмяФайла |
Запись выходной информации не на стандартное устройство вывода (консоль), а в файл с именем ИмяФайла |
/RAWDATA |
Просмотр дампа каждой секции |
/RELOCATIONS |
Просмотр таблицы перемещений |
/SECTION:Секция |
Просмотр информации только о секции, имеющей имя Секция |
/SUMMARY |
Просмотр минимальной информации о секциях |
/SYMBOLS |
Просмотр таблицы символов формата COFF |
В качестве примера приведем исходные тексты простейшей DLL-библиотеки DLLDemo.DLL, в которой определены всего две функции. Первая из них - это функция инициализации DLLEntryPoint, а вторая - функция FindApplicationWindow.
Функция инициализации DLLEntryPoint в нашем случае не выполняет никакой работы, однако когда она получает управление, на экране появляется одно из четырех сообщений (в зависимости от значения кода причины вызова). Таким образом, вы сможете проследить ход инициализации DLL-библиотеки при ее отображении в адресное пространство процессов, а также при отключении DLL-библиотеки от процессов.
В задачу функции FindApplicationWindow входит поиск главного окна приложения по заголовку этого окна. В случае успеха функция FindApplicationWindow возвращает идентификатор первого найденного окна с подходящим заголовком, а при неудаче - значение NULL. Вы можете использовать эту функцию для проверки, запущено ли указанное вами приложение, или нет.
Исходный текст DLL-библиотеки представлен в листинге 3.1.
Листинг 3.1. Файл dlldemo\dlldemo.c
// ================================================== // DLL-библиотека DLLDemo.DLL // Поиск окна по заданному заголовку // // (С) Фролов А.В., 1996 // Email: frolov@glas.apc.org // ================================================== #include <windows.h> #include <windowsx.h> #include "dlldemo.h" // Глобальная переменная, в которую записывается // идентификатор найденного окна или значение NULL, // если окно с заданным заголовком не найдено HWND hwndFound; // ------------------------------------------------- // Функция DLLEntryPoint // Точка входа DLL-библиотеки // ------------------------------------------------- BOOL WINAPI DLLEntryPoint( HMODULE hModule, // идентификатор модуля DWORD fdwReason, // причина вызова функции DLLEntryPoint LPVOID lpvReserved) // зарезервировано { switch(fdwReason) { // Подключение нового процесса case DLL_PROCESS_ATTACH: { MessageBox(NULL, "Process attached", "DLL Demo", MB_OK); break; } // Подключение новой задачи case DLL_THREAD_ATTACH: { MessageBox(NULL, "Thread attached", "DLL Demo", MB_OK); break; } // Отключение процесса case DLL_PROCESS_DETACH: { MessageBox(NULL, "Process detached", "DLL Demo", MB_OK); break; } // Отключение задачи case DLL_THREAD_DETACH: { MessageBox(NULL, "Thread detached", "DLL Demo", MB_OK); break; } } return TRUE; } // ------------------------------------------------- // Функция FindApplicationWindow // Поиск главного окна приложения по его заголовку // ------------------------------------------------- HWND FindApplicationWindow(LPSTR lpszWindowTitle) { // Запускаем цикл поиска окна с заголовком, // адрес которого передан функции через // параметр lpszWindowTitle EnumWindows(EnumWindowsProc, (LPARAM)lpszWindowTitle); // Возвращаем значение глобальной переменной hwndFound, // которое устанавливается функцией обратного вызова // EnumWindowsProc в зависимости от результата поиска return hwndFound; } // ------------------------------------------------- // Функция EnumWindowsProc // ------------------------------------------------- BOOL CALLBACK EnumWindowsProc( HWND hwnd, // идентификатор родительского окна LPARAM lParam) // адрес строки заголовка окна { // Буфер для хранения заголовка окна char szBuf[512]; // Получаем заголовок окна GetWindowText(hwnd, szBuf, 512); // Сравниваем заголовок со строкой, адрес которой // передан в функцию EnumWindowsProc через параметр lParam if(!strcmp((LPSTR)lParam, szBuf)) { // Если заголовок совпал, сохраняем идентификатор // текущего окна в глобальной переменной hwndFound hwndFound = hwnd; // Завершаем цикл просмотра окон return FALSE; } // Если заголовок не совпал, продолжаем поиск else { // Записываем в глобальную переменную hwndFound // значение NULL. Это признак того, что окно // с заданным заголовком не было найдено hwndFound = NULL; // Для продолжения поиска возвращаем значение TRUE return TRUE; } }
В файле dlldemo.h (листинг 3.2) находятся прототипы функций, определенных в нашей DLL-библиотеке.
Листинг 3.2. Файл dlldemo\dlldemo.h
BOOL WINAPI DLLEntryPoint(HMODULE hModule, DWORD fdwReason, LPVOID lpvReserved); HWND FindApplicationWindow(LPSTR lpszWindowTitle); BOOL CALLBACK EnumWindowsProc( HWND hwnd, // идентификатор родительского окна LPARAM lParam); // произвольное значение
Файл определения модуля dlldemo.def DLL-библиотеки представлен в листинге 3.3.
Листинг 3.3. Файл dlldemo\dlldemo.def
EXPORTS FindApplicationWindow @1
Итак, займемся исходными текстами DLL-библиотеки.
В области глобальных переменных определена переменная hwndFound. В эту переменную будет записан идентификатор найденного окна или значение NULL, если поиск окончился неудачно.
Функция DLLEntryPoint предназначена для инициализации библиотеки. В нашем случае функция инициализации просто выводит на экран различные сообщения в зависимости от кода причины вызова.
Функция FindApplicationWindow ищет главное окно приложения по заголовку этого окна, адрес которого передается ей через параметр lpszWindowTitle.
Для поиска окна мы использовали функцию EnumWindows. В качестве первого параметра этой функции передается адрес функции обратного вызова, которая будет использована для сравнения заголовков всех окон с заданным, а в качестве второго - адрес искомого заголовка:
EnumWindows(EnumWindowsProc, (LPARAM)lpszWindowTitle);
Если функция EnumWindowsProc найдет окно, она запишет его идентификатор в глобальную переменную hwndFound. Если же приложение с таким заголовком не запущено, в эту переменную будет записано значение NULL.
Функции обратного вызова EnumWindowsProc (имя функции может быть любым) передаются два параметра: идентификатор окна и 32-разрядное значение, которое передавалось функции EnumWindows в качестве второго параметра. В нашем случае это адрес заголовка искомого окна:
BOOL CALLBACK EnumWindowsProc( HWND hwnd, // идентификатор родительского окна LPARAM lParam) // адрес строки заголовка окна { char szBuf[512]; GetWindowText(hwnd, szBuf, 512); if(!strcmp((LPSTR)lParam, szBuf)) { hwndFound = hwnd; return FALSE; } else { hwndFound = NULL; return TRUE; } }
После вызова функции EnumWindows функция EnumWindowsProc будет вызываться в цикле для окна вернего уровня каждого запущенного приложения.
Зная идентификатор окна (который передается функции EnumWindowsProc через первый параметр), мы с помощью функции GetWindowText получаем заголовок окна и записываем его в буфер szBuf. Затем этот заголовок сравнивается с заданным при помощи функции strcmp. Адрес заданного заголовка мы получаем через параметр lParam.
Функция обратного вызова, адрес которой указан в первом параметре функции EnumWindows, может вернуть значение FALSE или TRUE.
В первом случае цикл просмотра окон заканчивается и функция EnumWindows возвращает управление вызвавшему ее приложению. Во втором случае просмотр окон будет продолжен до тех пор, пока функции обратного вызова не будут переданы идентификаторы главных окон всех запущенных приложений.
Если окно найдено, функция обратного вызова записывает его идентификатор в глобальную переменную hwndFound и возвращает значение FALSE, после чего поиск продолжается. Если же заголовки не совпадают, в эту переменную записывается значение NULL, после чего для продолжения поиска функция EnumWindowsProc возвращает значение TRUE.
Несколько слов о настройке проекта DLL-библиотеки DLLDemo.DLL.
При создании проекта DLL-библиотеки “с нуля” вы должны указать тип рабочего пространства проекта (Project Workspace) как Dynamic-Link Library (рис. 3.5).
Рис. 3.5. Создание нового проекта для DLL-библиотеки
Если в вашей DLL-библиотеке определена точка входа (функция инициализации), то в параметрах проекта вы должны указать ее имя. Для этого в системе Microsoft Visual C++ версии 4.0 выберите из меню Build строку Settings. На экране появится диалоговая панель Project Settings, показанная на рис. 3.6.
Рис. 3.6. Диалоговая панель Project Settings
Пользуясь кнопками в правом верхнем углу этой диалоговой панели, откройте страницу Link. Затем выберите из списка Category строку Output. В поле Entry-point symbol введите имя вашей функции инициализации и нажмите кнопку OK.
Если вы выполняете редактирование загрузочного модуля DLL-библиотеки в пакетном режиме, при запуске редактора связей укажите параметр /entry:”ИмяФункцииИнициализации”.
Напомним, что для функции инициализации DLL-библиотеки вы можете выбрать любое имя. Нужно только указать его в проекте или в параметрах редактора связей.
Приложение DLLCALL работает совместно с DLL-библиотекой DLLDemo.DLL, описанной в предыдущем разделе.
Главное окно приложения DLLCALL и меню File показано на рис. 3.7.
Рис. 3.7. Главное окно приложения DLLCALL
Если из меню File выбрать строку Find App Window, на экране появится диалоговая панель Find Application Window, показанная на рис. 3.8.
Рис. 3.8. Диалоговая панель Find Application Window
Здесь в поле Enter application window title to find вы должны ввести заголовок окна приложения. Если приложение с таким заголовком запущено, оно будет найдено, после чего на экране появится соответствующее сообщение (рис. 3.9).
Рис. 3.9. Сообщение о том, что заданное окно найдено
При инициализации DLL-библиотеки, вызванной подключением процесса DLLCALL, на экране возникает сообщение, показанное на рис. 3.10.
Рис. 3.10. Собщение о подключении процесса к DLL-библиотеке
Когда приложение DLLCALL отключается от DLL-библиотеки, на экран выводится сообщение, показанное на рис. 3.11.
Рис. 3.11. Сообщение об отключении процесса от DLL-библиотеки
Главный файл исходных текстов приложения DLLCALL представлен в листинге 3.4.
Листинг 3.4. Файл dlldemo\dllcall\dllcall.c
// ================================================== // Приложение DLLCall // Вызов функции из DLL-библиотеки // // (С) Фролов А.В., 1996 // Email: frolov@glas.apc.org // ================================================== #define STRICT #include <windows.h> #include <windowsx.h> #include "resource.h" #include "afxres.h" #include "dllcall.h" // Определяем тип: указатель на функцию typedef HWND (WINAPI *MYDLLPROC)(LPSTR); // Указатель на функцию, расположенную в // DLL-библиотеке MYDLLPROC GetAppWindow; // Буфер для заголовка окна, поиск которого // будет выполняться char szWindowTitle[512]; // Идентификатор DLL-библиотеки HANDLE hDLL; HINSTANCE hInst; char szAppName[] = "DLLCallApp"; char szAppTitle[] = "DLL Call Demo"; // ----------------------------------------------------- // Функция WinMain // ----------------------------------------------------- int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { WNDCLASSEX wc; HWND hWnd; MSG msg; // Сохраняем идентификатор приложения hInst = hInstance; // Преверяем, не было ли это приложение запущено ранее hWnd = FindWindow(szAppName, NULL); if(hWnd) { // Если было, выдвигаем окно приложения на // передний план if(IsIconic(hWnd)) ShowWindow(hWnd, SW_RESTORE); SetForegroundWindow(hWnd); return FALSE; } // Регистрируем класс окна memset(&wc, 0, sizeof(wc)); wc.cbSize = sizeof(WNDCLASSEX); wc.hIconSm = LoadImage(hInst, MAKEINTRESOURCE(IDI_APPICONSM), IMAGE_ICON, 16, 16, 0); wc.style = 0; wc.lpfnWndProc = (WNDPROC)WndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInst; wc.hIcon = LoadImage(hInst, MAKEINTRESOURCE(IDI_APPICON), IMAGE_ICON, 32, 32, 0); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1); wc.lpszMenuName = MAKEINTRESOURCE(IDR_APPMENU); wc.lpszClassName = szAppName; if(!RegisterClassEx(&wc)) if(!RegisterClass((LPWNDCLASS)&wc.style)) return FALSE; // Создаем главное окно приложения hWnd = CreateWindow(szAppName, szAppTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInst, NULL); if(!hWnd) return(FALSE); // Отображаем окно и запускаем цикл // обработки сообщений ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); while(GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } // ----------------------------------------------------- // Функция WndProc // ----------------------------------------------------- LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch(msg) { HANDLE_MSG(hWnd, WM_COMMAND, WndProc_OnCommand); HANDLE_MSG(hWnd, WM_DESTROY, WndProc_OnDestroy); default: return(DefWindowProc(hWnd, msg, wParam, lParam)); } } // ----------------------------------------------------- // Функция WndProc_OnDestroy // ----------------------------------------------------- #pragma warning(disable: 4098) void WndProc_OnDestroy(HWND hWnd) { PostQuitMessage(0); return 0L; } // ----------------------------------------------------- // Функция WndProc_OnCommand // ----------------------------------------------------- #pragma warning(disable: 4098) void WndProc_OnCommand(HWND hWnd, int id, HWND hwndCtl, UINT codeNotify) { switch (id) { case ID_FILE_EXIT: { // Завершаем работу приложения PostQuitMessage(0); return 0L; break; } case ID_FILE_FINDWINDOW: { // Отображаем диалоговую панель для ввода // заголовка главного окна приложения, // поиск которого будет выполняться if(DialogBox(hInst, MAKEINTRESOURCE(IDD_DLGFIND), hWnd, DlgProc)) { // Первый способ вызова функции из DLL-библиотеки: // прямой вызов с использованием библиотеки экспорта /* // Выполняем поиск окна с заголовком, заданным // при помощи диалоговой панели if(FindApplicationWindow(szWindowTitle) != NULL) MessageBox(NULL, "Application window was found", szAppTitle, MB_OK | MB_ICONINFORMATION); else MessageBox(NULL, "Application window was not found", szAppTitle, MB_OK | MB_ICONINFORMATION); */ // Второй способ вызова функции из DLL-библиотеки: // загрузка DLL-библиотеки функцией LoadLibrary // Загружаем DLL-библиотеку hDLL = LoadLibrary("DLLDEMO.DLL"); // Если библиотека загружена успешно, выполняем // вызов функции if(hDLL != NULL) { // Получаем адрес нужной нам функции GetAppWindow = (MYDLLPROC)GetProcAddress(hDLL, "FindApplicationWindow"); // Если адрес получен, вызываем функцию if(GetAppWindow != NULL) { // Выполняем поиск окна с заголовком, заданным // при помощи диалоговой панели if(GetAppWindow(szWindowTitle) != NULL) MessageBox(NULL, "Application window was found", szAppTitle, MB_OK | MB_ICONINFORMATION); else MessageBox(NULL, "Application window was not found", szAppTitle, MB_OK | MB_ICONINFORMATION); } // Освобождаем DLL-библиотеку FreeLibrary(hDLL); } } break; } case ID_HELP_ABOUT: { MessageBox(hWnd, "DLL Call Demo\n" "(C) Alexandr Frolov, 1996\n" "Email: frolov@glas.apc.org", szAppTitle, MB_OK | MB_ICONINFORMATION); return 0L; break; } default: break; } return FORWARD_WM_COMMAND(hWnd, id, hwndCtl, codeNotify, DefWindowProc); } // ----------------------------------------------------- // Функция DlgProc // ----------------------------------------------------- LRESULT WINAPI DlgProc(HWND hdlg, UINT msg, WPARAM wParam, LPARAM lParam) { switch(msg) { HANDLE_MSG(hdlg, WM_INITDIALOG, DlgProc_OnInitDialog); HANDLE_MSG(hdlg, WM_COMMAND, DlgProc_OnCommand); default: return FALSE; } } // ----------------------------------------------------- // Функция DlgProc_OnInitDialog // ----------------------------------------------------- BOOL DlgProc_OnInitDialog(HWND hdlg, HWND hwndFocus, LPARAM lParam) { return TRUE; } // ----------------------------------------------------- // Функция DlgProc_OnCommand // ----------------------------------------------------- #pragma warning(disable: 4098) void DlgProc_OnCommand(HWND hdlg, int id, HWND hwndCtl, UINT codeNotify) { switch (id) { case IDOK: { // Если пользователь нажал кнопку OK, // получаем текст из поля редактирования и // сохраняем его в глобальном буфере szWindowTitle GetDlgItemText(hdlg, IDC_EDIT1, szWindowTitle, 512); // Завершаем работу диалоговой панели EndDialog(hdlg, 1); return TRUE; } // Если пользователь нажимает кнопку Cancel, // завершаем работу диалоговой панели case IDCANCEL: { // Завершаем работу диалоговой панели EndDialog(hdlg, 0); return TRUE; } default: break; } return FALSE; }
Файл dllcall.h (листинг 3.5) содержит прототипы функций, определенных в приложении DLLCALL.
Листинг 3.5. Файл dlldemo\dllcall\dllcall.h
// ----------------------------------------------------- // Описание функций // ----------------------------------------------------- LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); void WndProc_OnCommand(HWND hWnd, int id, HWND hwndCtl, UINT codeNotify); void WndProc_OnDestroy(HWND hWnd); HWND FindApplicationWindow(LPSTR lpszWindowTitle); LRESULT WINAPI DlgProc(HWND hdlg, UINT msg, WPARAM wParam, LPARAM lParam); BOOL DlgProc_OnInitDialog(HWND hdlg, HWND hwndFocus, LPARAM lParam); void DlgProc_OnCommand(HWND hdlg, int id, HWND hwndCtl, UINT codeNotify);
В файле dllcall.rc (листинг 3.6) определены ресурсы приложения DLLCALL. Это меню, две пиктограммы и диалоговая панель, а также текстовые строки, которые не используются.
Листинг 3.6. Файл dlldemo\dllcall\dllcall.rc
//Microsoft Developer Studio generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "afxres.h" ////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ////////////////////////////////////////////////////////////// // Russian resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_RUS) #ifdef _WIN32 LANGUAGE LANG_RUSSIAN, SUBLANG_DEFAULT #pragma code_page(1251) #endif //_WIN32 ////////////////////////////////////////////////////////////// // // Menu // IDR_APPMENU MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "F&ind App Window", ID_FILE_FINDWINDOW MENUITEM SEPARATOR MENUITEM "E&xit", ID_FILE_EXIT END POPUP "&Help" BEGIN MENUITEM "&About...", ID_HELP_ABOUT END END #ifdef APSTUDIO_INVOKED ////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE DISCARDABLE BEGIN "resource.h\0" END 2 TEXTINCLUDE DISCARDABLE BEGIN "#include ""afxres.h""\r\n" "\0" END 3 TEXTINCLUDE DISCARDABLE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure // application icon // remains consistent on all systems. IDI_APPICON ICON DISCARDABLE "Dllcall.ico" IDI_APPICONSM ICON DISCARDABLE "Dllcalsm.ico" ////////////////////////////////////////////////////////////// // // Dialog // IDD_DLGFIND DIALOG DISCARDABLE 0, 0, 247, 74 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "Find Application Window" FONT 8, "MS Sans Serif" BEGIN EDITTEXT IDC_EDIT1,21,31,152,16,ES_AUTOHSCROLL DEFPUSHBUTTON "OK",IDOK,190,7,50,14 PUSHBUTTON "Cancel",IDCANCEL,190,24,50,14 GROUPBOX "Enter application window title to find", IDC_STATIC,13,13,167,49 END ////////////////////////////////////////////////////////////// // // DESIGNINFO // #ifdef APSTUDIO_INVOKED GUIDELINES DESIGNINFO DISCARDABLE BEGIN IDD_DLGFIND, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 240 TOPMARGIN, 7 BOTTOMMARGIN, 67 END END #endif // APSTUDIO_INVOKED #endif // Russian resources ////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED
Файл resource.h (листинг 3.7) содержит определения констант для файла описания ресурсов приложения.
Листинг 3.7. Файл dlldemo\dllcall\resource.h
//{{NO_DEPENDENCIES}} // Microsoft Developer Studio generated include file. // Used by DLLCall.rc // #define IDR_MENU1 101 #define IDR_APPMENU 101 #define IDI_APPICON 102 #define IDI_APPICONSM 103 #define IDD_DLGFIND 104 #define IDC_EDIT1 1000 #define ID_FILE_EXIT 40001 #define ID_HELP_ABOUT 40002 #define ID_FILE_FINDWINDOW 40003 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 105 #define _APS_NEXT_COMMAND_VALUE 40004 #define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif
Рассмотрим исходные тексты приложения DLLCALL.
В начале файла dllcall.c (листинг 3.4) мы определили тип MYDLLPROC как указатель на функцию, возвращающую знначение HWND и принимающую один параметр типа LPSTR.
Далее в области глобальных переменных мы определили переменную GetAppWindow с типом MYDLLPROC:
MYDLLPROC GetAppWindow;
Эта переменная будет использована для хранения указателя на функцию из динамически загружаемой DLL-библиотеки.
Глобальный буфер szWindowTitle предназначен для хранения заголовка окна, поиск которого будет выполнять наше приложение.
В глобальной переменной hDLL хранится идентификатор динамически загруженной DLL-библиотеки.
Функция WinMain сохраняет идентификатор прилождения в глобальной переменной hInst а затем проверяет, не было ли это приложение уже запущено. Если было, главное окно приложения выдвигается на передний план.
Далее функция WinMain регистрирует класс главного окна приложения, создает и отображает это окно. Затем запускается обычный цикл обработки сообщений.
Функция WndProc обрабатывает сообщения WM_COMMAND и WM_DESTROY, для чего вызываются, соответственно, функции WndProc_OnCommand и WndProc_OnDestroy.
Эта функция вызывается при уничтожении главного окна приложения. Ее задачей является завершение цикла обработки сообщений, для чего она вызывает функцию PostQuitMessage.
Эта функция обрабатывает сообщение WM_COMMAND, которое приходит от главного меню приложения.
Когда пользователь выбирает из меню File строку Find App Window, с помощью функции DialogBox на экран выводится диалоговая панель, предназначенная для ввода заголовка окна, которое нужно найти.
Если пользователь ввел заголовок и нажал в диалоговой панели кнопку OK, функция WndProc_OnCommand выполняет поиск окна, вызывая соответствующую функцию из DLL-библиотеки DLLDemo.DLL, исходные тексты которой мы только что рассмотрели.
В листинге мы подготовили два способа подключения DLL-библиотеки - прямой с использованием библиотеки экспорта и динамический.
Первый способ достаточно прост, однако предполагает, что в проект приложения DLLCALL будет включен файл библиотеки экспорта DLLDemo.LIB. Этот файл создается автоматически системой Microsoft Visual C++ при сборке проекта DLL-библиотеки.
Фрагмент кода, использующий прямое подключение, закрыт в листинге 3.4 символами комментария:
if(FindApplicationWindow(szWindowTitle) != NULL) MessageBox(NULL, "Application window was found", szAppTitle, MB_OK | MB_ICONINFORMATION); else MessageBox(NULL, "Application window was not found", szAppTitle, MB_OK | MB_ICONINFORMATION);
В этом фрагменте мы выполняем простой вызов функции FindApplicationWindow, определенной в DLL-библиотеке DLLDemo.DLL. Прототип функции FindApplicationWindow мы поместили в файл dllcall.h.
Второй фрагмент загружает DLL-библиотеку при помощи функции LoadLibrary, а в случае успеха затем получает указатель на функцию FindApplicationWindow. Для получения указателя здесь применяется функция GetProcAddress:
hDLL = LoadLibrary("DLLDEMO.DLL"); if(hDLL != NULL) { GetAppWindow = (MYDLLPROC)GetProcAddress(hDLL, "FindApplicationWindow"); if(GetAppWindow != NULL) { if(GetAppWindow(szWindowTitle) != NULL) MessageBox(NULL, "Application window was found", szAppTitle, MB_OK | MB_ICONINFORMATION); else MessageBox(NULL, "Application window was not found", szAppTitle, MB_OK | MB_ICONINFORMATION); } FreeLibrary(hDLL); }
Проверяя работу этих двух фрагментов кода, обратите внимение на последовательность появления сообщений о подключении процесса к DLL-библиотеке и отключении от нее.
При использовании прямого подключения DLL-библиотеки сообщение о подключении процесса появляется сразу после запуска приложения DLLCALL, даже еще до появления на экране главного окна этого приложения. Это и понятно - DLL-библиотека отображается в адресное пространство процесса при его запуске. Если нужная DLL-библиотека не будет найдена, процесс так и не сможет запуститься. При этом на экране появится соответствующее системное сообщение.
Когда DLL-библиотека загружается динамически, сообщение о подключении процесса появляется только после того, как пользователь выберет строку Find App Window из меню File, так как только после этого произойдет подключение. Сообщение об отключении процесса появится после отображения результатов поиска окна, так как в этот момент будет вызвана функция FreeLibrary.
Функция DlgProc обрабатывает сообщения WM_INITDIALOG и WM_COMMAND, поступающие в функцию диалога диалоговой панели, предназначенной для ввода заголовка окна. Эти сообщения обрабатываются, соответственно, функциями DlgProc_OnInitDialog и DlgProc_OnCommand.
Эта функция возаращает значение TRUE, разрешая отображение диалоговой панели. При создании собственного приложения вы можете добавить в эту функцию код инициализации органов управления диалоговой панели или связанных с этой панелью данных.
Функция DlgProc_OnCommand обрабатывает сообщение WM_COMMAND, поступающее в функцию диалога от органов управления, расположенных в диалоговой панели.
Если пользователь нажимает кнопку OK, функция DlgProc_OnCommand извлекает содержимое однострочного текстового редактора (введенное имя заголовока окна), вызывая для этого макрокоманду GetDlgItemText, и сохраняет это содержимое в глобальном буфере szWindowTitle. Затем функция завершает работу диалоговой панели с кодом 1, в результате чего приложение приступит к поиску окна с заданным заголовком.
В том случае, когда пользователь отказался от поиска, нажав в диалоговой панели кнопку Cancel, работа диалоговой панели завершается с нулевым кодом. Поиск окна в этом случае не выполняется.