3. Библиотеки динамической компоновки

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

Однако будем терпеливыми, и постараемся сосредоточиться, так как вся операционная система Windows и все ее драйверы (кроме виртуальных), а также другие расширения в некотором смысле есть ни что иное, как набор библиотек динамической компоновки. Редкое крупное приложение Windows обходится без собственных библиотек динамической компоновки, и ни одно приложение не может обойтись без вызова функций, расположенных в таких библиотеках. В частности, все функции программного интерфейса Windows находятся именно в библиотеках динамической компоновки DLL (Dynamic-Link Libraries ).

Что же это за библиотеки и почему они имеют такое большое значение?

3.1. Статическая и динамическая компоновка

Вспомним старые добрые времена, когда операционная система MS-DOS и компьютеры с памятью 1 Мбайт удовлетворяли запросы подавляющего большинства пользователей. Как создавался загрузочный файл программы для MS-DOS? Вы готовили исходный текст приложения на своем любимом языке программирования (Си, Паскаль, Модула-2 и т. д.), затем транслировали его для получения объектного модуля. После этого в дело включался редактор связей, который компоновал объектные модули, полученные в результате трансляции исходных текстов и модули из библиотек объектных модулей, в один exe-файл. В процессе запуска файл программы загружался полностью в оперативную память и ему передавалось управление.

Таким образом, редактор связей записывал в файл программы все модули, необходимые для работы. Для обращения к внешним по отношению к программам модулям операционной системы MS-DOS и модулям BIOS использовался механизм программных прерываний. В любой момент времени в оперативной и постоянной памяти компьютера находился весь код, необходимый для работы как запущенной программы, так и самой операционной системы MS-DOS.

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

В среде мультизадачной операционной системы, такой как Windows, OS/2 или UNIX, статическая компоновка неэффективна, так как приводит к неэкономному использованию очень дефицитного ресурса - оперативной памяти. Представьте себе, что в системе одновременно работают 5 приложений, и все они вызывают такие функции, как sprintf, memcpy, strcmp и т. д. Если приложения были собраны с использованием статической компоновки, в памяти будут находится одновременно 5 копий функции sprintf, 5 копий функции memcpy, и т. д (рис. 3.1). А ведь программы могут использовать и более сложные функции, например, предназначенные для работы с диалоговыми панелями или масштабируемыми шрифтами!

Рис. 3.1. Вызов функций при использовании статической компоновки

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

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

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

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

Рис. 3.2. Вызов функции при использовании динамической компоновки

В операционной системе Windows версии 3.1 файлы библиотек динамической компоновки (DLL-библиотеки) имеют расширение имени dll, хотя можно использовать любое другое, например, exe. В первых версиях Windows DLL-библиотеки располагались в файлах с расширением имени exe. Возможно поэтому файлы krnl286.exe, krnl386.exe, gdi.exe и user.exe имеют расширение имени exe, а не dll, не смотря на то, что перечисленные выше файлы, составляющие ядро операционной системы Windows, есть ни что иное, как DLL-библиотеки.

Механизм динамической компоновки используется не только в системе Windows. Более того, он был изобретен задолго до появления Windows. Например, в мультизадачных многопользовательских операционных системах VS1, VS2, MVS, VM, созданных для компьютеров IBM-370 и аналогичных (вспомните добрым словом ушедшую в прошлое серию ЕС ЭВМ и операционные системы SVS, TKS, СВМ, МВС), код функций, нужных параллельно работающим программам, располагается в отдельных библиотеках и может загружаться при необходимости в специально выделенную общую область памяти. Операционная система OS/2 также работает с DLL-библиотеками.

3.2. DLL-библиотеки в операционной системе Windows

Формат DLL-библиотеки почти идентичен формату загрузочного модуля приложения Windows, однако вы не можете "запустить" DLL-библиотеку на выполнение, как обычное приложение. И это понятно, так как назначение DLL-библиотек другое - они служат хранилищем функций, вызываемых приложениями во время работы. Функции, расположенные в DLL-библиотеках, могут вызывать функции, которые находятся в других библиотеках (рис. 3.3).

Рис. 3.3. Вызов функций из DLL-библиотек

Для того чтобы вам были понятны отличия между приложением и DLL-библиотекой, уточним такие понятия, как задача (task ), копия приложения (instance ) и модуль (module ).

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

Рис. 3.4. Копии приложения и модуль приложения

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

Копия приложения (instance) является контекстом, в котором выполняется модуль приложения. Идентификатор копии приложения, который вы получаете через параметр hInstance функции WinMain, является идентификатором сегмента данных DGROUP, используемого при выполнении программного модуля.

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

DLL-библиотека также является модулем. Она находится в памяти в единственном экземпляре, содержит сегменты кода и ресурсы, а так же один сегмент данных (рис. 3.5). Можно сказать, что для DLL-библиотеки создается одна копия (instance), состоящая только из сегмента данных, и один модуль, состоящий из кода и ресурсов.

Рис. 3.5. Структура DLL-библиотеки в памяти

DLL-библиотека, в отличие от приложения, не имеет стека и очереди сообщения.

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

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

Для чего используются DLL-библиотеки ?

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

С помощью DLL-библиотек можно организовать коллективное использование ресурсов или данных, расположенных в сегменте данных библиотеки. Более того, вы можете создать DLL-библиотеки, состоящие только из одних ресурсов, например, из пиктограмм или изображений bitmap. В состав Windows входит DLL-библиотека moricons.dll, состоящая из одних пиктограмм. Файлы с расширением fon представляют собой ни что иное, как DLL-библиотеки, содержащие шрифты в виде ресурса.

Функции, входящие в состав DLL-библиотеки, могут заказывать блоки памяти с атрибутом GMEM_SHARE. Такой блок памяти не принадлежит ни одному приложению и поэтому не освобождается автоматически при завершении работы приложения. Так как в Windows версии 3.1 все приложения используют общую глобальную память, блоки памяти с атрибутом GMEM_SHARE можно использовать для обмена данными между приложениями. Управлять таким обменом могут, например, функции, расположенные в соответствующей DLL-библиотеке. Однако в следующих версиях Windows каждое приложение будет работать в собственном адресном пространстве, поэтому для организации обмена данных между приложениями следует использовать специальный механизм динамического обмена данными DDE, который мы рассмотрим позже в отдельной главе.

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

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

Например, приложение может организовать "перехват" системных сообщений или функций, при этом соответствующие модули "перехватчика" необходимо располагать в фиксированном сегменте кода DLL-библиотеки. Для удаляемых (discardable) блоков памяти можно, вызвав функцию GlobalNotify, определить функцию, которой будет передаваться управление при попытке удалить блок из памяти. Такая функция должна находиться в фиксированном сегменте кода в DLL-библиотеке. Если ваше приложение обрабатывает аппаратные прерывания или само вызывает программные прерывания, ему также не обойтись без DLL-библиотек (единственный способ обработки прерываний в реальном времени - создание виртуального драйвера). Наконец, все обычные драйвера устройств в операционной системе Windows реализованы с помощью DLL-библиотек.

Даже если вы не собираетесь обрабатывать или вызывать прерывания и не разрабатываете собственный драйвер, отдельные подсистемы большого приложения имеет смысл оформлять в виде DLL-библиотек из соображений модульности и доступности библиотек для других приложений. Например, в приложении SMARTPAD мы создали орган управления Toolbar с использованием разработанного нами класса С++ Toolbar. Однако если бы мы сосредоточили все функции этого класса в DLL-библиотеке, нашим органом управления могли бы воспользоваться и другие созданные нами приложения.

Разумеется, у динамической компоновки есть и свои недостатки.

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

Во-вторых, в дополнение к exe-файлу вместе с приложением необходимо устанавливать один или несколько dll-файлов, что в некоторой степени усложняет процесс установки и сопровождения приложения. Может также возникнуть ситуация, при которой приложение не находит свою DLL-библиотеку, несмотря на то, что нужная библиотека есть на диске.

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

3.3. Структура DLL-библиотеки

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

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

Функция LibEntry

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

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

Задачей функции LibEntry является инициализация локальной области памяти, если она определена для DLL-библиотеки.

Функция LibEntry должна быть дальней функцией, составленной на языке ассемблера, так как она получает параметры через регистры процессора. Перечислим и опишем параметры, передаваемые функции LibEntry при загрузке DLL-библиотеки в память.

Регистры Описание
DS Селектор, указывающий на сегмент данных DLL-библиотеки. Отметим, что хотя сразу после загрузки DLL-библиотека имеет собственный сегмент данных, в нем не определена область локальных данных. Для определения области локальных данных функция LibEntry должна вызвать функцию LocalInit
DI Идентификатор DLL-библиотеки, присвоенный ей операционной системой Windows после загрузки. По своему назначению аналогичен идентификатору приложения hInstance, передаваемому обычному приложению через соответствующий параметр функции WinMain. Идентификатор DLL-библиотеки используется функциями библиотеки при создании таких, например, объектов, как окна (если окно создается функцией, расположенной в DLL-библиотеке, при его создании следует использовать не идентификатор приложения hInstance, вызвавшего функцию, а идентификатор DLL-библиотеки)
CX Требуемый размер локальной области данных, указанный в операторе HEAPSIZE файла определения модуля DLL-библиотеки. При вызове функции инициализации локальной области памяти LocalInit в качестве значения параметра uStartAddr следует использовать нуль, а в качестве значения параметра uEndAddr - содержимое регистра CX
ES:SI Дальний указатель на строку параметров, которая может быть указана при явной загрузке DLL-библиотеки (позже мы рассмотрим различные способы загрузки DLL-библиотеки). Обычно этот параметр не используется

Вам не надо определять функцию LibEntry самостоятельно, так как при создании файла DLL-библиотеки редактор связей, входящий в систему разработки, включит уже имеющийся в стандартной библиотеке модуль. Этот стандартный модуль выполняет всю необходимую работу по инициализации локальной области памяти DLL-библиотеки (с помощью функции LocalInit) и затем вызывает функцию LibMain, которая будет описана в следующем разделе.

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

Функция LibMain

Функция LibMain должна присутствовать в каждой стандартной DLL-библиотеке. Эту функцию вам надо определить самостоятельно, причем вы можете воспользоваться языком программирования С.

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

Приведем прототип функции LibMain :

int FAR PASCAL LibMain(HINSTANCE hInstance,
                       WORD wDataSegment,
                       WORD wHeapSize,
                       LPSTR lpszCmdLine);

Параметр hInstance при вызове функции содержит идентификатор DLL-библиотеки. Это ни что иное, как содержимое регистра DI перед вызовом функции LibEntry.

Через параметр wDataSegment передается селектор, соответствующий сегменту данных DLL-библиотеки. Если DLL-библиотека не имеет сегмента данных, этот параметр содержит идентификатор DLL-библиотеки (точнее, идентификатор модуля DLL-библиотеки). Значение параметра wDataSegment соответствует содержимому регистра DS перед вызовом функции LibEntry.

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

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

Если инициализация DLL-библиотеки выполнена успешно, функция LibMain должна возвратить ненулевое значение. Если в процессе инициализации произошла ошибка, следует возвратить нуль. В этом случае функция LibEntry также возвратит нуль. Это приведет к тому, что Windows выгрузит библиотеку из памяти.

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

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

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

Перед завершением работы приложение должно вызвать созданную вами функцию, освобождающую выделенную структуру данных. Она может называться, например, LibUnregister.

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

Приведем пример простейшего варианта функции LibMain:

int FAR PASCAL LibMain(HINSTANCE hInstance,
  WORD wDataSegment, WORD wHeapSize,
  LPSTR lpszCmdLine)
{
  if(wHeapSize != 0)
    UnlockData(0);
  return 1;
}

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

Функция WEP

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

Приведем прототип функции WEP (при использовании системы разработки Borland Turbo C++ for Windows):

int FAR PASCAL WEP(int bSystemExit);

Параметр bSystemExit может принимать значения WEP_FREE_DLL и WEP_SYSTEM_EXIT . Этот параметр указывает причину выгрузки DLL-библиотеки из памяти. В первом случае выгрузка выполняется потому, что либо функциями библиотеки не пользуется ни одно приложение, либо одно из приложений выдало запрос на выгрузку. Если же параметр имеет значение WEP_SYSTEM_EXIT, выгрузка библиотеки происходит из-за завершения работы операционной системы Windows.

Функция WEP должна всегда возвращать значение 1.

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

Приведем пример простейшей функции WEP:

int FAR PASCAL WEP(int bSystemExit)
{
  return 1;
}

В операционной системе Windows версии 3.0 на использование функции WEP накладывались серьезные ограничения, которые делали ее практически бесполезной. Например, размер стека, используемого для работы этой функции, составлял всего 256 байт, чего совершенно недостаточно для вызова функций программного интерфейса Windows. Есть и другие проблемы, однако мы не будем их затрагивать из-за того, что версия 3.0 уже сильно устарела, а в версии 3.1 все эти ограничения сняты. Поэтому вы можете использовать функцию WEP для любых процедур, в частности, для освобождения памяти, полученной при инициализации в функции LibMain.

Экспортируемые функции

Кроме функций LibMain и WEP в DLL-библиотеке могут быть определены и другие функции (как мы уже говорили, существуют DLL-библиотеки, состоящие из одних ресурсов). Это могут быть экспортируемые и неэкспортируемые функции.

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

Самый простой способ сделать функцию экспортируемой в системе разработки Borland Turbo C++ for Windows - указать в ее определении ключевое слово _export. Мы использовали этот способ при определении функции окна. Для экспортируемой функции создается специальный пролог, необходимый для правильной загрузки сегментного регистра DS.

Есть и другой способ - можно перечислить все экспортируемые функции в файле определения модуля при помощи оператора EXPORTS :

