Электронная библиотека книг Александра Фролова и Григория Фролова.
Shop2You.ru Создайте свой интернет-магазин
Библиотека
Братьев
Фроловых
[Назад] [Содеожание] [Дальше]

Программирование для Windows NT

© Александр Фролов, Григорий Фролов
Том 27, часть 2, М.: Диалог-МИФИ, 1996, 272 стр.

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

  • Статическая и динамическая компоновка
  • DLL-библиотеки в операционной системе Windows NT
  • Как работает DLL-библиотека
  • Исходные тексты DLL-библиотеки DLLDEMO
  • Приложение DLLCALL
  • Библиотеки динамической компоновки DLL(Dynamic Link Libraries) являются стержневым компонентом операционной системы Windows NT и многих приложений Windows. Без преувеличения можно сказать, что вся операционная система Windows, все ее драйверы, а также другие расширения есть ни что иное, как набор библиотек динамической компоновки. Редкое крупное приложение Windows не имеет собственных библиотек динамической компоновки, и ни одно приложение не может обойтись без вызова функций, расположенных в таких библиотеках. В частности, все функции программного интерфейса Windows NT находятся именно в библиотеках динамической компоновки DLL.

    В 13 томе “Библиотеки системного программиста”, который называется “Операционная система Microsoft Windows 3.1 для программиста. Часть третья” мы уже рассказывали о создании и использовании библиотек DLL в среде операционной системы Microsoft Windows версии 3.1. Что же касается операционных систем Microsoft Windows NT и Microsoft Windows 95, то в них библиотеки DLL создаются и работают по-другому.

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

    Прежде чем приступить к изучению особенностей использования библиотек динамической компоновки в операционной системе Microsoft Windows NT, напомним кратко, чем отличаются друг от друга статическая и динамическая компоновка.

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

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

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

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

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

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

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

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

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

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

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

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

    В операционной системе Microsoft Windows версии 3.1 после загрузки DLL-библиотека становилась как бы частью операционной системы. DLL-библиотека является модулем и находится в памяти в единственном экземпляре, содержит сегменты кода и ресурсы, а так же один сегмент данных (рис. 3.3). Можно сказать, что для DLL-библиотеки создается одна копия (instance), состоящая только из сегмента данных, и один модуль, состоящий из кода и ресурсов.

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

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

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

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

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

    Здесь мы обращаем ваше внимание на первое отличие механизма динамической компоновки в среде Microsoft Windows NT от аналогичного механизма для Microsoft Windows версии 3.1.

    Отображение страниц DLL-библиотеки

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

    На рис. 3.4 схематически показано отображение кода и данных DLL-библиотеки в адресные пространства двух приложений.

    Рис. 3.4. Отображение DLL-библиотеки в адресные пространства двух процессов

    На этом рисунке показано, что в глобальном пуле памяти находится один экземпляр кода DLL-библиотеки, который отображается в адресные пространства приложений (процессов). Что же касается данных DLL-библиотеки, то для каждого приложения в глобальном пуле создается отдельная область. Таким образом, различные приложения не могут в этом случае передавать друг другу данные через общую область данных DLL-библиотеки, как это можно было делать в среде операционной системы Microsoft Windows версии 3.1.

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

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

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

    Кроме того, для каждой DLL‑библиотеки система ведет счетчик использования (usage count). Содержимое этого счетчика увеличивается при очередной загрузке библиотеки в память и уменьшается при освобождении библиотеки.

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

    Обмен данными между приложениями через DLL-библиотеку

    В среде операционной системы Microsoft Windows версии 3.1 существовала возможность обмена данными между различными работающими параллельно приложениями через область локальной кучи (local heap) DLL-библиотеки. Эта область находилась в сегменте данных DLL-библиотеки и потому была доступна всем приложениям.

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

    Приложение также может сделать попытку изменить содержимое статической или глобальной переменной, с тем чтобы другие приложения могли прочитать новое значение. Однако этот способ передачи данных между приложениями не будет работать. Попытка изменения данных будет зафиксирована операционной сиситемой, которая создаст для этого приложения копию страницы памяти, в которой находится изменившиеся данные, с использованием механизма копирования при записи (copy-on-write). Мы рассказывали об этом механизме в предыдущем томе “Библиотеки системного программиста”.

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

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

    
    #pragma data_seg (“.shar”)
    

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

    
    SECTIONS .shar READ WRITE SHARED
    

    Можно также указать ключ редактору связей:

    
    -SECTION:.shar,RWS
    

    Строка RWS определяет атрибуты секции: R - READ, W - WRITE, S - SHARED.

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

    Как работает DLL-библиотека

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

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

    Функция LibEntry должна быть дальней функцией, составленной на языке ассемблера, так как она получает параметры через регистры процессора. Мы подробно описали функцию LibEntry и ее параметры в 13 томе “Библиотеки системного программиста”. Заметим, что использование языка ассемблера затрудняет создание мультиплатформных приложенй, поэтому в мультиплатформной операционной системе Microsoft Windows NT используется другой способ инициализации.

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

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

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

    Прототип функции LibMain и ее параметры были описаны в 13 томе “Библиотеки системного программиста”.

    Другая функция, которая присутствует в каждой 16-разрядной DLL-библиотеке, это функция WEP.

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

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

    Что же касается 32-разрядных DLL-библиотек операционной системы Microsoft Windows NT, то их инициализация и выгрузка из памяти происходит иначе.

    Инициализация DLL-библиотеки в среде Microsoft Windows NT

    В 32-разрядных DLL-библиотеках операционной системы Microsoft Windows NT вместо функций LibMain и WEP используется одна функция DLLEntryPoint, которая выполняет все необходимые задачи по инициализации библиотеки и при необходимости освобождает заказанные ранее ресурсы (имя функции инициализации может быть любым).

    Функции LibMain и WEP вызываются только один раз при загрузке библиотеки в память и при ее выгрузке. В отличие от них, функция DLLEntryPoint вызывается всякий раз, когда выполняется инициализация процесса или задачи, обращающихся к функциям библиотеки, а также при явной загрузке и выгрузке библиотеки функциями LoadLibrary XE "LoadLibrary" и FreeLibrary XE "FreeLibrary" .

    Ниже мы привели прототип функции DLLEntryPoint:

    
    BOOL WINAPI DllEntryPoint(
      HINSTANCE hinstDLL,  // идентификатор модуля DLL-библиотеки 
      DWORD     fdwReason, // код причины вызова функции 
      LPVOID    lpvReserved); // зарезервировано 
    

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

    Что же касается параметра fdwReason, то он зависит от причины, по которой произошел вызов функции DLLEntryPoint. Этот параметр может принимать следующие значения:

     Значение

     Описание

     DLL_PROCESS_ATTACH

     Библиотека отображается в адресное пространство процесса в результате запуска процесса или вызова функции LoadLibrary

     DLL_THREAD_ATTACH

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

     DLL_THREAD_DETACH

     Этот код причины передается функции DLLEntryPoint, когда задача завершает свою работу нормальным (не аварийным) способом

     DLL_PROCESS_DETACH

     Отображение DLL‑библиотеки в адресное пространство отменяется в результате нормального завершения процесса или вызова функции FreeLibrary

    Параметр lpvReserved зарезервирован. В SDK, тем не менее, сказано, что значение параметра lpvReserved равно NULL во всех случаях, кроме двух следующих:

    ·       когда параметр fdwReason равен DLL_PROCESS_ATTACH и используется статическая загрузка DLL-библиотеки;

    ·       когда параметр fdwReason равен DLL_PROCESS_DETACH и функция DLLEntryPoint вызвана в результате завершения процесса, а не вызова функции FreeLibrary

    В процессе инициализации функция DLLEntryPoint может отменить загрузку DLL-библиотеки. Если код причины вызова равен DLL_PROCESS_ATTACH, функция DLLEntryPoint отменяет загрузку библиотеки, возвращая значение FALSE. Если же инициализация выполнена успешно, функция должна возвратить значение TRUE.

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

    Приведем пример функции инициализации DLL-библиотеки:

    
    BOOL WINAPI DLLEntryPoint(
      HMODULE hModule,    // идентификатор модуля
      DWORD   fdwReason,  // причина вызова функции DLLEntryPoint
      LPVOID  lpvReserved)// зарезервировано
    {
      switch(fdwReason)
      {
        // Подключение нового процесса
        case DLL_PROCESS_ATTACH:
        {
          // Обработка подключения процесса
          . . .
          break;
        }
    
        // Подключение новой задачи
        case DLL_THREAD_ATTACH:
        {
          // Обработка подключения новой задачи
          . . .
          break;
        }
    
        // Отключение процесса
        case DLL_PROCESS_DETACH:
        {
           // Обработка отключения процесса
           . . .
           break;
        }
    
        // Отключение задачи
        case DLL_THREAD_DETACH:
        {
          // Обработка отключения задачи
          . . .
          break;
        }
      }
      return TRUE;
    }
    

    Экспортирование функций и глобальных переменных

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

    Кроме функций LibMain и WEP в 16-разрядных DLL-библиотеках операционной системы Microsoft Windows версии 3.1 и функции DLLEntryPoint в 32-разрядных библиотеках операционных систем Microsoft Windows NT и Microsoft Windows 95 могут быть определены экспортируемые и неэкспортируемые функции.

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

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

    Самый простой способ сделать функцию экспортируемой - перечислить все экспортируемые функции в файле определения модуля при помощи оператора EXPORTSXE:

    
    EXPORTS
      ИмяТочкиВхода [=ВнутрИмя] [@Номер] [NONAME] [CONSTANT]
        . . .
    

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

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

    С помощью параметра @Номер вы можете задать порядковый номер экспортируемой функции.

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

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

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

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

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

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

    
    EXPORTS
      DrawBitmap=MyDraw  @4
      ShowAll     
      HideAll
      MyPoolPtr   @5 CONSTANT
      GetMyPool   @8 NONAME
      FreeMyPool  @9 NONAME
    

    В приведенном выше примере в разделе EXPORTS перечислены имена нескольких экспортируемых функций DrawBitmap, ShowAll, HideAll, GetMyPool, FreeMyPool и глобальной переменной MyPoolPtr.

    Функция MyDraw, определенная в DLL-библиотеке, экспортируется под именем DrawBitmap. Она также доступна под номером 4.

    Функции ShowAll и HideAll экспортируются под своими “настоящими” именами, с которыми они определены в DLL-библиотеке. Для них не заданы порядковые номера.

    Функции GetMyPool и FreeMyPool экспортируются с флагом NONAME, поэтому к ним можно обращаться только по их порядковым номерам, которые равны, соответственно, 8 и 9.

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

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

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

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

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

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

    Для того чтобы редактор связей мог создать ссылку, в файл проекта приложения вы должны включить так называемую библиотеку импорта (import library). Эта библиотека создается автоматически системой разработки Microsoft Visual C++.

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

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

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

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

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

    
    HINSTANCE WINAPI LoadLibrary(LPCSTR lpszLibFileName);
    

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

    ·       каталог, из которого запущено приложение;

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

    ·       32-разрядный системный каталог Microsoft Windows NT;

    ·       16-разрядный системный каталог;

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

    ·       каталоги, перечисленные в переменной описания среды PATH

    Если файл DLL-библиотеки найден, функция LoadLibrary возвращает идентификатор модуля библиотеки. В противном случае возвращается значение NULL. При этом код ошибки можно получить при помощи функции GetLastError.

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

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

    
    typedef HWND (WINAPI *MYDLLPROC)(LPSTR);
    MYDLLPROC    GetAppWindow;
    HANDLE       hDLL;
    
    hDLL = LoadLibrary("DLLDEMO.DLL");
    if(hDLL != NULL)
    {
      GetAppWindow = (MYDLLPROC)GetProcAddress(hDLL, 
        "FindApplicationWindow");
      if(GetAppWindow != NULL)
      {
        if(GetAppWindow(szWindowTitle) != NULL)
          MessageBox(NULL, "Application window was found",
            szAppTitle, MB_OK | MB_ICONINFORMATION);
        else
          MessageBox(NULL, "Application window was not found",
            szAppTitle, MB_OK | MB_ICONINFORMATION);
      }
      FreeLibrary(hDLL);
    }
    

    Здесь вначале с помощью функции LoadLibrary выполняется попытка загрузки DLL-библиотеки DLLDEMO.DLL. В случае успеха приложение получает адрес точки входа для функции с именем FindApplicationWindow, для чего используется функция GetProcAddress XE "GetProcAddress" . Этой функцией мы займемся немного позже.

    Если точка входа получена, функция вызывается через указатель GetAppWindow.

    После использования DLL-библиотека освобождается при помощи функции FreeLibrary, прототип который показан ниже:

    
    void WINAPI FreeLibrary(HINSTANCE hLibrary);
    

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

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

    Каждый раз при освобождении DLL-библиотеки вызывается функция DLLEntryPoint с параметрами DLL_PROCESS_DETACH или DLL_THREAD_DETACH, выполняющая все необходимые завершающие действия.

    Теперь о функции GetProcAddress.

    Для того чтобы вызвать функцию из библиотеки, зная ее идентификатор, необходимо получить значение дальнего указателя на эту функцию, вызвав функцию GetProcAddress:

    
    FARPROC WINAPI GetProcAddress(HINSTANCE hLibrary, 
      LPCSTR lpszProcName);
    

    Через параметр hLibrary вы должны передать функции идентификатор DLL-библиотеки, полученный ранее от функции LoadLibrary.

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

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

    
    FARPROC lpMsg;
    FARPROC lpTellMe;
    lpMsg = GetProcAddress(hLib, "Msg");
    lpTellMe = GetProcAddress(hLib, MAKEINTRESOURCE(8));
    

    Перед тем как передать управление функции по полученному адресу, следует убедиться в том, что этот адрес не равен NULL:

    
    if(lpMsg != (FARPROC)NULL)
    {
      (*lpMsg)((LPSTR)"My message");
    }
    

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

    
    typedef int (PASCAL *LPGETZ)(int x, int y);
    LPGETZ lpGetZ;
    lpGetZ = (LPGETZ)GetProcAddress(hLib, "GetZ");
    

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

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

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

    
    UINT nPrevErrorMode;
    nPrevErrorMode = SetErrorMode(SEM_FAILCRITICALERRORS);
    hDLL = LoadLibrary("DLLDEMO.DLL");
    if(hDLL != NULL)
    {
      // Работа с DLL-библиотекой
      . . .
    }
    SetErrorMode(nPrevErrorMode);
    

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

    
    UINT WINAPI SetErrorMode(UINT fuErrorMode);
    

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

     Значение

     Описание

     SEM_FAILCRITICALERRORS

     Операционная система Microsoft Windows NT не выводит на экран сообщения обработчика критических ошибок, возвращая приложению соответствующий код ошибки

     SEM_NOGPFAULTERRORBOX

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

     SEM_NOOPENFILEERRORBOX

     Если Microsoft Windows NT не может открыть файл, на экран не выводится диалоговая панель с сообщением об ошибке

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

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

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

    
    LIBRARY     DLLNAME
    DESCRIPTION 'DLL-библиотека DLLNAME'
    EXPORTS
    DrawBitmap=MyDraw  @4
      ShowAll     
      HideAll
      MyPoolPtr   @5 CONSTANT
      GetMyPool   @8 NONAME
      FreeMyPool  @9 NONAME
    

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

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

    В комплекте системы разработки Microsoft Visual C++ входит программа dumpbin.exe, предназначенная для запуска из командной строки. С помощью этой утилиты вы сможете проанализировать содержимое любого загрузочного файла в формате COFF, в том числе DLL-библиотеки, определив имена экспортируемых функций, их порядковые номера, имена DLL-библиотек и номера функций, импортируемых из этих библиотек и т. д. Можно даже дизассемблировать секции кода с использованием таблицы символов, если такая имеется в файле.

    Выберем для исследования DLL-библиотеку comdlg32.dll, в которой находятся функции для работы со стандартными диалоговыми панелями.

    Вначале запустим программу dumpbin.exe, передав ей в качестве параметра имя DLL-библиотеки:

    
    c:\msdev\bin>dumpbin comdg32.dll > lst.txt
    

    Перед запуском программы dumpbin.exe мы скопировали файл comdg32.dll в каталог c:\msdev\bin.

    Программа запишет в файл lst.txt информацию о типе файла (DLL-библиотека) и перечислит названия секций и их размер, как это показано ниже:

    
    Microsoft (R) COFF Binary File Dumper Version 3.10.6038
    Copyright (C) Microsoft Corp 1992-1996. All rights reserved.
    Dump of file comdlg32.dll
    File Type: DLL
         Summary
            4000 .data
            1000 .edata
            2000 .rdata
            2000 .reloc
            9000 .rsrc
           17000 .text
    

    Ниже мы перечислили названия некоторых стандартных секций (полное описание вы найдете в документации, которая поставляется в составе использованного вами средства разработки приложений для Microsoft Windows NT):

     Название

     Описание

     .data

     Секция инициализированных данных

     .text

     Секция кода

     .rdata

     Данные, которые можно только читать во время выполнения

     .edata

     Таблица экспортируемых имен

     .reloc

     Таблица перемещений

     .rsrc

     Ресурсы

     .bss

     Секция неинициализированных данных

     .xdata

     Таблица обработки исключений

     .CRT

     Данные библиотеки C, которые можно только читать во время выполнения

     .debug

     Отладочная информация

     .tls

     Локальная память задач

    Для просмотра более подробной информации о секции следует воспользоваться параметром /SECTION:

    
    c:\msdev\bin>dumpbin comdg32.dll /SECTION:.data > lst.txt
    

    Результат выполнения этой команды показан ниже:

    
    Microsoft (R) COFF Binary File Dumper Version 3.10.6038
    Copyright (C) Microsoft Corp 1992-1996. All rights reserved.
    Dump of file comdlg32.dll
    File Type: DLL
    SECTION HEADER #3
       .data name
        35E4 virtual size
       1A000 virtual address
         E00 size of raw data
       18C00 file pointer to raw data
           0 file pointer to relocation table
           0 file pointer to line numbers
           0 number of relocations
           0 number of line numbers
    C8000040 flags
             Initialized Data
             Not Paged
             (no align specified)
             Read Write
         Summary
            4000 .data
    

    Аналогичная информация о секции .text приведена ниже:

    
    Microsoft (R) COFF Binary File Dumper Version 3.10.6038
    Copyright (C) Microsoft Corp 1992-1996. All rights reserved.
    Dump of file comdlg32.dll
    File Type: DLL
    SECTION HEADER #1
       .text name
       16FF9 virtual size
        1000 virtual address
       17000 size of raw data
         400 file pointer to raw data
           0 file pointer to relocation table
           0 file pointer to line numbers
           0 number of relocations
           0 number of line numbers
    68000020 flags
             Code
             Not Paged
             (no align specified)
             Execute Read
         Summary
           17000 .text
    

    Для просмотра списка имен экспортируемых функций и глобальных переменных запустите программу dumpbin.exe с параметром /EXPORTS:

    
    c:\msdev\bin>dumpbin comdg32.dll /EXPORTS > lst.txt
    

    Список появится в следующем виде:

    
    Microsoft (R) COFF Binary File Dumper Version 3.10.6038
    Copyright (C) Microsoft Corp 1992-1996. All rights reserved.
    Dump of file comdlg32.dll
    File Type: DLL
        Section contains the following Exports for comdlg32.dll
                       0 characteristics
                30D0D8ED time date stamp Fri Dec 15 05:09:49 1995
                    0.00 version
                       1 ordinal base
                      23 number of functions
                      23 number of names
    
                ordinal hint   name
    
                      1    0   ChooseColorA  (00012442)
                      2    1   ChooseColorW  (0001164F)
                      3    2   ChooseFontA  (00009235)
                      4    3   ChooseFontW  (00014028)
                      5    4   CommDlgExtendedError  (0000F92A)
                      6    5   FindTextA  (0001373A)
                      7    6   FindTextW  (0001372A)
                      8    7   GetFileTitleA  (0000FB57)
                      9    8   GetFileTitleW  (0000FAF1)
                     10    9   GetOpenFileNameA  (00004952)
                     11    A   GetOpenFileNameW  (0000F9EE)
                     12    B   GetSaveFileNameA  (0000493B)
                     13    C   GetSaveFileNameW  (0000FA37)
                     14    D   LoadAlterBitmap  (0000A450)
                     15    E   PageSetupDlgA  (00014277)
                     16    F   PageSetupDlgW  (00014373)
                     17   10   PrintDlgA  (0000738E)
                     18   11   PrintDlgW  (00014238)
                     19   12   ReplaceTextA  (0001375A)
                     20   13   ReplaceTextW  (0001374A)
                     21   14   WantArrows  (000122D5)
                     22   15   dwLBSubclass  (000018F7)
                     23   16   dwOKSubclass  (000018C4)
         Summary
            4000 .data
            1000 .edata
            2000 .rdata
            2000 .reloc
            9000 .rsrc
           17000 .text
    

    Наряду с именами функций здесь отображаются порядковые номера функций и их адреса (в скобках).

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

    
    Microsoft (R) COFF Binary File Dumper Version 3.10.6038
    Copyright (C) Microsoft Corp 1992-1996. All rights reserved.
    Dump of file comdlg32.dll
    File Type: DLL
      77DF1000: 83 3D EC C4 E0 77  cmp   dword ptr ds:[77E0C4ECh],0
                00
      77DF1007: 56                 push  esi
      77DF1008: 75 2D              jne   77DF1037
      77DF100A: 83 3D 8C C7 E0 77  cmp   dword ptr ds:[77E0C78Ch],0
                00
      77DF1011: 8B 74 24 08        mov   esi,dword ptr [esp+8]
      77DF1015: 0F 84 93 97 00 00  je    77DFA7AE
      77DF101B: 83 FE 01           cmp   esi,1
      77DF101E: 1B C0              sbb   eax,eax
      77DF1020: 24 FE              and   al,0FEh
      77DF1022: 05 02 7F 00 00     add   eax,7F02h
      77DF1027: 50                 push  eax
      77DF1028: 6A 00              push  0
    

    В заключение этого раздела приведем список параметров программы dumpbin.exe.

     Параметр

     Описание

     /ALL

     Просмотр всей доступной информации, исключая листинг дизассемблирования

     /ARCHIVEMEMBERS

     Просмотр минимальной информации об объектах библиотеки

     /DISASM

     Дизассемблирование секции кода

     /EXPORTS

     Просмотр экспортируемых имен

     /FPO

     Просмотр записи FPO (Frame Pointer Optimization)

     /HEADERS

     Просмотр заголовка файла и заголовков каждой секции, либо заголовка каждого объекта, расположенного в библиотеке

     /IMPORTS

     Просмотр импортированных имен

     /LINENUMBERS

     Просмотр номеров строк формата COFF

     /LINKERMEMBER

     Просмотр доступных символов, опередленных в библиотеке как public

     /OUT:ИмяФайла

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

     /RAWDATA

     Просмотр дампа каждой секции

     /RELOCATIONS

     Просмотр таблицы перемещений

     /SECTION:Секция

     Просмотр информации только о секции, имеющей имя Секция

     /SUMMARY

     Просмотр минимальной информации о секциях

     /SYMBOLS

     Просмотр таблицы символов формата COFF

    Исходные тексты DLL-библиотеки DLLDEMO

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

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

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

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

    Листинг 3.1. Файл dlldemo\dlldemo.c

    
    // ==================================================
    // DLL-библиотека DLLDemo.DLL
    // Поиск окна по заданному заголовку
    //
    // (С) Фролов А.В., 1996
    // Email: frolov@glas.apc.org
    // ==================================================
    
    #include <windows.h>
    #include <windowsx.h>
    #include "dlldemo.h"
    
    // Глобальная переменная, в которую записывается
    // идентификатор найденного окна или значение NULL, 
    // если окно с заданным заголовком не найдено
    HWND hwndFound;
    
    // -------------------------------------------------
    // Функция DLLEntryPoint
    // Точка входа DLL-библиотеки
    // -------------------------------------------------
    BOOL WINAPI DLLEntryPoint(
      HMODULE hModule,    // идентификатор модуля
      DWORD   fdwReason,  // причина вызова функции DLLEntryPoint
      LPVOID  lpvReserved) // зарезервировано
    {
      switch(fdwReason)
      {
        // Подключение нового процесса
        case DLL_PROCESS_ATTACH:
        {
          MessageBox(NULL, "Process attached", "DLL Demo", MB_OK);
          break;
        }
    
        // Подключение новой задачи
        case DLL_THREAD_ATTACH:
        {
          MessageBox(NULL, "Thread attached", "DLL Demo", MB_OK);
          break;
        }
    
        // Отключение процесса
        case DLL_PROCESS_DETACH:
        {
          MessageBox(NULL, "Process detached", "DLL Demo", MB_OK);
          break;
        }
    
        // Отключение задачи
        case DLL_THREAD_DETACH:
        {
          MessageBox(NULL, "Thread detached", "DLL Demo", MB_OK);
          break;
        }
      }
      return TRUE;
    }
    
    // -------------------------------------------------
    // Функция FindApplicationWindow
    // Поиск главного окна приложения по его заголовку
    // -------------------------------------------------
    HWND FindApplicationWindow(LPSTR lpszWindowTitle)
    {
      // Запускаем цикл поиска окна с заголовком,
      // адрес которого передан функции через
      // параметр lpszWindowTitle
      EnumWindows(EnumWindowsProc, (LPARAM)lpszWindowTitle);
          
      // Возвращаем значение глобальной переменной hwndFound,
      // которое устанавливается функцией обратного вызова
      // EnumWindowsProc в зависимости от результата поиска
      return hwndFound;
    }
    
    
    // -------------------------------------------------
    // Функция EnumWindowsProc
    // -------------------------------------------------
    BOOL CALLBACK EnumWindowsProc(
      HWND   hwnd,   // идентификатор родительского окна
      LPARAM lParam) // адрес строки заголовка окна
    {
      // Буфер для хранения заголовка окна
      char szBuf[512];
    
      // Получаем заголовок окна
      GetWindowText(hwnd, szBuf, 512);
      
      // Сравниваем заголовок со строкой, адрес которой 
      // передан в функцию EnumWindowsProc через параметр lParam
      if(!strcmp((LPSTR)lParam, szBuf))
      {
        // Если заголовок совпал, сохраняем идентификатор
        // текущего окна в глобальной переменной hwndFound
        hwndFound = hwnd; 
        
        // Завершаем цикл просмотра окон
        return FALSE;
      }
      
      // Если заголовок не совпал, продолжаем поиск
      else
      {
        // Записываем в глобальную переменную hwndFound
        // значение NULL. Это признак того, что окно
        // с заданным заголовком не было найдено
        hwndFound = NULL;    
    
        // Для продолжения поиска возвращаем значение TRUE
        return TRUE;
      }
    }
    
    

    В файле dlldemo.h (листинг 3.2) находятся прототипы функций, определенных в нашей DLL-библиотеке.

    Листинг 3.2. Файл dlldemo\dlldemo.h

    
    BOOL WINAPI DLLEntryPoint(HMODULE hModule,
      DWORD fdwReason, LPVOID lpvReserved);
    
    HWND FindApplicationWindow(LPSTR lpszWindowTitle);
    
    BOOL CALLBACK EnumWindowsProc(
      HWND   hwnd,   // идентификатор родительского окна
      LPARAM lParam); // произвольное значение
    

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

    Листинг 3.3. Файл dlldemo\dlldemo.def

    
    EXPORTS
      FindApplicationWindow @1
    

    Итак, займемся исходными текстами DLL-библиотеки.

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

    Функция DLLEntryPoint предназначена для инициализации библиотеки. В нашем случае функция инициализации просто выводит на экран различные сообщения в зависимости от кода причины вызова.

    Функция FindApplicationWindow ищет главное окно приложения по заголовку этого окна, адрес которого передается ей через параметр lpszWindowTitle.

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

    
    EnumWindows(EnumWindowsProc, (LPARAM)lpszWindowTitle);
    

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

    Функции обратного вызова EnumWindowsProc (имя функции может быть любым) передаются два параметра: идентификатор окна и 32-разрядное значение, которое передавалось функции EnumWindows в качестве второго параметра. В нашем случае это адрес заголовка искомого окна:

    
    BOOL CALLBACK EnumWindowsProc(
      HWND   hwnd,   // идентификатор родительского окна
      LPARAM lParam) // адрес строки заголовка окна
    {
      char szBuf[512];
      GetWindowText(hwnd, szBuf, 512);
      if(!strcmp((LPSTR)lParam, szBuf))
      {
        hwndFound = hwnd; 
        return FALSE;
      }
      else
      {
        hwndFound = NULL;    
        return TRUE;
      }
    }
    

    После вызова функции EnumWindows функция EnumWindowsProc будет вызываться в цикле для окна вернего уровня каждого запущенного приложения.

    Зная идентификатор окна (который передается функции EnumWindowsProc через первый параметр), мы с помощью функции GetWindowText получаем заголовок окна и записываем его в буфер szBuf. Затем этот заголовок сравнивается с заданным при помощи функции strcmp. Адрес заданного заголовка мы получаем через параметр lParam.

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

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

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

    Несколько слов о настройке проекта DLL-библиотеки DLLDemo.DLL.

    При создании проекта DLL-библиотеки “с нуля” вы должны указать тип рабочего пространства проекта (Project Workspace) как Dynamic-Link Library (рис. 3.5).

    Рис. 3.5. Создание нового проекта для DLL-библиотеки

    Если в вашей DLL-библиотеке определена точка входа (функция инициализации), то в параметрах проекта вы должны указать ее имя. Для этого в системе Microsoft Visual C++ версии 4.0 выберите из меню Build строку Settings. На экране появится диалоговая панель Project Settings, показанная на рис. 3.6.

    Рис. 3.6. Диалоговая панель Project Settings

    Пользуясь кнопками в правом верхнем углу этой диалоговой панели, откройте страницу Link. Затем выберите из списка Category строку Output. В поле Entry-point symbol введите имя вашей функции инициализации и нажмите кнопку OK.

    Если вы выполняете редактирование загрузочного модуля DLL-библиотеки в пакетном режиме, при запуске редактора связей укажите параметр /entry:”ИмяФункцииИнициализации”.

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

    Приложение DLLCALL

    Приложение DLLCALL работает совместно с DLL-библиотекой DLLDemo.DLL, описанной в предыдущем разделе.

    Главное окно приложения DLLCALL и меню File показано на рис. 3.7.

    Рис. 3.7. Главное окно приложения DLLCALL

    Если из меню File выбрать строку Find App Window, на экране появится диалоговая панель Find Application Window, показанная на рис. 3.8.

    Рис. 3.8. Диалоговая панель Find Application Window

    Здесь в поле Enter application window title to find вы должны ввести заголовок окна приложения. Если приложение с таким заголовком запущено, оно будет найдено, после чего на экране появится соответствующее сообщение (рис. 3.9).

    Рис. 3.9. Сообщение о том, что заданное окно найдено

    При инициализации DLL-библиотеки, вызванной подключением процесса DLLCALL, на экране возникает сообщение, показанное на рис. 3.10.

    Рис. 3.10. Собщение о подключении процесса к DLL-библиотеке

    Когда приложение DLLCALL отключается от DLL-библиотеки, на экран выводится сообщение, показанное на рис. 3.11.

    Рис. 3.11. Сообщение об отключении процесса от DLL-библиотеки

    Главный файл исходных текстов приложения DLLCALL представлен в листинге 3.4.

    Листинг 3.4. Файл dlldemo\dllcall\dllcall.c

    
    // ==================================================
    // Приложение DLLCall
    // Вызов функции из DLL-библиотеки
    //
    // (С) Фролов А.В., 1996
    // Email: frolov@glas.apc.org
    // ==================================================
    
    #define STRICT
    #include <windows.h>
    #include <windowsx.h>
    #include "resource.h"
    #include "afxres.h"
    
    #include "dllcall.h"
    
    // Определяем тип: указатель на функцию
    typedef HWND (WINAPI *MYDLLPROC)(LPSTR);
    
    // Указатель на функцию, расположенную в 
    // DLL-библиотеке
    MYDLLPROC GetAppWindow;
    
    // Буфер для заголовка окна, поиск которого
    // будет выполняться
    char szWindowTitle[512];
    
    // Идентификатор DLL-библиотеки
    HANDLE hDLL;
    
    HINSTANCE hInst;
    char szAppName[]  = "DLLCallApp";
    char szAppTitle[] = "DLL Call Demo";
    
    // -----------------------------------------------------
    // Функция WinMain
    // -----------------------------------------------------
    int APIENTRY 
    WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
            LPSTR lpCmdLine, int nCmdShow)
    {
      WNDCLASSEX wc;
      HWND hWnd;
      MSG msg;
      
      // Сохраняем идентификатор приложения
      hInst = hInstance;
    
      // Преверяем, не было ли это приложение запущено ранее
      hWnd = FindWindow(szAppName, NULL);
      if(hWnd)
      {
        // Если было, выдвигаем окно приложения на
        // передний план
        if(IsIconic(hWnd))
    	    ShowWindow(hWnd, SW_RESTORE);
    	  SetForegroundWindow(hWnd);
        return FALSE;
      }
    
      // Регистрируем класс окна
      memset(&wc, 0, sizeof(wc));
      wc.cbSize = sizeof(WNDCLASSEX);
      wc.hIconSm = LoadImage(hInst,
        MAKEINTRESOURCE(IDI_APPICONSM), 
        IMAGE_ICON, 16, 16, 0);
      wc.style = 0;
      wc.lpfnWndProc = (WNDPROC)WndProc;
      wc.cbClsExtra  = 0;
      wc.cbWndExtra  = 0;
      wc.hInstance = hInst;
      wc.hIcon = LoadImage(hInst,
        MAKEINTRESOURCE(IDI_APPICON), 
        IMAGE_ICON, 32, 32, 0);
      wc.hCursor = LoadCursor(NULL, IDC_ARROW);
      wc.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1);
      wc.lpszMenuName = MAKEINTRESOURCE(IDR_APPMENU);
      wc.lpszClassName = szAppName;
      if(!RegisterClassEx(&wc))
        if(!RegisterClass((LPWNDCLASS)&wc.style))
    	  return FALSE;
        
      // Создаем главное окно приложения
      hWnd = CreateWindow(szAppName, szAppTitle, 
         WS_OVERLAPPEDWINDOW, 
         CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, 
         NULL, NULL, hInst, NULL);
      if(!hWnd) return(FALSE);
    
      // Отображаем окно и запускаем цикл 
      // обработки сообщений
      ShowWindow(hWnd, nCmdShow);
      UpdateWindow(hWnd);
      while(GetMessage(&msg, NULL, 0, 0))
      {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      }
      return msg.wParam;
    }
    
    // -----------------------------------------------------
    // Функция WndProc
    // -----------------------------------------------------
    LRESULT WINAPI
    WndProc(HWND hWnd, UINT msg, WPARAM wParam, 
            LPARAM lParam)
    {
      switch(msg)
      {
        HANDLE_MSG(hWnd, WM_COMMAND, WndProc_OnCommand);
        HANDLE_MSG(hWnd, WM_DESTROY, WndProc_OnDestroy);
    
        default:
          return(DefWindowProc(hWnd, msg, wParam, lParam));
      }
    }
    
    // -----------------------------------------------------
    // Функция WndProc_OnDestroy
    // -----------------------------------------------------
    #pragma warning(disable: 4098)
    void WndProc_OnDestroy(HWND hWnd)
    {
      PostQuitMessage(0);
      return 0L;
    }
    
    // -----------------------------------------------------
    // Функция WndProc_OnCommand
    // -----------------------------------------------------
    #pragma warning(disable: 4098)
    void WndProc_OnCommand(HWND hWnd, int id, 
      HWND hwndCtl, UINT codeNotify)
    {
      switch (id)
      {
        case ID_FILE_EXIT:  
        {
          // Завершаем работу приложения
          PostQuitMessage(0);
          return 0L;
          break;
        }
    	  
        case ID_FILE_FINDWINDOW:
        {
          // Отображаем диалоговую панель для ввода
          // заголовка главного окна приложения,
          // поиск которого будет выполняться
          if(DialogBox(hInst, MAKEINTRESOURCE(IDD_DLGFIND), 
            hWnd, DlgProc))
          {
            // Первый способ вызова функции из DLL-библиотеки:
            // прямой вызов с использованием библиотеки экспорта
    
    /*
            // Выполняем поиск окна с заголовком, заданным
            // при помощи диалоговой панели
            if(FindApplicationWindow(szWindowTitle) != NULL)
              MessageBox(NULL, "Application window was found",
                szAppTitle, MB_OK | MB_ICONINFORMATION);
            
              else
              MessageBox(NULL, "Application window was not found",
                szAppTitle, MB_OK | MB_ICONINFORMATION);
    */
    
            // Второй способ вызова функции из DLL-библиотеки:
            // загрузка DLL-библиотеки функцией LoadLibrary
    
            // Загружаем DLL-библиотеку
            hDLL = LoadLibrary("DLLDEMO.DLL");
    
            // Если библиотека загружена успешно, выполняем
            // вызов функции
            if(hDLL != NULL)
            {
              // Получаем адрес нужной нам функции
              GetAppWindow = 
                (MYDLLPROC)GetProcAddress(hDLL, 
                  "FindApplicationWindow");
    
              // Если адрес получен, вызываем функцию
              if(GetAppWindow != NULL)
              {
                // Выполняем поиск окна с заголовком, заданным
                // при помощи диалоговой панели
                if(GetAppWindow(szWindowTitle) != NULL)
                  MessageBox(NULL, "Application window was found",
                    szAppTitle, MB_OK | MB_ICONINFORMATION);
                else
                  MessageBox(NULL, 
                    "Application window was not found",
                    szAppTitle, MB_OK | MB_ICONINFORMATION);
              }
              
              // Освобождаем DLL-библиотеку
              FreeLibrary(hDLL);
            }
          }
     
          break;      
        }
        
        case ID_HELP_ABOUT:
        {
          MessageBox(hWnd, 
            "DLL Call Demo\n"
            "(C) Alexandr Frolov, 1996\n"
            "Email: frolov@glas.apc.org",
            szAppTitle, MB_OK | MB_ICONINFORMATION);
    	     return 0L;
    	     break;
        }
    
      	default:
    	    break;
      }
      return FORWARD_WM_COMMAND(hWnd, id, hwndCtl, codeNotify,
        DefWindowProc);
    }
    
    // -----------------------------------------------------
    // Функция DlgProc
    // -----------------------------------------------------
    LRESULT WINAPI
    DlgProc(HWND hdlg, UINT msg, WPARAM wParam, 
            LPARAM lParam)
    {
      switch(msg)
      {
        HANDLE_MSG(hdlg, WM_INITDIALOG, DlgProc_OnInitDialog);
        HANDLE_MSG(hdlg, WM_COMMAND,    DlgProc_OnCommand);
    
    	default:
        return FALSE;
      }
    }
    
    // -----------------------------------------------------
    // Функция DlgProc_OnInitDialog
    // -----------------------------------------------------
    
    BOOL DlgProc_OnInitDialog(HWND hdlg, HWND hwndFocus, 
                              LPARAM lParam)
    {
      return TRUE;
    }
    
    // -----------------------------------------------------
    // Функция DlgProc_OnCommand
    // -----------------------------------------------------
    #pragma warning(disable: 4098)
    void DlgProc_OnCommand(HWND hdlg, int id, 
      HWND hwndCtl, UINT codeNotify)
    {
      switch (id)
      {
        case IDOK:
        {
          // Если пользователь нажал кнопку OK,
          // получаем текст из поля редактирования и
          // сохраняем его в глобальном буфере szWindowTitle
          GetDlgItemText(hdlg, IDC_EDIT1, szWindowTitle, 512);  
          
          // Завершаем работу диалоговой панели
          EndDialog(hdlg, 1);
          return TRUE;
        }
    
        // Если пользователь нажимает кнопку Cancel,
        // завершаем работу диалоговой панели
        case IDCANCEL:
        {
          // Завершаем работу диалоговой панели
          EndDialog(hdlg, 0);
          return TRUE;
        }
        default:
          break;
      }
      return FALSE;
    }
    

    Файл dllcall.h (листинг 3.5) содержит прототипы функций, определенных в приложении DLLCALL.

    Листинг 3.5. Файл dlldemo\dllcall\dllcall.h

    
    // -----------------------------------------------------
    // Описание функций
    // -----------------------------------------------------
    LRESULT WINAPI
    WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
    
    void WndProc_OnCommand(HWND hWnd, int id, 
      HWND hwndCtl, UINT codeNotify);
    
    void WndProc_OnDestroy(HWND hWnd);
    
    HWND FindApplicationWindow(LPSTR lpszWindowTitle);
    
    LRESULT WINAPI
    DlgProc(HWND hdlg, UINT msg, WPARAM wParam, LPARAM lParam);
    
    BOOL DlgProc_OnInitDialog(HWND hdlg, HWND hwndFocus, 
                              LPARAM lParam);
    
    void DlgProc_OnCommand(HWND hdlg, int id, 
      HWND hwndCtl, UINT codeNotify);
    

    В файле dllcall.rc (листинг 3.6) определены ресурсы приложения DLLCALL. Это меню, две пиктограммы и диалоговая панель, а также текстовые строки, которые не используются.

    Листинг 3.6. Файл dlldemo\dllcall\dllcall.rc

    
    //Microsoft Developer Studio generated resource script.
    //
    #include "resource.h"
    
    #define APSTUDIO_READONLY_SYMBOLS
    //////////////////////////////////////////////////////////////
    //
    // Generated from the TEXTINCLUDE 2 resource.
    //
    #include "afxres.h"
    
    //////////////////////////////////////////////////////////////
    #undef APSTUDIO_READONLY_SYMBOLS
    
    //////////////////////////////////////////////////////////////
    // Russian resources
    
    #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_RUS)
    #ifdef _WIN32
    LANGUAGE LANG_RUSSIAN, SUBLANG_DEFAULT
    #pragma code_page(1251)
    #endif //_WIN32
    
    //////////////////////////////////////////////////////////////
    //
    // Menu
    //
    
    IDR_APPMENU MENU DISCARDABLE 
    BEGIN
        POPUP "&File"
        BEGIN
            MENUITEM "F&ind App Window",   ID_FILE_FINDWINDOW
            MENUITEM SEPARATOR
            MENUITEM "E&xit",              ID_FILE_EXIT
        END
        POPUP "&Help"
        BEGIN
            MENUITEM "&About...",          ID_HELP_ABOUT
        END
    END
    
    #ifdef APSTUDIO_INVOKED
    //////////////////////////////////////////////////////////////
    //
    // TEXTINCLUDE
    //
    
    1 TEXTINCLUDE DISCARDABLE 
    BEGIN
        "resource.h\0"
    END
    
    2 TEXTINCLUDE DISCARDABLE 
    BEGIN
        "#include ""afxres.h""\r\n"
        "\0"
    END
    
    3 TEXTINCLUDE DISCARDABLE 
    BEGIN
        "\r\n"
        "\0"
    END
    
    #endif    // APSTUDIO_INVOKED
    
    //////////////////////////////////////////////////////////////
    //
    // Icon
    //
    
    // Icon with lowest ID value placed first to ensure 
    // application icon
    // remains consistent on all systems.
    IDI_APPICON             ICON    DISCARDABLE     "Dllcall.ico"
    IDI_APPICONSM           ICON    DISCARDABLE     "Dllcalsm.ico"
    
    //////////////////////////////////////////////////////////////
    //
    // Dialog
    //
    
    IDD_DLGFIND DIALOG DISCARDABLE  0, 0, 247, 74
    STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
    CAPTION "Find Application Window"
    FONT 8, "MS Sans Serif"
    BEGIN
        EDITTEXT        IDC_EDIT1,21,31,152,16,ES_AUTOHSCROLL
        DEFPUSHBUTTON   "OK",IDOK,190,7,50,14
        PUSHBUTTON      "Cancel",IDCANCEL,190,24,50,14
        GROUPBOX        "Enter application window title to find", 
                        IDC_STATIC,13,13,167,49
    END
    
    //////////////////////////////////////////////////////////////
    //
    // DESIGNINFO
    //
    
    #ifdef APSTUDIO_INVOKED
    GUIDELINES DESIGNINFO DISCARDABLE 
    BEGIN
        IDD_DLGFIND, DIALOG
        BEGIN
            LEFTMARGIN, 7
            RIGHTMARGIN, 240
            TOPMARGIN, 7
            BOTTOMMARGIN, 67
        END
    END
    #endif    // APSTUDIO_INVOKED
    
    #endif    // Russian resources
    //////////////////////////////////////////////////////////////
    #ifndef APSTUDIO_INVOKED
    //////////////////////////////////////////////////////////////
    //
    // Generated from the TEXTINCLUDE 3 resource.
    //
    
    //////////////////////////////////////////////////////////////
    #endif    // not APSTUDIO_INVOKED
    

    Файл resource.h (листинг 3.7) содержит определения констант для файла описания ресурсов приложения.

    Листинг 3.7. Файл dlldemo\dllcall\resource.h

    
    //{{NO_DEPENDENCIES}}
    // Microsoft Developer Studio generated include file.
    // Used by DLLCall.rc
    //
    #define IDR_MENU1                       101
    #define IDR_APPMENU                     101
    #define IDI_APPICON                     102
    #define IDI_APPICONSM                   103
    #define IDD_DLGFIND                     104
    #define IDC_EDIT1                       1000
    #define ID_FILE_EXIT                    40001
    #define ID_HELP_ABOUT                   40002
    #define ID_FILE_FINDWINDOW              40003
    
    // Next default values for new objects
    // 
    #ifdef APSTUDIO_INVOKED
    #ifndef APSTUDIO_READONLY_SYMBOLS
    #define _APS_NEXT_RESOURCE_VALUE        105
    #define _APS_NEXT_COMMAND_VALUE         40004
    #define _APS_NEXT_CONTROL_VALUE         1001
    #define _APS_NEXT_SYMED_VALUE           101
    #endif
    #endif
    

    Рассмотрим исходные тексты приложения DLLCALL.

    Глобальные переменные и определения

    В начале файла dllcall.c (листинг 3.4) мы определили тип MYDLLPROC как указатель на функцию, возвращающую знначение HWND и принимающую один параметр типа LPSTR.

    Далее в области глобальных переменных мы определили переменную GetAppWindow с типом MYDLLPROC:

    
    MYDLLPROC GetAppWindow;
    

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

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

    В глобальной переменной hDLL хранится идентификатор динамически загруженной DLL-библиотеки.

    Функция WinMain

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

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

    Функция WndProc

    Функция WndProc обрабатывает сообщения WM_COMMAND и WM_DESTROY, для чего вызываются, соответственно, функции WndProc_OnCommand и WndProc_OnDestroy.

    Функция WndProc_OnDestroy

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

    Функция WndProc_OnCommand

    Эта функция обрабатывает сообщение WM_COMMAND, которое приходит от главного меню приложения.

    Когда пользователь выбирает из меню File строку Find App Window, с помощью функции DialogBox на экран выводится диалоговая панель, предназначенная для ввода заголовка окна, которое нужно найти.

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

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

    Первый способ достаточно прост, однако предполагает, что в проект приложения DLLCALL будет включен файл библиотеки экспорта DLLDemo.LIB. Этот файл создается автоматически системой Microsoft Visual C++ при сборке проекта DLL-библиотеки.

    Фрагмент кода, использующий прямое подключение, закрыт в листинге 3.4 символами комментария:

    
    if(FindApplicationWindow(szWindowTitle) != NULL)
      MessageBox(NULL, "Application window was found",
        szAppTitle, MB_OK | MB_ICONINFORMATION);
    else
      MessageBox(NULL, "Application window was not found",
        szAppTitle, MB_OK | MB_ICONINFORMATION);
    

    В этом фрагменте мы выполняем простой вызов функции FindApplicationWindow, определенной в DLL-библиотеке DLLDemo.DLL. Прототип функции FindApplicationWindow мы поместили в файл dllcall.h.

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

    
    hDLL = LoadLibrary("DLLDEMO.DLL");
    if(hDLL != NULL)
    {
      GetAppWindow = 
       (MYDLLPROC)GetProcAddress(hDLL, "FindApplicationWindow");
      if(GetAppWindow != NULL)
      {
        if(GetAppWindow(szWindowTitle) != NULL)
          MessageBox(NULL, "Application window was found",
            szAppTitle, MB_OK | MB_ICONINFORMATION);
        else
          MessageBox(NULL, "Application window was not found",
            szAppTitle, MB_OK | MB_ICONINFORMATION);
      }
      FreeLibrary(hDLL);
    }
    

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

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

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

    Функция DlgProc

    Функция DlgProc обрабатывает сообщения WM_INITDIALOG и WM_COMMAND, поступающие в функцию диалога диалоговой панели, предназначенной для ввода заголовка окна. Эти сообщения обрабатываются, соответственно, функциями DlgProc_OnInitDialog и DlgProc_OnCommand.

    Функция DlgProc_OnInitDialog

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

    Функция DlgProc_OnCommand

    Функция DlgProc_OnCommand обрабатывает сообщение WM_COMMAND, поступающее в функцию диалога от органов управления, расположенных в диалоговой панели.

    Если пользователь нажимает кнопку OK, функция DlgProc_OnCommand извлекает содержимое однострочного текстового редактора (введенное имя заголовока окна), вызывая для этого макрокоманду GetDlgItemText, и сохраняет это содержимое в глобальном буфере szWindowTitle. Затем функция завершает работу диалоговой панели с кодом 1, в результате чего приложение приступит к поиску окна с заданным заголовком.

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

    [Назад] [Содеожание] [Дальше]