лекции / Shchupak_Yu._Win32_API_Razrabotka_prilozheniy_dlya_Windows
.pdf
Вызов функций из DLL |
521 |
|
|
Рис. 11.1. Результаты трех вызовов функции Print из библиотеки MyLib.dll
Завершая эксперимент, давайте посмотрим с помощью отладчика Visual Studio, в какой момент происходит загрузка нашей библиотеки? Информацию, выводи мую отладчиком, следует смотреть в окне Output. Нажмите клавишу F10 для запус ка отладчика в пошаговом режиме. Появившаяся на левом поле окна редактирова ния желтая стрелка показывает строчку кода, которая будет выполнена после очередного нажатия клавиши F10. После первого нажатия (когда еще не выполни лось ни одной строчки кода) в окно Output выводится следующая информация:
Loaded 'C:\WINNT\system32\ntdll.dll', no matching symbolic information found. Loaded symbols for 'F:\ProgWinApi\ImplClient\Debug\MyLib.dll'
Loaded 'C:\WINNT\system32\user32.dll', no matching symbolic information found. Loaded 'C:\WINNT\system32\kernel32.dll', no matching symbolic information found. Loaded 'C:\WINNT\system32\GDI32.DLL', no matching symbolic information found.
Таким образом, мы видим, что библиотека MyLib.dll загружается действительно на этапе загрузки приложения — после загрузки системной библиотеки ntdll.dll.
В заключение отметим, что способ неявной загрузки DLL используется доволь но широко из за своей простоты. Однако этому способу присущи определенные недостатки и ограничения:
Все подключенные DLL загружаются всегда, даже если в течение всего сеанса работы программа ни разу не обратится ни к одной из них.
Если хотя бы одна из требуемых DLL отсутствует, то загрузка исполняемого файла прекращается, а система выдает сообщение наподобие следующего: «A required DLL file MyLib.dll was not found» — даже если отсутствие этой DLL некритично для исполнения программы в нужном для вас режиме.
Явная загрузка DLL
Явная загрузка устраняет отмеченные выше недостатки ценой некоторого услож нения кода. Программисту приходится самому заботиться о загрузке DLL и под ключении экспортируемых функций. Зато явная загрузка позволяет подгружать DLL по мере необходимости и дает возможность программе обрабатывать ситуа ции, возникающие при отсутствии DLL.
В случае явной загрузки процесс работы с DLL происходит в три этапа:
1.Загрузка DLL с помощью функции LoadLibrary (или ее расширенного аналога LoadLibraryEx). В случае успешной загрузки функция возвращает дескриптор hLib типа HMODULE, что позволяет в дальнейшем обращаться к этой DLL.
2.Вызовы функции GetProcAddress для получения указателей на требуемые функ ции или другие объекты. В качестве первого параметра функция GetProcAddress получает дескриптор hLib, в качестве второго параметра — C строку с иденти
522 |
Глава 11. Библиотеки динамической компоновки DLL |
|
|
фикатором импортируемого объекта. Далее полученный указатель использу ется клиентом. Например, если это указатель на функцию, то осуществляется вызов нужной функции.
3.Когда загруженная динамическая библиотека больше не нужна, рекомендуется
ееосвободить, вызвав функцию FreeLibrary. Освобождение библиотеки не озна чает, что операционная система немедленно удалит ее из памяти. Задержка выгрузки предусмотрена на тот случай, когда эта же DLL через некоторое вре мя вновь понадобится какому то процессу. Но если возникнут проблемы с опе ративной памятью, Windows в первую очередь удаляет из памяти освобожден ные библиотеки.
Чтобы продемонстрировать, как работает явная загрузка DLL, создадим кли ентское приложение ExplClient, использующее уже знакомую нам функцию Print из библиотеки MyLib.dll. Текст тестовой программы приведен в листинге 11.3.
Листинг 11.3. Проект ExplClient
//////////////////////////////////////////////////////////////////////
// ExplClient.cpp #include <windows.h>
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
//Дескриптор загружаемой DLL HMODULE hLib;
//Загружаем библиотеку
hLib = LoadLibrary("MyLib.dll"); if (!hLib) {
MessageBox(NULL, "Библиотека MyLib.dll не найдена", "Error", MB_OK); return 1;
}
//MYPROC - тип указателя на функцию, совместимый с прототипом функции,
//вызываемой из DLL
typedef void (*MYPROC) (LPCTSTR str);
//Объявляем указатель на функцию типа MYPROC MYPROC pMyProc;
//Получаем адрес функции Print
pMyProc = (MYPROC)(GetProcAddress(hLib,"Print")); if (!pMyProc) {
MessageBox(NULL, "В DLL отсутствует функция Print", "Error", MB_OK); return 1;
}
//Используем функцию Print из DLL pMyProc("Hello!");
pMyProc("The experiment was a success!"); pMyProc("By!");
//Выгружаем библиотеку из памяти FreeLibrary(hLib);
return 0;
}
//////////////////////////////////////////////////////////////////////
Вызов функций из DLL |
523 |
|
|
Создайте проект типа Win32Applicationи добавьте в его состав файл ExplClient.cpp. Обратите внимание на то, что заголовочный файл MyLib.h в клиентском прило жении, использующем явную загрузку, подключать не надо. Также нет нужды ука
зывать библиотеку импорта в настройках проекта.
После компиляции приступим к тестированию программы. Библиотечный мо дуль MyLib.dll должен находиться в одном каталоге с EXE модулем. Если програм ма запускается из среды Visual Studio, то следует скопировать MyLib.dll в папку Debug. Проверка выполнения приложения показывает, что его поведение не отли чается от поведения программы ImplClient.exe.
Отложенная загрузка DLL
Этот вариант загрузки появился значительно позже первых двух видов, описан ных выше. Например, в среде Visual Studio поддержка данной возможности реа лизована, начиная с шестой версии.
DLL отложенной загрузки (delay load DLL) — это неявно связываемая DLL, которая не загружается до тех пор, пока ваш код не обратится к какому нибудь экспортируемому из нее идентификатору. Такие DLL могут быть полезны в следу ющих ситуациях:
Если ваше приложение использует несколько DLL, его инициализация может занимать длительное время, требуемое загрузчику для проецирования всех DLL на адресное пространство процесса. DLL отложенной загрузки позволяют ре шить эту проблему, распределяя загрузку DLL в ходе выполнения приложения.
Если приложение предназначено для работы в различных версиях ОС, то часть функций может появиться лишь в поздних версиях ОС и не использоваться в текущей версии. Но если программа не вызывает конкретной функции, то DLL ей не нужна, и она может спокойно продолжать работу. При обращении же к не существующей функции можно предусмотреть выдачу пользователю соответ ствующего предупреждения.
Использовать отложенную загрузку DLL достаточно просто, так как большую часть работы берут на себя компилятор и компоновщик, вам необходимо только задать требуемые настройки в проекте клиентского приложения.
Напомним, что для реализации метода неявной загрузки DLL мы добавляли в список библиотек, используемых компоновщиком, требуемую библиотеку им порта (в нашем примере — MyLib.lib). Для реализации метода отложенной загрузки требуется также повторить это действие, но дополнительно в указанный список нужно добавить еще и системную библиотеку импорта delayimp.lib. Кроме этого, требуется добавить в опциях компоновщика флаг /delayload:MyLib.dll.
Перечисленные настройки заставляют компоновщик выполнить следующие операции:
внедрить в ЕХЕ модуль специальную функцию _delayLoadHelper;
удалить MyLib.dll из раздела импорта исполняемого модуля, чтобы загрузчик операционной системы не пытался выполнить неявную загрузку этой библио теки на этапе загрузки приложения;
добавить в ЕХЕ файл новый раздел отложенного импорта со списком функ ций, импортируемых из MyLib.dll;
преобразовать вызовы функций из DLL к вызовам _delayLoadHelper.
Загрузка ресурсов из DLL |
525 |
|
|
Загрузка ресурсов из DLL
Помимо функций, динамические библиотеки могут содержать и ресурсы — строки, пиктограммы, рисунки, и т. д. Хранение ресурсов в DLL особенно удобно при со здании приложений с многоязычным интерфейсом. В этом случае, заменив одну DLL на другую, мы заменяем все надписи в программе, скажем, с русского языка на английский, не меняя при этом кода приложения! Аналогично можно менять пиктограммы, внешний вид диалогов и т. д.
Создание DLL, содержащей только ресурсы, осуществляется аналогично со зданию DLL с исполняемым кодом, но вместо файла *.cpp в проект добавляется файл *.rc. Технология добавления в проект ресурсов различных типов изложена в главе 5.
Однако следует учесть некоторые тонкости. Предположим, что нам нужно со здать библиотеку MyDll.dll, содержащую только ресурсы, при помощи проекта MyDll. Напомним, что после вызова редактора ресурсов соответствующего типа и подго товки самого ресурса (например, таблицы строк) необходимо сохранить создан ный ресурс в файле MyDll.rc. При этом редактор ресурсов автоматически создает заголовочный файл resource.h, содержащий все идентификаторы, встречающиеся в файле MyDll.rc. Впоследствии файл resource.h должен быть подключен директи вой #include к тем файлам клиентского приложения, в которых используются ре сурсы из библиотеки MyDll.dll.
До сих пор нас устраивало имя заголовочного файла resource.h, так как он ис пользовался только в одном проекте. Теперь же, создавая DLL, предназначенную для экспорта в другие проекты, мы должны побеспокоиться об уникальности име ни ее заголовочного файла. Поэтому в технологической цепочке подготовки про екта DLL к компиляции должна появиться операция «изменить имя файла resource.h на имя MyDll.h».
Кроме этого, в настройках компоновщика в проекте DLL необходимо добавить флаг /noentry. Это флаг означает, что DLL содержит только ресурсы, и поэтому ей не нужна входная функция DllMain.
Рассмотрим в качестве примера, как можно расширить функциональность про граммы ImplClient (листинг 11.2), если все строки, которые она использует, вынес ти в отдельную DLL. Но сначала создадим две библиотеки: MyResEng.dll — с англо язычным текстом, и MyResRus.dll — с русскоязычным текстом.
Библиотека MyResEng.dll. Для ее создания выполните следующие шаги:
1.Создайте проект типа Win32 Dynamic-Link Library с именем MyResEng.
2.Вызовите окно редактора таблицы строк, действуя через Insert Resource String Table New.
3.Введите в таблицу три строки:
Идентификатор |
Строка |
|
|
IDS_1 |
Hello! |
IDS_2 |
The experiment was a success! |
IDS_3 |
By! |
|
|
4.Cохраните таблицу строк (File Save As) в файле MyResEng.rc.
5.Откройте файл MyResEng.rc в текстовом режиме и замените ссылку на заголо вочный файл resource.h ссылкой на MyResEng.h.
526 |
Глава 11. Библиотеки динамической компоновки DLL |
|
|
6.Откройте папку проекта и переименуйте файл resource.h на MyResEng.h.
7.Добавьте в состав проекта (вкладка FileView) файл ресурсов MyResEng.rc и заго ловочный файл MyResEng.h.
8.В настройках компоновщика Project Settings Link Project Options добавьте флаг /noentry.
9.Откомпилируйте (F7).
Впапке Debug вы найдете библиотечный модуль MyResEng.dll. Библиотека им порта *.lib в данном случае не нужна, поэтому она и не создается компоновщиком.
Библиотека MyResRus.dll создается аналогично при помощи проекта MyResRus. Единственное отличие — содержание строк, которые вы введете в таблицу строк:
Идентификатор Строка
IDS_1 |
Привет! |
IDS_2 |
Опыт удался! |
IDS_3 |
Ïîêà! |
|
|
Ну и конечно, соответствующие файлы должны иметь имена MyResRus.rc и MyResRus.h. После компиляции будет создан модуль MyResRus.dll.
Клиентское приложение DllResClient.
1.Создайте проект типа Win32 Application с именем DllResClient.
2.Добавьте в его состав файл DllResClient.cpp с текстом, приведенным в листинге 11.5, а также файлы MyLib.h, MyResEng.h и MyResRus.h.
Листинг 11.5. Проект DllResClient
//////////////////////////////////////////////////////////////////////
// |
DllResClient.cpp |
|
|
// |
-------------------------------------------------------------------- |
|
|
// Запуск программы из командной строки с одним параметром: |
|||
// |
DllResClient.exe Eng |
- для англоязычного пользователя |
|
// |
DllResClient.exe Rus |
- для русскоязычного пользователя |
|
//-------------------------------------------------------------------- |
|
|
|
#include |
<windows.h> |
|
|
#include |
"MyLib.h" |
|
|
#include |
"MyResEng.h" |
|
|
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
char cmdLine[100]; strcpy(cmdLine, lpCmdLine);
HMODULE hLib;
if (!strcmp(cmdLine, "Eng"))
hLib = LoadLibraryEx("MyResEng.dll", 0, LOAD_LIBRARY_AS_DATAFILE); else if (!strcmp(cmdLine, "Rus"))
hLib = LoadLibraryEx("MyResRus.dll", 0, LOAD_LIBRARY_AS_DATAFILE); else {
MessageBox(NULL, "Отсутствует или неверно задан параметр в командной строке", "Error", MB_OK);
return 0;
} |
|
char buf[100]; |
|
LoadString(hLib, IDS_1, buf, 99); |
|
Print(buf); |
продолжение |
|
528 |
|
Глава 11. Библиотеки динамической компоновки DLL |
|
|
|
DWORD fdwReason, |
// |
флаг причины вызова функции |
LPVOID lpvReserved |
// |
дополнительная информация |
); |
|
|
В момент вызова функция получает информацию от операционной системы через свои параметры.
Первый параметр, hinstDLL, принимает значение дескриптора модуля DLL, яв ляющееся, по сути, виртуальным адресом загрузки DLL. Если в библиотеке име ются вызовы функций, которым нужен данный дескриптор, необходимо сохранить значение hinstDLL в глобальной переменной.
Второй параметр, fdwReason, может принимать одно из следующих значений:
Значение |
Интерпретация |
|
|
DLL_PROCESS_ATTACH |
Уведомление о том, что DLL загружена в адресное пространство |
|
процесса либо в результате его старта, либо в результате вызова |
|
функции LoadLibrary. |
DLL_THREAD_ATTACH |
Уведомление о том, что текущий процесс создал новый поток. Это |
|
уведомление посылается всем DLL, подключенным к процессу. |
|
Вызов DllMain происходит в контексте нового потока. |
DLL_THREAD_DETACH |
Уведомление о том, что поток корректно завершается. Вызов |
|
DllMain происходит в контексте завершающегося потока. |
DLL_PROCESS_DETACH |
Уведомление о том, что DLL отключается от адресного |
|
пространства процесса в результате одного из трех событий: |
|
а) неудачное завершение загрузки DLL; б) вызов функции |
|
FreeLibrary; в) завершение процесса. |
|
|
Если при вызове функции используется первый параметр со значением DLL_PROCESS_ATTACH, то по значению третьего параметра можно выяснить, каким способом загружается DLL. При явной загрузке параметр lpvReserved равен нулю, а при неявной загрузке принимает ненулевое значение.
Следует отметить, что:
Поток, вызвавший DllMain со значением DLL_PROCESS_ATTACH, не вызывает по вторно DllMain со значением DLL_THREAD_ATTACH.
Когда DLL загружается вызовом функции LoadLibrary, существующие потоки не вызывают DllMain для вновь загруженной библиотеки.
Функция DllMain не вызывается, если поток или процесс завершаются по при чине вызова функции TerminateThread или TerminateProcess.
Поскольку функция DllMain должна обрабатывать все возможные причины сво его вызова, ее код обычно выглядит примерно так:
BOOL APIENTRY DllMain( HANDLE hModule, DWORD dwReason, LPVOID lpReserved)
{
switch (dwReason) {
case DLL_PROCESS_ATTACH:
//Выполняем действия по инициализации (например, выделение памяти)
//Если произошла ошибка, то возвращаем FALSE;
return TRUE;
case DLL_THREAD_ATTACH:
// Выполняем действия по инициализации, связанные с новым потоком break;
case DLL_THREAD_DETACH:
Локальная память потока (TLS) |
529 |
|
|
// Освобождаем переменные, связанные с потоком break;
case DLL_PROCESS_DETACH:
// Выполняем все действия по деинициализации break;
}
return TRUE; // этот код возврата игнорируется
}
Если DLL проектируется с учетом ее использования в многопоточном прило жении, то ей может потребоваться так называемая локальная память потока (TLS). Действия по инициализации и деинициализации такой памяти также осуществ ляются в функции DllMain.
Пример использования функции DllMain приведен далее в листинге 11.7.
Локальная память потока (TLS)
Мы уже говорили, что процесс, загрузивший DLL, получает собственную копию глобальных данных, используемых этой библиотекой. Это защищает процессы, использующие DLL, от взаимного влияния друг на друга. Но если DLL использу ется несколькими потоками одного процесса, то глобальные переменные библио теки разделяются всеми потоками. Это может создавать неприятные проблемы.
Рассмотрим, например, многопоточное приложение MtClient (листинг 11.6), ис пользующее библиотеку динамической загрузки MyLib.dll.
Листинг 11.6. Проект MtClient
//////////////////////////////////////////////////////////////////////
// MtClient.cpp #include <windows.h> #include "MyLib.h"
DWORD WINAPI ThreadFuncA(LPVOID);
DWORD WINAPI ThreadFuncB(LPVOID);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); //====================================================================
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
HANDLE hThreadA = CreateThread(NULL, 0, ThreadFuncA, 0, 0, NULL); HANDLE hThreadB = CreateThread(NULL, 0, ThreadFuncB, 0, 0, NULL);
MessageBox(NULL, "Завершить основной поток?", "WinMain", MB_OK); return 0;
}
//==================================================================== DWORD WINAPI ThreadFuncA(LPVOID lpv)
{
Print("Поток |
А: Привет!"); |
Print("Поток |
А: Эксперимент удался!"); |
Print("Поток |
À: Ïîêà!"); |
return 0; |
|
530 |
Глава 11. Библиотеки динамической компоновки DLL |
|
|
}
//==================================================================== DWORD WINAPI ThreadFuncB(LPVOID lpv)
{
Print("Поток Б: Hello!");
Print("Поток Б: The experiment was a success!"); Print("Поток Б: By!");
return 0;
}
//////////////////////////////////////////////////////////////////////
В этой программе основной поток (функция WinMain) создает два дочерних по тока с входными функциями ThreadFuncA и ThreadFuncB. В каждом дочернем потоке имеются три вызова DLL функции Print.
Создайте проект типа Win32 Application с именем MtClient и добавьте в его состав файлы MtClient.cpp и MyLib.h. Скопируйте в папку проекта библиотеку импорта MyLib.lib. Добавьте в настройки компоновщика ссылку на библиотеку импорта
MyLib.lib.
После компиляции проекта скопируйте в папку Debug файл MyLib.dll и запусти те EXE файл на выполнение. На экране появятся три диалоговых окна, сформи рованных вызовами функции MessageBox:
N |
Заголовок окна |
Текст в окне |
|
|
|
1 |
MyLib: Print: вызов 1 |
Поток А: Привет! |
2 |
MyLib: Print: вызов 2 |
Поток Б: Hello! |
3 |
WinMain |
Завершить основной поток? |
|
|
|
Однако, скорее всего, вы увидите только третье окно, поскольку первые два окна будут лежать под ним. Так уж устроена функция MessageBox — она размещает диалоговое окно всегда в центре экрана. Но, переместив с помощью мыши эти окна в разные позиции, вы получите возможность увидеть одновременно все три окна.
Обратите внимание на заголовок окна, сформированного функцией Print из по тока Á. В заголовке указано, что это уже второй вызов данной функции, хотя мы точно знаем, что для потока Á он является первым. Если мы продолжим работать с программой, щелкая на кнопке OK в появляющихся диалоговых окнах, то уви дим, что нумерация вызовов функции Print является сквозной (общей для всех потоков). Причина очевидна — эта нумерация формируется при помощи глобаль ной переменной count в файле MyLib.cpp.
А теперь представьте, что мы создаем серверное приложение, которое обраба тывает каждый клиентский запрос, создавая отдельный поток, и в этом потоке вызывается DLL функция, выполняющая некоторую работу. Но кроме основной работы она подсчитывает количество обращений клиента, чтобы потом выставить банковский счет за обслуживание. Понятно, что глобальная переменная count в ис ходном коде DLL плохо справится с такой задачей.
Возможны и другие ситуации, когда некоторый объект должен быть глобаль ным для отдельного потока, но при этом не разделяться другими потоками про цесса.
Чтобы обеспечить такую возможность, в Win32 API был создан механизм ло кальной памяти потока (Thread Local Storage — TLS). Существуют две разновид ности такой памяти: а) динамическая TLS, б) статическая TLS.