EXPORTS
  DrawBitmap  @4
  ShowAll     
  HideAll
  GetMyPool   @8
  FreeMyPool  @9

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

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

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

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

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

Импортирование функций

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

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

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

Библиотека импорта

Для того чтобы редактор связей мог создать ссылку, в файл проекта приложения вы должны включить так называемую библиотеку импорта (import library ), созданную приложением Import Lib , входящим в состав системы разработки Borland Turbo C++ for Windows. Соответствующее средство имеется в SDK, а также в любой другой системе разработки приложений Windows.

Библиотека импорта создается либо на основе dll-файла библиотеки, либо на основе файла определения модуля, используемого для создания DLL-библиотеки.

В любом случае вам надо запустить приложение Import Lib, и из меню "File" выбрать строку "File Select...". При этом на экране появится диалоговая панель "Select File", с помощью которой вы можете выбрать нужный вам dll- или def-файл (рис. 3.6).

Рис. 3.6. Работа с приложением Import Library

После выбора файла приложение Import Lib создаст библиотеку импорта в виде lib-файла, расположенного в том же каталоге, что и исходный dll- или def-файл. Этот файл необходимо включить в проект создаваемого вами приложения, пользующегося функциями DLL-библиотеки.

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

В состав системы разработки Borland C++ for Windows версии 4.0 и 4.01 входит DLL-библиотека, содержащая функции стандартной библиотеки компилятора. Эта библиотека расположена в файле с именем bc40rtl.dll. В проекте приложения вы можете определить, что для стандартных функций следует использовать динамическую компоновку. В этом случае размер полученного загрузочного модуля заметно сократится, однако для работы приложения будет нужен файл bc40rtl.dll.

Использование оператора IMPORTS

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

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

IMPORTS
  Msg=dllsrc.4
  dllsrc.TellMe

Во второй строке приведенного выше примера приложение импортирует функцию Msg из DLL-библиотеки dllsrc.dll, причем порядковый номер указанной функции в библиотеке равен 4.

В третьей строке из DLL-библиотеки dllsrc.dll импортируется функция с именем TellMe, причем ее порядковый номер не используется.

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

Динамический импорт функций во время выполнения приложения

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

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

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

HINSTANCE WINAPI LoadLibrary(LPCSTR lpszLibFileName);

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

текущий каталога;

каталог, в котором находится операционная система Windows;

системный каталог Windows;

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

каталоги, перечисленные в операторе описания среды PATH, расположенном в файле autoexec.bat;

каталоги, расположенные в сети, если для них определены пути поиска.

Если файл DLL-библиотеки найден, функция LoadLibrary возвращает идентификатор модуля библиотеки. В противном случае возвращается код ошибки, который по своему значению меньше величины HINSTANCE_ERROR, определенной в файле windows.h. Возможны следующие коды ошибок:

Код ошибки Описание
0 Мало памяти, неправильный формат загружаемого файла
2 Файл не найден
3 Путь к файлу не существует
5 Была предпринята попытка динамически загрузить приложение или произошла ошибка при совместном использовании файлов. Также возможно, что была предпринята попытка доступа к файлу в сети пользователем, не имеющим на это достаточных прав
6 Данная библиотека требует отдельный сегмент данных для каждой задачи
8 Мало памяти для запуска приложения
10 Неправильная версия операционной системы Windows
11 Неправильный формат загрузочного файла приложения Windows
12 Данное приложение разработано для операционной системы, отличной от Microsoft Windows
13 Данное приложение разработано для MS-DOS версии 4.0
14 Неизвестный тип исполняемого файла
15 Была предпринята попытка загрузить приложение, разработанное для реального режима работы процессора в среде ранних версий операционной системы Windows
16 Была предпринята попытка загрузить вторую копию исполняемого файла, содержащего сегменты данных, отмеченные как multiple, но не защищенные от записи
19 Попытка загрузки компрессованного выполнимого файла
20 Файл, содержащий DLL-библиотеку, имеет неправильный формат
21 Данное приложение работает только в среде 32-битового расширения Windows

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

В качестве примера приведем фрагмент исходного текста приложения, загружающего DLL-библиотеку из файла srcdll.dll:

HINSTANCE hLib;
hLib = LoadLibrary("srcdll.dll");
if(hLib >= HINSTANCE_ERROR)
{
  // Работа с DLL-библиотекой
}
FreeLibrary(hLib);

Если DLL-библиотека больше не нужна, ее следует освободить с помощью функции FreeLibrary :

void WINAPI FreeLibrary(HINSTANCE hLibrary);

В качестве параметра этой функции следует передать идентификатор освобождаемой библиотеки.

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

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

Для того чтобы вызвать функцию из библиотеки, зная ее идентификатор, необходимо получить значение дальнего указателя на эту функцию, вызвав функцию 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 (FAR PASCAL *LPGETZ)(int x, int y);
LPGETZ lpGetZ;
lpGetZ = (LPGETZ)GetProcAddress(hLib, "GetZ");

А что произойдет, если приложение при помощи функции LoadLibrary попытается загрузить DLL-библиотеку, которой нет на диске?

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

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

Приведем прототип функции SetErrorMode:

UINT WINAPI SetErrorMode(UINT fuErrorMode);

Эта функция позволяет отключать встроенный в Windows обработчик прерывания MS-DOS INT 24h (критическая ошибка). В качестве параметра этой функции можно указывать комбинацию следующих значений:

Значение Описание
SEM_FAILCRITICALERRORS Операционная система Windows не выводит на экран сообщение обработчика критических ошибок, возвращая приложению соответствующий код ошибки
SEM_NOGPFAULTERRORBOX На экран не выводится сообщение об ошибке защиты памяти. Этот флаг может использоваться только при отладке приложений, если они имеют собственный обработчик такой ошибки
SEM_NOOPENFILEERRORBOX Если Windows не может открыть файл, на экран не выводится диалоговая панель с сообщением об ошибке

Функция SetErrorMode возвращает предыдущий режим обработки ошибки.

Файл определения модуля для DLL-библиотеки

Файл определения модуля для DLL-библиотеки отличается от соответствующего файла обычного приложения Windows. В качестве примера приведем образец такого файла:

LIBRARY     DLLNAME
DESCRIPTION 'DLL-библиотека DLLNAME'
EXETYPE     windows
CODE        moveable discardable
DATA        moveable single
HEAPSIZE    1024
EXPORTS
  DrawBitmap  @4
  ShowAll     
  HideAll
  GetMyPool   @8
  FreeMyPool  @9

В файле определения модуля DLL-библиотеки вместо оператора NAME должен находиться оператор LIBRARY , определяющий имя модуля DLL-библиотеки, под которым она будет известна Windows.

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

Оператор CODE используется для определения атрибутов сегмента кода DLL-библиотеки.

Особенностью оператора DATA является использование параметра SINGLE. Так как DLL-библиотека находится в памяти в единственном экземпляре и может иметь только один сегмент данных, для описания сегмента данных библиотеки требуется параметр SINGLE.

Оператор HEAPSIZE определяет начальное значение локальной области памяти, которая будет выделена для DLL-библиотеки функцией LibEntry на этапе инициализации. Если DLL-библиотека не должна иметь локальную область данных, в качестве параметра следует указать нулевое значение:

HEAPSIZE 0

И, наконец, оператор EXPORTS предназначен для перечисления имен и порядковых номеров функций, экспортируемых DLL-библиотекой.

Анализ DLL-библиотек при помощи утилиты tdump.exe

В комплекте системы разработки Borland Turbo C++ for Windows входит утилита tdump.exe , предназначенная для работы в среде MS-DOS. С помощью этой утилиты вы сможете проанализировать содержимое любой DLL-библиотеки, определив имена экспортируемых функций, их порядковые номера, имена DLL-библиотек и номера функций, импортируемых из этих библиотек и т. д.

В системном каталоге Windows имеется DLL-библиотека toolhelp.dll, предназначенная для создания отладочных средств и работы с внутренними структурами данных Windows. Мы выбрали эту библиотеку для наших исследований в основном из-за ее небольшого размера. Описание самой библиотеки выходит за рамки данного тома "Библиотеки системного программиста".

Итак, скопируйте библиотеку в любой временный каталог и введите в среде MS-DOS (или в среде виртуальной машины MS-DOS) следующую команду:

tdump toolhelp.dll toolhelp.map

В результате в файл toolhelp.map будет записано подробное описание библиотеки toolhelp.dll. Мы приведем полученный листинг с нашими комментариями.

Turbo Dump  Version 3.1 Copyright (c) 1988, 1992 Borland International
                   Display of File TOOLHELP.DLL

В начале файла DLL-библиотеки находится заголовок в формате MS-DOS. Он нас не интересует.

Old Executable Header

DOS File Size                               3730h  ( 14128. )
Load Image Size                              359h  (   857. )
Relocation Table entry count                0000h  (     0. )
Relocation Table address                    0040h  (    64. )
Size of header record      (in paragraphs)  0004h  (     4. )
Minimum Memory Requirement (in paragraphs)  0000h  (     0. )
Maximum Memory Requirement (in paragraphs)  FFFFh  ( 65535. )
File load checksum                          0000h  (     0. )
Overlay Number                              0000h  (     0. )

Initial Stack Segment  (SS:SP)   0000:00B8
Program Entry Point    (CS:IP)   0000:0000

Далее в листинге приводится информация о заголовке загрузочного файла в формате Windows. Это так называемый заголовок нового исполняемого формата.

New Executable header
Operating system                                    Windows
File Load CRC                                       0AABAA86Bh
Program Entry Point   (CS:IP)      0001:016A
Initial Stack Pointer (SS:SP)      0000:0000
Auto Data Segment Index            0002h  (     2. )
Initial Local Heap Size            0200h  (   512. )
Initial Stack Size                 0000h  (     0. )
Segment count                      0002h  (     2. )
Module reference count             0002h  (     2. )
Moveable Entry Point Count         0000h  (     0. )
File alignment unit size           0010h  (    16. )
DOS File Size                      3730h  ( 14128. )
Linker Version                     5.20

Обратите внимание, что в заголовке присутствует информация об адресе точки входа (Program entry Point). Начальное содержимое указателя стека (Initial Stack Pointer) равно нулю, так же как и начальный размер стека (Initial Stack Size). Это понятно, так как DLL-библиотека не имеет собственного стека.

В то же время начальный размер локальной области данных отличен от нуля и равен 512 байт (Initial Local Heap Size). Из заголовка можно также определить, что модуль DLL-библиотеки состоит из двух сегментов (Segment Count).

Далее в листинге приведены различные флаги. В частности, видно, что сегмент данных DGROUP имеет атрибуты single и может использоваться совместно различными приложениями (shared). Данный модуль может работать только в защищенном режиме (Protected mode only) и является ни чем иным, как DLL-библиотекой (Module type - Dynamic link Library(DLL)).

Program Flags
    DGROUP                 : single (shared)
    Global initializaton   : No 
    Protected mode only    : Yes
    Application type       : Uses windowing API
    Self Loading           : No 
    Errors in image        : No 
    Module type            : Dynamic link Library (DLL)
Other EXE Flags
    2.X protected mode     : No 
    2.X proportional font  : No 
    Gangload area          : Yes
Start of Gangload Area                              03E0h
Length of Gangload Area                             3160h
Miminum code swap area size                         0
Expected Windows Version                            3.00

Затем в листинге перечисляются различные таблицы с указанием их смещения и размера:

Segment Table                Offset: 00C0h     Length: 0010h
Resource Table               Offset: 00D0h    Length: 0018h
Resident Names Table         Offset: 00E8h      Length: 0012h
Module Reference Table       Offset: 00FAh    Length: 0004h
Imported Names Table         Offset: 00FEh      Length: 000Dh
Entry Table                  Offset: 010Bh       Length: 0070h
Nonresident Names Table      Offset: 017Bh   Length: 0236h

Это таблица сегментов. В ней описаны два сегмента. Первый сегмент является сегментом кода, второй - сегментом данных.

Segment Table                   offset: 00C0h

    Segment Number: 01h
    Segment Type:   CODE     Alloc Size : 2EEEh
    Sector Offset:  0040h   File length: 2EEEh
    Attributes: Preloaded  Relocations  

    Segment Number: 02h
    Segment Type:   DATA     Alloc Size : 0120h
    Sector Offset:  033Eh   File length: 0120h
    Attributes: Sharable  Preloaded  

Далее приводится информация о ресурсах DLL-библиотеки. Таблица ресурсов описывает единственный ресурс типа Version info, описывающий версию модуля.

Resource Table                  offset: 00D0h

    Sector size: 0010h

    type: Version info
          Identifier: 1             
              offset: 03540h length: 01F0h
          Attributes: Moveable   Shareable       

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

Resident Name Table             offset: 00E8h
    Module Name: 'TOOLHELP'
    Name: WEP                          Entry: 0001

Таблица ссылок на модули содержит имена модулей (DLL-библиотек), на которые ссылаются функции, расположенные в исследуемой библиотеке. Как видно из листинга, функции библиотеки toolhelp.dll ссылаются на модули KERNEL и USER. Эти модули являются основными компонентами операционной системы Windows и расположены, соответственно, в файлах krnl386.exe, krnl286.exe и user.exe.

Module Reference Table          offset: 00FAh
    Module  1: KERNEL                                  
    Module  2: USER                                    


Imported Names Table            offset: 00FEh
    name                               offset
    KERNEL                              0001h
    USER                                0008h

Таблица входов (Entry Table) описывает экспортируемые функции. Для каждой функции приводится ее порядковый номер и смещение.

