Часть III. Более сложные примеры программирования в Windows
Глава 1. Примеры программ, использующих таймер
Таймер является одним из мощных инструментов, предоставляемых операционной системой и позволяющих решать самые разнообразные задачи. С таймером Вы познакомились, когда занимались консольными приложениями. Там мы пользовались функциями timeSetEvent и timeKillEvent. Для консольных приложений это очень удобные функции. В оконных приложениях чаще используют функции SetTimer и KillTimer. Особенность таймера, создаваемого функцией SetTimer, заключается в том, что сообщение WM_TIMER, которое начинает посылать система приложению после выполнения функции SetTimer, приходит со всеми другими сообщениями наравне, на общих основаниях. Следовательно, интервал между двумя приходами сообщения WM_TIMER может несколько варьироваться. В большинстве случаев это не существенно.
У сообщения таймера есть еще одна особенность. Если система посылает сообщение приложению, а предыдущее сообщение еще стоит в очереди, то система объединяет эти два сообщения. Таким образом, "вынужденный простой" не приводит к приходу на приложение подряд нескольких сообщений таймера.
Вот те задачи, которые можно решить с помощью таймера.
-
Отслеживание времени: секундомер, часы и т.д. Нарушение периодичности не имеет значения, так
как по приходе сообщения время можно отследить, вызвав функцию получения системного времени.
Таймер - один из способов осуществления многозадачности. Можно установить сразу несколько
таймеров на разные функции, в результате периодически будет исполняться то одна, то другая
функция. Более подробно о многозадачности будет сказано в следующей главе.
Периодический вывод на экран обновленной информации.
Автосохранение - функция особенно полезная для редакторов.
Задание темпа изменения каких-либо объектов на экране.
Мультипликация - по приходе сообщения от таймера обновляется графическое содержимое экрана или
окна, так что возникает эффект мультипликации.
Рассмотрим, как нужно обращаться с функцией SetTimer. Вот параметры этой функции.
1-й параметр - дескриптор окна, с которым ассоциируется таймер. Если этот параметр сделать равным
NULL (0), то будет проигнорирован и второй параметр.
2-й параметр - определяет идентификатор таймера.
3-й параметр - определяет интервал посылки сообщения WM_TIMER.
4-й параметр - определяет адрес функции, на которую будет приходить сообщение
WM_TIMER. Если параметр равен NULL, то сообщение будет приходить на
функцию окна.
Если функция выполнилась успешно, то возвращаемым значением будет являться идентификатор таймера, который, естественно, будет совпадать со вторым параметром, если первый параметр будет отличным от NULL. В случае неудачи функция возвратит ноль.
Из сказанного следует, что функция может быть вызвана тремя способами:
-
Задан дескриптор окна, а четвертый параметр задается равным нулю.
Задан дескриптор окна, а четвертый параметр определяет функцию, на которую будет приходить
сообщение WM_TIMER.
Дескриптор окна равен NULL, а четвертый параметр определяет функцию, на которую
будет приходить сообщение WM_TIMER. Идентификатор таймера в этом случае будет
определяться по возвращаемому функцией значению.
Функция, на которую приходит сообщение WM_TIMER, имеет следующие параметры:
1-й параметр - дескриптор окна, с которым ассоциирован таймер.
2-й параметр - сообщение WM_T1MER.
3-й параметр - идентификатор таймера.
4-й параметр - время в миллисекундах, которое прошло с момента запуска Windows.
Функция KillTimer удаляет созданный параметр и имеет следующие параметры:
1-й параметр - дескриптор окна.
2-й параметр - идентификатор таймера.
I
Первый пример, рассматриваемый в данном разделе, представляет простейший пример таймера. Таймер отсчитывает десять тиков и закрывает диалоговое окно, выдавая MessageBox с сообщением об окончании работы программы. Данная программа являет собой пример организации таймера на базе самой функции окна.
Заметим, что начиная с данной главы большинство приводимых программ могут транслироваться и в пакете MASM32, и в пакете TASM32 без всяких изменений.
// файл timer.rc // определение констант #define WS_SYSMENU 0x00080000L #define WS_MINIMIZEBOX 0x00020000L #define WS_MAXIMIZEBOX 0x00010000L // стиль - кнопка #define BS_PUSHBUTTON 0x00000000L // кнопка в окне должна быть видимой #define WS_VISIBLE 0x10000000L // центрировать текст на кнопке #define BS_CENTER 0x00000300L // стиль кнопки #define WS_CHILD 0х40000000L // возможность фокусировать элемент // при помощи клавиши TAB #define WS_TABSTOP 0x00010000L #define DS_3DLOOK 0x0004L //определение диалогового окна DIAL1 DIALOG 0, 0, 240, 120 STYLE WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | DS_3DLOOK CAPTION "Пример диалогового окна с таймером" FONT 8, "Arial" { // кнопка, идентификатор 5 CONTROL "Выход", 5, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 180, 76, 50, 14 } ; файл timer.inc ; константы ; сообщение приходит при закрытии окна WM_CLOSE equ 10h WM_INITDIALOG equ 110h WM_COMMAND equ 111h WM_TIMER equ 113h ; прототипы внешних процедур IFDEF MASM EXTERN ReleaseDC@8:NEAR EXTERN GetDC@4:NEAR EXTERN TextOutA@20:NEAR EXTERN MessageBoxA@16:NEAR EXTERN ExitProcess@4:NEAR EXTERN GetModuleHandleA@4:NEAR EXTERN DialogBoxParamA@20:NEAR EXTERN EndDialog@8:NEAR EXTERN SendMessageA@16:NEAR EXTERN SetTimer@16:NEAR EXTERN KillTimer@8:NEAR ELSE EXTERN ReleaseDC:NEAR EXTERN GetDC:NEAR EXTERN TextOutA:NEAR EXTERN MessageBoxA:NEAR EXTERN ExitProcess:NEAR EXTERN GetModuleHandleA:NEAR EXTERN DialogBoxParamA:NEAR EXTERN EndDialog:NEAR EXTERN SendMessageA:NEAR EXTERN SetTimer:NEAR EXTERN KillTimer:NEAR ReleaseDC@8 = ReleaseDC GetDC@4 = GetDC TextOutA@20 = TextOutA MessageBoxA@16 = MessageBoxA ExitProcess@4 = ExitProcess GetModuleHandleA@4 = GetModuleHandleA DialogBoxParamA@20 = DialogBoxParamA EndDialog@8 = EndDialog SendMessageA@16 = SendMessageA SetTimer@16 = SetTimer KillTimer@8 = KillTimer ENDIF ; структуры ; структура сообщения MSGSTRUCT STRUC MSHWND DD ? MSMESSAGE DD ? MSWPARAM DD ? MSLPARAM DD ? MSTIME DD ? MSPT DD ? MSGSTRUCT ENDS ; файл timer.asm .386P ; плоская модель .MODEL FLAT, stdcall include timer.inc ; директивы компоновщику для подключения библиотек IFDEF MASM ; для компоновщика LINK.EXE includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib includelib c:\masm32\lib\gdi32.lib ELSE ; для компоновщика TLINK32.EXE includelib c:\tasm32\lib\import32.lib ENDIF ;------------------------------------------------- ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' MSG MSGSTRUCT <?> HINST DD 0 ; дескриптор приложения PA DB "DIAL1",0 COUNT DD 0 TEXT DB 0 CAP DB 'Сообщение',0 MES DB 'Выход по таймеру',0 _DATA ENDS ; сегмент кода_TEXT SEGMENT DWORD PUBLIC USE32 'CODE' START: ; получить дескриптор приложения PUSH 0 CALL GetModuleHandleA@4 MOV [HINST], EAX ;---------------------------------------- PUSH 0 PUSH OFFSET WNDPROC PUSH 0 PUSH OFFSET PA PUSH [HINST] CALL DialogBoxParamA@20 CMP EAX,-1 JNE KOL KOL: ;---------------------------------------- PUSH 0 CALL ExitProcess@4 ;---------------------------------------- ; процедура окна ; расположение параметров в стеке ; [EBP+014Н] ; LPARAM ; [EBP+10Н] ; WAPARAM ; [EBP+0CH] ; MES ; [EBP+8] ; HWND WNDPROC PROC PUSH EBP MOV EBP,ESP PUSH EBX PUSH ESI PUSH EDI ;--------------- CMP DWORD PTR [EBP+0CH],WM_CLOSE JNE L1 ; здесь реакция на закрытие окна L3: ; удалить таймер PUSH 1 ; идентификатор таймера PUSH DWORD PTR [EBP+08H] CALL KillTimer@8 ; закрыть диалог PUSH 0 PUSH DWORD PTR [EBP+08H] CALL EndDialog@8 JMP FINISH L1: CMP DWORD PTR [EBP+0CH], WM_INITDIALOG JNE L5 ; здесь начальная инициализация ; установить таймер PUSH 0 ; параметр = NULL PUSH 1000 ; интервал 1 с. PUSH 1 ; идентификатор таймера PUSH DWORD PTR [EBP+08H] CALL SetTimer@16 JMP FINISH L5: CMP DWORD PTR [EBP+0CH], WM_COMMAND JNE L2 ; кнопка выхода? CMP WORD PTR [EBP+10H],5 JNE FINISH JMP L3 L2: CMP DWORD PTR [EBP+0CH],WM_TIMER JNE FINISH ; не пора ли заканчивать ? CMP COUNT,9 ; выход без предупреждения JA L3 ; выход через сообщение JE L4 ; пришло сообщение таймера ; подготовить текст MOV EAX, COUNT ADD EAX,49 MOV TEXT, AL ; получить контекст PUSH DWORD PTR [EBP+08H] CALL GetDC@4 ; запомнить контекст PUSH EAX ; вывести значение счетчика PUSH 1 PUSH OFFSET TEXT PUSH 10 PUSH 10 PUSH EAX CALL TextOutA@20 ; удалить контекст POP EAX PUSH EAX PUSH DWORD PTR [EBP+08H] CALL ReleaseDC@8 ; увеличить счетчик INC COUNT JMP FINISH L4: INC COUNT ; сообщение о выходе по таймеру PUSH 0 PUSH OFFSET CAP PUSH OFFSET MES PUSH DWORD PTR [EBP+08H] ; дескриптор окна CALL MessageBoxA@16 JMP L3 FINISH: POP EDI POP ESI POP EBX POP EBP MOV EAX,0 RET 16 WNDPROC ENDP ;------------------------------------------------- _TEXT ENDS END START
Рис. 3.1.1 Пример реализации простейшего таймера.
Прокомментируем программу на Рис. 3.1.1. Организация таймера здесь проста и очевидна. Единственное, что может вызвать трудность понимания, это то, как удается оставить MessageBox и одновременно закрыть диалоговое окно. Но здесь тоже все достаточно просто: сообщение появляется при COUNT=9, а когда приходит следующее сообщение, то COUNT уже больше 9, и выполняется ветка закрытия диалогового окна.
Трансляция программы.
Пакет MASM32.
ML /c /coff /DMASM timer.asm RC timer.rc LINK /SUBSYSTEM:WINDOWS timer.obj timer.resПакет TASM32.
TASM32 /ml timer.asm BRCC32 timer.rc TLINK32 -aa timer.obj,,,,,timer.res
II
Следующая программа несколько сложнее предыдущей. Здесь действуют два таймера. Можно считать, что запускаются одновременно две задачи39. Одна задача с периодичностью 0.5 сек. получает системное время и формирует строку для вывода (STRCOPY). Эта задача имеет свою собственную функцию, на которую приходит сообщение WM_TIMER. Вторая задача работает в рамках функции окна. Эта задача с периодичностью 1 сек. выводит время и дату в окно редактирования, расположенное на диалоговом окне. Таким образом, две задачи взаимодействуют друг с другом посредством глобальной переменной STRCOPY.
Еще один важный момент хотелось бы отметить в связи с данной программой. Поскольку на функцию таймера приходит сообщение, в котором указан идентификатор таймера, мы можем на базе одной функции реализовать любое количество таймеров.
// файл timer2.rc // определение констант #define WS_SYSMENU 0x00080000L // элементы на окне должны быть изначально видимы #define WS_VISIBLE 0x10000000L // бордюр вокруг элемента #define WS_BORDER 0x00800000L // при помощи TAB можно по очереди активизировать элементы #define WS_TABSTOP 0x00010000L // текст в окне редактирования прижат к левому краю #define ES_LEFT 0x0000L // стиль всех элементов на окне #define WS_CHILD 0x40000000L // запрещается ввод с клавиатуры #define ES_READONLY 0x0800L #define DS_3DLOOK 0x0004L // определение диалогового окна DIAL1 DIALOG 0, 0, 240, 100 STYLE WS_SYSMENU | DS_3DLOOK CAPTION "Диалоговое окно с часами и датой" FONT 8, "Arial" { CONTROL "", 1, "edit", ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_READONLY, 100, 5, 130, 12 } ; файл timer2.inc ; константы ; сообщение приходит при закрытии окна WM_CLOSE equ 10h ;сообщение приходит при создании окна WM_INITDIALOG equ 110h ;сообщение приходит при событии с элементом на окне WM_COMMAND equ 111h ;сообщение от таймера WM_TIMER equ 113h ; сообщение посылки текста элементу WM_SETTEXT equ 0Ch ; прототипы внешних процедур IFDEF MASM EXTERN SendDlgItemMessageA@20:NEAR EXTERN wsprintfA:NEAR EXTERN GetLocalTime@4:NEAR EXTERN ExitProcess@4:NEAR EXTERN GetModuleHandleA@4:NEAR EXTERN DialogBoxParamA@20:NEAR EXTERN EndDialog@8:NEAR EXTERN SetTimer@16:NEAR EXTERN KillTimer@8:NEAR ELSE EXTERN SendDlgItemMessageA:NEAR EXTERN _wsprintfA:NEAR EXTERN GetLocalTime:NEAR EXTERN ExitProcess:NEAR EXTERN GetModuleHandleA:NEAR EXTERN DialogBoxParamA:NEAR EXTERN EndDialog:NEAR EXTERN SetTimer:NEAR EXTERN KillTimer:NEAR SendDlgItemMessageA@20 = SendDlgItemMessageA wsprintfA = _wsprintfA GetLocalTime@4 = GetLocalTime ExitProcess@4 = ExitProcess GetModuleHandleA@4 = GetModuleHandleA DialogBoxParamA@20 = DialogBoxParamA EndDialog@8 = EndDialog SetTimer@16 = SetTimer KillTimer@8 = KillTimer ENDIF ; структуры ; структура сообщения MSGSTRUCT STRUC MSHWND DD ? MSMESSAGE DD ? MSWPARAM DD ? MSLPARAM DD ? MSTIME DD ? MSPT DD ? MSGSTRUCT ENDS ; структура данных дата-время DAT STRUC year DW ? month DW ? dayweek DW ? day DW ? hour DW ? min DW ? sec DW ? msec DW ? DAT ENDS ; файл timer2.asm .386P ; плоская модель .MODEL FLAT, stdcall include timer2.inc ; директивы компоновщику для подключения библиотек IFDEF MASM ; для компоновщика LINK.EXE includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib includelib c:\masm32\lib\gdi32.lib ELSE ; для компоновщика TLINK32.EXE includelib c:\tasm32\lib\import32.lib ENDIF ;------------------------------------------------------ ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' MSG MSGSTRUCT <?> HINST DD 0 ; дескриптор приложения PA DB "DIAL1",0 TIM DB "Дата %u/%u/%u Время %u:%u:%u",0 STRCOPY DB 50 DUP (?) DATA DAT <0> _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' START: ; получить дескриптор приложения PUSH 0 CALL GetModuleHandleA@4 MOV [HINST], EAX ; создать диалоговое окно PUSH 0 PUSH OFFSET WNDPROC PUSH 0 PUSH OFFSET PA PUSH [HINST] CALL DialogBoxParamA@20 CMP EAX,-1 JNE KOL ; сообщение об ошибке KOL: ;-------------------------------------- PUSH 0 CALL ExitProcess@4 ;-------------------------------------- ; процедура окна ; расположение параметров в стеке ; [EBP+014Н] ; LPARAM ; [EBP+10Н] ; WAPARAM ; [EBP+0CH] ; MES ; [EBP+8] ; HWND WNDPROC PROC PUSH EBP MOV EBP,ESP PUSH EBX PUSH ESI PUSH EDI ;----------------- CMP DWORD PTR [EBP+0CH],WM_CLOSE JNE L1 ; здесь реакция на закрытие окна ; удалить таймер 1 PUSH 1 ; идентификатор таймера PUSH DWORD PTR [EBP+08H] CALL KillTimer@8 ; удалить таймер 2 PUSH 2 ; идентификатор таймера PUSH DWORD PTR [EBP+08H] CALL KillTimer@8 ; закрыть диалог PUSH 0 PUSH DWORD PTR [EBP+08H] CALL EndDialog@8 JMP FINISH L1: CMP DWORD PTR [EBP+0CH], WM_INITDIALOG JNE L2 ; здесь начальная инициализация ; установить таймер 1 PUSH 0 ; параметр = NULL PUSH 1000 ; интервал 1 с. PUSH 1 ; идентификатор таймера PUSH DWORD PTR [EBP+08H] CALL SetTimer@16 ; установить таймер 2 PUSH OFFSET TIMPROC ; параметр = NULL PUSH 500 ; интервал 0.5 с. PUSH 2 ; идентификатор таймера PUSH DWORD PTR [EBP+08H] CALL SetTimer@16 JMP FINISH L2: CMP DWORD PTR [EBP+0CH],WM_TIMER JNE FINISH ; отправить строку в окно PUSH OFFSET STRCOPY PUSH 0 PUSH WM_SETTEXT PUSH 1 ; идентификатор элемента PUSH DWORD PTR [EBP+08H] CALL SendDlgItemMessageA@20 FINISH: POP EDI POP ESI POP EBX POP EBP MOV EAX,0 RET 16 WNDPROC ENDP ;-------------------------------- ;процедура таймера ; расположение параметров в стеке ; [EBP+014Н] ; LPARAM - промежуток запуска Windows ; [EBP+10Н] ; WAPARAM - идентификатор таймера ; [EBP+0CH] ; WM_TIMER ; [EBP+8] ; HWND TIMPROC PROC PUSH EBP MOV EBP,ESP ; получить локальное время PUSH OFFSET DATA CALL GetLocalTime@4 ; получить строку для вывода даты и времени MOVZX EAX,DATA.sec PUSH EAX MOVZX EAX,DATA.min PUSH EAX MOVZX EAX,DATA.hour PUSH EAX MOVZX EAX,DATA.year PUSH EAX MOVZX EAX,DATA.month PUSH EAX MOVZX EAX,DATA.day PUSH EAX PUSH OFFSET TIM PUSH OFFSET STRCOPY CALL wsprintfA ; восстановить стек ADD ESP,32 POP EBP RET 16 TIMPROC ENDP _TEXT ENDS END START
Puc. 3.1.2. Пример использования двух таймеров.
Трансляция программы.
Пакет MASM32.
ML /c /coff /DMASM timer2.asm RC timer2.rc LINK /SUBSYSTEM:WINDOWS timer2.obj timer2.resПакет TASM32.
TASM32 /ml timer2.asm BRCC32 timer2.rc TLINK32 -aa timer2.obj,,,,,timer2.res
Puc. 3.1.3. Результат работы программы на Рис. 3.1.2.
Обращаю Ваше внимание на весьма полезную функцию GetLocalTime. Информация, полученная с помощью этой функции (см. структуру DAT), легко может быть использована для самых разных целей, в том числе и для вывода на экран. Аналогично, с помощью функции SetLocalTime, Вы сможете установить текущее время. Для получения времени по Гринвичу используется функция GetSystemTime, которая, соответственно, с помощью SetSystemTime используется для установки времени в Гринвичском выражении. Аргументом во всех этих функциях является уже упомянутая выше структура (точнее указатель на нее).
39 Вообще говоря, три, так как само диалоговое окно также работает независимо.
III
В данном разделе мы рассмотрим весьма интересный вопрос о всплывающих подсказках. В визуальных языках программирования всплывающие подсказки организуются посредством установки соответствующих свойств объектов, расположенных на объекте-контейнере. Наша с вами задача разработать механизм, позволяющий без каких-либо дополнительных библиотек устанавливать подсказки на любые объекты, расположенные в окне. Итак, приступаем.
1. Прежде всего заметим, что всплывающая подсказка - это всего лишь окно с определенными свойствами. Вот эти свойства: DS_3DLOOK, WS_POPUP, WS_VISIBLE, WS_BORDER. В принципе можно экспериментировать - добавлять или удалять свойства. Но без одного свойства Вы никак не обойдетесь - это WS_POPUP. Собственно POPUP можно перевести как поплавок. Кроме того, определение всплывающего окна в файле ресурсов не должно содержать опции CAPTION.
2. Появление подсказки не должно менять ситуацию в диалоговом окне. Это значит - вызов подсказки должен быть немодальным, при помощи функции CreateDialogIndirect. Кроме того, следует предусмотреть переустановку фокуса на диалоговое окно. Для этого достаточно в нужном месте (см. Рис. 3.1.3) вызвать функцию SetFocus.
3. Итак, подсказка - это диалоговое окно, и, следовательно, оно должно иметь свою функцию. Что должна содержать эта функция? По крайней мере, обработку трех событий: WM_INITDIALOG, WM_PA1NT, WM_TIMER. По получении сообщения WM_INITDIALOG следует определить размер и положение подсказки. Кроме того, если мы предполагаем, что подсказка должна спустя некоторое время исчезать, следует установить таймер. По получении сообщения WM_PAINT следует вывести в окно подсказки текст. Если определять размер окна подсказки точно по строке выводимого текста, то цвет фона подсказки будет полностью определяться цветом выводимого текста. Наконец по приходе сообщения WM_TIMER мы закрываем подсказку.
4. С самой подсказкой более или менее ясно. Определимся теперь, как и где будет вызываться эта
подсказка. Мне более импонирует такой подход: в основном диалоговом окне определяем таймер, в
функции которого и будет проверяться положение курсора. В зависимости от этого положения и будет
вызываться или удаляться подсказка. В функции таймера должно предусмотреть:
-
Проверку положения курсора. Если курсор оказался на данном элементе, то вызывать подсказку.
При этом желательно, чтобы подсказка появлялась бы с некоторой задержкой. Последнее можно
обеспечить введением счетчика - вызывать подсказку, если счетчик превысил некоторое значение.
Необходимо обеспечить удаление подсказки, если курсор покидает данный элемент.
На Рис. 3.1.3 представлена программа, которая демонстрирует описанный выше подход. На Рис. 3.1.4 представлено диалоговое окно с подсказками. В принципе, означенный подход не является единственным, и, разобравшись в данном подходе, вы сможете пофантазировать и придумать свои способы создания подсказок.
// файл HINT.RC // определение констант #define WS_SYSMENU 0x00080000L // элементы на окне должны быть изначально видимы #define WS_VISIBLE 0x10000000L // бордюр вокруг элемента #define WS_BORDER 0x00800000L // при помощи TAB можно по очереди активизировать элементы #define WS_TABSTOP 0x00010000L // текст в окне редактирования прижат к левому краю #define ES_LEFT 0x0000L // стиль всех элементов на окне #define WS_CHILD 0x40000000L // стиль - кнопка #define BS_PUSHBUTTON 0x00000000L // центрировать текст на кнопке #define BS_CENTER 0x00000300L // тип окна - "поплавок" #define WS_POPUP 0x80000000L // стиль - диалоговое окно Windows 95 #define DS_3DLOOK 0x0004L // определение диалогового окна DIAL1 DIALOG 0, 0, 240, 100 STYLE WS_SYSMENU | DS_3DLOOK CAPTION "Окно с всплывающими подсказками" FONT 8, "Arial" { // окно редактирования, идентификатор 1 CONTROL "", 1, "edit", ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP , 100, 5, 130, 12 // кнопка, идентификатор 2 CONTROL "Выход", 2, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 180, 76, 50, 14 } // диалоговое окно подсказки HINTW DIALOG 0, 0, 240, 8 STYLE DS_3DLOOK | WS_POPUP | WS_VISIBLE | WS_BORDER FONT 8, "MS Sans Serif" { } ; файл HINT.INC ;константы ;цвет фона окна подсказки RED = 255 GREEN = 255 BLUE = 150 RGBB equ (RED or (GREEN shl 8)) or (BLUE shl 16) ;цвет текста окна подсказки RED = 20 GREEN = 20 BLUE = 20 RGBT equ (RED or (GREEN shl 8)) or (BLUE shl 16) ;сообщение приходит при закрытии окна WM_CLOSE equ 10h WM_INITDIALOG equ 110h WM_COMMAND equ 111h WM_TIMER equ 113h WM_SETTEXT equ 0Ch WM_COMMAND equ 111h WM_PAINT equ 0Fh ;прототипы внешних процедур IFDEF MASM EXTERN CreateDialogParamA@20:NEAR EXTERN SetFocus@4:NEAR EXTERN lstrcpyA@8:NEAR EXTERN DestroyWindow@4:NEAR EXTERN lstrlenA@4:NEAR EXTERN GetDlgItem@8:NEAR EXTERN GetCursorPos@4:NEAR EXTERN TextOutA@20:NEAR EXTERN SetBkColor@8:NEAR EXTERN SetTextColor@8:NEAR EXTERN BeginPaint@8:NEAR EXTERN EndPaint@8:NEAR EXTERN GetTextExtentPoint32A@16:NEAR EXTERN MoveWindow@24:NEAR EXTERN GetWindowRect@8:NEAR EXTERN ReleaseDC@8:NEAR EXTERN GetDC@4:NEAR EXTERN SendDlgItemMessageA@20:NEAR EXTERN ExitProcess@4:NEAR EXTERN GetModuleHandleA@4:NEAR EXTERN DialogBoxParamA@20:NEAR EXTERN EndDialog@8:NEAR EXTERN SetTimer@16:NEAR EXTERN KillTimer@8:NEAR ELSE EXTERN CreateDialogParamA:NEAR EXTERN SetFocus:NEAR EXTERN lstrcpyA:NEAR EXTERN DestroyWindow:NEAR EXTERN lstrlenA:NEAR EXTERN GetDlgItem:NEAR EXTERN GetCursorPos:NEAR EXTERN TextOutA:NEAR EXTERN SetBkColor:NEAR EXTERN SetTextColor:NEAR EXTERN BeginPaint:NEAR EXTERN EndPaint:NEAR EXTERN GetTextExtentPoint32A:NEAR EXTERN MoveWindow:NEAR EXTERN GetWindowRect:NEAR EXTERN ReleaseDC:NEAR EXTERN GetDC:NEAR EXTERN SendDlgItemMessageA:NEAR EXTERN ExitProcess:NEAR EXTERN GetModuleHandleA:NEAR EXTERN DialogBoxParamA:NEAR EXTERN EndDialog:NEAR EXTERN SetTimer:NEAR EXTERN KillTimer:NEAR CreateDialogParamA@20 = CreateDialogParamA SetFocus@4 = SetFocus lstrcpyA@8 = IstrcpyA DestroyWindow@4 = DestroyWindow lstrlenA@4 = IstrlenA GetDlgItem@8 = GetDlgItem GetCursorPos@4 = GetCursorPos TextOutA@20 = TextOutA SetBkColor@8 = SetBkColor SetTextColor@8 = SetTextColor BeginPaint@8 = BeginPaint EndPaint@8 = EndPaint GetTextExtentPoint32A@16 = GetTextExtentPoint32A MoveWindow@24 = MoveWindow GetWindowRect@8 = GetWindowRect ReleaseDC@8 = ReleaseDC GetDC@4 = GetDC SendDlgItemMessageA@20=SendDlgItemMessageA ExitProcess@4 = ExitProcess GetModuleHandleA@4 = GetModuleHandleA DialogBoxParamA@20 = DialogBoxParamA EndDialog@8 = EndDialog SetTimer@16 = SetTimer KillTimer@8 = KillTimer ENDIF ; структуры ; структура сообщения MSGSTRUCT STRUC MSHWND DD ? MSMESSAGE DD ? MSWPARAM DD ? MSLPARAM DD ? MSTIME DD ? MSPT DD ? MSGSTRUCT ENDS ; структура размера окна RECT STRUC L DD ? T DD ? R DD ? B DD ? RECT ENDS ;структура размер SIZ STRUC X DD ? Y DD ? SIZ ENDS ; структура для BeginPaint PAINTSTR STRUC hdc DWORD 0 fErase DWORD 0 left DWORD 0 top DWORD 0 right DWORD 0 bottom DWORD 0 fRes DWORD 0 fIncUp DWORD 0 Reserv DB 32 dup (0) PAINTSTR ENDS ; структура для получения позиции курсора POINT STRUC X DD ? Y DD ? POINT ENDS ; файл HINT.ASM .386P ; плоская модель .MODEL FLAT, stdcall include hint.inc ; директивы компоновщику для подключения библиотек IFDEF MASM ; для компоновщика LINK.EXE includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib includelib c:\masm32\lib\gdi32.lib ELSE ; для компоновщика TLINK32.EXE includelib c:\tasm32\lib\import32.lib ENDIF ;------------------------------------------------- ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' MSG MSGSTRUCT <?> HINST DD 0 ; дескриптор приложения PA DB "DIAL1",0 HIN DB "HINTW",0 XX DD ? YY DD ? ;------------------------------- R1 RECT <?> R2 RECT <?> S SIZ <?> PS PAINTSTR <?> PT POINT <?> ; дескрипторы окон-подсказок, для первого и второго элемента H1 DD 0 H2 DD 0 ; строка-подсказка HINTS DB 60 DUP (?) ; перечень подсказок HINT1 DB "Редактирование строки",0 HINT2 DB "Кнопка выхода",0 ; для временного хранения контекста устройства DC DD ? ; счетчик P1 DD ? _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' START: ; получить дескриптор приложения PUSH 0 CALL GetModuleHandleA@4 MOV [HINST], EAX ;---------------------------- PUSH 0 PUSH OFFSET WNDPROC PUSH 0 PUSH OFFSET PA PUSH [HINST] CALL DialogBoxParamA@20 CMP EAX,-1 JNE KOL KOL: ;----------------------------- PUSH 0 CALL ExitProcess@4 ;----------------------------- ; процедура окна ; расположение параметров в стеке ; [EBP+014Н] ; LPARAM ; [EBP+10Н] ; WAPARAM ; [EBP+0CH] ; MES ; [EBP+8] ; HWND WNDPROC PROC PUSH EBP MOV EBP,ESP PUSH EBX PUSH ESI PUSH EDI ;------------------------------ CMP DWORD PTR [EBP+0CH],WM_CLOSE JNE L1 ; здесь реакция на закрытие окна ; удалить таймер L4: PUSH 2 ; идентификатор таймера PUSH DWORD PTR [EBP+08H] CALL KillTimer@8 ; закрыть диалог PUSH 0 PUSH DWORD PTR [EBP+08H] CALL EndDialog@8 JMP FINISH L1: CMP DWORD PTR [EBP+0CH], WM_INITDIALOG JNE L2 ; здесь начальная инициализация ; установить таймер PUSH OFFSET TIMPROC PUSH 500 ; интервал 0.5 с. PUSH 2 ; идентификатор таймера PUSH DWORD PTR [EBP+08H] CALL SetTimer@16 JMP FINISH L2: CMP DWORD PTR [EBP+0CH],WM_COMMAND JNE L3 ; кнопка выхода? CMP WORD PTR [EBP+10H],2 JNE L3 JMP L4 L3: FINISH: POP EDI POP ESI POP EBX POP EBP MOV EAX,0 RET 16 WNDPROC ENDP ;--------------------------------------------- ; процедура таймера ; расположение параметров в стеке ; [EBP+014Н] ; LPARAM - промежуток запуска Windows ; [EBP+10Н] ; WAPARAM - идентификатор таймера ; [EBP+0CH] ; WM_TIMER ; [EBP+8] ; HWND TIMPROC PROC PUSH EBP MOV EBP,ESP ; получить положение курсора PUSH OFFSET PT CALL GetCursorPos@4 ; запомнить координаты MOV EAX,PT.X MOV XX,EAX MOV EAX,PT.Y MOV YY,EAX ; получить положение элементов ; окно редактирования PUSH 1 PUSH DWORD PTR [EBP+08H] CALL GetDlgItem@8 PUSH OFFSET R1 PUSH EAX CALL GetWindowRect@8 ; кнопка выхода PUSH 2 PUSH DWORD PTR [EBP+08H] CALL GetDlgItem@8 PUSH OFFSET R2 PUSH EAX CALL GetWindowRect@8 ; увеличить счетчик INC P1 MOV ECX,XX MOV EDX,YY ; проверка условий .IF H1==0 && P1>5 .IF EDX<=R1.B && EDX>=R1.T && ECX>=R1.L && ECX<=R1.R ; подготовить строку PUSH OFFSET HINT1 PUSH OFFSET HINTS CALL lstrcpyA@8 ; создать диалоговое окно - подсказку PUSH 0 PUSH OFFSET HINT PUSH DWORD PTR [EBP+08H] PUSH OFFSET HIN PUSH [HINST] CALL CreateDialogParamA@20 MOV H1,EAX ; установить фокус PUSH DWORD PTR [EBP+08H] CALL SetFocus@4 ; обнулить счетчик MOV P1,0 JMP _END .ENDIF .ENDIF .IF H1!=0 .IF (EDX>R1.B || EDX<R1.T) || (ECX<R1.L || ECX>R1.R) ; удаление подсказки в связи с перемещением курсора PUSH H1 CALL DestroyWindow@4 MOV H1,0 JMP _END .ENDIF .ENDIF .IF H2==0 && P1>5 .IF EDX<=R2.B && EDX>=R2.T && ECX>=R2.L && ECX<=R2.R ; подготовить строку PUSH OFFSET HINT2 PUSH OFFSET HINTS CALL lstrcpyA@8 ; создать диалоговое окно - подсказку PUSH 0 PUSH OFFSET HINT PUSH DWORD PTR [EBP+08H] PUSH OFFSET HIN PUSH [HINST] CALL CreateDialogParamA@20 MOV H2,EAX ; установить фокус PUSH DWORD PTR [EBP+08H] CALL SetFocus@4 ; обнулить счетчик MOV P1,0 JMP _END .ENDIF .ENDIF .IF H2!=0 .IF (EDX>R2.B || EDX<R2.T) || (ECX<R2.L || ECX>R2.R) ;удаление подсказки в связи с перемещением курсора PUSH H2 CALL DestroyWindow@4 MOV H2,0 JMP _END .ENDIF .ENDIF ; восстановить стек _END: POP EBP RET 16 TIMPROC ENDP ; процедура окна всплывающей подсказки HINT PROC PUSH EBP MOV EBP,ESP CMP DWORD PTR [EBP+0CH],WM_INITDIALOG JNE NO_INIT ; инициализация ; получить контекст PUSH DWORD PTR [EBP+08H] CALL GetDC@4 MOV DC,EAX ; получить длину строки PUSH OFFSET HINTS CALL lstrlenA@4 ; получить длину и ширину строки PUSH OFFSET S PUSH EAX PUSH OFFSET HINTS PUSH DC CALL GetTextExtentPoint32A@16 ; установить положение и размер окна-подсказки PUSH 0 PUSH S.Y ADD S.X,2 PUSH S.X SUB YY,20 PUSH YY ADD XX,10 PUSH XX PUSH DWORD PTR [EBP+08H] CALL MoveWindow@24 ; закрыть контекст PUSH DC PUSH DWORD PTR [EBP+08H] CALL ReleaseDC@8 ; установить таймер PUSH 0 PUSH 6000 ; интервал 6 с. PUSH 3 ; идентификатор таймера PUSH DWORD PTR [EBP+08H] CALL SetTimer@16 JMP FIN NO_INIT: CMP DWORD PTR [EBP+0CH],WM_PAINT JNE NO_PAINT ; перерисовка окна ; получить контекст PUSH OFFSET PS PUSH DWORD PTR [EBP+08H] CALL BeginPaint@8 MOV DC,EAX ; установить цвета фона и текста подсказки PUSH RGBB PUSH EAX CALL SetBkColor@8 PUSH RGBT PUSH DC CALL SetTextColor@8 ; вывести текст PUSH OFFSET HINTS CALL lstrlenA@4 PUSH EAX PUSH OFFSET HINTS PUSH 0 PUSH 0 PUSH DC CALL TextOutA@20 ; закрыть контекст PUSH OFFSET PS PUSH DWORD PTR [EBP+08H] CALL EndPaint@8 JMP FIN NO_PAINT: CMP DWORD PTR [EBP+0CH],WM_TIMER JNE FIN ; обработка события таймера ; удалить таймер и удалить диалоговое окно ; подсказка удаляется в связи с истечением срока 6 с. PUSH 3 PUSH DWORD PTR [EBP+08H] CALL KillTimer@8 PUSH DWORD PTR [EBP+08H] CALL DestroyWindow@4 FIN: POP EBP RET 16 HINT ENDP _TEXT ENDS END START
Puc. 3.1.3. Пример диалогового окна с всплывающими подсказками.
Рис. 3.1.4. Диалоговое окно с всплывающими подсказками.
Комментарий к программе на Рис. 3.1.3.
1. Прежде всего обращу ваше внимание, что в данной программе мы используем условные конструкции времени выполнения. Данный шаг вполне закономерен и обусловлен только необходимостью несколько сократить объем, а также упростить читаемость программы. Вложенность условных конструкций, а также расстановка скобок вызваны желанием сократить длину строки и одновременно необходимостью транслировать программу как с помощью MASM32, так и с помощью TASM32. Я уже имел возможность сказать, что два ассемблера весьма сильно отличаются, когда речь идет о макросредствах.
2. Трансляция программы.
Пакет MASM32.
ML /c /coff /DMASM hint.asm RC hint.rc LINK /SUBSYSTEM:WINDOWS hint.obj hint.resПакет TASM32.
TASM32 /ml hint.asm BRCC32 hint.rc TLINK32 -aa hint.obj,,,,,hint.res
3. Как Вы, наверное, уже поняли, процедура таймера проверяет каждые 0.5 секунды положение курсора. Если курсор находится на элементе (окне редактирования или кнопке) и подсказка еще не вызвана (H1 или Н2 отлична от нуля), то вызывается подсказка. При этом учитывается еще величина счетчика (P1), чтобы подсказка появлялась с некоторой задержкой. Если при очередном вызове процедуры окажется, что курсор находится уже вне элемента, а подсказка еще на экране, то она удаляется. Данный механизм не учитывает тот случай, когда курсор быстро перейдет от одного элемента к другому. В этом случае вероятна ситуация, когда на экране окажется две подсказки. Впрочем, первая подсказка должна тут же исчезнуть.
4. В нашей программе на диалоговом окне расположено всего два элемента: окно редактирования и кнопка. Мы хотели показать, что в принципе не имеет значения, какой элемент: для любого из них может быть установлена подсказка. Положение подсказки по отношению к курсору легко регулируется, и Вы можете сами менять его.
5. Функция GetCursorPos получает положение курсора в абсолютных координатах
относительно экрана. Здесь не возникает проблем, т.к. функция GetWindowRect также
получает положение элемента окна в абсолютных координатах. Предварительно нам приходится
определять дескриптор окна при помощи функции GetDlgItem.
Глава 2. Многозадачное программирование
В предыдущей главе нами были рассмотрены возможности использования таймеров в прикладной задаче. Задав один или несколько таймеров, мы принуждаем систему вызывать одну или несколько процедур в автоматическом режиме. Посредством таймеров мы тем самым можем реализовать многозадачный режим в рамках одного процесса. Более того, было показано, что такие подзадачи могут взаимодействовать друг с другом. Это и понятно, ведь все эти подзадачи разделяют одно адресное пространство и, следовательно, информация от одной подзадачи к другой может передаваться через глобальные переменные. Это весьма интересный и сложный вопрос о том, как задачи и подзадачи могут взаимодействовать друг с другом. Мы рассмотрим его несколько позднее. Сейчас же рассмотрим многозадачность в операционной системе Windows с самого начала.
I
Под процессом будем понимать объект, создаваемый операционной системой Windows обычно при загрузке исполняемого модуля и получающий в единоличное пользование:
-
Виртуальную память, выделяемую для него операционной системой.
Дескрипторы открываемых им файлов.
Список загруженных им в его собственную память динамических модулей (DLL).
Созданные им подпроцессы или потоки, исполняемые независимо друг от друга, в собственной
памяти процесса.
Думаю, данное определение весьма ясно раскрывает суть понятия процесс. Но для большинства рассматриваемых в данной главе проблем, достаточно было бы дать и более простое определение. Например, такое: всякий исполняемый модуль (.ЕХЕ), запущенный в операционной системе Windows, становится процессом.
Теперь что касается подпроцесса. Смысл его достаточно прост: каждый процесс в отведенном для него адресном пространстве может порождать еще процессы. Эти процессы выполняются независимо друг от друга и от порождающего их процесса. Однако порождающий процесс может при желании "убить" любой из порожденных им процессов. Такие процессы называют еще потоками, а также цепочками или нитями40. Теперь мы понимаем, что в предыдущей главе при помощи таймера мы создавали потоки. Однако в операционной системе Windows для создания потоков есть и специальные средства, речь о которых пойдет ниже.
Теперь немного поговорим о типах многозадачности. В старой 16-битной Windows переключение между задачами происходило только тогда, когда задача отдавала управление операционной системе. Такая многозадачность называется невытесняющей. В определенном смысле это было даже хуже, чем в операционной системе MS DOS. Там элементы многозадачности осуществлялись при помощи так называемых TSR-программ (см. [1]). Такие программы назывались еще резидентными. Они перехватывали прерывание от таймера, клавиатуры или другого устройства и имели возможность время от времени получать управление.
Положение, существовавшее в старой операционной системе Windows, требовало от программиста выполнения джентльменского правила - не захватывать надолго время микропроцессора. Некоторым решением проблемы являлось использование таймеров (в чем мы уже убедились), а также использование функции PeekMessage вместо GetMessage. Функция PeekMessage, в отличие от GetMessage, возвращает управление сразу, даже если в очереди нет ни одного сообщения.
В 32-битных операционных системах Windows (Windows 9x, Windows NT, Windows 2000) реализована вытесняющая схема многозадачности, в которой переключением между процессами и потоками занимается операционная система. Если процесс слишком долго выполняет некоторую операцию, то курсор над окном процесса преобразуется в песочные часы. При этом другие процессы будут по-прежнему выполняться, и Вы сможете переключаться на них. А вот доступ к окну данного процесса может оказаться затруднительным. Решить данную проблему можно уже упомянутым способом, заменив в цикле ожидания GetMessage на PeekMessage. Однако более правильным решением будет разбиение процесса на некоторое количество потоков.
Созданием потоков мы займемся в следующих разделах, а оставшаяся часть данного раздела будет посвящена созданию процессов. Ваше приложение может создавать процессы, запустив ту или иную ЕХЕ-программу, которые будут работать независимо от основного приложения. Одновременно Ваше приложение может при необходимости удалить запущенное им приложение из памяти. Запустить приложение (создать процесс) можно при помощи функции CreateProcess. Сейчас мы дадим описание этой функции. Ниже объясняются ее параметры.
1-й параметр - указывает на имя запускаемой программы. Имя может содержать полный путь к программе. 2-й параметр - его значение зависит от того, является первый параметр NULL (0) или нет. Если первый параметр указывает на строку, то данный параметр трактуется как командная строка запуска (без имени программы). Если первый параметр равен NULL, то данный параметр рассматривается как командная строка, первый элемент которой представляет собой имя программы. Если путь к программе не указан, то функция CreateProcess осуществляет поиск программы по определенному алгоритму:-
Поиск в каталоге, откуда была запущена программа.
Поиск в текущем каталоге.
Поиск в системном каталоге (можно получить через GetSystemDirectory). Обычно
системным каталогом является C:\WINDOWS\SYSTEM.
Поиск в каталоге Windows (можно получить через GetWindowsDirectory). Обычно
этим каталогом является C:\WINDOWS.
Поиск в каталогах, перечисленных в параметре PATH окружения.
10-й параметр. Указывает на структуру, заполняемую при выполнении запуск приложения. Вот эта структура:
PROCINF STRUC hProcess DD ? ; дескриптор созданного процесса. hThread DD ? ; дескриптор главного потока нового процесса. Idproc DD ? ; идентификатор созданного процесса. idThr DD ? ; идентификатор главного потока нового процесса. PROCINF ENDS
Основное отличие дескриптора от идентификатора заключается в том, что дескриптор уникален лишь в пределах данного процесса, идентификатор же является глобальной величиной. Посредством идентификатора может быть найдена база данных текущего процесса. У читателя, я думаю, сразу возникнет вопрос: а чем дескриптор приложения, который мы получаем при помощи функции GetModuleHandle, от только что упомянутых величин? Дескриптор приложения, или дескриптор модуля есть величина локальная, т.е. действующая в пределах данного процесса и, как правило, равная адресу загрузки модуля в виртуальное адресное пространство. Дескриптор модуля имеется у любого модуля, загруженного в память, в том числе и у подчиненных DLL-библиотек.
Рассмотрим теперь структуру, на которую указывает 9-й параметр функции CreateProcess. Вот эта структура:
STARTUP STRUC cb DD 0 lpReserved DD 0 lpDesktop DD 0 lpTitle DD 0 dwX DD 0 dwY DD 0 dwXSize DD 0 dwYSize DD 0 dwXCountChars DD 0 dwYCountChars DD 0 dwFillAttribute DD 0 dwFlags DD 0 wShowWindow DW 0 cbReserved2 DW 0 lpReserved2 DD 0 hStdInput DD 0 hStdOutput DD 0 hStdError DD 0 STARTUP ENDS
Итак, разберем смысл полей этой структуры.
cb - размер данной структуры в байтах. Заполняется обязательно.
lpReserved - резерв, должно быть равно нулю.
lpDesktop - имя рабочего стола (и рабочей станции). Имеет смысл только для Windows
NT.
lpTitle - название окна для консольных приложений, создающих свое окно. Для
остальных приложений должно быть равно 0.
dwX - координата X левого верхнего угла окна.
dwY - координата Y левого верхнего угла окна.
dwXSize - размер окна по X.
dwYSize - размер окна по Y.
dwXCountChars - размер буфера консоли по X.
dwYCountChars - размер буфера консоли по Y.
dwFillAttribute - начальный цвет текста. Имеет значение только для консольных
приложений.
dwFlags - флаг значения полей. Вот значение этого флага.
Макро-значение флага | Значение константы | Смысл значения |
---|---|---|
STARTF_USESHOWWINDOW | 1h | Разрешить поле dwShowWindow |
STARTF_USESIZE | 2h | Разрешить dwXSize и dwYSize |
STARTF_USEPOSITI0N | 4h | Разрешить dwX и dwY |
STARTF_USECOUNTCHARS | 8h | Разрешить dwXCountChars и dwYCountChars |
STARTF_USEFILLATTR1BUTE | 10h | Разрешить dwFillAttribute |
STARTF_FORCEONFEEDBACK | 40h | Включить возврат курсора |
STARTF_FORCEOFFFEEDBACK | 80h | Выключить возврат курсора |
STARTF_USESTDHANDLES | 100h | Разрешить hStdInput |
wShowWindow - определяет способ отображения окна.
cbReserved2 - резерв, должно быть равно 0.
hStdInput - дескриптор ввода (для консоли).
hStdOutput - дескриптор вывода (для консоли).
hStdError - дескриптор вывода сообщения об ошибке (для консоли).
Следующий пример (Рис. 3.2.1) представляет собой простейший пример создания процесса. В качестве программы, порождающей процесс, взят редактор WINWORD.EXE. Для проверки правильности работы примера Вам придется указать путь к WINWORD на Вашем компьютере (переменная PATH). Обратите внимание на то, что приложение появляется на экране в свернутом виде и как это достигается. Как видите, функция CreateProcess совсем не так уж страшна, как кажется на первый взгляд.
// файл proces.rc // определение констант #define WS_SYSMENU 0x00080000L #define WS_POPUP 0x80000000L #define DS_3DLOOK 0x0004L //ресурс - меню MENUP MENU { POPUP "&Запуск Word" { MENUITEM "&3апустить", 1 MENUITEM "&Удалить", 2 MENUITEM "Выход из &программы", 3 } } // определение диалогового окна DIAL1 DIALOG 0, 0, 240, 120 STYLE WS_POPUP | WS_SYSMENU | DS_3DLOOK CAPTION "Пример немодального диалогового окна" FONT 8, "Arial" { } ; файл proces.inc ; константы STARTF_USESHOWWINDOW equ 1h SW_SHOWMINIMIZED equ 2h ; сообщение приходит при закрытии окна WM_CLOSE equ 10h WM_INITDIALOG equ 110h WM_COMMAND equ 111h ; прототипы внешних процедур IFDEF MASM ; для MASM EXTERN TerminateProcess@8:NEAR EXTERN CreateProcessA@40:NEAR EXTERN DialogBoxParamA@20:NEAR EXTERN EndDialog@8:NEAR EXTERN MessageBoxA@16:NEAR EXTERN ExitProcess@4:NEAR EXTERN GetModuleHandleA@4:NEAR EXTERN LoadMenuA@8:NEAR EXTERN SetMenu@8:NEAR EXTERN TranslateMessage@4:NEAR ELSE ; для TASM EXTERN TerminateProcess:NEAR EXTERN CreateProcessA:NEAR EXTERN DialogBoxParamA:NEAR EXTERN EndDialog:NEAR EXTERN MessageBoxA:NEAR EXTERN ExitProcess:NEAR EXTERN GetModuleHandleA:NEAR EXTERN LoadMenuA:NEAR EXTERN SetMenu:NEAR EXTERN TranslateMessage:NEAR TerminateProcess@8 = TerminateProcess CreateProcessA@40 = CreateProcessA DialogBoxParamA@20 = DialogBoxParamA EndDialog@8 = EndDialog MessageBoxA@16 = MessageBoxA ExitProcess@4 = ExitProcess GetModuleHandleA@4 = GetModuleHandleA LoadMenuA@8 = LoadMenuA SetMenu@8 = SetMenu TranslateMessage@4 = TranslateMessage ENDIF ; структуры ; структура сообщения MSGSTRUCT STRUC MSHWND DD ? MSMESSAGE DD ? MSWPARAM DD ? MSLPARAM DD ? MSTIME DD ? MSPT DD ? MSGSTRUCT ENDS ; структура для CreateProcess STARTUP STRUC cb DD 0 lpReserved DD 0 lpDesktop DD 0 lpTitle DD 0 dwX DD 0 dwY DD 0 dwXSize DD 0 dwYSize DD 0 dwXCountChars DD 0 dwYCountChars DD 0 dwFillAttribute DD 0 dwFlags DD 0 wShowWindow DW 0 cbReserved2 DW 0 lpReserved2 DD 0 hStdInput DD 0 hStdOutput DD 0 hStdError DD 0 STARTUP ENDS ; структура - информация о процессе PROCINF STRUC hProcess DD ? hThread DD ? Idproc DD ? idThr DD ? PROCINF ENDS ; файл proces.asm .386P ; плоская модель .MODEL FLAT, stdcall include proces.inc ; директивы компоновщику для подключения библиотек IFDEF MASM ; директивы MASM includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib ELSE ; директивы TASM includelib c:\tasm32\lib\import32.lib ENDIF ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' NEWHWND DD 0 MSG MSGSTRUCT <?> STRUP STARTUP <?> INF PROCINF <?> HINST DD 0 ; дескриптор приложения PA DB "DIAL1",0 PMENU DB "MENUP",0 PATH DB "C:\Program Files\Office\WINWORD.EXE",0 _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' START: ; получить дескриптор приложения PUSH 0 CALL GetModuleHandleA@4 MOV [HINST], EAX ; создать модальный диалог PUSH 0 PUSH OFFSET WNDPROC PUSH 0 PUSH OFFSET PA PUSH [HINST] CALL DialogBoxParamA@20 PUSH 0 CALL ExitProcess@4 ;----------------------------------- ; процедура окна ; расположение параметров в стеке ; [EBP+014Н] ; LPARAM ; [EBP+10Н] ; WAPARAM ; [EBP+0CH] ; MES ; [EBP+8] ; HWND WNDPROC PROC PUSH EBP MOV EBP,ESP PUSH EBX PUSH ESI PUSH EDI ; сообщение при закрытии окна CMP DWORD PTR [EBP+0CH],WM_CLOSE JNE L1 ; закрыть диалоговое окно JMP L5 L1: ; сообщение при инициализации окна CMP DWORD PTR [EBP+0CH],WM_INITDIALOG JNE L3 ; загрузить меню PUSH OFFSET PMENU PUSH [HINST] CALL LoadMenuA@8 ; установить меню PUSH EAX PUSH DWORD PTR [EBP+08H] CALL SetMenu@8 JMP FINISH ; проверяем, не случилось ли чего с пунктами ; меню в диалоговом окне L3: CMP DWORD PTR [EBP+0CH],WM_COMMAND JNE FINISH CMP WORD PTR [EBP+10H],3 JE L5 CMP WORD PTR [EBP+10H],2 JE L7 CMP WORD PTR [EBP+10H],1 JE L6 JMP FINISH ; закрыть диалоговое окно L5: PUSH 0 PUSH DWORD PTR [EBP+08H] CALL EndDialog@8 JMP FINISH ; запустить программу WORD L6: ; заполняем структуру для запуска ; окно должно появляться в свернутом виде MOV STRUP.cb,68 MOV STRUP.lpReserved,0 MOV STRUP.lpDesktop,0 MOV STRUP.lpTitle,0 MOV STRUP.dwFlags, STARTF_USESHOWWINDOW MOV STRUP.cbReserved2,0 MOV STRUP.lpReserved2,0 MOV STRUP.wShowWindow,SW_SHOWMINIMIZED ; запуск приложения Winword PUSH OFFSET INF PUSH OFFSET STRUP PUSH 0 PUSH 0 PUSH 0 PUSH 0 PUSH 0 PUSH 0 PUSH OFFSET PATH PUSH 0 CALL CreateProcessA@40 JMP FINISH ; удалить из памяти процесс L7: PUSH 0 ; код выхода PUSH INF.hProcess CALL TerminateProcess@8 FINISH: MOV EAX,0 POP EDI POP ESI POP EBX POP EBP RET 16 WNDPROC ENDP _TEXT ENDS END START
Рис. 3.2.1. Пример создания процесса.
Трансляция программы PROCES.ASM на Рис. 3.2.1.
Пакет MASM32.
ML /c /coff /DMASM proces.asm RC proces.rc LINK /SUBSYSTEM:WINDOWS proces.obj proces.resПакет TASM32.
TASM32 /ml proces.asm BRCC32 proces.rc TLINK32 -aa proces.obj,,,,,proces.res
Думаю, что другой комментарий для программы на Рис. 3.2.1 не требуется.
40 В английской терминологии поток называется "THREAD", что буквально переводится как нить.
II
Теперь пришла пора вплотную заняться потоками. Вначале я намерен решить задачу из предыдущей главы (Рис. 3.1.2) при помощи потока.
Поток может быть создан при помощи функции CreateThread. Рассмотрим параметры этой функции.
1-й параметр. Указатель на структуру атрибутов доступа. Имеет значение только для Windows NT. Обычно полагается NULL. 2-й параметр. Размер стека потока. Если параметр равен нулю, то берется размер стека по умолчанию, равный размеру стека родительского потока. 3-й параметр. Указатель на потоковую функцию, с вызова которой начинается исполнение потока. 4-й параметр. Параметр для потоковой функции. 5-й параметр. Флаг, определяющий состояние потока. Если флаг равен 0, то выполнение потока начинается немедленно. Если значение флага потока равно CREATE_SUSPENDED (4H), то поток находится в состояние ожидания и запускается по выполнению функции ResumeThread. 6-й параметр. Указатель на переменную, куда будет помещен дескриптор потока.Как уже было сказано, выполнение потока начинается с потоковой функции. Окончание работы этой функции приводит к естественному окончанию работы потока. Поток также может закончить свою работу, выполнив функцию ExitThread с указанием кода выхода. Наконец, порождающий поток может закончить работу порожденного потока при помощи функции TerminateThread. В нашем примере на Рис. 3.2.2 запускаемый процесс не может сам закончить свою работу и прекращает свою работу вместе с приложением по команде TerminateThread. Надо сказать, что такое завершение, вообще говоря, является аварийным и не рекомендуется к обычному употреблению. Связано это с тем, что при таком завершении не выполняется никаких действий по освобождению занятых ресурсов (блоки памяти, открытые файлы и т.п.). Поэтому стройте свои приложения так, что бы поток завершался по выходу из потоковой процедуры. Таким образом, в некотором смысле приведенный ниже пример является демонстрацией того, чего не рекомендуется делать.
Вообще идеальной нам кажется ситуация, когда функция окна берет на себя только реакцию на события, происходящие с элементами, а всю трудоемкую работу (сложные вычисления, файловая обработка) должны взять на себя потоки. Кстати, поток может создавать новые потоки, так что в результате может возникнуть целое дерево.
Как я уже сказал, ниже представлена программа, использующая поток для вычисления и вывода в окно редактирования текущей даты и времени. Заметим в этой связи, что если бы такая обработка была реализована в оконной функции, Вы бы сразу почувствовали разницу - окно почти бы перестало реагировать на внешнее воздействие.
// файл thread.rc // определение констант #define WS_SYSMENU 0x00080000L // элементы на окне должны быть изначально видимы #define WS_VISIBLE 0x10000000L // бордюр вокруг элемента #define WS_BORDER 0x00800000L // при помощи TAB можно по очереди активизировать элементы #define WS_TABSTOP 0x00010000L // текст в окне редактирования прижат к левому краю #define ES_LEFT 0x000OL // стиль всех элементов на окне #define WS_CHILD 0x40000000L // запрещается ввод с клавиатуры #define ES_READONLY 0x0800L // стиль - кнопка #define BS_PUSHBUTTON 0x00000000L // центрировать текст на кнопке #define BS_CENTER 0x00000300L #define DS_3DLOOK 0x0004L // определение диалогового окна DIAL1 DIALOG 0, 0, 240, 100 STYLE WS_SYSMENU | DS_3DLOOK CAPTION "Пример использования потока" FONT 8, "Arial" { // окно редактирования, идентификатор 1 CONTROL "", 1, "edit", ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_READONLY, 100, 5, 130, 12 // кнопка, идентификатор 2 CONTROL "Выход", 2, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 180, 76, 50, 14 } ; файл thread.inc ; константы ; сообщение приходит при закрытии окна WM_CLOSE equ 10h ; сообщение приходит при создании окна WM_INITDIALOG equ 110h ; сообщение приходит при событии с элементом на окне WM_COMMAND equ 111h ; сообщение посылки текста элементу WM_SETTEXT equ 0Ch ; прототипы внешних процедур IFDEF MASM ; для MASM EXTERN SendMessageA@16:NEAR EXTERN GetDlgItem@8:NEAR EXTERN Sleep@4:NEAR EXTERN TerminateThread@8:NEAR EXTERN CreateThread@24:NEAR EXTERN wsprintfA:NEAR EXTERN GetLocalTime@4:NEAR EXTERN ExitProcess@4:NEAR EXTERN GetModuleHandleA@4:NEAR EXTERN DialogBoxParamA@20:NEAR EXTERN EndDialog@8:NEAR ELSE ; для TASM EXTERN SendMessageA:NEAR EXTERN GetDlgItem:NEAR EXTERN Sleep:NEAR EXTERN TerminateThread:NEAR EXTERN CreateThread:NEAR EXTERN _wsprintfA:NEAR EXTERN GetLocalTime:NEAR EXTERN ExitProcess:NEAR EXTERN GetModuleHandleA:NEAR EXTERN DialogBoxParamA:NEAR EXTERN EndDialog:NEAR SendMessageA@16 = SendMessageA GetDlgItem@8 = GetDlgItem Sleep@4 = Sleep TerminateThread@8 = TerminateThread CreateThread@24 = CreateThread wsprintfA = _wsprintfA GetLocalTime@4 = GetLocalTime ExitProcess@4 = ExitProcess GetModuleHandleA@4 = GetModuleHandleA DialogBoxParamA@20 = DialogBoxParamA EndDialog@8 = EndDialog ENDIF ; структуры ; структура сообщения MSGSTRUCT STRUC MSHWND DD ? MSMESSAGE DD ? MSWPARAM DD ? MSLPARAM DD ? MSTIME DD ? MSPT DD ? MSGSTRUCT ENDS ; структура данных дата-время DAT STRUC year DW ? month DW ? dayweek DW ? day DW ? hour DW ? min DW ? sec DW ? msec DW ? DAT ENDS ; файл thread.asm .386P ; плоская модель .MODEL FLAT, stdcall include thread.inc ; директивы компоновщику для подключения библиотек IFDEF MASM ; для компоновщика LINK.EXE includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib includelib c:\masm32\lib\gdi32.lib ELSE ; для компоновщика TLINK32.EXE includelib c:\tasm32\lib\import32.lib ENDIF ;---------------------------------------------------- ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' MSG MSGSTRUCT <?> HINST DD 0 ; дескриптор приложения PA DB "DIAL1",0 TIM DB "Дата %u/%u/%u Время %u:%u:%u",0 STRCOPY DB 50 DUP (?) DATA DAT <0> HTHR DD ? _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' START: ; получить дескриптор приложения PUSH 0 CALL GetModuleHandleA@4 MOV [HINST], EAX ; создать диалоговое окно PUSH 0 PUSH OFFSET WNDPROC PUSH 0 PUSH OFFSET PA PUSH [HINST] CALL DialogBoxParamA@20 CMP EAX,-1 JNE KOL ; сообщение об ошибке KOL: ;------------------------------------ PUSH 0 CALL ExitProcess@4 ;------------------------------------ ; процедура окна ; расположение параметров в стеке ; [EBP+014Н] ; LPARAM ; [EBP+10Н] ; WAPARAM ; [EBP+0CH] ; MES ; [EBP+8] ; HWND WNDPROC PROC PUSH EBP MOV EBP,ESP PUSH EBX PUSH ESI PUSH EDI ;------------------- CMP DWORD PTR [EBP+0CH],WM_CLOSE JNE L1 L3: ; здесь реакция на закрытие окна ; удалить поток PUSH 0 PUSH HTHR CALL TerminateThread@8 ; закрыть диалог PUSH 0 PUSH DWORD PTR [EBP+08H] CALL EndDialog@8 JMP FINISH L1: CMP DWORD PTR [EBP+0CH],WM_INITDIALOG JNE L2 ; здесь начальная инициализация ; получить дескриптор окна редактирования PUSH 1 PUSH DWORD PTR [EBP+08H] CALL GetDlgItem@8 ; создать поток PUSH OFFSET HTHR ; сюда дескриптор потока PUSH 0 PUSH EAX ; параметр PUSH OFFSET GETTIME ; адрес процедуры PUSH 0 PUSH 0 CALL CreateThread@24 JMP FINISH L2: CMP DWORD PTR [EBP+0CH],WM_COMMAND JNE FINISH ; кнопка выхода? CMP WORD PTR [EBP+10H],2 JE L3 FINISH: POP EDI POP ESI POP EBX POP EBP MOV EAX,0 RET 16 WNDPROC ENDP ; потоковая функция ; [EBP+8] параметр=дескриптор окна редактирования GETTIME PROC PUSH EBP MOV EBP,ESP L0: ; задержка в 1 секунду PUSH 1000 CALL Sleep@4 ; получить локальное время PUSH OFFSET DATA CALL GetLocalTime@4 ; получить строку для вывода даты и времени MOVZX EAX,DATA.sec PUSH EAX MOVZX EAX,DATA.min PUSH EAX MOVZX EAX,DATA.hour PUSH EAX MOVZX EAX,DATA.year PUSH EAX MOVZX EAX,DATA.month PUSH EAX MOVZX EAX,DATA.day PUSH EAX PUSH OFFSET TIM PUSH OFFSET STRCOPY CALL wsprintfA ; отправить строку в окно редактирования PUSH OFFSET STRCOPY PUSH 0 PUSH WM_SETTEXT PUSH DWORD PTR [EBP+08H] CALL SendMessageA@16 JMP L0 ; бесконечный цикл POP EBP RET 4 GETTIME ENDP _TEXT ENDS END START
Рис. 3.2.2. Пример создания потока.
Комментарий к программе на Рис. 3.2.2.
Трансляция программы THREAD.ASM на Рис. 3.2.2.
Пакет MASM32.
ML /c /coff /DMASM thread.asm RC thread.rc LINK /SUBSYSTEM:WINDOWS thread.obj thread.resПакет TASM32.
TASM32 /ml thread.asm BRCC32 thread.rc TLINK32 -aa thread.obj,,,,,thread.res
Прошу читателя взять на вооружение весьма полезную функцию Sleep. Эта функция особенно часто используется именно в потоках, дабы несколько высвободить процессорное время.
III
Поговорим теперь о многопотоковой программе. В принципе, если не предполагается, что потоки как-то взаимодействуют друг с другом, технически не имеет значения, запущен один или несколько потоков. Сложности возникают, когда работа одного потока зависит от деятельности другого потока. Здесь возможны самые разные ситуации. Давайте рассмотрим все по порядку.
Нам уже пришлось столкнуться с ситуацией, когда два параллельных процесса взаимодействовали друг с другом посредством глобальных переменных. Точнее, один процесс готовил данные для другого процесса. Здесь не было никакой сложности: просто один процесс с некоторой периодичностью менял содержимое переменной, а второй процесс, с другой периодичностью, читал из этой переменной. Если период обновления данных меньше периода взятия данных, то мы почти с достоверностью (почти!) получаем, что второй процесс будет получать всегда свежие данные. Иногда это, т.е. соотношение между периодом обновления и периодом получения данных, как в нашем случае, вообще не имеет никакого значения.
Часто случается, что данные невозможно получать периодически. Они могут, например, зависеть от деятельности третьего процесса. Как же второй процесс узнает, что данные уже готовы для передачи? На первый взгляд проблема решается введением дополнительной переменной, назовем ее FLAG. Примем, что при FLAG=0 данные не готовы, а при FLAG=1 данные готовы. Далее действует весьма простая схема.
NO_DAT: CMP FLAG,1 JNE NO_DAT ... ;передача данных ... MOV FLAG,0 ...
Это фрагмент, как Вы понимаете, для второго потока. Первый же поток также должен проверять переменную FLAG и, если FLAG=0, поместить новые данные и установить значение переменной FLAG, равное единице. Данная схема совсем не плоха, например, когда один процесс ждет окончания работы другого процесса. Другими словами, данные являются результатом всей работы этого процесса. Например, запущен компилятор, а другой поток ждет окончания его работы, дабы вывести результаты этой работы на некое устройство. Эта ситуация весьма распространена, но все же случай этот частный.
А что будет, если процесс передачи данных должен производится многократно? Легко видеть, что данная схема будет работать и в более сложном случае. Важно, однако, чтобы второй процесс менял содержимое FLAG только после того, как он возьмет данные, а первый процесс - после того, как положит данные. Если нарушить это правило, то может возникнуть коллизия, когда, например, второй процесс еще не взял данные, а они уже изменились.
Такой подход можно осуществить и в более общем случае, когда два потока (или - два процесса) должны поочередно получать доступ к одному ресурсу. Как легко видеть, данный подход предполагает, что ресурс открыт либо для одного, либо для другого процесса. Если бы поставить задачу несколько иным образом - процесс либо открыт, либо закрыт для доступа, то возникло бы некоторое затруднение. Действительно, вероятна такая ситуация, когда оба потока ожидают открытия ресурса. Другими словами, они непрерывно осуществляют проверку переменной FLAG (CMP FLAG,1). Может статься, что они оба почти одновременно обратятся к ресурсу. Совершенно ясно, что здесь возникает необходимость в "третьей силе", которая бы занималась распределением доступа к ресурсу. Например, посылала бы сообщение вначале одному потоку и, если он ожидает доступа, давала доступ именно ему, а затем подобный процесс повторяется со вторым потоком.
Схема, описанная выше, весьма хороша, но при условии поочередного обращения к ресурсу. Если это в общем случае не выполняется, то данный подход может дать серьезный сбой. Все приведенные здесь рассуждения имеют одну цель - показать, что проблема взаимодействия потоков и процессов, их синхронизации, является и сложной, и актуальной для многозадачных операционных систем. Отсюда следует, что такие операционные системы должны иметь свои собственные средства синхронизации.41
Оставшаяся часть данного раздела будет всецело посвящена средствам синхронизации операционной системы Windows.
Семафоры. Семафор представляет собой глобальный объект, позволяющий синхронизировать работу двух или нескольких процессов или потоков. Для программиста семафор - это просто счетчик. Если счетчик равен N, это означает, что к ресурсу имеют доступ N процессов. Рассмотрим функции для работы с семафорами.
CreateSemaphore - создает глобальный объект-семафор. Возвращает дескриптор семафора. Параметры функции: 1-й параметр. Указатель на структуру, определяющую атрибуты доступа. Может иметь значение для Windows NT. Обычно данный параметр полагается равным NULL. 2-й параметр. Начальное значение счетчика семафора. Определяет, сколько задач имеют доступ к ресурсу вначале. 3-й параметр. Количество задач, которые имеют одновременный доступ к ресурсу. 4-й параметр. Указатель на строку, содержащую имя семафора.
OpenSemaphore - открыть уже созданный семафор. Возвращает дескриптор семафора.
Данную функцию используют не так часто. Обычно создают семафор и присваивают его дескриптор
глобальной переменной, а потом используют этот дескриптор в порождаемых потоках. Параметры функции:
1-й параметр. Определяет желаемый уровень доступа к семафору. Возможные значения:
SEMAPHORE_MODIFY_STATE = 2Н, разрешить использование функции ReleaseSemaphore,
SYNCHRONIZE = 100000Н, разрешить использование любой функции ожидания, только для
Windows NT,
SEMAPHORE_ALL_ACCESS = 0F0000h + SYNCHRONIZE + 3H, специфицирует все
возможные флаги доступа к семафору.
(в книге пропущен) 2-й параметр. Указывает, может ли наследоваться дескриптор семафора, возвращаемый данной функцией,
процессом, созаваемым функцией CreateProcess; 0 - не может.
(также в книге пропущен) 3-й параметр. Указатель на ASCIIZ-строку, содержащую имя семафора.
WaitForSingleObject - ожидать открытие семафора. При успешном завершении, т.е.
открытии доступа к объекту, функция возвращает 0. Значение 102h будет
означать, что закончился заданный период ожидания. Параметры функции:
1-й параметр. Дескриптор семафора.
2-й параметр. Время ожидания в миллисекундах. Если параметр равен INFINITE = 0FFFFFFFFh,
то время ожидания не ограничено.
ReleaseSemaphore - освободить семафор и позволить получить доступ к ресурсу другим процессам. Параметры функции: 1-й параметр. Дескриптор семафора. 2-й параметр. Определяет, какое значение должно быть добавлено к счетчику семафора. Чаще всего этот параметр равен единице. 3-й параметр. Указатель на переменную, куда должно быть помещено предыдущее значение счетчика.
Рассмотрим алгоритм работы с семафором. Сначала при помощи функции CreateSemaphore создается семафор и его дескриптор присваивается глобальной переменной. Перед попыткой обращения к ресурсам, доступ к которым необходимо ограничить, поток должен вызвать функцию WaitForSingleObject. При открытии доступа функция возвращает 0. По окончании работы с ресурсом следует вызвать функцию ReleaseSemaphore. Тем самым увеличивается счетчик доступа на 1. С помощью семафора можно регулировать количество потоков, которые одновременно могут иметь доступ к ресурсу. Максимальное значение счетчика как раз и определяет, сколько потоков могут получить доступ к ресурсу одновременно. Но обычно, как я уже говорил, максимальное значение полагают равным 1.
События. Событие является объектом, очень похожим на семафор, но в несколько видоизмененном виде. Рассмотрим функции для работы с событиями.
CreateEvent - создает объект-событие. Параметры функции.
1-й параметр. Имеет тот же смысл, что и первый параметр функции CreateSemaphore.
Обычно полагается равным NULL.
2-параметр. Если параметр не равен нулю, то событие может быть сброшено при помощи функции
ResetEvent. Иначе событие сбрасывается при доступе к нему какого либо процесса.
3-й параметр. Если параметр равен 0, то событие инициализируется как сброшенное, в
противном случае сразу же подается сигнал о наступлении соответствующей ситуации.
4-й параметр. Указатель на строку, которая содержит имя события.
Ожидание события осуществляется, как и в случае с семафором, функцией WaitForSingleObject.
Функция OpenEvent аналогична функции OpenSemaphore, и на ней мы останавливаться не будем.
SetEvent - подать сигнал о наступлении события. Параметры функции.
1-й параметр. Дескриптор события.
Критические секции. Понятие критической секции позволяет уберечь определенные области программы так, чтобы в этой области программы в данный момент времени исполнялся бы только один поток. Рассмотрим функции для работы с критической секцией.
InitializeCriticalSection - данная функция создает объект под названием критическая
секция. Параметры функции.
1-й параметр. Указатель на структуру, указанную ниже. Поля данной структуры используются только
внутренними процедурами, и смысл их безразличен.
CRITICAL_SECTION STRUCT DebugInfo DWORD ? LockCount LONG ? RecursionCount LONG ? OwningThread HANDLE ? LockSemaphore HANDLE ? SpinCount DWORD ? CRITICAL_SECTION ENDS
EnterCriticalSection - войти в критическую секцию. После выполнения этой функции данный поток становится владельцем данной секции. Следующий поток, вызвав данную функцию, будет находиться в состоянии ожидания. Параметр функции такой же, что и в предыдущей функции.
LeaveCriticalSection - покинуть критическую секцию. После этого второй поток, который был остановлен функцией EnterCriticalSection, станет владельцем критической секции. Параметр функции LeaveCriticalSection такой же, как и у предыдущих функций.
DeleteCriticalSection - удалить объект "критическая секция". Параметр аналогичен предыдущим.
Программно можно определить несколько объектов критической секции, с которыми будут работать несколько потоков. Мы не зря, говоря о критических секциях, упоминаем только потоки. Разные процессы не могут использовать данный объект синхронизации.
Теперь рассмотрим пример использования критической секции. Примеры использования семафоров и событий Вы сможете найти в книге [4]. Изложим вкратце идею, положенную в основу примера на Рис. 3.2.3.
Два потока обращаются время от времени к процедуре, выводящей очередной символ из строки в окно. В результате такой конкурентной деятельности должна быть напечатана строка.
Часть процедуры, выводящей очередной символ, сделана критической, поэтому доступ к выводу в окно в данный момент времени имеет только один поток.
; файл thread2.inc ; константы ; сообщение приходит при закрытии окна WM_DESTROY equ 2 ; сообщение приходит при создании окна WM_CREATE equ 1 ; сообщение при щелчке левой кнопкой мыши в области окна WM_LBUTTONDOWN equ 201h ; сообщение при щелчке правой кнопкой мыши в области окна WM_RBUTTONDOWN equ 204h ; свойства окна CS_VREDRAW equ 1h CS_HREDRAW equ 2h CS_GLOBALCLASS equ 4000h WS_OVERLAPPEDWINDOW equ 000CF0000H stylcl equ CS_HREDRAW + CS_VREDRAW + CS_GLOBALCLASS DX0 equ 300 DY0 equ 200 ; компоненты цветов RED equ 50 GREEN equ 50 BLUE equ 255 RGBW equ (RED or (GREEN shl 8)) or (BLUE shl 16) RGBT equ 255 ; красный ; идентификатор стандартной иконки IDI_APPLICATION equ 32512 ; идентификатор курсора IDC_CROSS equ 32515 ; режим показа окна - нормальный SW_SHOWNORMAL equ 1 ; прототипы внешних процедур IFDEF MASM EXTERN Sleep@4:NEAR EXTERN CreateThread@24:NEAR EXTERN InitializeCriticalSection@4:NEAR EXTERN EnterCriticalSection@4:NEAR EXTERN LeaveCriticalSection@4:NEAR EXTERN DeleteCriticalSection@4:NEAR EXTERN GetTextExtentPoint32A@16:NEAR EXTERN CreateWindowExA@48:NEAR EXTERN DefWindowProcA@16:NEAR EXTERN DispatchMessageA@4:NEAR EXTERN ExitProcess@4:NEAR EXTERN GetMessageA@16:NEAR EXTERN GetModuleHandleA@4:NEAR EXTERN LoadCursorA@8:NEAR EXTERN LoadIconA@8:NEAR EXTERN PostQuitMessage@4:NEAR EXTERN RegisterClassA@4:NEAR EXTERN ShowWindow@8:NEAR EXTERN TranslateMessage@4:NEAR EXTERN UpdateWindow@4:NEAR EXTERN TextOutA@20:NEAR EXTERN CreateSolidBrush@4:NEAR EXTERN SetBkColor@8:NEAR EXTERN SetTextColor@8:NEAR EXTERN GetDC@4:NEAR EXTERN DeleteDC@4:NEAR ELSE EXTERN Sleep:NEAR EXTERN CreateThread:NEAR EXTERN InitializeCriticalSection:NEAR EXTERN EnterCriticalSection:NEAR EXTERN LeaveCriticalSection:NEAR EXTERN DeleteCriticaiSection:NEAR EXTERN GetTextExtentPoint32A:NEAR EXTERN CreateWindowExA:NEAR EXTERN DefWindowProcA:NEAR EXTERN DispatchMessageA:NEAR EXTERN ExitProcess:NEAR EXTERN GetMessageA:NEAR EXTERN GetModuleHandleA:NEAR EXTERN LoadCursorA:NEAR EXTERN LoadIconA:NEAR EXTERN PostQuitMessage:NEAR EXTERN RegisterClassA:NEAR EXTERN ShowWindow:NEAR EXTERN TranslateMessage:NEAR EXTERN UpdateWindow:NEAR EXTERN TextOutA:NEAR EXTERN CreateSolidBrush:NEAR EXTERN SetBkColor:NEAR EXTERN SetTextColor:NEAR EXTERN GetDC:NEAR EXTERN DeleteDC:NEAR Sleep@4 = Sleep CreateThread@24 = CreateThread InitializeCriticalSection@4 = InitializeCriticalSection EnterCriticalSection@4 = EnterCriticalSection LeaveCriticalSection@4 = LeaveCriticalSection DeleteCriticalSection@4 = DeleteCriticaiSection GetTextExtentPoint32A@16 = GetTextExtentPoint32A CreateWindowExA@48 = CreateWindowExA DefWindowProcA@16 = DefWindowProcA DispatchMessageA@4 = DispatchMessageA ExitProcess@4 = ExitProcess GetMessageA@16 = GetMessageA GetModuleHandleA@4 = GetModuleHandleA LoadCursorA@8 = LoadCursorA LoadIconA@8 = LoadIconA PostQuitMessage@4 = PostQuitMessage RegisterClassA@4 = RegisterClassA ShowWindow@8 = ShowWindow TranslateMessage@4 = TranslateMessage UpdateWindow@4 = UpdateWindow TextOutA@20 = TextOutA CreateSolidBrush@4 = CreateSolidBrush SetBkColor@8 = SetBkColor SetTextColor@8 = SetTextColor GetDC@4 = GetDC DeleteDC@4 = DeleteDC ENDIF ; структуры ; структура сообщения MSGSTRUCT STRUC MSHWND DD ? MSMESSAGE DD ? MSWPARAM DD ? MSLPARAM DD ? MSTIME DD ? MSPT DD ? MSGSTRUCT ENDS ;------------- WNDCLASS STRUC CLSSTYLE DD ? CLSLPFNWNDPROC DD ? CLSCBCLSEXTRA DD ? CLSCBWNDEXTRA DD ? CLSHINSTANCE DD ? CLSHICON DD ? CLSHCURSOR DD ? CLSHBRBACKGROUND DD ? MENNAME DD ? CLSNAME DD ? WNDCLASS ENDS ; структура для работы с критической секцией CRIT STRUC DD ? DD ? DD ? DD ? DD ? DD ? CRIT ENDS ; структура для определения длины текста SIZET STRUC X1 DWORD ? Y1 DWORD ? SIZET ENDS ; файл thread2.asm .386P ; плоская модель .MODEL FLAT, stdcall ;------------------------------------------------ include thread2.inc ; подключения библиотек IFDEF MASM includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib includelib c:\masm32\lib\gdi32.lib ELSE includelib c:\tasm32\lib\import32.lib ENDIF ;------------------------------------------------ ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' NEWHWND DD 0 MSG MSGSTRUCT <?> WC WNDCLASS <?> SZT SIZET <?> HINST DD 0 TITLENAME DB 'Вывод в окно двумя потоками',0 NAM DB 'CLASS32',0 XT DWORD 30 YT DWORD 30 HW DD ? DC DD ? TEXT DB 'Текст в окне красный',0 SPA DB ' ' DB ' ',0 IND DD 0 SK CRIT <?> THR1 DD ? THR2 DD ? FLAG1 DD 0 FLAG2 DD 0 _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' START: ; получить дескриптор приложения PUSH 0 CALL GetModuleHandleA@4 MOV [HINST], EAX REG_CLASS: ; заполнить структуру окна ; стиль MOV [WC.CLSSTYLE],stylcl ; процедура обработки сообщений MOV [WC.CLSLPFNWNDPROC],OFFSET WNDPROC MOV [WC.CLSCBCLSEXTRA],0 MOV [WC.CLSCBWNDEXTRA],0 MOV EAX,[HINST] MOV [WC.CLSHINSTANCE],EAX ;----------иконка окна PUSH IDI_APPLICATION PUSH 0 CALL LoadIconA@8 MOV [WC.CLSHICON], EAX ;----------курсор окна PUSH IDC_CROSS PUSH 0 CALL LoadCursorA@8 MOV [WC.CLSHCURSOR], EAX ;---------- PUSH RGBW ; цвет кисти CALL CreateSolidBrush@4 ; создать кисть MOV [WC.CLSHBRBACKGROUND],EAX MOV DWORD PTR [WC.MENNAME],0 MOV DWORD PTR [WC.CLSNAME],OFFSET NAM PUSH OFFSET WC CALL RegisterClassA@4 ; создать окно зарегистрированного класса PUSH 0 PUSH [HINST] PUSH 0 PUSH 0 PUSH DY0 ; DY0 - высота окна PUSH DX0 ; DX0 - ширина окна PUSH 100 ; координата Y PUSH 100 ; координата X PUSH WS_OVERLAPPEDWINDOW PUSH OFFSET TITLENAME ; имя окна PUSH OFFSET NAM ; имя класса PUSH 0 CALL CreateWindowExA@48 ; проверка на ошибку CMP EAX,0 JZ _ERR MOV [NEWHWND], EAX ; дескриптор окна ;------------------------------------ PUSH SW_SHOWNORMAL PUSH [NEWHWND] CALL ShowWindow@8 ; показать созданное окно ;------------------------------------ PUSH [NEWHWND] CALL UpdateWindow@4 ;перерисовать видимую часть окна ; петля обработки сообщений MSG_LOOP: PUSH 0 PUSH 0 PUSH 0 PUSH OFFSET MSG CALL GetMessageA@16 CMP AX, 0 JE END_LOOP PUSH OFFSET MSG CALL TranslateMessage@4 PUSH OFFSET MSG CALL DispatchMessageA@4 JMP MSG_LOOP END_LOOP: ; выход из программы (закрыть процесс) PUSH [MSG.MSWPARAM] CALL ExitProcess@4 _ERR: JMP END_LOOP ;------------------------------------- ; процедура окна ; расположение параметров в стеке ; [EBP+014Н] ; LPARAM ; [EBP+10Н] ; WAPARAM ; [EBP+0CH] ; MES ; [EBP+8] ; HWND WNDPROC PROC PUSH EBP MOV EBP,ESP PUSH EBX PUSH ESI PUSH EDI CMP DWORD PTR [EBP+0CH],WM_DESTROY JE WMDESTROY CMP DWORD PTR [EBP+0CH],WM_CREATE JE WMCREATE CMP DWORD PTR [EBP+0CH],WM_LBUTTONDOWN JNE CONTIN ; проверить флаг запуска CMP FLAG1,0 JNE DEFWNDPROC MOV FLAG1,1 ; инициализировать указатели LEA EAX,TEXT MOV IND,EAX MOV XT,30 ; запуск первого потока PUSH OFFSET THR1 PUSH 0 PUSH EAX PUSH OFFSET THREAD1 PUSH 0 PUSH 0 CALL CreateThread@24 ; запуск второго потока PUSH OFFSET THR2 PUSH 0 PUSH EAX PUSH OFFSET THREAD2 PUSH 0 PUSH 0 CALL CreateThread@24 JMP DEFWNDPROC CONTIN: CMP DWORD PTR [EBP+0CH],WM_RBUTTONDOWN JNE DEFWNDPROC ; проверить флаг запуска CMP FLAG2,0 JNE DEFWNDPROC MOV FLAG2,1 ; инициализировать указатели LEA EAX,SPA MOV IND,EAX MOV XT,30 ; запуск первого потока PUSH OFFSET THR1 PUSH 0 PUSH EAX PUSH OFFSET THREAD1 PUSH 0 PUSH 0 CALL CreateThread@24 ; запуск второго потока PUSH OFFSET THR2 PUSH 0 PUSH EAX PUSH OFFSET THREAD2 PUSH 0 PUSH 0 CALL CreateThread@24 JMP DEFWNDPROC WMCREATE: MOV EAX,DWORD PTR [EBP+08H] ; запомнить дескриптор окна в глобальной переменной MOV HW,EAX ; инициализировать критическую секцию PUSH OFFSET SK CALL InitializeCriticalSection@4 MOV EAX, 0 JMP FINISH DEFWNDPROC: PUSH DWORD PTR [EBP+14H] PUSH DWORD PTR [EBP+10H] PUSH DWORD PTR [EBP+0CH] PUSH DWORD PTR [EBP+08H] CALL DefWindowProcA@16 JMP FINISH WMDESTROY: ; удалить критическую секцию PUSH OFFSET SK CALL DeleteCriticalSection@4 PUSH 0 CALL PostQuitMessage@4 ; WM_QUIT MOV EAX, 0 FINISH: POP EDI POP ESI POP EBX POP EBP RET 16 WNDPROC ENDP ; вывод OUTSTR PROC ; проверяем, не закончился ли текст MOV EBX,IND CMP BYTE PTR [EBX],0 JNE NO_0 RET NO_0: ; вход в критическую секцию PUSH OFFSET SK CALL EnterCriticalSection@4 ;----------------- PUSH HW CALL GetDC@4 MOV DC,EAX ;---------------- цвет фона = цвет окна PUSH RGBW PUSH EAX CALL SetBkColor@8 ;---------------- цвет текста (красный) PUSH RGBT PUSH DC CALL SetTextColor@8 ;---------------- вывести текст PUSH 1 PUSH IND PUSH YT PUSH XT PUSH DC CALL TextOutA@20 ;- вычислить длину текста в пикселях текста PUSH OFFSET SZT PUSH 1 PUSH IND PUSH DC CALL GetTextExtentPoint32A@16 ; увеличить указатели MOV EAX,SZT.X1 ADD XT,EAX INC IND ;---------------- закрыть контекст PUSH DC CALL DeleteDC@4 ; выход из критической секции PUSH OFFSET SK CALL LeaveCriticalSection@4 RET OUTSTR ENDP ; первый поток THREAD1 PROC L01: ; проверить, не конец ли текста MOV EBX,IND CMP BYTE PTR [EBX],0 JE _END1 ; вывод очередного символа CALL OUTSTR ; задержка PUSH 1000 CALL Sleep@4 JMP L01 _END1: RET 4 THREAD1 ENDP ; второй поток THREAD2 PROC L02: ; проверить, не конец ли текста MOV EBX,IND CMP BYTE PTR [EBX],0 JE _END2 ; вывод очередного символа CALL OUTSTR ; задержка PUSH 1000 CALL Sleep@4 JMP L02 _END2: RET 4 THREAD2 ENDP _TEXT ENDS END START
Рис. 3.2.3. Пример синхронизации двух потоков посредством критической секции.
Комментарий к программе.
Трансляция программы THREAD2.ASM на Рис. 3.2.3.
Пакет MASM32.
ML /c /coff /DMASM thread2.asm LINK /SUBSYSTEM:WINDOWS thread2.objПакет TASM32.
TASM32 /ml thread2.asm TLINK32 -aa thread2.obj
При нажатии левой кнопки мыши начинается вывод текстовой строки. При нажатии правой кнопки мыши - выведенная строка стирается. Флаги FLAG1 и FLAG2 введены для того, чтобы вывод строки и вывод пустой строки можно было производить только один раз.
Для того чтобы несколько замедлить вывод текста, мы вводим задержку (Sleep) в цикл вызова процедуры OUTSTR в каждом потоке.
Обратите внимание, что буквы выводятся в окно в основном парами. Объясняется это тем, что пока один из потоков выводит символ, второй уже ждет разрешения, и, как только первый поток выходит из критической секции, второй поток сразу выводит следующий символ.
После в обоих потоках срабатывает задержка (функция Sleep).
Заключая разговор о критических секциях, отмечу, что это наиболее быстрый способ синхронизации. К минусам данного подхода относится невозможность доступа к секции сразу нескольких потоков, а также отсутствие специальных средств, чтобы подсчитывать число обращений к ресурсу.
Взаимоисключения. Мы не упомянули еще один способ синхронизации, отложив его подробное описание на следующие главы. Здесь же отметим, что этот способ называется "взаимоисключением" или мьютексом (Mutex). Данный способ синхронизации не удобен для работы с потоками, он более пригоден для процессов. Данный объект создается при помощи функции CreateMutex. Все процессы, пытающиеся создать уже созданный объект, получают дескриптор уже существующего, созданного другим процессом объекта "взаимоисключение". Особенность данного объекта прежде всего в том, что им может владеть только один процесс. В документации фирмы Microsoft рекомендуется использовать данный объект для определения, запущено уже данное приложение или нет. Но об этом речь пойдет ниже.
41 К слову сказать, в однозадачной операционной системе MS DOS проблема совместного функционирования резидентных программ стояла весьма остро. Не смотря на то, что программисты, их писавшие, добивались весьма больших успехов, все же одновременная работа нескольких резидентных программ часто приводила к весьма заметным конфликтам.
Глава 3. Создание динамических библиотек
I
Использование динамических библиотек (по-другому - библиотек динамической компоновки) - это способ осуществления модульности в период выполнения программы. Динамическая библиотека (Dynamic Link Library - DLL) позволяет упростить и саму разработку программного обеспечения. Вместо того чтобы каждый раз перекомпилировать огромные ЕХЕ-программы, достаточно перекомпилировать лишь отдельный динамический модуль. Кроме того, доступ к динамической библиотеке возможен сразу из нескольких исполняемых модулей, что делает многозадачность более гибкой. Структуре исполняемых модулей будет посвящена отдельная глава, но уже сейчас могу сказать, что структура DLL-модуля практически такая же, как и ЕХЕ-модуля. Тот, кто программировал под MS DOS, должен быть знаком с понятием оверлея. По своей функциональности динамическая библиотека очень похожа на оверлей, но название "динамическая библиотека" более удачно42.
При написании ЕХЕ-модулей Вы уже познакомились с тем, как определять импортируемые функции. Достаточно объявить эти функции как EXTERN. При создании динамической библиотеки Вам придется указывать и импортируемые, и экспортируемые функции.
Для того чтобы двигаться дальше, введу такое понятие, как связывание. Собственно, я уже ввел это понятие, когда рассматривал работу редактора связей. Во время трансляции связываются имена, указанные в программе как внешние, (EXTERN) с соответствующими именами из библиотек, которые указываются при помощи директивы IMPORTLIB. Такое связывание называется ранним (или статическим). Напротив, в случае с динамической библиотекой связывание происходит во время выполнения модуля. Такое связывание называется поздним (или динамическим). При этом позднее связывание может происходить в автоматическом режиме в начале запуска программы и при помощи специальных API-функций (см. ниже), по желанию программиста. При этом говорят о явном и неявном связывании. Сказанное иллюстрирует Рис. 3.3.1. Заметим также, что использование динамической библиотеки экономит дисковое пространство, т.к. представленная в библиотеке процедура содержится лишь один раз, в отличие от процедур, помещаемых в модули из статических библиотек43.
В среде Windows практикуются два механизма связывания: по символьным именам и по порядковым номерам. В первом случае функция, определенная в динамической библиотеке, идентифицируется по ее имени, во втором - по порядковому номеру, который должен быть задан при трансляции. Связывание по порядковому номеру в основном практиковалось в старой операционной системе Windows З.х. На наш взгляд, связывание по имени - более удобный механизм.
Рис. 3.3.1. Иллюстрация понятия связывания в ассемблере.
Динамическая библиотека может содержать также ресурсы. Так, файлы шрифтов представляют собой динамические библиотеки, единственным содержимым которых являются ресурсы. Должно сказать, что динамическая библиотека как бы становится продолжением Вашей программы, загружаясь в адресное пространство процесса. Соответственно, данные процесса доступны из динамической библиотеки, и, наоборот, данные динамической библиотеки доступны для процесса.
В любой динамической библиотеке следует определить точку входа (процедура входа). По умолчанию за
точку входа принимают метку, указываемую за директивой END (например, END START).
При загрузке динамической библиотеки и выгрузке динамической библиотеки автоматически вызывается
процедура входа. Заметим при этом, что каким бы способом ни была загружена динамическая библиотека
(явно или неявно), выгрузка динамической библиотеки из памяти будет происходить автоматически при
закрытии процесса или потока. В принципе, процедура входа может быть использована для некоторой
начальной инициализации переменных. Довольно часто эта процедура остается пустой. При вызове
процедуры входа в нее помещаются три параметра:
1-й параметр. Идентификатор DLL-модуля.
2-й параметр. Причина вызова (см. ниже).
3-й параметр. Резерв.
Рассмотрим подробнее второй параметр процедуры входа. Вот четыре возможных значения этого параметра:
DLL_PROCESS_DETACH equ 0 DLL_PROCESS_ATTACH equ 1 DLL_THREAD_ATTACH equ 2 DLL_THREAD_DETACH equ 3
DLL_PROCESS_ATTACH - сообщает, что динамическая библиотека загружена в адресное пространство вызывающего процесса.
DLL_THREAD_ATTACH - сообщает, что Текущий процесс создает новый поток. Такое сообщение посылается всем динамическим библиотекам, загруженным к этому времени процессом.
DLL_PROCESS_DETACH - сообщает, что динамическая библиотека выгружается из адресного пространства процесса.
DLL_THREAD_DETACH - сообщает, что некий поток, созданный данным процессом, в адресное
пространство которого загружена данная динамическая библиотека, уничтожается.
42 Оверлей (Overlay) в переводе означает перекрытие - указание на то, что в оверлейную область памяти могут загружаться по очереди разные части оверлея, перекрывая друг друга.
43 Вообще говоря, библиотеки, используемые нами для программирования в Windows, такие как import32.lib, user32.lib и т.п., правильнее называть не статическими библиотеками, а библиотеками импорта. В них нет программного кода, а лишь информация, используема для трансляции.
II
Перейдем теперь к разбору программных примеров динамических библиотек. На Рис. 3.3.2 приводится пример простейшей динамической библиотеки. Данная динамическая библиотека, по сути, ничего не делает. Просто при загрузке библиотеки, ее выгрузки, а также вызове процедуры DLLP1 будет вызвано обычное Windows-сообщение. Обратите внимание, как определяется процесс загрузки и выгрузки библиотеки. Заметим также, что процедура входа должна возвращать ненулевое значение. Процедура DLLP1 обрабатывает также один параметр, передаваемый через стек обычным способом.
.386P ; плоская модель IFDEF MASM .MODEL FLAT, stdcall ELSE .MODEL FLAT ENDIF PUBLIC DLLP1 ; константы ; сообщения, приходящие при открытии ; динамической библиотеки DLL_PROCESS_DETACH equ 0 DLL_PROCESS_ATTACH equ 1 DLL_THREAD_ATTACH equ 2 DLL_THREAD_DETACH equ 3 IFDEF MASM ; MASM ; прототипы внешних процедур EXTERN MessageBoxA@16:NEAR ; директивы компоновщику для подключения библиотек includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib ELSE ; TASM EXTERN MessageBoxA:NEAR MessageBoxA@16 = MessageBoxA includelib c:\tasm32\lib\import32.lib ENDIF ;-------------------------------------------------- ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' TEXT1 DB 'Вход в библиотеку',0 TEXT2 DB 'Выход из библиотеки',0 MS DB 'Сообщение из библиотеки',0 TEXT DB 'Вызов процедуры из DLL',0 _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' ; [EBP+10H] ; резервный параметр ; [EBP+0CH] ; причина вызова ; [EBP+8] ; идентификатор DLL-модуля DLLENTRY: MOV EAX,DWORD PTR [EBP+0CH] CMP EAX,0 JNE D1 ; закрытие библиотеки PUSH 0 PUSH OFFSET MS PUSH OFFSET TEXT2 PUSH 0 CALL MessageBoxA@16 JMP _EXIT D1: CMP EAX,1 JNE _EXIT ; открытие библиотеки PUSH 0 PUSH OFFSET MS PUSH OFFSET TEXT1 PUSH 0 CALL MessageBoxA@16 _EXIT: MOV EAX,1 RET 12 ;——————————————————— ; [EBP+8] ; параметр процедуры DLLP1 PROC EXPORT PUSH EBP MOV EBP,ESP CMP DWORD PTR [EBP+8],1 JNE _EX PUSH 0 PUSH OFFSET MS PUSH OFFSET TEXT PUSH 0 CALL MessageBoxA@16 _EX: POP EBP RET 4 DLLP1 ENDP _TEXT ENDS END DLLENTRY
Puc. 3.3.2. Простейшая DLL-библиотека.
Программа на Рис. 3.3.2 может быть оттранслирована как с помощью MASM32, так и TASM32 (см. ниже). На этом стоит остановиться более подробно. Прежде всего обратите внимание, что за процедурой, вызываемой из другого модуля, мы указали ключевое слово EXPORT. Это слово необходимо для правильной трансляции в MASM. Для TASM этого не нужно, но, к счастью, этот транслятор просто не замечает наличия какого-либо слова после PROC. Зато для TASM процедура DLLP1 должна быть определена как PUBLIC, кроме того, для трансляции в пакете TASM необходимо подготовить DEF-файл и указать его в командной строке TLINK32. Для создания динамических библиотек в строке link следует указать ключ /DLL, а в строке tlink32 -Tpd (no умолчанию работает ключ -Tpe). Ключ /ENTRY:DLLENTRY в строке link можно опустить, так как точка входа определяется из директивы END DLLENTRY.
Трансляция динамической библиотеки на Рис. 3.3.2.
MASM32.
ml /c /coff /DMASM dll1.asm link /subsystem:windows /DLL /ENTRY:DLLENTRY dll1.objTASM32.
tasm32 /ml dll1.asm tlink32 -aa -Tpd dll1.obj,,,,dll1.defСодержимое dll1.def:
EXPORTS DLLP1
Ниже на Рис. 3.3.3 представлена программа, которая загружает динамическую библиотеку, показанную на Рис. 3.3.2. Это пример позднего связывания. Библиотека должна быть вначале загружена при помощи функции LoadLibrary. Затем определяется адрес процедуры с помощью функции GetProcAddress, после чего можно осуществлять вызов. Как и следовало ожидать, MASM помещает в динамическую библиотеку вместо DLLP1 имя _DLLP1@0, тогда как TASM помещает имя без искажения. Это мы учитываем в нашей программе. Мы учитываем также возможность ошибки при вызове функций LoadLibrary и GetProcAddress. В этой связи укажем, как (в какой последовательности) ищет библиотеку функция LoadLibrary:
-
Поиск в каталоге, откуда была запущена программа.
Поиск в текущем каталоге.
В системном директории (GetSystemDirectory).
В директории Windows (GetWindowsDirectory).
В каталогах, указанных в окружении (PATH).
В конце программы мы выгружаем из памяти динамическую библиотеку, что, кстати, могли бы и не делать, т.к. по выходе из программы эта процедура выполняется автоматически.
.386P ; плоская модель .MODEL FLAT, stdcall ; константы ; прототипы внешних процедур IFDEF MASM ;MASM EXTERN GetProcAddress@8:NEAR EXTERN LoadLibraryA@4:NEAR EXTERN FreeLibrary@4:NEAR EXTERN ExitProcess@4:NEAR EXTERN MessageBoxA@16:NEAR ; директивы компоновщику для подключения библиотек includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib ELSE ; TASM EXTERN GetProcAddress:NEAR EXTERN LoadLibraryA:NEAR EXTERN FreeLibrary:NEAR EXTERN ExitProcess:NEAR EXTERN MessageBoxA:NEAR ; директивы компоновщику для подключения библиотек includelib c:\tasm32\lib\import32.lib GetProcAddress@8 = GetProcAddress LoadLibraryA@4 = LoadLibraryA FreeLibrary@4 = FreeLibrary ExitProcess@4 = ExitProcess MessageBoxA@16 = MessageBoxA ENDIF ;----------------------------------------- ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' TXT DB 'Ошибка динамической библиотеки',0 MS DB 'Сообщение',0 LIBR DB 'DLL1.DLL',0 HLIB DD ? IFDEF MASM NAMEPROC DB '_DLLP1@0',0 ELSE NAMEPROC DB 'DLLP1',0 ENDIF _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' START: ; загрузить библиотеку PUSH OFFSET LIBR CALL LoadLibraryA@4 CMP EAX,0 JE _ERR MOV HLIB,EAX ; получить адрес процедуры PUSH OFFSET NAMEPROC PUSH HLIB CALL GetProcAddress@8 CMP EAX,0 JNE YES_NAME ; сообщение об ошибке _ERR: PUSH 0 PUSH OFFSET MS PUSH OFFSET TXT PUSH 0 CALL MessageBoxA@16 JMP _EXIT YES_NAME: PUSH 1 ; параметр CALL EAX ; закрыть библиотеку PUSH HLIB CALL FreeLibrary@4 ; библиотека автоматически закрывается также ; при выходе из программы ; выход _EXIT: PUSH 0 CALL ExitProcess@4 _TEXT ENDS END START
Рис. 3.3.3. Вызов динамической библиотеки. Явное связывание.
Трансляция программы на Рис. 3.3.3 ничем не отличается от трансляции обычных программ.
MASM32.
ml /c /coff /DMASM dllex.asm link /subsystem:windows dllex.objTASM32.
tasm32 /ml dllex.asm tlink32 -aa dllex.obj
III
Хотя я указал, что, на мой взгляд, неявное связывание менее гибко, я нашел необходимым привести пример неявного связывания. Мы здесь рассмотрим только вызывающую программу, так как вызываемая программа, естественно не меняется. Как видите, текст программы стал несколько проще. Здесь важно заметить, что, во-первых, необходимо объявить вызываемую из динамической библиотеки процедуру как внешнюю, а, во-вторых, подключить статическую библиотеку DLLP1.LIB.
.386P ; плоская модель IFDEF MASM .MODEL FLAT, stdcall ELSE .MODEL FLAT ENDIF ; константы ; прототипы внешних процедур includelib dll1.lib IFDEF MASM ; MASM EXTERN DLLP1@0:NEAR EXTERN ExitProcess@4:NEAR ; директивы компоновщику для подключения библиотек includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib ELSE ; директивы компоновщику для подключения библиотек includelib c:\tasm32\lib\import32.lib EXTERN DLLP1:NEAR EXTERN ExitProcess:NEAR DLLP1@0 = DLLP1 ExitProcess@4 = ExitProcess ENDIF ;----------------------------------------------------- ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' START: PUSH 1 ; параметр CALL DLLP1@0 ; выход _EXIT: PUSH 0 CALL ExitProcess@4 _TEXT ENDS END START
Рис. 3.3.4. Вызов динамической библиотеки. Неявное связывание.
У читателя, скорее всего, возникнет вопрос, откуда появляется библиотека DLLP1.LIB. Здесь все достаточно просто. Транслятор MASM создает библиотеку автоматически, а в трансляторе TASM есть утилита IMPLIB, которая создает статическую библиотеку непосредственно из динамической библиотеки.
Трансляция программы на Рис. 3.3.4.
MASM32.
ml /c /coff /DMASM dllex.asm link /subsystem:windows dllex.objTASM32.
tasm32 /ml dllex.asm implib dll1.lib dll1.dll tlink32 -aa dllex.obj
Ранее было сказано, что возможным механизмом связывания является определения адреса процедуры через порядковый номер. Изложим схему того, как это можно сделать. Сначала Вы должны сопоставить процедуре, которая будет вызываться из динамической библиотеки, некое двухбайтное число. Это делается посредством строки в DEF-файле: EXPORTS DLLP1@1. Здесь процедуре DLLP1 сопоставляется номер 1. DEF-файлы мы уже использовали при трансляции в TASM. Точно также их можно использовать и в MASM (ключ /DEF:имя для программы link). После этого производится трансляция, и динамическая библиотека готова. Теперь при вызове функции GetProcAddress вторым параметром следует указать порядковый номер, точнее двойное слово, младшее слово которого есть порядковый номер, а старшее слово равно нулю. И все будет работать точно также, как и раньше. Лично я не вижу особой необходимости использовать такой подход.
IV
На Рис. 3.3.5 представлен текст динамической библиотеки и программы, вызывающей процедуру из этой библиотеки. Здесь ничего сложного, просто я хотел продемонстрировать, что основной процесс и динамическая библиотека используют одно и тоже адресное пространство. Процесс передает адреса строк, которые находятся в блоке данных основного процесса. В свою очередь, процедура возвращает в основной процесс адрес строки, находящейся в блоке данных динамической библиотеки.
; динамическая библиотека DLL2.ASM .386P ; плоская модель IFDEF MASM .MODEL FLAT, stdcall ELSE .MODEL FLAT ENDIF PUBLIC DLLP1 ; константы ; сообщения, приходящие при открытии ; динамической библиотеки DLL_PROCESS_DETACH equ 0 DLL_PROCESS_ATTACH equ 1 DLL_THREAD_ATTACH equ 2 DLL_THREAD_DETACH equ 3 IFDEF MASM ; MASM ; прототипы внешних процедур EXTERN MessageBoxA@16:NEAR ; директивы компоновщику для подключения библиотек includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib ELSE ; TASM EXTERN MessageBoxA:NEAR MessageBoxA@16 = MessageBoxA includelib c:\tasm32\lib\import32.lib ENDIF ;-------------------------------------------------- ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' TEXT DB "Строка в динамической библиотеке",0 _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' ; [EBP+10H] ; резервный параметр ; [EBP+0CH] ; причина вызова ; [EBP+8] ; идентификатор DLL-модуля DLLENTRY: MOV EAX,DWORD PTR [EBP+0CH] CMP EAX,0 JNE D1 ; закрытие библиотеки JMP _EXIT D1: CMP EAX,1 JNE _EXIT ; открытие библиотеки _EXIT: MOV EAX,1 RET 12 ;--------------------- ; адреса параметров ; [EBP+8] ; [EBP+0CH] DLLP1 PROC EXPORT PUSH EBP MOV EBP,ESP PUSH 0 PUSH DWORD PTR [EBP+0CH] PUSH DWORD PTR [EBP+8] PUSH 0 CALL MessageBoxA@16 POP EBP LEA EAX,TEXT RET 8 DLLP1 ENDP _TEXT ENDS END DLLENTRY ; основной модуль DLLEX2.ASM, вызывающий ; процедуру из динамической библиотеки .386P ; плоская модель .MODEL FLAT, stdcall ; константы ; прототипы внешних процедур IFDEF MASM ; MASM EXTERN GetProcAddress@8:NEAR EXTERN LoadLibraryA@4:NEAR EXTERN FreeLibrary@4:NEAR EXTERN ExitProcess@4:NEAR EXTERN MessageBoxA@16:NEAR includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib ELSE ; TASM includelib c:\tasm32\lib\import32.lib EXTERN GetProcAddress:NEAR EXTERN LoadLibraryA:NEAR EXTERN FreeLibrary:NEAR EXTERN ExitProcess:NEAR EXTERN MessageBoxA:NEAR GetProcAddress@8 = GetProcAddress LoadLibraryA@4 = LoadLibraryA FreeLibrary@4 = FreeLibrary ExitProcess@4 = ExitProcess MessageBoxA@16 = MessageBoxA ENDIF ;---------------------------------------------- ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' TXT DB 'Ошибка динамической библиотеки',0 MS DB 'Сообщение',0 LIBR DB 'DLL2.DLL',0 HLIB DD ? MS1 DB 'Сообщение из библиотеки',0 TEXT DB 'Строка содержится в основном модуле',0 IFDEF MASM NAMEPROC DB '_DLLP1@0',0 ELSE NAMEPROC DB 'DLLP1',0 ENDIF _DATA ENDS ; сегмент кода_TEXT SEGMENT DWORD PUBLIC USE32 'CODE' ; [EBP+10H] ; резервный параметр ; [EBP+0CH] ; причина вызова ; [EBP+8] ; идентификатор DLL-модуля START: ; загрузить библиотеку PUSH OFFSET LIBR CALL LoadLibraryA@4 CMP EAX,0 JE _ERR MOV HLIB,EAX ; получить адрес PUSH OFFSET NAMEPROC PUSH HLIB CALL GetProcAddress@8 CMP EAX,0 JNE YES_NAME ; сообщение об ошибке _ERR: PUSH 0 PUSH OFFSET MS PUSH OFFSET TXT PUSH 0 CALL MessageBoxA@16 JMP _EXIT YES_NAME: PUSH OFFSET MS1 PUSH OFFSET TEXT CALL EAX PUSH 0 PUSH OFFSET MS PUSH EAX PUSH 0 CALL MessageBoxA@16 ; закрыть библиотеку PUSH HLIB CALL FreeLibrary@4 ; библиотека автоматически закрывается также ; при выходе из программы ; выход _EXIT: PUSH 0 CALL ExitProcess@4 _TEXT ENDS END START
Рис. 3.3.5. Основной модуль и динамическая библиотека. Передача параметров.
Трансляция программы на Рис. 3.3.5.
MASM32.
ml /c /coff /DMASM dllex2.asm link /subsystem:windows dllex2.objTASM32.
tasm32 /ml dllex2.asm tlink32 -aa -Tpd dllex2.obj
Ниже мы рассмотрим весьма интересный пример (Рис. 3.3.6). Основной процесс использует ресурсы загруженной им динамической библиотеки. Я уже говорил, что файлы шрифтов, по сути, являются динамическими библиотеками. Не правда ли удобно: ресурсы можно поместить отдельно от основной программы в динамическую библиотеку, загружая их по мере необходимости? Наша программа вначале загружает иконку из ресурсов динамической библиотеки и устанавливает ее на окно. Если Вы будете щелкать правой кнопкой мыши, направив курсор на окно, то будет вызываться процедура из динамической библиотеки, которая будет поочередно устанавливать то один, то другой значок на окно.
// файл dll3.rc // идентификаторы #define IDI_ICON1 3 #define IDI_ICON2 10 // определили иконку IDI_ICON1 ICON "ico1.ico" IDI_ICON2 ICON "ico2.ico" ; динамическая библиотека DLL3.ASM .386P PUBLIC SETIC ; плоская модель IFDEF MASM .MODEL FLAT, stdcall ELSE .MODEL FLAT ENDIF ; константы WM_SETICON equ 80h IFDEF MASM ; MASM ; прототипы внешних процедур EXTERN LoadIconA@8:NEAR EXTERN PostMessageA@16:NEAR ; директивы компоновщику для подключения библиотек includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib ELSE ; TASM ; прототипы внешних процедур EXTERN LoadIconA:NEAR EXTERN PostMessageA:NEAR ; директивы компоновщику для подключения библиотек includelib c:\tasm32\lib\import32.lib LoadIconA@8 = LoadIconA PostMessageA@16 = PostMessageA ENDIF ;------------------------------------------------- ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' PRIZ DB 0 _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' DLLENTRY: MOV EAX,1 RET 12 ; [EBP+8] ; [EBP+0CH] SETIC PROC EXPORT PUSH EBP MOV EBP,ESP ; выбрать, какую иконку устанавливать CMP PRIZ,0 JZ IC_1 MOV PRIZ,0 PUSH 3 JMP CONT IC_1: MOV PRIZ,1 PUSH 10 CONT: ; загрузить иконку из ресурсов библиотеки PUSH DWORD PTR [EBP+0CH] ; идентификатор ; динамической ; библиотеки CALL LoadIconA@8 ; установить значок окна PUSH EAX PUSH 0 PUSH WM_SETICON PUSH DWORD PTR [EBP+08H] ; дескриптор окна CALL PostMessageA@16 POP EBP RET 8 SETIC ENDP _TEXT ENDS END DLLENTRY // файл dllex3.rc // определение констант #define WS_SYSMENU 0x00080000L #define WS_MINIMIZEBOX 0x00020000L #define WS_MAXIMIZEBOX 0x00010000L #define DS_3DLOOK 0x0004L // определение диалогового окна DIAL1 DIALOG 0, 0, 340, 120 STYLE WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | DS_3DLOOK CAPTION "Диалоговое окно с иконкой из динамической библиотеки" FONT 8, "Arial" { } ; основной модуль DLLEX3.ASM, вызывающий ; процедуру из динамической библиотеки .386P ; плоская модель IFDEF MASM .MODEL FLAT, stdcall ELSE .MODEL FLAT ENDIF ; константы ; сообщение приходит при закрытии окна WM_CLOSE equ 10h WM_INITDIALOG equ 110h WM_SETICON equ 80h WM_LBUTTONDOWN equ 201h ; прототипы внешних процедур IFDEF MASM ; MASM EXTERN PostMessageA@16:NEAR EXTERN GetProcAddress@8:NEAR EXTERN LoadLibraryA@4:NEAR EXTERN FreeLibrary@4:NEAR EXTERN ExitProcess@4:NEAR EXTERN GetModuleHandleA@4:NEAR EXTERN DialogBoxParamA@20:NEAR EXTERN EndDialog@8:NEAR EXTERN LoadIconA@8:NEAR ; директивы компоновщику для подключения библиотек includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib ELSE ; директивы компоновщику для подключения библиотек includelib c:\tasm32\lib\import32.lib EXTERN PostMessageA:NEAR EXTERN GetProcAddress:NEAR EXTERN LoadLibraryA:NEAR EXTERN FreeLibrary:NEAR EXTERN ExitProcess:NEAR EXTERN GetModuleHandleA:NEAR EXTERN DialogBoxParamA:NEAR EXTERN EndDialog:NEAR EXTERN LoadIconA:NEAR PostMessageA@16 = PostMessageA LoadIconA@8 = LoadIconA EndDialog@8 = EndDialog GetModuleHandleA@4 = GetModuleHandleA DialogBoxParamA@20 = DialogBoxParamA GetProcAddress@8 = GetProcAddress LoadLibraryA@4 = LoadLibraryA FreeLibrary@4 = FreeLibrary ExitProcess@4 = ExitProcess ENDIF ;------------------------------------------------ ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' LIBR DB 'DLL3.DLL',0 HLIB DD ? HINST DD ? PA DB "DIAL1",0 IFDEF MASM NAMEPROC DB "_SETIC@0",0 ELSE NAMEPROC DB "SETIC",0 ENDIF _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' START: ; получить дескриптор приложения PUSH 0 CALL GetModuleHandleA@4 ; создать диалог MOV [HINST], EAX PUSH 0 PUSH OFFSET WNDPROC PUSH 0 PUSH OFFSET PA PUSH [HINST] CALL DialogBoxParamA@20 ; выход _EXIT: PUSH 0 CALL ExitProcess@4 ; процедура окна ; расположение параметров в стеке ; [EBP+014Н] ; LPARAM ; [EBP+10Н] ; WAPARAM ; [EBP+0CH] ; MES ; [EBP+8] ; HWND WNDPROC PROC PUSH EBP MOV EBP,ESP PUSH EBX PUSH ESI PUSH EDI ;------------------- CMP DWORD PTR [EBP+0CH],WM_CLOSE JNE L1 ; закрыть библиотеку ; библиотека автоматически закрывается также ; при выходе из программы PUSH HLIB CALL FreeLibrary@4 PUSH 0 PUSH DWORD PTR [EBP+08H] CALL EndDialog@8 JMP FINISH L1: CMP DWORD PTR [EBP+0CH],WM_INITDIALOG JNE L2 ; загрузить библиотеку PUSH OFFSET LIBR CALL LoadLibraryA@4 MOV HLIB,EAX ; загрузить иконку PUSH 3 ; идентификатор иконки PUSH [HLIB] ; идентификатор процесса CALL LoadIconA@8 ; установить иконку PUSH EAX PUSH 0 ; тип иконки (маленькая) PUSH WM_SETICON PUSH DWORD PTR [EBP+08H] CALL PostMessageA@16 JMP FINISH L2: CMP DWORD PTR [EBP+0CH],WM_LBUTTONDOWN JNE FINISH ; получить адрес процедуры из динамической библиотеки PUSH OFFSET NAMEPROC PUSH HLIB CALL GetProcAddress@8 ; вызвать процедуру с двумя параметрами PUSH [HLIB] PUSH DWORD PTR [EBP+08H] CALL EAX FINISH: POP EDI POP ESI POP EBX POP EBP MOV EAX,0 RET 16 WNDPROC ENDP _TEXT ENDS END START
Рис. З.З.6. Пример загрузки ресурса из динамической библиотеки.
Трансляция программ на Рис. 3.3.6.
MASM32:
ml /c /coff /DMASM dllex3.asm rc dllex3.rc link /subsystem:windows dllex3.obj dllex3.res ml /c /coff /DMASM dll3.asm rc dll3.rc link /subsystem:windows /DLL /ENTRY:DLLENTRY dll3.obj dll3.resTASM32:
tasm32 /ml dllex3.asm brcc32 dllex3.rc tlink32 -aa -Tpe dllex3.obj,,,,,dllex3.res tasm32 /ml dll3.asm brcc32 dll3.rc tlink32 -aa -Tpd dll3.obj,,,,dll3.def,dll3.res
Содержимое файла dll3.def:
EXPORTS SETIC
Как мы уже не раз с Вами убеждались, динамическая библиотека становится частью программы, обладая вместе с процессом всеми ее возможностями. Так, при помощи функций GetMessage и PeekMessage она может получать сообщения, предназначенные для процесса. Если Вы хотите создать в динамической библиотеке окно, то Вам следует воспользоваться идентификатором вызвавшей динамическую библиотеку программы.
V
Рассмотрим теперь вопрос о том, как используют динамическую библиотеку различные экземпляры приложения или разные процессы. Если Вы немного знакомы с принципом функционирования операционной системы Windows, то, возможно, такая постановка вопроса у Вас вызовет недоумение. "У каждого приложения свое адресное пространство, куда загружается динамическая библиотека", - скажете Вы. Конечно, это не совсем рационально, но зато безопасно. О памяти мы еще подробно будем говорить в гл. 3.6, здесь же заметим, что, вообще говоря, приложение может инициализировать так называемую разделяемую память. Мы вернемся к этому вопросу еще неоднократно, сейчас же рассмотрим этот вопрос чисто технически, применительно к динамическим библиотекам. Рассмотрим конкретную ситуацию. Запускаемое приложение загружает динамическую библиотеку и вызывает процедуру из динамической библиотеки, которая меняет данные, расположенные опять же в динамической библиотеке. Запустим теперь второй экземпляр приложения. Оно загружает еще один экземпляр динамической библиотеки. Могут быть ситуации, когда желательно, чтобы второе запущенное приложение "знало", что по команде первого приложения данные уже изменились. Ясно, что в этом случае данные, которыми оперирует динамическая библиотека, должны быть общими. Технически это делается очень просто. У редактора связей LINK есть опция /section: имя, атрибуты, которая позволяет объявить явно свойства данной секции. Мы будем говорить о секциях далее, здесь же достаточно сказать, секция - это просто сегмент в старом понимании. В редакторе связей TLINK32 то же действие можно осуществить с помощью файла .DEF.
Представленные на Рис. 3.3.7 программы весьма просты. По сути, они лишь иллюстрируют возможности использования разделяемой памяти. Перед выходом из процедуры динамической библиотеки изменяется строка, хранящаяся в разделяемой секции памяти. При этом приложение не заканчивает своей работы. При запуске второго экземпляра приложения на экран выводится уже измененное первым приложением значение строки.
; динамическая библиотека DLL4.ASM .386P ; плоская модель IFDEF MASM .MODEL FLAT, stdcall ELSE .MODEL FLAT ENDIF PUBLIC DLLP1 IFDEF MASM ; MASM ; прототипы внешних процедур EXTERN MessageBoxA@16:NEAR ; директивы компоновщику для подключения библиотек includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib ELSE ; TASM EXTERN MessageBoxA:NEAR MessageBoxA@16 = MessageBoxA includelib c:\tasm32\lib\import32.lib ENDIF ;-------------------------------------------------- ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' TEXT DB "В динамической библиотеке",0 MS DB "Сообщение",0 _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' ; [EBP+10H] ; резервный параметр ; [EBP+0CH] ; причина вызова ; [EBP+8] ; идентификатор DLL-модуля DLLENTRY: MOV EAX,1 RET 12 ;------------------ ;адреса параметров DLLP1 PROC EXPORT PUSH EBP MOV EBP,ESP PUSH 0 PUSH OFFSET MS PUSH OFFSET TEXT PUSH 0 CALL MessageBoxA@16 ; изменим строку, расположенную в разделяемой памяти MOV TEXT,'И' MOV TEXT+1,'з' POP EBP RET DLLP1 ENDP _TEXT ENDS END DLLENTRY ; основной модуль DLLEX4.ASM, вызывающий ; процедуру из динамической библиотеки .386P ; плоская модель .MODEL FLAT, stdcall ; константы ; прототипы внешних процедур IFDEF MASM ; MASM EXTERN GetProcAddress@8:NEAR EXTERN LoadLibraryA@4:NEAR EXTERN FreeLibrary@4:NEAR EXTERN ExitProcess@4:NEAR EXTERN MessageBoxA@16:NEAR ; директивы компоновщику для подключения библиотек includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib ELSE ; директивы копоновщику для подключения библиотек includelib c:\tasm32\lib\import32.lib EXTERN GetProcAddress:NEAR EXTERN LoadLibraryA:NEAR EXTERN FreeLibrary:NEAR EXTERN ExitProcess:NEAR EXTERN MessageBoxA:NEAR GetProcAddress@8 = GetProcAddress LoadLibraryA@4 = LoadLibraryA FreeLibrary@4 = FreeLibrary ExitProcess@4 = ExitProcess MessageBoxA@16 = MessageBoxA ENDIF ;---------------------------------- ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' TXT DB 'Ошибка динамической библиотеки',0 MS DB 'Сообщение',0 LIBR DB 'DLL4.DLL',0 HLIB DD ? IFDEF MASM NAMEPROC DB '_DLLP1@0',0 ELSE NAMEPROC DB 'DLLP1',0 ENDIF _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' ; [EBP+10H] ; резервный параметр ; [EBP+0CH] ; причина вызова ; [EBP+8] ; идентификатор DLL-модуля START: ; загрузить библиотеку PUSH OFFSET LIBR CALL LoadLibraryA@4 CMP EAX,0 JE _ERR MOV HLIB,EAX ; получить адрес PUSH OFFSET NAMEPROC PUSH HLIB CALL GetProcAddress@8 CMP EAX,0 JNE YES_NAME ; сообщение об ошибке _ERR: PUSH 0 PUSH OFFSET MS PUSH OFFSET TXT PUSH 0 CALL MessageBoxA@16 JMP _EXIT YES_NAME: CALL EAX PUSH 0 PUSH OFFSET MS PUSH OFFSET MS PUSH 0 CALL MessageBoxA@16 ; закрыть библиотеку ; библиотека автоматически закрывается также ; при выходе из программы PUSH OFFSET NAMEPROC PUSH HLIB CALL FreeLibrary@4 ; выход _EXIT: PUSH 0 CALL ExitProcess@4 _TEXT ENDS END START
Рис. 3.3.7. Пример использования разделяемой памяти в динамической библиотеке.
Трансляция программ на Рис. 3.3.7.
MASM32.
ml /c /coff /DMASM dll4.asm link /subsystem:windows /DLL /section:.data,SRW dll4.obj ml /c /coff /DMASM dllex4.asm link /subsystem:windows dllex4.obj
Атрибуты опции SECTION: S-SHARED, R-READ, W-WRITE.
TASM32.
tasm32 /ml dll4.asm tlink32 -aa -Tpd dll4.obj,,,,dll4.def tasm32 /ml dllex4.asm tlink32 -aa dllex4.obj
Содержимое DEF-файла:
SECTIONS .DATA SHARED EXPORTS DLLP1