
Практическая работа № 1. Разработка динамически подключаемых библиотек Цель работы
Изучение основных понятий, связанных с динамическим связыванием.
Получение базовых навыков по созданию и использованию динамически загружаемых библиотек.
Краткие теоретические сведения
Основы динамически подключаемых библиотек
Динамически подключаемые библиотеки представляют из себя отдельные файлы с функциями, которые вызываются программами и другими динамическими библиотеками для выполнения определенных задач. Как правило, они непосредственно не выполняются и обычно не получают сообщений. Динамически подключаемая библиотека активизируется только тогда, когда другой модуль вызывает одну из функций, находящихся в библиотеке.
Динамическое связывание (dinamic linking) – это процедура, которую Windows использует для того, чтобы связать вызов функции в одном из модулей с реальной функцией из модуля библиотеки во время выполнения программы.
Некоторые динамически подключаемые библиотеки (например, файлы шрифтов) содержат только ресурсы (resource only). В них содержатся только данные (обычно в виде ресурсов), и нет текстов программ.
Хотя модуль динамически подключаемой библиотеки может иметь любое расширение (например, .EXE или .FON), стандартным расширением, принятым в Windows, является .DLL. Только те динамически подключаемые библиотеки, которые имеют расширение .DLL, Windows загрузит автоматически. Если файл имеет другое расширение, то программа должна загрузить модуль библиотеки явно. Для этого используется функция LoadLibrary или LoadLibraryEx.
Для создания динамически подключаемой библиотеки, необходимо создать Win32 проект типа DLL. При создании динамически подключаемой библиотеки (если есть хотя бы одна экспортируемая функция или экспортируемая глобальная переменная) создается также библиотека импорта – файл, имеющий то же имя, что и библиотека, и расширение .LIB.
Библиотека импорта представляет собой особую форму файла объектной библиотеки, и используются компоновщиком для разрешения ссылок на функции в исходном коде программы. В библиотеках импорта находится информация, необходимая компоновщику для установки таблицы ссылок внутри файла .EXE, предназначенной для динамического связывания.
Библиотеки импорта используются только при разработке программы. Динамически подключаемые библиотеки используются во время выполнения программы.
Динамически подключаемая библиотека должна находиться на диске, когда выполняется программа, использующая эту библиотеку. Если операционной системе Windows требуется загрузить модуль динамически подключаемой библиотеки перед запуском программы, для которой этот модуль необходим, то файл библиотеки должен храниться в том же каталоге, что и файл .EXE программы, или в текущем каталоге, или в системном каталоге Windows, или в каталоге, доступном из переменной окружения PATH. (Именно в таком порядке в каталогах происходит поиск.)
Модуль динамически подключаемой библиотеки в отличии от приложения Windows включает функцию входа/выхода DllMain вместо WinMain.
Эта функция используется для выполнения инициализации и деинициализации. Если эти операции выполнять не надо, то в ней нужно только возвратить значение TRUE. При построении динамически подключаемой библиотеки (*.DLL) появляются еще два новых файла: библиотека импорта (*.LIB) и библиотека экспорта (*.EXP). Библиотека экспорта — это побочный эффект процесса компоновки и её можно удалить.
Кроме главной функции модуль динамически подключаемой библиотеки содержит другие, в том числе, и экспортируемые функции. Экспортировать также можно и глобальные переменные.
Чтобы функция или глобальная переменная была доступна приложениям, она должна быть сделана экспортируемой. Для этого нужно объявить ее с директивой компилятору __declspec(dllexport).
Чтобы избежать искажения имен, в описании прототипа функции нужно указать также директиву компоновщику extern “C”. Другой подход – добавить к проекту DLL библиотеки файл определений def. В файл определений нужно включить секцию экспорта:
EXPORTS
Список имен экспортируемых функций.
Для того, чтобы построить приложение, использующее экспортируемые функции или экспортируемые глобальные переменные библиотеки динамической компоновки, нужно в нем объявить прототипы используемых экспортируемых функций или/и экспортируемых глобальных переменных с директивой компилятору __declspec(dllimport). Также необходимо в переменную среды GUILIBS компоновщика добавить библиотеку импорта, соответствующую данной динамически подключаемой библиотеке.
Для того, чтобы построить динамически подключаемую библиотеку с функциями, которые могут вызываться из нескольких потоков одной программы, нужно компилировать библиотеку не с переменной среды GFLAGS, а с переменной среды GFLAGSMT.
Разделяемая память в DLL
ОС Windows изолирует друг от друга приложения, которые в одно и то же время используют одни и те же динамически подключаемые библиотеки. Однако, иногда это нежелательно. Может понадобиться написать DLL, содержащую некоторую область памяти, которая могла бы быть разделена между различными приложениями, или, может быть, различными экземплярами одного приложения. Это подразумевает использование разделяемой памяти, которую можно реализовать через файл, проецируемый в память.
Другой способ создания общей разделяемой памяти – обозначить, что инициализированные переменные должны находиться в специальной области памяти, которая обозначается как разделяемая (shared):
#pragma data_seg("Имя разделяемой области")
Обявления инициализированных переменных
#pragma data_seg( )
Первая директива #pragma создает область данных с именем "Имя разделяемой области". Эту область можно назвать как угодно, хотя компоновщик распознает только первые восемь символов. Все инициализированные переменные, расположенные после директивы #pragma попадают в область памяти "Имя разделяемой области". Второй директивой #pragma отмечен конец области данных. Важно специально инициализировать эти переменные, в противном случае компилятор помещает их вместо области памяти "Имя разделяемой области", в обычную неинициализируемую область.
Компоновщику необходимо сообщить о том, что область памяти "Имя разделяемой области" является разделяемой. Для этого компоновщику нужно задать параметр -SECTION следующим образом:
-section:Имя разделяемой области,rws
Буквы "rws" обозначают, что область памяти имеет атрибуты для чтения (read), записи (write) и разделения (shared) данных.
Пример:
#pragma data_seg("shared")
Обявления инициализированных переменных
PSTR pszStrings[MAX_STRINGS] = { NULL };
int iTotal = 0;
#pragma data_seg( )
Опция компоновщику -SECTION: shared, rws
Точка входа/выхода библиотеки
Первым параметром hInstance функции DllMain является описатель экземпляра библиотеки. Если в библиотеке используются ресурсы, для которых требуется описатель экземпляра (например, DialogBox), необходимо сохранить hInstance в глобальной переменной. Последний параметр функции DllMain резервируется системой.
Второй параметр fdwReason может принимать одно из четырех значений (коды уведомления), которое идентифицирует причину вызова функции DllMain системой Windows.
Значение DLL_PROCESS_ATTACH параметра fdwReason означает, что динамически подключаемая библиотека отображена в адресном пространстве процесса. Это сигнал библиотеке выполнить какие-то задачи по инициализации, которые требуются для обслуживания последующих запросов от процесса. Такая инициализация может включать в себя, например, выделение памяти. Во время выполнения программы функция DllMain вызывается с параметром DLL_PROCESS_ATTACH только однажды. Любой другой процесс, использующий ту же динамически подключаемую библиотеку, приводит к новому вызову функции DllMain с параметром DLL_PROCESS_ATTACH, но это происходит уже от имени нового процесса. Если инициализация проходит удачно, возвращаемым значением функции DllMain должно быть ненулевое значение. Нулевое возвращаемое значение приведет к тому, что Windows не запустит программу.
Значение DLL_PROCESS_DETACH параметра fdwReason означает, что динамически подключаемая библиотека больше процессу не нужна. Это дает возможность библиотеке освободить занимаемые ресурсы системы.
Значение DLL_THREAD_ATTACH параметра fdwReason означает, что связанный процесс создал новый поток. Когда поток завершается, Windows вызывает функцию DllMain с параметром DLL_THREAD_DETACH. Если библиотека была загружена после создания потока, то существует вероятность того, что вызов функции DllMain с параметром DLL_THREAD_DETACH произойдет без предварительного вызова ее с параметром DLL_THREAD_ATTACH. Когда функция DllMain вызывается с параметром DLL_THREAD_DETACH, поток еще существует.
Динамическое связывание без импорта
Вместо того, чтобы Windows выполняла динамическое связывание при первой загрузке программы в оперативную память, можно связать программу с модулем библиотеки во время выполнении программы.
Для этого в приложении необходимо определить тип данных, соответствующий типу экспортируемой функции библиотеки динамической компоновки. Затем определить переменную типа HANDLE для описателя библиотеки и переменную типа экспортируемой функции. С помощью функции LoadLibrary загрузить библиотеку и получить ее дескриптор. Используя дескриптор библиотеки и имя экспортируемой функции с помощью вызова функции GetProcAddress и приведения типа связать функцию библиотеки с ранее объявленной для нее переменной. Через имя этой переменной произвести вызов функции библиотеки. После того как библиотека станет не нужной ее необходимо освободить вызовом функции FreeLibrary.
Пример. Нужно вызвать функцию Rectangle: Rectangle(hdc, xLeft, yTop, xRight, yBottom), код, которой в библиотеке GDI32.DLL.
Сначала воспользуемся функцией typedef для определения типа функции Rectangle:
typedef BOOL(WINAPI *PFNRECT)(HDC, int, int, int, int);
Затем определяем две переменные:
HINSTANCE hLibrary;
PFNRECT pfnRectangle;
Теперь устанавливаем значение переменной hLibrary равным описателю библиотеки, а значение переменной
pfnRectangle — равным адресу функции Rectangle:
hLibrary = LoadLibrary("GDI32.DLL");
pfnRectangle =(PFNRECT) GetProcAddress(hLibrary, "Rectangle"");
(Или pfnRectangle =(PFNRECT) GetProcAddress(hLibrary, MAKEINTRESOURCE(N)), где N – порядковый номер экспортируемой функции библиотеки)
Функции LoadLibrary возвращает NULL, если не удается найти файл библиотеки или случается какая-то другая ошибка. Теперь можно вызывать функцию и затем освободить библиотеку:
pfnRectangle(hdc, xLeft, yTop, xRight, yBottom);
FreeLibrary(hLibrary);
С помощью функции GetProcAddress можно получить доступ и к экспортируемым глобальным переменным библиотеки.
Библиотеки, содержащие только ресурсы
Любая функция в динамически подключаемой библиотеке, которую программы Windows или другие библиотеки могут использовать, должны экспортироваться. Однако, динамически подключаемая библиотека может не содержать никаких экспортируемых функций. В этом случае она содержит ресурсы.
Для создания такой библиотеки в соответствующий проект необходимо добавить все необходимые ресурсы и перечислить их в в файле описания ресурсов проекта.
Если в приложении используется динамическое связывание без импорта, то перед использованием ресурса его необходимо загрузить и получить его описатель с помощью функции вида LoadXXXX, передав ей в качестве первого параметра описатель библиотеки.
Пример: загрузка битового образа:
hBitmap = LoadBitmap(hLibrary, MAKEINTRESOURCE(iCurrent));
Здесь hLibrary – описатель модуля загруженной библиотеки с ресурсом битового образа, параметр iCurrent – идентификатор битового образа в файле описания ресурсов. Эта функция вернет ошибку, если битовый образ, соответствующий числу iCurrent, содержит ошибку, или, если недостаточно памяти для загрузки битового образа. В противном случае будет возвращен дескриптор этого битового образа.
Описание некоторых вспомогательных функций, используемых в работе.
HDC CreateCompatibleDC(
HDC hdc // описатель контекста устройства
) – при успешном выполнении возвращает описатель контекста памяти, совместимый с действительным контекстом устройства, иначе возвращает NULL. Контекст памяти имеет поверхность отображения, существующую только в памяти.
int GetObject(
HGDIOBJ hgdiobj, //описатель графического объекта
int cbBuffer,// размер буфера для информации об объекте
LPVOID lpvObject // указатель на буфер для информации об объекте
) – получает информацию о графическом объекте. При успешном завершении, если последний параметр действительный указатель, возвращает число байт, сохраненных в буфере. При успешном завершении, если последний параметр пуст (NULL), возвращает число байт, требуемых для сохранения информации об объекте. При неудачном завершении возвращает 0. Для получения дополнительной информации об ошибке необходимо вызвать функцию GetLastError.
Если необходимо получить информацию о битовом образе, следует сделать вызов типа GetObject(hBitmap, sizeof(BITMAP), &bm), где переменная bm имеет тип BITMAP, а описатель hBitmap имеет тип HBITMAP. Описание структуры BITMAP:
typedef struct tagBITMAP { // bm
LONG bmType; /* определяет тип битового образа, должен равняться 0 */
LONG bmWidth; // определяет ширину в пикселях битового образа
LONG bmHeight; // определяет высоту в пикселях битового образа
LONG bmWidthBytes; /* ширина битового образа в байтах (должна быть четной) */
WORD bmPlanes; // число цветовых плоскостей
WORD bmBitsPixel; // число битов на пиксель
LPVOID bmBits; // указатель на массив битов
} BITMAP;
BOOL BitBlt(
HDC hdcDest, // описатель контекста устройства назначения
int nXDest, /* x-координата в логических единицах верхнего левого угла прямоугольника назначения */
int nYDest, /* y-координата в логических единицах верхнего левого угла прямоугольника назначения */
int nWidth, /* ширина в логических единицах прямоугольника назначения*/
int nHeight, /* высота в логических единицах прямоугольника назначения*/
HDC hdcSrc, // описатель контекста устройства источника
int nXSrc, /* x-координата в логических единицах верхнего левого угла прямоугольника источника */
int nYSrc, /* y-координата в логических единицах верхнего левого угла прямоугольника источника */
DWORD dwRop // код растровой операции
) – выполняет перенос битового блока цветовых данных, соответствующих заданному прямоугольнику пикселей, одного контекста устройства в другой контекст устройств. При успешном завершении возвращает TRUE, в противном случае – FALSE. Для получения дополнительной информации об ошибке необходимо вызвать функцию GetLastError.
Код растровой операции определяет, каким образом цвета прямоугольника источника комбинируются с цветами прямоугольника приемника, чтобы дать результирующий цвет. Всего возможно 256 кодов растровой операции, 15 из них имеют следующие имена:
BLACKNESS – заполняет прямоугольник назначения черным цветом;
DSTINVERT – инвертирует побитого цвета прямоугольника назначения;
MERGECOPY – получает цвета прямоугольника назначения побитовой операцией AND, примененной к цветам шаблона (кисти) и цветам прямоугольника источника;
MERGEPAINT – получает цвета прямоугольника назначения побитовой операцией OR, примененной к инвертированным цветам прямоугольника источника и цветам области прямоугольника назначения;
NOTSRCCOPY – инвертирует цвета прямоугольника источника и копирует в прямоугольник назначения;
NOTSRCERASE – комбинирует цвета прямоугольника назначения с цветами области прямоугольника назначения с помощью побитовой операции OR и побитого инвертирует результат;
PATCOPY – заполняет прямоугольник назначения цветом шаблона (кисти);
PATINVERT – комбинирует цвета шаблона (кисти) с цветами области прямоугольника назначения с помощью побитовой операции XOR;
PATPAINT – комбинирует цвета шаблона (кисти) с цветами, полученными побитовой операцией инвертирования цветов прямоугольника источника, с помощью побитовой операцией OR; результат этой операции комбинируется с цветами области прямоугольника назначения с помощью побитовой операции OR;
SRCAND – комбинирует цвета прямоугольника источника с цветами области прямоугольника назначения с помощью побитовой операции AND;
SRCCOPY – копирует прямоугольник источник в область прямоугольника назначения;
SRCERASE – комбинирует посредством побитовой операции AND цвета прямоугольника источника с цветами, полученными операцией побитового инвертирования цветов области прямоугольника назначения;
SRCINVERT – комбинирует цвета прямоугольника источника с цветами области прямоугольника назначения с помощью побитовой операции XOR;
SRCPAINT – комбинирует цвета прямоугольника источника с цветами области прямоугольника назначения с помощью побитовой операции OR;
WHITENESS – заполняет прямоугольник назначения белым цветом.
Для загрузки битового образа и возвращения его дескриптора следует выполнить вызов типа: HBITMAP hBitmap = LoadBitmap(hLibrary /*дескриптор библиотеки динамической компоновки*/, MAKEINTRESOURCE(iBitmap) /*числовой идентификатор ресурса битового образа*/).