Entry Table                     offset: 010Bh
  Fixed Segment Records  (  1 Entries)    Segment: 0001h
    Entry    1: Offset: 018Ah   Exported   Single data

  Null Entries: 48

  Fixed Segment Records  ( 34 Entries)   Segment: 0001h
    Entry   50: Offset: 057Bh   Exported   Single data
    Entry   51: Offset: 0318h   Exported   Single data
    Entry   52: Offset: 0399h   Exported   Single data
    Entry   53: Offset: 02A2h   Exported   Single data
    Entry   54: Offset: 0417h   Exported   Single data
    Entry   55: Offset: 04A9h   Exported   Single data
    Entry   56: Offset: 090Eh   Exported   Single data
    Entry   57: Offset: 095Eh   Exported   Single data
    Entry   58: Offset: 09E9h   Exported   Single data
    Entry   59: Offset: 0A90h   Exported   Single data
    Entry   60: Offset: 0AD9h   Exported   Single data
    Entry   61: Offset: 0B15h   Exported   Single data
    Entry   62: Offset: 0B8Ch   Exported   Single data
    Entry   63: Offset: 0CAAh   Exported   Single data
    Entry   64: Offset: 0CEDh   Exported   Single data
    Entry   65: Offset: 0D2Eh   Exported   Single data
    Entry   66: Offset: 0F1Ch   Exported   Single data
    Entry   67: Offset: 0F67h   Exported   Single data
    Entry   68: Offset: 0FCAh   Exported   Single data
    Entry   69: Offset: 28B0h   Exported   Single data
    Entry   70: Offset: 2925h   Exported   Single data
    Entry   71: Offset: 11CEh   Exported   Single data
    Entry   72: Offset: 13F4h   Exported   Single data
    Entry   73: Offset: 1B72h   Exported   Single data
    Entry   74: Offset: 1C29h   Exported   Single data
    Entry   75: Offset: 2060h   Exported   Single data
    Entry   76: Offset: 2111h   Exported   Single data
    Entry   77: Offset: 26EAh   Exported   Single data
    Entry   78: Offset: 29C4h   Exported   Single data
    Entry   79: Offset: 2B6Ch   Exported   Single data
    Entry   80: Offset: 2DAEh   Exported   Single data
    Entry   81: Offset: 0D68h   Exported   Single data
    Entry   82: Offset: 0D97h   Exported   Single data
    Entry   83: Offset: 0DC0h   Exported   Single data

Имена и порядковые номера экспортируемых функций приведены в таблице нерезидентных имен (Non-Resident Name Table):

Non-Resident Name Table         offset: 017Bh
    Module Description: 'TOOLHELP - Debug/Tool Helper library'
    Name: TASKSETCSIP                  Entry:    81
    Name: MEMMANINFO                   Entry:    72
    Name: STACKTRACEFIRST              Entry:    66
    Name: MEMORYWRITE                  Entry:    79
    Name: GLOBALINFO                   Entry:    53
    Name: TASKNEXT                     Entry:    64
    Name: CLASSNEXT                    Entry:    70
    Name: GLOBALENTRYHANDLE            Entry:    54
    Name: GLOBALHANDLETOSEL            Entry:    50
    Name: INTERRUPTREGISTER            Entry:    75
    Name: STACKTRACECSIPFIRST          Entry:    67
    Name: LOCALNEXT                    Entry:    58
    Name: INTERRUPTUNREGISTER          Entry:    76
    Name: MODULENEXT                   Entry:    60
    Name: LOCALINFO                    Entry:    56
    Name: TASKFINDHANDLE               Entry:    65
    Name: TASKSWITCH                   Entry:    83
    Name: MEMORYREAD                   Entry:    78
    Name: NOTIFYREGISTER               Entry:    73
    Name: GLOBALNEXT                   Entry:    52
    Name: TIMERCOUNT                   Entry:    80
    Name: MODULEFINDHANDLE             Entry:    62
    Name: MODULEFIRST                  Entry:    59
    Name: GLOBALENTRYMODULE            Entry:    55
    Name: STACKTRACENEXT               Entry:    68
    Name: GLOBALFIRST                  Entry:    51
    Name: SYSTEMHEAPINFO               Entry:    71
    Name: TERMINATEAPP                 Entry:    77
    Name: TASKFIRST                    Entry:    63
    Name: NOTIFYUNREGISTER             Entry:    74
    Name: TASKGETCSIP                  Entry:    82
    Name: CLASSFIRST                   Entry:    69
    Name: MODULEFINDNAME               Entry:    61
    Name: LOCALFIRST                   Entry:    57

Далее в листинге описываются ссылки на импортируемые модули. Каждая такая ссылка состоит из имени модуля (в нашем случае это KERNEL или USER) и порядкового номера импортируемой функции. Сделав дамп файла krnl386.exe при помощи утилиты tdump.exe, вы сможете определить, что ссылке KERNEL.3 соответствует функция GetVersion, ссылке KERNEL.4 - функция LocalInit, а ссылке KERNEL.5 - функция LocalAlloc.

Segment Relocation Records

    Segment 0001h relocations

    type    offset    target
    BASE    2BBBh     0001h:0000h
    BASE    2E93h     0002h:0000h
    PTR     020Fh     KERNEL.3   
    PTR     0177h     KERNEL.4   
    PTR     27D9h     KERNEL.5   
    PTR     28A5h     KERNEL.7   
    PTR     2761h     KERNEL.137 
    OFFS    2D47h     KERNEL.114 
    BASE    0214h     KERNEL.18  
    PTR     2E66h     USER.13  
    PTR     0E2Bh     KERNEL.150 
    PTR     01EAh     KERNEL.28  
    PTR     27B0h     KERNEL.36  
    PTR     23E5h     KERNEL.170 
    PTR     223Dh     KERNEL.47  
    PTR     23FDh     KERNEL.176 
    PTR     224Fh     KERNEL.50  
    PTR     0B66h     USER.430 
    PTR     28F6h     USER.179 
    PTR     29A5h     KERNEL.72  
    OFFS    2EC1h     KERNEL.178 
    PTR     1D79h     KERNEL.202 
    OFFS    27EFh     0001h:16C5h
    LOBYTE  2ACEh     KERNEL.114  additive
    LOBYTE  2CA4h     KERNEL.114  additive
    Relocations: 25

3.4. Приложение DLLCALL

Исходный текст простейшей DLL-библиотеки приведен в листинге 3.1. Как видно из листинга, в библиотеке определены всего три функции - LibMain, WEP и Msg.

Листинг 3.1. Файл dllcall/dllsrc.cpp

#define  STRICT
#include <windows.h>

// ========================================================
// Функция LibMain
// Получает управление только один раз при
// загрузке DLL-библиотеки в память
// ========================================================

#pragma argsused
int FAR PASCAL LibMain(HINSTANCE hInstance,
                       WORD wDataSegment,
                       WORD wHeapSize,
                       LPSTR lpszCmdLine)
{
  // После инициализации локальной области данных
  // функция LibEntry фиксирует сегмент данных.
  // Его необходимо расфиксировать.
  if(wHeapSize != 0)
    // Расфиксируем сегмент данных
    UnlockData(0);

  // Возвращаем 1. Это означает, что инициализация
  // DLL-библиотеки выполнена успешно
  return 1;
}

// ========================================================
// Функция WEP
// Получает управление только один раз при
// удалении DLL-библиотеки из памяти
// ========================================================

#pragma argsused
int FAR PASCAL WEP(int bSystemExit)
{
  return 1;
}

// ========================================================
// Функция Msg
// Выводит на экран диалоговую панель с сообщением
// ========================================================

void FAR PASCAL _export Msg(LPSTR lpszMsg)
{
  MessageBox(NULL, lpszMsg, "DLLSRC", MB_OK);
}

Функция LibMain проверяет размер локальной области данных, заказанной для DLL-приложения функцией LibEntry. Этот размер определяется значением, указанным в файле определения модуля DLL-библиотеки при помощи оператора HEAPSIZE и передается функции LibMain через параметр wHeapSize. Если DLL-библиотека имеет локальную область данных, на этапе инициализации ее необходимо расфиксировать для того чтобы разрешить Windows при необходимости произвольно изменять логический адрес сегмента данных в процессе перемещения сегментов. Использованный способ расфиксирования сегмента данных был описан в предыдущей главе и основан на использовании макрокоманды UnlockData.

После инициализации, которая в нашем простейшем примере всегда выполняется успешно, функция WinMain возвращает признак успешной инициализации - значение 1.

Задача функции WEP в нашем случае сводится к возврату значения 1, означающего успешное завершение.

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

Файл определения модуля для DLL-библиотеки имеет некоторые особенности (листинг 3.2).

Листинг 3.2. Файл dllcall/dll.def

; =============================
; Файл определения модуля
; =============================
LIBRARY        DLLSRC
DESCRIPTION    'DLL-библиотека DLLSRC, (C) 1994, Frolov A.V.'
EXETYPE        windows
CODE           preload moveable discardable
DATA           preload moveable single
HEAPSIZE       1024

Во-первых, в нем нет оператора NAME, предназначенного для идентификации приложения. Вместо него используется оператор LIBRARY. Этот оператор указывает, что данный файл описывает не приложение, а DLL-библиотеку.

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

И в-третьих, в файле определения модуля нет оператора STACKSIZE, так как DLL-библиотека не может иметь стек.

Для создания библиотеки вы можете воспользоваться файлом dllsrc.prj, который есть на дискете, продаваемой вместе с книгой в каталоге dllcall.

В файла проекта DLL-библиотеки, сделанной с помощью системы разработки Borland Turbo C++ for Windows, вам необходимо указать правильный тип приложения. Для этого в меню "Options" выберите строку "Application..." и в появившейся не экране диалоговой панели "Application Options" нажмите на кнопку "Windows DLL" (рис. 3.7). После этого нажмите на кнопку "OK".

Рис. 3.7. Диалоговая панель "Application Options"

Обратите внимание на то, что по умолчанию при создании DLL-библиотеки все функции, определенные в ней, становятся экспортируемыми (Windows DLL all functions exportable). Это означает, что все функции, определенные вами в DLL-библиотеке, имеют специальный пролог и эпилог, предназначенный для правильной загрузки регистра DS (в этот регистр записывается селектор сегмента данных DLL-библиотеки). Кроме того, все эти функции перечисляются в заголовке загрузочного dll-файла библиотеки.

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

Исходный текст приложения DLLCALL, вызывающего функцию Msg из нашей DLL-библиотеки, приведен в листинге 3.3.

Листинг 3.3. Файл dllcall/dllcall.cpp

// ----------------------------------------
// Вызов функции, расположенной в
// DLL-библиотеке
// ----------------------------------------

#define STRICT
#include <windows.h>

// Объявление функции, расположенной в DLL-библиотеке
extern void FAR PASCAL Msg(LPSTR lpszMsg);

// =====================================
// Функция WinMain
// =====================================
#pragma argsused

int PASCAL
WinMain(HINSTANCE hInstance, 
        HINSTANCE hPrevInstance,
        LPSTR     lpszCmdLine, 
        int       nCmdShow)
{
  // Вызываем функцию из DLL-библиотеки
  Msg((LPSTR)"Вам привет из DLL!");

  return 0;
}

Обратите внимание на то, что функция Msg описана как внешняя:

extern void FAR PASCAL Msg(LPSTR lpszMsg);

Файл определения модуля для приложения DLLCALL приведен в листинге 3.4. Он не имеет никаких особенностей.

Листинг 3.4. Файл dllcall/dllcall.def

; =============================
; Файл определения модуля
; =============================
NAME        DLLCALL
DESCRIPTION 'Приложение DLLCALL, (C) 1994, Frolov A.V.'
EXETYPE     windows
STUB        'winstub.exe'
STACKSIZE   8120
HEAPSIZE    1024
CODE        preload moveable discardable
DATA        preload moveable multiple

Файл проекта приложения DLLCALL включает в себя библиотеку импорта dllsrc.lib, которую мы сделали из DLL-библиотеки dllsrc.dll при помощи приложения Import Lib, входящего в состав системы разработки Borland Turbo C++ for Windows.

3.5. Приложение DISCARD

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

Мы отметили, что приложение может создать собственную функцию извещения, которая будет получать управление при попытке Windows удалить глобальные блоки памяти, заказанные как удаляемые (если для них определен флаг GMEM_NOTIFY). Однако, как мы уже говорили, функция извещения должна располагаться в DLL-библиотеке, причем в фиксированном сегменте кода, так как она может быть вызвана из любого контекста, а не только из контекста вашего приложения.

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

Приложение DISCARD (листинг 3.5), сделанное на базе приложения GMEM, заказывает удаляемый блок памяти и назначает собственную процедуру извещения об удалении глобальных блоков памяти. Процедура размещена в DLL-библиотеке dll.dll, исходные тексты которой мы также приведем.

Листинг 3.5. Файл discard/discard.cpp

#define STRICT
#include <windows.h>
#include <windowsx.h>
#include <dos.h>

// Прототип функции извещения о том, что Windows
// планирует удалить блок памяти
// Так как функция NotifyProc составлена на языке
// программирования C (а не C++), мы описываем ее
// как extern "C"   
extern "C" BOOL CALLBACK _export NotifyProc(HGLOBAL hglbl);

#pragma argsused
int PASCAL
WinMain(HINSTANCE hInstance,
   HINSTANCE hPrevInstance,
   LPSTR     lpszCmdLine, int nCmdShow)
{
  BYTE szBuf[100];
  HGLOBAL hmemGlDiscard;
  LPVOID  lpvoidGlobal;
  LPVOID  lpvoidGlDiscard;
  DWORD   dwMaxFreeMem;

  // Определяем размер доступной памяти
  dwMaxFreeMem = GlobalCompact(-1l);

  wsprintf(szBuf, "Доступно памяти:\t%lu\n",
        dwMaxFreeMem);
  MessageBox(NULL, (LPSTR)szBuf, "Global Block", MB_OK);

  // Устанавливаем процедуру извещения, которая
  // получит управление при попытке удалить
  // блок памяти 
  GlobalNotify((GNOTIFYPROC)NotifyProc);

  // Заказываем удаляемый блок памяти размером 200000 байт
  // Для включения режима извещения необходимо
  // указать флаг GMEM_NOTIFY
  hmemGlDiscard =
      GlobalAlloc(GMEM_MOVEABLE | GMEM_DISCARDABLE 
       | GMEM_NOTIFY, 200000l);

  if(hmemGlDiscard != NULL)
  {
    // Если мы его получили, пытаемся удалить блок
    GlobalDiscard(hmemGlDiscard);

    // Фиксируем блок памяти
    lpvoidGlDiscard = GlobalLock(hmemGlDiscard);

    if(lpvoidGlDiscard != (LPVOID) NULL)
    {
      // Так как наша процедура извещения запрещает
      // удаление блока, попытка его фиксирования
      // должна закончится успешно. В этом случае
      // мы выводим идентификатор блока памяти и его
      // логический адрес
      wsprintf(szBuf, "hmemGlDiscard=\t%04.4X\n"
        "lpvoidGlDiscard=\t%04.4X:%04.4X",
        hmemGlDiscard,
        FP_SEG(lpvoidGlDiscard), FP_OFF(lpvoidGlDiscard));
      MessageBox(NULL, (LPSTR)szBuf, "Global Block", MB_OK);

      // Разрешаем перемещение блока
      GlobalUnlock(hmemGlDiscard);
    }
    else
    {
      // Если блок памяти не удалось зафиксировать,
      // проверяем, не был ли он удален
      if(GlobalFlags(hmemGlDiscard) & GMEM_DISCARDED)
      {
         // Так как мы запретили удаление блока, следующее
         // сообщение не должно появиться на экране
         MessageBox(NULL, "Блок удален и мы его"
           " восстанавливаем",
           "Global Block", MB_OK);

         // Восстанавливаем удаленный блок памяти
         hmemGlDiscard = GlobalReAlloc(hmemGlDiscard,
           200000l,
           GMEM_MOVEABLE | GMEM_DISCARDABLE);

         // Фиксируем блок памяти
         lpvoidGlDiscard = GlobalLock(hmemGlDiscard);

         if(lpvoidGlDiscard != (LPVOID) NULL)
         {
           // Выводим идентификатор и логический адрес
           // зафиксированного блока памяти
           wsprintf(szBuf, "hmemGlDiscard=\t%04.4X\n"
             "lpvoidGlDiscard=\t%04.4X:%04.4X",
             hmemGlDiscard,
             FP_SEG(lpvoidGlDiscard),
             FP_OFF(lpvoidGlDiscard));
           MessageBox(NULL, (LPSTR)szBuf, "Global Block",
             MB_OK);

           // Освобождаем блок памяти
           GlobalUnlock(hmemGlDiscard);
         }
         else
         {
           MessageBox(NULL, "Ошибка при фиксировании блока",
           "Global Block", MB_OK);
         }
      }
    }

    // Отдаем удаляемый блок памяти операционной системе
    GlobalFree(hmemGlDiscard);
  }
  else
  {
    MessageBox(NULL, "Мало памяти для удаляемого блока",
      "Global Block", MB_OK);
  }
  return 0;
}

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

extern "C" BOOL CALLBACK _export NotifyProc(HGLOBAL hglbl);

После запуска приложения функция WinMain определяет размер доступной памяти, вызывая функцию GlobalCompact, а затем выводит его на экран.

После этого функция WinMain устанавливает процедуру извещения, вызывая функцию GlobalNotify и передавая ей в качестве параметра адрес внешней по отношению к приложению функции извещения NotifyProc, расположенной в DLL-библиотеке:

GlobalNotify((GNOTIFYPROC)NotifyProc);

Каждая копия приложения может вызывать функцию GlobalNotify только один раз.

Задача функции извещения заключается в том, чтобы предотвратить удаление блока памяти. Это сделать очень просто, достаточно того, чтобы функция извещения вернула нулевое значение.

После установки функции извещения приложение заказывает удаляемый блок памяти, указывая среди прочих флаг GMEM_NOTIFY :

hmemGlDiscard =
  GlobalAlloc(GMEM_MOVEABLE | GMEM_DISCARDABLE 
  | GMEM_NOTIFY, 200000l);

Если этот флаг не будет указан, Windows при необходимости удалит блок без вызова процедуры извещения.

После получения блока памяти приложение пытается его удалить, вызывая функцию GlobalDiscard (можно было бы, конечно, подождать, пока этот блок будет удален самой операционной системой Windows, однако вы можете прождать до утра):

GlobalDiscard(hmemGlDiscard);

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

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

Файл определения модуля приложения DISCARD приведен в листинге 3.6.

Листинг 3.6. Файл discard/discard.def

; =============================
; Файл определения модуля
; =============================
NAME        DISCARD
DESCRIPTION 'Приложение DISCARD, (C) 1994, Frolov A.V.'
EXETYPE     windows
STUB        'winstub.exe'
STACKSIZE   8120
HEAPSIZE    1024
CODE        preload moveable discardable
DATA        preload moveable multiple

Исходный текст DLL-библиотеки приведен в листинге 3.7.

Листинг 3.7. Файл discard/dll.c

#define  STRICT
#include <windows.h>

// ========================================================
// Функция LibMain
// Получает управление только один раз при
// загрузке DLL-библиотеки в память
// ========================================================

#pragma argsused
int FAR PASCAL LibMain(HINSTANCE hInstance,
                       WORD wDataSegment,
                       WORD wHeapSize,
                       LPSTR lpszCmdLine)
{
  // После инициализации локальной области данных
  // функция LibEntry фиксирует сегмент данных.
  // Его необходимо расфиксировать.
  if(wHeapSize != 0)
    // Расфиксируем сегмент данных
    UnlockData(0);

  // Возвращаем 1. Это означает, что инициализация
  // DLL-библиотеки выполнена успешно
  return 1;
}

// ========================================================
// Функция WEP
// Получает управление только один раз при
// удалении DLL-библиотеки из памяти
// ========================================================

#pragma argsused
int FAR PASCAL WEP(int bSystemExit)
{
  return 1;
}

// ========================================================
// Функция NotifyProc
// Она получает управление, если в текущей задаче
// предпринимается попытка удаления блока памяти.
// В этом случае функция возвращает значение 0,
// в результате чего Windows отменяет удаление блока
// ========================================================

#pragma argsused
BOOL FAR PASCAL _export NotifyProc(HGLOBAL hglbl)
{
  return 0;
}

Эта библиотека исключительно проста, так как помимо стандартных функций LibMain и WEP в ней определена только одна функция извещения NotifyProc.

Изучение последней также не вызовет ни малейшего затруднения. Функция NotifyProc возвращает значение 0, запрещая Windows удалять блок памяти. Если надо разрешить удаление блока памяти, необходимо вернуть значение 1.

Обратим внимание на файл определения модуля DLL-библиотеки, приведенный в листинге 3.8.

Листинг 3.8. Файл discard/dll.def

; =============================
; Файл определения модуля
; =============================
LIBRARY        DLL
DESCRIPTION    'DLL-библиотека DLL, (C) 1994, Frolov A.V.'
EXETYPE        windows
CODE           preload fixed
DATA           preload moveable single
HEAPSIZE       1024
EXPORTS
  NotifyProc @10

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

Теперь о том, почему для создания DLL-библиотеки мы выбрали язык C, а не С++.

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

Если же исходный текст DLL-библиотеки составлен на языке C, эти проблемы исчезнут. Однако в этом случае при вызове таких экспортируемых функций из приложений, составленных на языке C++, вам придется объявить их как extern "C".

Если же для разработки DLL-библиотеки используется язык C++, для обеспечения доступа к экспортируемым функциям вы можете либо использовать библиотеку импорта, созданную приложением IMPLIB, либо описать экспортируемую функцию следующим образом (в качестве примера использован исходный текст одной из функций DLL-библиотеки, описанной в разделе "Приложение WINHOOK"):

extern "C"
void WINAPI _export RemoveKbHook(void)
{
  if(bHooked)
  {
    UnhookWindowsHookEx(hhookMsg);
    UnhookWindowsHookEx(hhook);
  }
}

Описание extern "C" отменяет для определяемой функции соглашение языка C++ об именах функций.

3.6. Фильтры

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

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

Для чего можно использовать фильтры?

С помощью фильтров вы можете перехватить сообщения, поступающие в любые диалоговые панели, полосы просмотра, меню и т. п., причем как для отдельного приложения, так и для всех приложений сразу. Можно "отобрать" сообщения у функций GetMessage, PeekMessage, SendMessage. Можно записывать и затем проигрывать события, связанные с перемещением мыши и использованием клавиатуры. Фильтры облегчают создание обучающих приложений, которые работают совместно с обычными приложениями Windows в режиме обучения. Обучающие приложения может контролировать действия пользователя, когда он работает с приложением.

В качестве примера в разделе "Приложение WINHOOK" мы расскажем вам о том, как можно с помощью фильтров переключать раскладку клавиатуры. Мы приведем исходные тексты несложного руссификатора клавиатуры, позволяющего использовать в операционной системе Windows символы кириллицы.

Установка фильтра

Для установки фильтра в операционной системе Windows версии 3.1 следует использовать функцию SetWindowsHookEx (в Windows версии 3.0 для этой цели была предназначена функция SetWindowsHook ):

HHOOK WINAPI SetWindowsHookEx(
  int idHook,            // тип фильтра
  HOOKPROC lpfn,         // адрес функции фильтра
  HINSTANCE hInstance,   // идентификатор приложения
  HTASK hTask); // задача, для которой устанавливается фильтр

Параметр idHook определяет тип встраиваемого фильтра. В качестве значения для этого параметра можно использовать одну из следующих констант:

Константа Назначение фильтра
WH_CALLWNDPROC Для функции окна. Используется в Windows версии 3.0 и 3.1. Можно устанавливать для отдельной задачи или для всей системы
WH_CBT Для обучающих программ. Можно устанавливать для отдельной задачи или для всей системы
WH_DEBUG Для отладкиИспользуется в Windows версии 3.1. Можно устанавливать для отдельной задачи или для всей системы
WH_GETMESSAGE Фильтр сообщений. Получает управление после выборки сообщения функцией GetMessageИспользуется в Windows версии 3.0 и 3.1. Можно устанавливать для отдельной задачи или для всей системы
WH_HARDWARE Фильтр для сообщений, поступающих от нестандартного аппаратного обеспечения, такого как система перьевого ввода. Используется в Windows версии 3.1. Можно устанавливать для отдельной задачи или для всей системы
WH_JOURNALPLAYBACK Фильтр для "проигрывания" событийИспользуется в Windows версии 3.0 и 3.1. Можно устанавливать только для всей системы
WH_JOURNALRECORD Фильтр для записи событийИспользуется в Windows версии 3.0 и 3.1. Можно устанавливать только для всей системы
WH_KEYBOARD Фильтр сообщений, поступающих от клавиатуры. Используется в Windows версии 3.1. Можно устанавливать для отдельной задачи или для всей системы
WH_MOUSE Фильтр сообщений, поступающих от мыши. Используется в Windows версии 3.1. Можно устанавливать для отдельной задачи или для всей системы
WH_MSGFILTER Фильтр сообщений, который получает управление после выборки, но перед обработкой сообщений, поступающих от диалоговых панелей или меню. Используется в Windows версии 3.0 и 3.1. Можно устанавливать для отдельной задачи или для всей системы
WH_SHELL Фильтр для получения различных извещений от операционной системы Windows. Используется в Windows версии 3.1. Можно устанавливать для отдельной задачи или для всей системы
WH_SYSMSGFILTER Фильтр вызывается операционной системой после того, как диалоговая панель или меню получат сообщение, но перед обработкой этого сообщения. Данный фильтр может обрабатывать сообщения для любых запущенных приложений Windows. Используется в Windows версии 3.0 и 3.1. Можно устанавливать только для всей системы

Параметр lpfn функции SetWindowsHookEx должен определять адрес функции встраиваемого фильтра (или, иными словами, функции перехвата сообщений).

Функция фильтра может находиться либо в приложении, устанавливающем фильтр, либо (что значительно лучше) в DLL-библиотеке. Если функция находится в приложении, или в DLL-библиотеке, загружаемой явным образом при помощи функции LoadLibrary, в качестве параметра lpfn следует использовать значение, полученное от функции MakeProcInstance. Если же для импортирования функций из DLL-библиотеки используется библиотека импорта (как в описанном ниже приложении WINHOOK), параметр lpfn может содержать непосредственный указатель на функцию фильтра.

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

Последний параметр функции hTask должен содержать идентификатор задачи, для которой определяется функция фильтра. Если последний параметр указан как NULL, фильтр встраивается для всех задач, работающих в системе.

Если приложение встраивает фильтр для себя, оно должно определить идентификатор задачи, соответствующий собственной копии приложения. Это проще всего сделать, вызвав функцию GetCurrentTask :

HTASK WINAPI GetCurrentTask(void);

Можно определить идентификатор задачи исходя из идентификатора окна, созданного этой задачей. Для этого следует воспользоваться функцией GetWindowTask :

HTASK WINAPI GetWindowTask(HWND hwnd);

Эта функция возвращает идентификатор задачи, создавшей окно с идентификатором hwnd.

Функция SetWindowsHookEx возвращает 32-разрядный идентификатор функции фильтра, который следует сохранить для дальнейшего использования, или NULL при ошибке.

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

hhook = SetWindowsHookEx(WH_KEYBOARD,
  (HOOKPROC)KbHookProc,
  hInst, NULL); 

Исходный текст функции фильтра приведен (с сильными сокращениями) ниже:

extern "C"
LRESULT CALLBACK KbHookProc(int code,
  WPARAM wParam, LPARAM lParam)
{
  ...
  ...
  ...
  // Вызываем следующий в цепочке перехватчик
  return CallNextHookEx(hhook, code, wParam, lParam);
}

После выполнения всех необходимых действий функция фильтра передает управление по цепочке другим фильтрам (что необязательно). Для этого вызывается функция CallNextHookEx :

LRESULT WINAPI CallNextHookEx(
  HHOOK hHook,
  int code,
  WPARAM wParam,
  LPARAM lParam);

Параметр hHook содержит идентификатор текущей функции фильтра.

Параметр code содержит код фильтра, который должен передаваться следующему фильтру в цепочке.

Параметры wParam и lParam содержат, соответственно, 16- и 32-битовый дополнительные параметры.

Параметры code, wParam и lParam функции CallNextHookEx полностью соответствуют параметрам функции фильтра, которая будет рассмотрена нами позже.

Отмена фильтра

Фильтр, установленный функцией SetWindowsHookEx, можно отменить или удалить при помощи функции UnhookWindowsHookEx :

BOOL WINAPI UnhookWindowsHookEx(HHOOK hHook);

В качестве единственного параметра этой функции следует передать идентификатор функции фильтра, полученный от функции SetWindowsHookEx.

Если отмена фильтра выполнена успешно, функция UnhookWindowsHookEx возвращает значение TRUE. В случае возникновения ошибки возвращается FALSE.

Функции фильтра

Функция фильтра должна иметь следующий прототип:

LRESULT CALLBACK HookProc(
  int code,
  WPARAM wParam,
  LPARAM lParam)

Имя функции фильтра может быть любым, так как при установке фильтра вы должны указать функции SetWindowsHookEx указатель на функцию фильтра.

Параметр code может определять действия, выполняемые функцией фильтра. В операционной системе Windows версии 3.0 этот параметр мог принимать отрицательные значения. В этом случае функция фильтра должна была передавать управление следующему в цепочке фильтру при помощи функции DefHookProc. Операционная система Windows версии 3.1 никогда не вызывает функцию фильтра с отрицательным значением параметра code.

Назначение параметров wParam и lParam зависит от типа фильтра, задаваемого при помощи параметра idHook функции SetWindowsHookEx. Мы не будем подробно описывать все возможные типы фильтров, так как это займет очень много времени. При необходимости вы сможете найти полное описание в документации, поставляющейся вместе с SDK. Рассмотрим в деталях только самые интересные, на наш взгляд, типы фильтров.

Фильтр WH_CALLWNDPROC

Приведем прототип функции фильтра типа WH_CALLWNDPROC (для функции можно использовать любое имя):

LRESULT CALLBACK CallWndProc(
  int code,       // код действия
  WPARAM wParam,  // флаг текущей задачи
  LPARAM lParam)  // адрес структуры с сообщением

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

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

Если сообщение послано текущей задачей, значение параметра wParam отлично от нуля, в противном случае оно равно NULL.

Параметр lParam содержит указатель на структуру, которая описывает перехваченное сообщение (эта структура на описана в файле windows.h):

struct _Msg
{
  LPARAM lParam;  // параметр lParam сообщения
  WPARAM wParam;  // параметр wParam сообщения
  UINT   uMsg;    // код сообщения
  HWND   hWnd;    // идентификатор окна, для которого
};                // предназначено сообщение

Функция фильтра должна всегда возвращать нулевое значение.

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

Фильтр WH_CBT

Большинство приложений, созданных фирмой Microsoft для своей операционной системы Windows, имеют встроенные обучающие системы, предназначенные для того, чтобы пользователь мог быстрее освоить работу с приложением. Например, текстовый процессор Microsoft Word for Windows версий 2.0 и 6.0 имеет очень удобную и легкую в использовании обучающую систему. Эта обучающая система не только рассказывает пользователю о том, как надо работать с текстовым процессором, но и, что очень важно, дает ему возможность попробовать выполнить те или иные действия самостоятельно. Когда пользователь пытается работать самостоятельно, все выглядит так, как будто он имеет дело с настоящим текстовым процессором, но при этом его действия ограничиваются обучающей системой.

Для облегчения создания подобных обучающих программ приложения могут установить фильтр WH_CBT, специально предназначенный для организации контроля за действиями пользователя со стороны обучающих программ. Мы не беремся утверждать, что обучающая программа для текстового процессора Microsoft Word for Windows построена с использованием этого фильтра, однако, вполне вероятно, что это так и есть.

Приведем прототип функции фильтра WH_CBT:

LRESULT CALLBACK CBTProc(               
  int code,       // код действия      
  WPARAM wParam,  // назначение зависит от кода действия
  LPARAM lParam); // назначение зависит от кода действия

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

Фильтр может разрешить или запретить выполнение перечисленных выше операций, возвращая соответствующее значение.

В зависимости от параметра code меняется назначение параметров wParam и lParam. Перечислим возможные значения для параметра code и кратко опишем назначение остальных параметров функции фильтра WH_CBT.

Значение параметра code Описание
HCBT_ACTIVATE Фильтр вызывается перед активизацией любого окна. Если функция возвращает FALSE, окно активизируется, если TRUE - нет. Параметр wParam содержит идентификатор активизирующегося окна. Параметр lParam содержит указатель на структуру CBTACTIVATESTRUCT :
typedef struct tagCBTACTIVATESTRUCT { 
  BOOL fMouse;
  HWND hWndActive;
} CBTACTIVATESTRUCT;
Флаг fMouse содержит TRUE, если окно активизируется в результате щелчка мыши, поле hWndActive содержит идентификатор активного окна
HCBT_CREATEWND Фильтр вызывается перед созданием окна. Если функция фильтра возвращает FALSE, создание окна разрешается, если TRUE - нет. Параметр wParam содержит идентификатор создаваемого окна. Параметр lParam содержит дальний указатель LPCREATESTRUCT на структуру CREATESTRUCT :
typedef struct tagCREATESTRUCT
{
    void FAR* lpCreateParams;
    HINSTANCE hInstance;
    HMENU     hMenu;
    HWND      hwndParent;
    int       cy;
    int       cx;
    int       y;
    int       x;
    LONG      style;
    LPCSTR    lpszName;
    LPCSTR    lpszClass;
    DWORD     dwExStyle;
} CREATESTRUCT;
HCBT_DESTROYWND Фильтр вызывается перед уничтожением окна. Если функция возвращает TRUE, уничтожение окна отменяется. Параметр wParam содержит идентификатор уничтожаемого окна. Параметр lParam содержит 0
HCBT_MINMAX Фильтр вызывается перед выполнением минимизации или максимизации окна. Если функция фильтра возвращает TRUE, перечисленные операции не выполняются. Параметр wParam содержит идентификатор окна, для которого выполняется минимизация или максимизация. Старшее слово параметра lParam равно нулю, младшее содержит одну из констант, описанных в файле windows.h с префиксом SW_, такие как SW_HIDE, SW_SHOWNORMAL, SW_SHOWMINIMIZED, SW_SHOWNOACTIVATE, SW_SHOW, SW_MINIMIZE, SW_SHOWMINNOACTIVE, SW_SHOWNA, SW_RESTORE
HCBT_MOVESIZE Фильтр вызывается перед перемещением или изменением размера окна. Если функция фильтра возвращает TRUE, выполнение операции отменяется. Параметр wParam содержит идентификатор окна, для которого выполняется перемещение или изменение размера. Параметр lParam содержит дальний указатель на структуру RECT, описывающую прямоугольную область
HCBT_SYSCOMMAND Фильтр вызывается при обработке системной команды. Вызов функции фильтра выполняется из функции DefWindowProc. Если функция фильтра возвращает TRUE, выполнение системной команды отменяется. Параметр wParam содержит код системной команды, такой, как SC_CLOSE, SC_HSCROLL, SC_MINIMIZE и т. д.Если параметр wParam содержит код команды SC_HOTKEY (активизация окна, связанного с клавишей ускоренного выбора, назначенной приложением), младшее слово параметра lParam содержит идентификатор окна, для которого назначена клавиша ускоренного выбора. Для остальных команд значение этого параметра не определено
HCBT_CLICKSKIPPED Фильтр вызывается при удалении сообщения мыши из системной очереди сообщений, при условии, что дополнительно определен фильтр WH_MOUSE. Параметр wParam содержит код сообщения мыши. Параметр lParam содержит дальний указатель на структуру MOUSEHOOKSTRUCT :
typedef struct tagMOUSEHOOKSTRUCT
{
  POINT pt;
  HWND  hwnd;
  UINT  wHitTestCode;
  DWORD dwExtraInfo;
} MOUSEHOOKSTRUCT;


HCBT_KEYSKIPPED Фильтр вызывается при удалении клавиатурного сообщения из системной очереди сообщений, при условии, что дополнительно определен фильтр WH_KEYBOARD. Параметр wParam содержит виртуальный код клавиши.содержимое параметра lParam аналогично содержимому соответствующего параметра клавиатурных сообщений WM_KEYDOWN и WM_KEYUP
HCBT_SETFOCUS Фильтр вызывается при установке фокуса ввода. Если функция фильтра возвращает значение TRUE, фокус не устанавливается. Параметр wParam содержит идентификатор окна, получающего фокус ввода. Младшее слово парамера lParam содержит идентификатор окна, теряющего фокус ввода. Старшее слово парамера lParam всегда содержит NULL
HCBT_QS Фильтр вызывается при удалении из системной очереди сообщения WM_QUEUESYNC, предназначенного для использования обучающим приложением. Это сообщение позволяет ему определить момент завершения того или иного события в главном приложении. Тех, кого интересуют подробности, мы отсылаем к документации, поставляющейся вместе с SDK

Фильтр WH_DEBUG

Приведем прототип функции фильтра типа WH_DEBUG :

LRESULT CALLBACK DebugProc(
  int code,       // код действия
  WPARAM wParam,  // идентификатор задачи
  LPARAM lParam); // адрес структуры DEBUGHOOKINFO

Фильтр WH_DEBUG предназначен для отладчиков и должен находиться в DLL-библиотеке. Он вызывается перед вызовом других фильтров, установленных функцией SetWindowsHookEx.

Параметр wParam содержит идентификатор задачи, которая установила фильтр.

Параметр lParam содержит дальний указатель на структуру DEBUGHOOKINFO :

typedef struct tagDEBUGHOOKINFO
{
  HMODULE  hModuleHook;
  LPARAM   reserved;
  LPARAM   lParam;
  WPARAM   wParam;
  int      code;
} DEBUGHOOKINFO;

В этой структуре в поле hModuleHook находится идентификатор модуля, содержащего функцию фильтра, поля lParam, wParam, code содержат параметры, передаваемые функции фильтра. Поле reserved не используется.

Функция фильтра типа WH_DEBUG может предотвратить вызов другого фильтра, для чего она должна возвратить значение TRUE. Если она вернет FALSE, управление будет передано соответствующему фильтру.

Фильтр WH_GETMESSAGE

Фильтр WH_GETMESSAGE получает управление, когда функция GetMessage или PeekMessage возвращают выбранное из очереди сообщение. Функция фильтра должна находиться в DLL-библиотеке.

Приведем прототип функции фильтра типа WH_GETMESSAGE:

LRESULT CALLBACK GetMsgProc(
  int code,       // код действия
  WPARAM wParam,  // не определено
  LPARAM lParam); // адрес структуры MSG

Параметр lParam содержит указатель на структуру MSG , содержащую перехваченное сообщение:

typedef struct tagMSG
{
  HWND   hwnd;
  UINT   message;
  WPARAM wParam;
  LPARAM lParam;
  DWORD  time;
  POINT  pt;
} MSG;

Функция фильтра может изменить любой параметр сообщения и даже его код. В последнем случае произойдет замена одного сообщения на другое.

Возвращаемое функцией фильтра значение должно всегда равняться нулю.

Фильтр WH_HARDWARE

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

Приведем прототип функции фильтра типа WH_HARDWARE:

LRESULT CALLBACK HardwareProc(
  int code,       // код действия
  WPARAM wParam,  // не определено
  LPARAM lParam); // адрес структуры HARDWAREHOOKSTRUCT

Структура HARDWAREHOOKSTRUCT описана в файле windows.h:

typedef struct tagHARDWAREHOOKSTRUCT
{
   HWND    hWnd;
   UINT    wMessage;
   WPARAM  wParam;
   LPARAM  lParam;
} HARDWAREHOOKSTRUCT;

В этой структуре поле hWnd содержит идентификатор окна, которому предназначено сообщение, поле wMessage содержит код сообщения, поля wParam и lParam содержат дополнительную информацию, зависящую от кода сообщения.

Фильтр WH_JOURNALRECORD

Фильтр WH_JOURNALRECORD вызывается, когда Windows удаляет сообщения из системной очереди. Функция фильтра должна находиться в DLL-библиотеке.

Приведем прототип функции фильтра типа WH_JOURNALRECORD:

LRESULT CALLBACK JournalRecordProc(
  int code,       // код действия
  WPARAM wParam,  // содержит NULL
  LPARAM lParam); // адрес структуры EVENTMSG

Данный фильтр предназначен для записи перехваченных сообщений в память или файл. Он не может изменять или удалять сообщения из системной очереди.

Параметр code может принимать одно из трех значений:

Значение параметра code Описание
HC_ACTION Windows извлекает сообщение из системной очереди
HC_SYSMODALON Windows выводит на экран системную модальную диалоговую панель. Начиная с этого момента приложение должно остановить запись сообщений
HC_SYSMODALOFF Windows удаляет системную модальную диалоговую панель, так что теперь можно продолжить запись сообщений

Структура EVENTMSG описана в файле windows.h:

typedef struct tagEVENTMSG
{
  UINT   message;
  UINT   paramL;
  UINT   paramH;
  DWORD  time;
} EVENTMSG;

Фильтр WH_JOURNALPLAYBACK

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

Приведем прототип функции фильтра типа WH_JOURNALPLAYBACK:

LRESULT CALLBACK JournalPlaybackProc(
  int code,       // код действия
  WPARAM wParam,  // содержит NULL
  LPARAM lParam); // адрес структуры EVENTMSG

Перед возвратом управления функция фильтра WH_JOURNALPLAYBACK должна записать по адресу, переданному ей через параметр lParam, структуру данных, записанную ранее функцией фильтра WH_JOURNALRECORD.

Функция фильтра должна вернуть интервал времени ожидания (в машинных тиках) перед тем как Windows начнет обработку сообщения. Если вернуть нулевое значение, ожидание выполняться не будет.

Фильтр WH_KEYBOARD

Фильтр WH_KEYBOARD получает управление, когда функции GetMessage или PeekMessage возвращают сообщения WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, WM_SYSKEYUP или WM_CHAR. Функция фильтра должна находиться в DLL-библиотеке.

Приведем прототип функции фильтра типа WH_KEYBOARD:

LRESULT CALLBACK KeyboardProc(
  int code,       // код действия
  WPARAM wParam,  // код виртуальной клавиши
  LPARAM lParam); // дополнительная информация

Параметр code может принимать значения HC_ACTION и HC_NOREMOVE. В первом случае перехваченное сообщение после обработки будет удалено из системной очереди сообщений, во втором - останется в этой очереди (т. к. оно было выбрано при помощи функции PeekMessage с параметром PM_NOREMOVE). Если сообщение останется в очереди, таблица состояния клавиатуры, которую можно получить при помощи функции GetKeyboardState, может не отражать состояние клавиатуры на момент выборки сообщения.

Параметры wParam и lParam содержат ту же самую информацию, что и соответствующие параметры клавиатурных сообщений WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_CHAR.

Параметр wParam содержит код виртуальной клавиши, соответствующей нажатой физической клавише. Именно этот параметр используется приложениями для идентификации нажатой клавиши.

Приведем описание отдельных бит парамера lParam, через который передается дополнительная информация о клавиатурном сообщении .

Бит Описание
0-15 Счетчик повторов. Если нажать клавишу и держать ее в нажатом состоянии, несколько сообщений WM_KEYDOWN и WM_SYSKEYDOWN будут слиты в одно. Количество объединенных таким образом сообщений
16-23 OEM скан-код клавиши. Изготовители аппаратуры (OEM - Original Equipment Manufacturer) могут заложить в своей клавиатуре различное соответствие скан-кодов и обозначений клавиш. Скан-код генерируется клавиатурным контроллером. Это тот самый код, который получают в регистре AH программы MS-DOS, вызывая прерывание INT16h
24 Флаг расширенной клавиатуры. Этот бит установлен в 1, если сообщение соответствует клавише, имеющейся только на расширенной 101- или 102-клавишной клавиатуре. Это может быть одна из следующих клавиш: <Home>, <End>, <PgUp>, <PgDn>, <Insert>, <Delete>, клавиши дополнительной клавиатуры.
25-26 Не используются
27-28 Зарезервированы для использования Windows
29 Код контекста. Этот бит равен 1, если сообщение соответствует комбинации клавиши <Alt> с любой другой, и 0 в противном случае
30 Предыдущее состояние клавиши. Если перед приходом сообщения клавиша, соответствующая сообщению, была в нажатом состоянии, этот бит равен 1. В противном случае бит равен 0
31 Флаг изменения состояния клавиши (transition state). Если клавиша была нажата, бит равен 0, если отпущена - 1

При помощи этого фильтра приложение может удалить клавиатурное сообщение. Для удаления сообщения функция фильтра должна вернуть значение 1. Если же возвращенное значение будет равно 0, сообщение будет обработано системой.

Фильтр WH_MOUSE

Фильтр WH_MOUSE получает управление, когда функции GetMessage или PeekMessage возвращают сообщения мыши. Функция фильтра должна находиться в DLL-библиотеке.

Приведем прототип функции фильтра типа WH_MOUSE:

LRESULT CALLBACK MouseProc(
  int code,       // код действия
  WPARAM wParam,  // код сообщения
  LPARAM lParam); // указатель на структуру MOUSEHOOKSTRUCT

Так же, как и для предыдущего фильтра, параметр code может принимать значения HC_ACTION и HC_NOREMOVE.

Параметр wParam содержит код сообщения, поступившего от мыши.

Через параметр lParam передается указатель на структуру MOUSEHOOKSTRUCT :

typedef struct tagMOUSEHOOKSTRUCT
{
   POINT  pt;
   HWND   hwnd;
   UINT   wHitTestCode;
   DWORD  dwExtraInfo;
} MOUSEHOOKSTRUCT;

Эта структура содержит дополнительную информацию, имеющую отношение к сообщению.

Поле pt является структурой типа POINT, в которой находятся экранные x- и y-координаты курсора мыши. Поле hwnd содержит идентификатор окна, в функцию которого будет направлено сообщение. В поле wHitTestCode находится код тестирования, определяющий область окна, соответствующую расположению курсора мыши на момент генерации сообщения. Поле dwExtraInfo содержит дополнительную информацию, которую можно получить с помощью функции GetMessageExtraInfo :

LPARAM WINAPI GetMessageExtraInfo(void);

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

Фильтр WH_MSGFILTER

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

Приведем прототип функции фильтра типа WH_MSGFILTER:

LRESULT CALLBACK MessageProc(
  int code,       // код действия
  WPARAM wParam,  // не определено
  LPARAM lParam); // указатель на структуру MSG

Параметр code может принимать значения MSGF_DIALOGBOX (ввод в диалоговой панели), MSGF_SCROLLBAR (ввод в области полосы просмотра), MSGF_MENU (ввод в меню) или MSGF_NEXTWINDOW (пользователь активизирует следующее окно, нажимая комбинацию клавиш <Alt + Tab> или <Alt + Esc>).

Если фильтр обрабатывает сообщение, функция фильтра должна вернуть ненулевое значение, если нет - нулевое.

Фильтр WH_SYSMSGFILTER

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

Приведем прототип функции фильтра типа WH_SYSMSGFILTER:

LRESULT CALLBACK SysMsgProc(
  int code,       // код действия
  WPARAM wParam,  // не определено
  LPARAM lParam); // указатель на структуру MSG

Если фильтр обрабатывает сообщение, функция фильтра должна вернуть ненулевое значение, если нет - нулевое.

Приложение может установить одновременно фильтры WH_SYSMSGFILTER и WH_MSGFILTER, в этом случае вначале вызывается фильтр WH_SYSMSGFILTER, а затем - фильтр WH_MSGFILTER. Если же функция фильтра WH_SYSMSGFILTER возвращает ненулевое значение, фильтр WH_MSGFILTER не вызывается.

Фильтр WH_SHELL

Фильтр WH_SHELL предназначен для приложений-оболочек (shell application) и позволяет получать необходимые извещения от операционной системы Windows.

Приведем прототип функции фильтра типа WH_SHELL:

LRESULT CALLBACK ShellProc(
  int code,       // код действия
  WPARAM wParam,  // флаг текущей задачи
  LPARAM lParam); // не определено

Параметр code может принимать одно из следующих значений:

Значение параметра code Описание
HSHELL_ACTIVATESHELLWINDOW Оболочка должна активизировать свое главное окно
HSHELL_WINDOWCREATED Создано окно верхнего уровня, которое не принадлежит ни одному приложению. Это окно будет существовать во время работы функции фильтра. Идентификатор созданного окна передается через параметр wParam
HSHELL_WINDOWDESTROYED Описанное выше окно верхнего уровня будет уничтожено после завершения работы функции фильтра. Идентификатор уничтожаемого окна передается через параметр wParam

Функция фильтра должна вернуть нулевое значение.

3.7. Приложение WINHOOK

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

Для того чтобы Windows стала "понимать" русские символы, необходимо решить несколько задач.

Во-первых, клавиатура персонального компьютера IBM PC не предназначена для ввода символов кириллицы. В операционной системе MS-DOS эта проблема решалась с помощью резидентных программ, перехватывающих аппаратное прерывание от клавиатуры и заменяющих коды символов. Для операционной системы Microsoft Windows этот способ непригоден, так как для работы с клавиатурой используется специальный драйвер.

Во-вторых, необходимо обеспечить отображение символов кириллицы на экране монитора. В MS-DOS для отображения символов кириллицы в текстовом режиме можно было выполнить загрузку таблиц знакогенератора, расположенных в памяти видеоконтроллера. Операционная система Windows не использует текстовый режим, и, соответственно, не использует таблицы знакогенератора. Зато для нее предусмотрен механизм шрифтов различного типа - матричных, векторных и масштабируемых. Очевидно, что единственный способ отображения символов кириллицы в среде Windows - это подготовка версий шрифтов с кириллицей.

В-третьих, необходимо обеспечить правильную работу таких функций, как OemToAnsi и AnsiToOem. Эти функции должны выполнять преобразования, учитывая кодировку символов, которая сама по себе зависит от национального языка.

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

В этом разделе мы приведем исходные тексты приложения WINHOOK, которое решает первую из перечисленных выше задач, т. е. обеспечивает ввод с клавиатуры символов кириллицы. Приложение WINHOOK позволяет вам изменять раскладку клавиатуры с используемой по умолчанию на определенную в ресурсах приложения. Переключение раскладки выполняется при помощи левой или правой клавиши <Control>. Сразу после запуска приложения включается стандартная раскладка клавиатуры. Если вы нажмете на клавишу <Control> три раза, то услышите звуковой сигнал, после чего раскладка клавиатуры меняется на альтернативную и вы можете вводить символы кириллицы.

Приложение WINHOOK создает "непотопляемое" окно, которое располагается над поверхностью любого другого окна в Windows. Вы можете перемещать это окно мышью или изменять его размеры. В зависимости от используемой раскладки клавиатуры в окне отображается одно из двух слов: DEFAULT для стандартной раскладки и CYRILLIC для дополнительной (рис. 3.8).

Рис. 3.8. Главное окно приложения WINHOOK при использовании различных раскладок клавиатуры

Если для работы с кириллицей вы используете такие приложения, как CyrWin или ParaWin, перед запуском приложения WINHOOK их следует либо завершить, либо переключить раскладку клавиатуры на стандартную.

Для того чтобы завершить работу приложения WINHOOK, надо сделать его окно текущим, щелкнув по нему левой клавишей мыши, а затем нажать комбинацию клавиш <Alt + F4>.

Основной файл приложения WINHOOK приведен в листинге 3.9.

Листинг 3.9. Файл winhook/winhook.cpp

// ================================================
// Приложение WINHOOK
// Простейший руссификатор клавиатуры
// для Microsoft Windows
// Работает совместно с DLL-библиотекой kbhook.dll
// ================================================

#define STRICT
#include <windows.h>
#include <windowsx.h>
#include <dos.h>
#include <mem.h>

#include "kbhook.hpp"

// ----------------------------------------------------
// Прототипы функций
// ----------------------------------------------------

extern "C"
void WINAPI _export SetKbHook(HWND hwnd);

extern "C"
void WINAPI _export RemoveKbHook(void);

BOOL InitApp(HINSTANCE);
LRESULT CALLBACK _export WndProc(HWND, UINT, WPARAM, LPARAM);

// ----------------------------------------------------
// Глобальные переменные
// ----------------------------------------------------

char const szClassName[]   = "WINHOOKAppClass";
char const szWindowTitle[] = "WINHOOK Application";

TEXTMETRIC tm;
int cxChar, cyChar;
RECT rc;
static BOOL bCyrillic = FALSE;

// =====================================
// Функция WinMain
// =====================================
#pragma argsused

int PASCAL
WinMain(HINSTANCE hInstance,
        HINSTANCE hPrevInstance,
        LPSTR     lpszCmdLine,
        int       nCmdShow)
{
  MSG  msg;   // структура для работы с сообщениями
  HWND hwnd;  // идентификатор главного окна приложения

  // Можно запускать только одну копию приложения
  if(hPrevInstance)
    return 0;

  // Инициализация копии приложения
  if(!InitApp(hInstance))
      return FALSE;

  // Получаем координаты окна Desktop.
  // Это окно занимает всю поверхность экрана
  // и на нем расположены все остальные окна
  GetWindowRect(GetDesktopWindow(), &rc);

  // Создаем временное окно с толстой
  // рамкой для изменения размера, но без
  // заголовка и системного меню.
  // При создании окна указываем произвольные
  // размеры окна и произвольное расположение
  hwnd = CreateWindow(
    szClassName, szWindowTitle, 
    WS_POPUPWINDOW | WS_THICKFRAME,
    100, 100,
    100, 100,
    0, 0, hInstance, NULL); 

  if(!hwnd)
    return FALSE;

  // Передвигаем окно в правый нижний
  // угол экрана и делаем его самым
  // верхним, т. е. это окно будет
  // всегда находиться над другими окнами
  SetWindowPos(hwnd, HWND_TOPMOST,
    rc.right  - cxChar * 15,
    rc.bottom - cyChar * 3,
    cxChar * 10, cyChar * 2, 0);

  // Отображаем окно в новом месте
  ShowWindow(hwnd, nCmdShow);
  UpdateWindow(hwnd);

  // Запускаем цикл обработки сообщений
  while(GetMessage(&msg, 0, 0, 0))
  {
    DispatchMessage(&msg);
  }
  return msg.wParam;
}

// =====================================
// Функция InitApp
// =====================================

BOOL
InitApp(HINSTANCE hInstance)
{
  ATOM aWndClass; // атом для кода возврата
  WNDCLASS wc;    // структура для регистрации
                  // класса окна

  memset(&wc, 0, sizeof(wc));
  wc.style         = CS_HREDRAW | CS_VREDRAW;
  wc.lpfnWndProc   = (WNDPROC) WndProc;
  wc.cbClsExtra    = 0;
  wc.cbWndExtra    = 0;
  wc.hInstance     = hInstance;
  wc.hIcon   = LoadIcon(NULL, IDI_APPLICATION);
  wc.hCursor = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
  wc.lpszMenuName  = (LPSTR)NULL;
  wc.lpszClassName = (LPSTR)szClassName;

  aWndClass = RegisterClass(&wc);
  return (aWndClass != 0);
}

// =====================================
// Функция WndProc
// =====================================

LRESULT CALLBACK _export
WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
  HDC        hdc;
  PAINTSTRUCT ps;

  switch (msg)
  {
    case WM_CREATE:
    {
      // Устанавливаем перехватчики
      SetKbHook(hwnd);

      // Получаем контекст отображения
      hdc = GetDC(hwnd);

      // Выбираем шрифт с фиксированной шириной букв
      SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));

      // Заполняем структуру информацией
      // о метрике шрифта, выбранного в
      // контекст отображения
      GetTextMetrics(hdc, &tm);

      // Запоминаем значение ширины для
      // самого широкого символа
      cxChar = tm.tmMaxCharWidth;

      // Запоминаем значение высоты букв с
      // учетом межстрочного интервала
      cyChar = tm.tmHeight + tm.tmExternalLeading;

      ReleaseDC(hwnd, hdc);
      return 0;
    }

    // Для обеспечения возможности перемещения
    // окна, не имеющего заголовка, встраиваем
    // свой обработчик сообщения WM_NCHITTEST
    case WM_NCHITTEST:
    {
      long lRetVal;

      // Вызываем функцию DefWindowProc и проверяем
      // возвращаемое ей значение
      lRetVal = DefWindowProc(hwnd, msg, wParam, lParam);

      // Если курсор мыши находится на одном из
      // элементов толстой рамки, предназначенной
      // для изменения размера окна, возвращаем
      // неизмененное значение, полученное от
      // функции DefWindowProc
      if(lRetVal == HTLEFT        ||
         lRetVal == HTRIGHT       ||
         lRetVal == HTTOP         ||
         lRetVal == HTBOTTOM      ||
         lRetVal == HTBOTTOMRIGHT ||
         lRetVal == HTTOPRIGHT    ||
         lRetVal == HTTOPLEFT     ||
         lRetVal == HTBOTTOMLEFT)
      {
        return lRetVal;
      }

      // В противном случае возвращаем значение
      // HTCAPTION, которое соответствует
      // заголовку окна.
      else
      {
        return HTCAPTION;
      }
    }

    case WM_DESTROY:
    {
      // Перед завершением работы приложения
      // удаляем перехватчики
      RemoveKbHook();

      PostQuitMessage(0);
      return 0;
    }

    // Это сообщение приходит от DLL-библиотеки
    // при переключении раскладки клавиатуры
    case WM_KBHOOK:
    {
      // Получаем флаг раскладки
      bCyrillic = (BOOL)wParam;

      // Выдаем звуковой сигнал
      MessageBeep(0);

      // Перерисовываем окно приложения
      InvalidateRect(hwnd, NULL, FALSE);
      return 0;
    }

    case WM_PAINT:
    {
      BYTE   szBuf[10];
      RECT    rc;

      // Получаем контекст отображения
      hdc = BeginPaint(hwnd, &ps);

      // Выбираем шрифт с фиксированной шириной букв
      SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));

      // Получаем координаты и размер окна
      GetClientRect(hwnd, &rc);

      // В зависимости от состояния флага раскладки
      // клавиатуры выбираем надпись для
      // отображения в окне
      if(bCyrillic)
        lstrcpy(szBuf, (LPCSTR)"CYRILLIC");
      else
        lstrcpy(szBuf, (LPCSTR)"DEFAULT ");

      // Выводим надпись в центре окна
      DrawText(hdc, (LPSTR)szBuf, 8, &rc,
        DT_CENTER | DT_VCENTER | DT_NOCLIP | DT_SINGLELINE);

      EndPaint(hwnd, &ps);
    }
  }
  return DefWindowProc(hwnd, msg, wParam, lParam);
}

После запуска приложения функция WinMain определяет размеры окна DeskTop, которые равны размеру экрана, и создает главное окно приложения в виде временного окна с толстой рамкой для изменения размера без заголовка и системного меню (такое окно имеет стиль WS_POPUPWINDOW | WS_THICKFRAME).

Для того чтобы сделать окно "непотопляемым", а заодно и изменить его размеры, мы вызываем функцию SetWindowPos , передав ей в качестве второго параметра константу HWND_TOPMOST:

SetWindowPos(hwnd, HWND_TOPMOST,
   rc.right  - cxChar * 15,
   rc.bottom - cyChar * 3,
   cxChar * 10, cyChar * 2, 0);

Функция SetWindowPos позволяет изменить размеры и расположение окна относительно экрана и относительно других окон:

BOOL WINAPI SetWindowPos(
  HWND hwnd,            // идентификатор окна
  HWND hwndInsertAfter, // расположение окна
  int x,                // горизонтальное положение 
  int y,                // вертикальное положение
  int cx,               // ширина
  int cy,               // высота
  UINT fuFlags);        // флаги расположения окна

Для параметра hwndInsertAfter, определяющего расположение окна относительно других окон, можно использовать следующие значения:

Значение Описание
HWND_BOTTOM Окно следует расположить под другими окнами
HWND_TOP Окно будет расположено над другими окнами
HWND_TOPMOST Окно следует расположить над всеми другими окнами, имеющими расположение HWND_TOPMOST
HWND_NOTOPMOST Окно будет расположено над всеми HWND_TOP-окнами, но под окном, имеющим расположение HWND_TOPMOST

Параметры x, y, cx и cy определяют, соответственно, горизонтальное и вертикальное расположение окна, его ширину и высоту.

Параметр fuFlags может принимать следующие значения:

Значение Описание
SWP_DRAWFRAME Следует нарисовать рамку, определенную в классе окна
SWP_HIDEWINDOW Окно будет скрыто
SWP_NOACTIVATE Окно не будет активизировано
SWP_NOMOVE Окно не будет перемещаться, при указании этого флага параметры x и y игнорируются
SWP_NOSIZE Окно не будет изменять свои размеры, параметры cx и cy игнорируются
SWP_NOREDRAW Не следует выполнять перерисовку окна. После перемещения приложение должно перерисовать окно самостоятельно
SWP_NOZORDER Не следует изменять расположение окна относительно других окон, параметр hwndInsertAfter игнорируется
SWP_SHOWWINDOW Отобразить окно

После перемещения и изменения расположения главного окна функция WinMain приложения WINHOOK отображает окно и запускает цикл обработки сообщений.

Функция главного окна приложения во время обработки сообщения WM_CREATE вызывает функцию SetKbHook, которая определена в созданной нами для этого приложения DLL-библиотеке kbhook.dll (исходные тексты этой библиотеки будут приведены ниже). Функция SetKbHook устанавливает два фильтра - типа WH_KEYBOARD и WH_GETMESSAGE. В качестве параметра этой функции передается идентификатор главного окна приложения WINHOOK. Когда пользователь переключает раскладку клавиатуры, DLL-библиотека, пользуясь этим идентификатором, пришлет в функцию окна приложения WINHOOK сообщение с кодом WM_KBHOOK.

Затем обработчик сообщения WM_CREATE получает контекст отображения и определяет метрику системного шрифта с фиксированной шириной букв, который будет использоваться для отображения используемой раскладки клавиатуры в главном окне приложения.

В функции главного окна предусмотрен обработчик сообщения WM_NCHITTEST, позволяющий перемещать окно, не имеющее заголовка. Мы уже использовали аналогичный прием в приложении TMCLOCK, описанном в 11 томе "Библиотеки системного программиста".

Перед завершением работы приложение WINHOOK удаляет установленные ранее фильтры, для чего при обработке сообщения WM_DESTROY вызывается функция RemoveKbHook, определенная в DLL-библиотеке kbhook.dll.

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

После определения раскладки обработчик сообщения WM_KBHOOK выдает звуковой сигнал и перерисовывает главное окно приложения, вызывая функцию InvalidateRect.

Перерисовку окна выполняет обработчик сообщения WM_PAINT. С помощью функции DrawText он пишет название раскладки клавиатуры (DEFAULT или CYRILLIC) в центре главного окна приложения.

Сообщение WM_KBHOOK определено в include-файле winhook.hpp (листинг 3.10).

Листинг 3.10. Файл winhook/winhook.hpp

#define WM_KBHOOK (WM_USER + 1000)

Код этого сообщения получается при помощи сложения константы WM_USER и числа 1000 (вы можете выбрать другое число в диапазоне от 0 до 0x7fff).

Константа WM_USER специально предназначена для определения приложениями своих собственных кодов сообщений. Если приложение определит свое собственное сообщение и его код будет находиться в пределах от WM_USER до WM_USER + 0x7fff, код такого сообщения не будет конфликтовать с кодами сообщений операционной системы Windows.

Файл определения модуля приложения WINHOOK приведен в листинге 3.11.

Листинг 3.11. Файл winhook/winhook.def

; =============================
; Файл определения модуля
; =============================
NAME        WINHOOK
DESCRIPTION 'Приложение WINHOOK, (C) 1994, Frolov A.V.'
EXETYPE     windows
STUB        'winstub.exe'
STACKSIZE   8120
HEAPSIZE    1024
CODE        preload moveable discardable
DATA        preload moveable multiple

Теперь займемся DLL-библиотекой kbhook.dll, предназначенной для совместной работы с приложением WINHOOK. Исходный текст этой библиотеки представлен в листинге 3.12.

Листинг 3.12. Файл winhook/kbhook.cpp

// ================================================
// DLL-библиотека kbhook.dll
// Устанавливает перехватчики на сообщения,
// поступающие от клавиатуры и на системную
// очередь сообщений.
// Если нажать подряд 3 раза клавишу <Control>,
// изменится раскладка клавиатуры
// ================================================

#define  STRICT
#include <windows.h>
#include "kbhook.hpp"

// -----------------------------------------------
// Глобальные переменные
// -----------------------------------------------

// Идентификатор модуля DLL-библиотеки
static HINSTANCE hInst;

// Идентификатор окна приложения, установившего
// перехватчики
static HWND hwndClient;

// Идентификаторы перехватчиков
static HHOOK hhook = 0;
static HHOOK hhookMsg = 0;

// Флаг переключения на русскую клавиатуру
static BOOL bCyrillic = FALSE;

// Флаг установки перехватчиков
static BOOL bHooked = FALSE;

// Счетчик 
static int nHotKeyCount = 0;

// Массив для записи состояния клавиатуры
BYTE aKeyStates[256];

// Указатели на таблицы перекодировки,
// которые будут загружены из ресурсов
static char far * lpXlatTable;
static char far * lpXlatTableCaps;

// Положение ресурсов в файле
static HRSRC   hResource;
static HRSRC   hResourceCaps;

// Идентификаторы таблиц перекодировки
static HGLOBAL hXlatTable;
static HGLOBAL hXlatTableCaps;

// -----------------------------------------------
// Прототипы функций
// -----------------------------------------------

extern "C"
LRESULT CALLBACK KbHookProc(int code,
  WPARAM wParam, LPARAM lParam);

extern "C"
LRESULT CALLBACK MsgHookProc(int code,
  WPARAM wParam, LPARAM lParam);

// ========================================================
// Функция LibMain
// Получает управление только один раз при
// загрузке DLL-библиотеки в память
// ========================================================

#pragma argsused
int CALLBACK
LibMain(HINSTANCE hInstance,
                  WORD wDataSegment,
                  WORD wHeapSize,
                  LPSTR lpszCmdLine)
{
  // После инициализации локальной области данных
  // функция LibEntry фиксирует сегмент данных.
  // Его необходимо расфиксировать.
  if(wHeapSize != 0)
    // Расфиксируем сегмент данных
    UnlockData(0);

  // Запоминаем идентификатор модуля DLL-библиотеки
  hInst = hInstance;

  // Определяем расположение ресурсов
  hResource   = FindResource(hInstance, "XlatTable", "XLAT");
  hResourceCaps = 
    FindResource(hInstance, "XlatTableCaps", "XLAT");

  // Получаем идентификаторы ресурсов
  hXlatTable  = LoadResource(hInstance, hResource);
  hXlatTableCaps  = LoadResource(hInstance, hResourceCaps);

  // Фиксируем ресурсы в памяти, получая их адрес
  lpXlatTable = (char far *)LockResource(hXlatTable);
  lpXlatTableCaps = (char far *)LockResource(hXlatTableCaps);

  // Если адрес равен NULL, при загрузке или
  // фиксации одного из ресурсов произошла ошибка
  if(lpXlatTable == NULL || lpXlatTableCaps == NULL)
  {
    return(0);
  }

  // Выключаем клавишу <Caps Lock>. Для этого
  // получаем и записываем в массив  aKeyStates состояние
  // клавиатуры, затем изменяем состояние нужной нам
  // клавиши, используя ее виртуальный код как индекс
  GetKeyboardState(aKeyStates);
  aKeyStates[VK_CAPITAL] = 0;
  SetKeyboardState(aKeyStates);

  // Возвращаем 1. Это означает, что инициализация
  // DLL-библиотеки выполнена успешно
  return 1;
}

// ========================================================
// Функция WEP
// Получает управление только один раз при
// удалении DLL-библиотеки из памяти
// ========================================================

#pragma argsused
int CALLBACK
WEP(int bSystemExit)
{
  // Расфиксируем и освобождаем ресурсы
  UnlockResource(hXlatTable);
  FreeResource(hXlatTable);
  UnlockResource(hXlatTableCaps);
  FreeResource(hXlatTableCaps);

  return 1;
}

// ========================================================
// Функция SetKbHook
// Устанавливает системные перехватчики на сообщения,
// поступающие от клавиатуры, и на системную
// очередь сообщений
// ========================================================

extern "C"
void WINAPI _export SetKbHook(HWND hwnd)
{
  // Устанавливаем перехватчики только
  // в том случае, если они не были
  // установлены ранее 
  if(!bHooked)
  {
    // Установка перехватчика на сообщения,
    // поступающие от клавиатуры
    hhook = SetWindowsHookEx(WH_KEYBOARD,
      (HOOKPROC)KbHookProc,
      hInst, NULL); 

    // Установка перехватчика на системную
    // очередь сообщений
    hhookMsg = SetWindowsHookEx(WH_GETMESSAGE,
      (HOOKPROC)MsgHookProc,
      hInst, NULL);

    // Включаем флаг установки перехватчиков
    bHooked = TRUE;

    // Сохраняем идентификатор окна приложения,
    // установившего перехватчики
    hwndClient = hwnd;
  }
}

// ========================================================
// Функция RemoveKbHook
// Удаляет системные перехватчики
// ========================================================

extern "C"
void WINAPI _export RemoveKbHook(void)
{
  // Если перехватчики были установлены,
  // удаляем их
  if(bHooked)
  {
    UnhookWindowsHookEx(hhookMsg);
    UnhookWindowsHookEx(hhook);
  }
}

// ========================================================
// Функция KbHookProc
// Перехватчик сообщений от клавиатуры
// ========================================================

extern "C"
LRESULT CALLBACK KbHookProc(int code,
  WPARAM wParam, LPARAM lParam)
{
  // Проверка флага обработки сообщений. Если содержимое
  // параметра code меньше нуля, передаем сообщение
  // функции CallNextHookEx без изменений
  if(code < 0)
  {
    CallNextHookEx(hhook, code, wParam, lParam);
    return 0;
  }

  // Если пришло сообщение от клавиши <Control>,
  // проверяем, была ли эта клавиша нажата
  // три раза подряд
  if(wParam  == VK_CONTROL)
  { 
    if(!(HIWORD(lParam) & 0x8000))
    {
      nHotKeyCount++;

      // Если клавиша <Control> была нажата три
      // раза подряд, инвертируем флаг bCyrillic и посылаем
      // сообщение приложению, использующему
      // данную DLL-библиотеку
      if(nHotKeyCount == 3)
      {
        nHotKeyCount = 0;
        bCyrillic = ~bCyrillic;

        // Посылаем сообщение приложению, установившему
        // перехватчики. В качестве параметра wParam
        // сообщения передаем значение флага bCyrillic
        PostMessage(hwndClient, WM_KBHOOK,
          (WPARAM)bCyrillic, 0L);
      }
    }
  }

  // Если после клавиши <Control> была нажата любая
  // другая клавиша, сбрасываем счетчик
  else
  {
    nHotKeyCount = 0;
  }

  // Вызываем следующий в цепочке перехватчик
  return CallNextHookEx(hhook, code, wParam, lParam);
}

// ========================================================
// Функция MsgHookProc
// Перехватчик для системной очереди сообщений
// ========================================================

extern "C"
LRESULT CALLBACK MsgHookProc(int code,
  WPARAM wParam, LPARAM lParam)
{
  LPMSG lpmsg;
  WPARAM wMsgParam;

  // Проверка флага обработки сообщений. Если содержимое
  // параметра code меньше нуля, передаем сообщение
  // функции CallNextHookEx без изменений
  if(code < 0)
  {
    CallNextHookEx(hhook, code, wParam, lParam);
    return 0;
  }

  // Получаем указатель на структуру MSG,
  // в которой находится перехваченное сообщение
  lpmsg = (LPMSG)lParam;

  // Запоминаем виртуальный код клавиши
  wMsgParam = lpmsg->wParam;

  // Сбрасываем флаг замены сообщения
  // WM_KEYDOWN на сообщение WM_CHAR
  BOOL bChange = FALSE;

  // Если перехвачено сообщение WM_KEYDOWN,
  // проверяем код виртуальной клавиши и при
  // необходимости выполняем замену сообщения
  if(lpmsg->message == WM_KEYDOWN)
  {
    // Замена выполняется только в режиме
    // русской клавиатуры
    if(bCyrillic)
    {
      // Если нажата клавиша, соответствующая
      // русской букве, включаем флаг bChange
      switch(wMsgParam)
      {
        // Проверяем "особые" буквы
        case 0xdb: // "Х"
        case 0xdd: // "Ъ"
        case 0xba: // "Ж"
        case 0xde: // "Э"
        case 0xbc: // "Б"
        case 0xbe: // "Ю"
        case 0xbf: // "Ў"
        {
          bChange = TRUE;
          break;
        }

        // Проверяем остальные буквы
        default:
        {
          if((lpmsg->wParam <= 0x5d && lpmsg->wParam > 0x2f))
            bChange = TRUE;
        }
      }

      // Если нажата клавиша, соответствующая русской
      // букве, выполняем замену сообщения WM_KEYDOWN на
      // сообщение WM_CHAR
      if(bChange)
      {
        // Делаем замену кода сообщения
        lpmsg->message = WM_CHAR;

        // Необходимо учитывать состояние клавиш
        // <Caps Lock> и <Shift>
        if(GetKeyState(VK_CAPITAL) & 0x1)
        {
          if(GetKeyState(VK_SHIFT) & 0x8000)
          {
            // Перекодировка по таблице строчных букв
            lpmsg->wParam =
              lpXlatTable[(lpmsg->wParam) & 0xff];
          }
          else
          {
            // Перекодировка по таблице прописных букв
            lpmsg->wParam =
              lpXlatTableCaps[(lpmsg->wParam) & 0xff];
          }
        }
        else
        {
          if(GetKeyState(VK_SHIFT) & 0x8000)
          {
            // Перекодировка по таблице прописных букв
            lpmsg->wParam = 
              lpXlatTableCaps[(lpmsg->wParam) & 0xff];
          }
          else
          {
            // Перекодировка по таблице строчных букв
            lpmsg->wParam = 
              lpXlatTable[(lpmsg->wParam) & 0xff];
          }
        }

        // Сбрасываем флаг замены
        bChange = FALSE;
      }
    }
  }

  // Передаем управление следующему в цепочке
  // перехватчику сообщений
  CallNextHookEx(hhook, code, wParam, lParam);

  return 0;
}

Функция LibMain, выполняющая инициализацию DLL-библиотеки, после инициализации сегмента данных, запоминает идентификатор модуля DLL-библиотеки, переданный ей через параметр hInstance, в глобальной переменной hinst.

Затем эта функция находит, загружает в память и фиксирует две таблицы, необходимые для использования дополнительной раскладки клавиатуры. Первая таблица загружается из ресурса "XlatTable". Она содержит таблицу перекодировки кодов виртуальных клавиш в ANSI-коды русских строчных букв. Вторая таблица загружается из ресурса "XlatTableCaps" и содержит таблицу перекодировки кодов виртуальных клавиш в ANSI-коды русских прописных букв.

После загрузки и фиксирования адреса таблиц записываются в глобальные переменные с именами lpXlatTable и lpXlatTableCaps.

Так как перекодировка кодов виртуальных клавиш в ANSI-коды должна выполняться с учетом состояния клавиши <Caps Lock>, для упрощения перекодировки функция LibMain устанавливает эту клавишу в выключенное состояние, пользуясь функциями GetKeyboardState и SetKeyboardState :

GetKeyboardState(aKeyStates);
aKeyStates[VK_CAPITAL] = 0;
SetKeyboardState(aKeyStates);

Указанные функции, а также использованный способ изменения состояния клавиш был описан в 11 томе "Библиотеки системного программиста".

Функция WEP выполняет расфиксирование и освобождение ресурсов, вызывая функции UnlockResource и FreeResource.

Для установки фильтров в нашей DLL-библиотеки определена функция SetKbHook:

extern "C"
void WINAPI _export SetKbHook(HWND hwnd)
{
  if(!bHooked)
  {
    hhook = SetWindowsHookEx(WH_KEYBOARD,
      (HOOKPROC)KbHookProc,
      hInst, NULL); 
    hhookMsg = SetWindowsHookEx(WH_GETMESSAGE,
      (HOOKPROC)MsgHookProc,
      hInst, NULL);
    bHooked = TRUE;
    hwndClient = hwnd;
  }

При определении этой функции мы использовали ключевые слова extern "C", в результате чего транслятор C++ не выполняет преобразование имени функции в соответствии с типом ее параметров и типом возвращаемого значения.

Перед установкой фильтров функция проверяет содержимое глобального флага bHooked. Этот флаг изначально находится в сброшенном состоянии и устанавливается только после встраивания фильтров. Проверка флага позволяет исключить ошибочное повторное встраивание фильтров.

Функция SetKbHook устанавливает два фильтра - типа WH_KEYBOARD и типа WH_GETMESSAGE. Оба фильтра встраиваются для всей системы в целом, так как последний параметр функции SetWindowsHookEx указан как NULL.

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

Для удаления фильтров в DLL-библиотеке определена функция RemoveKbHook:

extern "C"
void WINAPI _export RemoveKbHook(void)
{
  if(bHooked)
  {
    UnhookWindowsHookEx(hhookMsg);
    UnhookWindowsHookEx(hhook);
  }
}

Эта функция проверяет состояние флага bHooked, и, если фильтры были установлены, удаляет их, вызывая для каждого фильтра функцию UnhookWindowsHookEx.

Функция фильтра типа WH_KEYBOARD пропускает через себя все клавиатурные сообщения, определяя момент, когда пользователь нажмет подряд три раза клавишу <Control>.

Как только это произошло, функция фильтра инвертирует флаг bCyrillic и посылает приложению WINHOOK сообщение WM_KBHOOK. В качестве параметра wParam посылаемого сообщения используется значение флага bCyrillic, что позволяет определить приложению номер используемой раскладки клавиатуры:

PostMessage(hwndClient, WM_KBHOOK, (WPARAM)bCyrillic, 0L);

Фильтр типа WH_MSGFILTER расположен в функции MsgHookProc.

Если перехвачено сообщение WM_KEYDOWN, фильтр проверяет номер используемой раскладки клавиатуры. Если используется стандартная раскладка клавиатуры, сообщение "пропускается" через фильтр без изменений. Для дополнительной раскладки клавиатуры выполняется проверка кода виртуальной клавиши.

Если этот код соответствует клавишам русских букв или цифр в верхнем ряду клавиатуры, фильтр преобразует сообщение WM_KEYDOWN в сообщение WM_CHAR, пользуясь таблицами перекодировок, загруженным из ресурсов DLL-библиотеки. При этом учитывается состояние клавиш <Shift> и <Caps Lock>. Для определения состояния переключающих клавиш мы использовали функцию GetKeyState.

Таблицы перекодировки для прописных и строчных букв описаны в файле ресурсов DLL-библиотеки (листинг 3.13).

Листинг 3.13. Файл winhook/kbhook.rc

/* Таблица перекодировки */

XlatTableCaps XLAT 
BEGIN
'00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F'
'10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F'
'20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F'
'25 21 2D 2F 22 3A 2C 2E AD 3F 3A 3B 3C 3D 3E 3F'
'40 D4 C8 D1 C2 D3 C0 CF D0 D8 CE CB C4 DC D2 D9'
'C7 C9 CA DB C5 C3 CC D6 D7 CD DF 5B 5C 5D 5E 5F'
'60 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F'
'70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F'
'80 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F'
'90 91 92 93 94 95 96 97 98 99 9A 9B 9C 9D 9E 9F'
'A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF'
'B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 C6 BB C1 BD DE A1'
'C0 C1 C2 C3 C4 C5 C6 C7 C8 C9 CA CB CC CD CE CF'
'D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 DA D5 DC DA DD DF'
'E0 E1 E2 E3 E4 E5 E6 E7 E8 E9 EA EB EC ED EE EF'
'F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF'

END

XlatTable XLAT
BEGIN
'00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F'
'10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F'
'20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F'
'30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F'
'40 F4 E8 F1 E2 F3 E0 EF F0 F8 EE EB E4 FC F2 F9'
'E7 E9 EA FB E5 E3 EC F6 F7 ED FF 5B 5C 5D 5E 5F'
'60 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F'
'70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F'
'80 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F'
'90 91 92 93 94 95 96 97 98 99 9A 9B 9C 9D 9E 9F'
'A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF'
'B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 E6 BB E1 BD FE BF'
'C0 C1 C2 C3 C4 C5 C6 C7 C8 C9 CA CB CC CD CE CF'
'D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 DA F5 DC FA FD DF'
'E0 E1 E2 E3 E4 E5 E6 E7 E8 E9 EA EB EC ED EE EF'
'F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF'

END

Файл определения модуля DLL-библиотеки приведен в листинге 3.14.

Листинг 3.14. Файл winhook/kbhook.def

; =============================
; Файл определения модуля
; =============================
LIBRARY        KBHOOK
DESCRIPTION    'DLL-библиотека KBHOOK, (C) 1994, Frolov A.V.'
EXETYPE        windows
CODE           preload fixed
DATA           preload moveable single
HEAPSIZE       1024
EXPORTS
  SetKbHook    @10
  RemoveKbHook @11
  KbHookProc   @12
  MsgHookProc  @13

С помощью этого файла экспортируются функции SetKbHook и RemoveKbHook, предназначенные, соответственно, для установки и удаления фильтров, а также функции фильтров KbHookProc и MsgHookProc.