Matt, who works for a California programming-tools vendor, specializes in debuggers and file-format programming. This article contains material that will appear in greater detail in Matt's upcoming book, Windows Internals (Addison-Wesley, 1993). He can be contacted through the DDJ offices.
The Windows messaging system is like a heart: It pumps the lifegiving message stream on which all Windows apps depend. Windows messages signal when the mouse moves, a menu item is selected, and a window is created. Dialogs, menus, and other controls rely on messages to communicate with each other; messages also serve as a form of interprocess communication. Even the KERNEL module, which is supposed to lie below the level of the messaging system (implemented in USER.EXE), uses messages to indicate changes in the global heap. Truly understanding Windows means becoming familiar with the inner workings of its messaging system.
This article provides a detailed look at this complex, not fully documented area of Windows 3.1 and presents pseudocode for key routines such as GetMessage(), DispatchMessage(), PeekMessage(), and SendMessage(). I also cover internal functions in Windows that even Undocumented Windows (Addison-Wesley, 1992) does not discuss; these are presented using their real names, which I obtained by examining the symbolic information in the debugging versions of the Windows DLLs.
The Five Kinds of Messages
There are five ways that messages enter the message stream. I used GetQueueStatus(), newly documented and improved in Windows 3.1, to look at return values (QS_*), which are defined in WINDOWS.H. The five categories are:
- Input messages (values of QS_KEY, QS_MOUSEMOVE, and QS_MOUSEBUTTON). Although GetQueueStatus() assigns different QS values, you can consider them all to be input messages generated by hardware devices, which get stored in the shared system message queue.
- Posted messages (QS_POSTMESSAGE). These messages are placed in the application message queue via PostMessage() or PostAppMessage(). There's one application message queue per program.
- Paint messages (QS_PAINT). Like QS_TIMER messages, paint messages don't wait in a queue, but are generated as needed when an application requests a message. The Windows window manager is responsible for knowing if a particular window needs updating. When a window region is invalidated, the messaging system is informed that a repaint is necessary (the QS_PAINT flag is set). Then, when an application asks for a message, a WM_PAINT message is composed.
- Timer messages (QS_TIMER). These are similar to QS_PAINT; both are generated on-the-fly when an application calls GetMessage() or PeekMessage(), instead of being stored in message queues, and thus they do not fill up the queues.
- Sent messages (QS_SENDMESSAGE). SendMessage() sends a message to any window and guarantees that the receiving window will reply before anything else occurs. Sending messages between two windows of the same application is not hard; sending messages between two different tasks is more difficult. Because each window procedure must operate in its normal task context, the Windows scheduler must come into play. Accomplishing this correctly involves synchronization between the two tasks.
The Application Message Queue
Every window in the system is associated with a particular application message queue. In reviewing the fields in a WND data structure (described in Undocumented Windows), note the one that contains a message-queue handle. When a message is posted, this field determines to which queue the message will be added. Even the desktop window has a message queue associated with it.
But the application queue is much more than a holding area for posted messages. Because it contains most of the data used by the Windows messaging system, think of the queue as a sort of command center linking a window handle to a particular task, and serving as the keeper of the status bits vital to GetMessage()/PeekMessage(). The application message queue is closely tied to the application's task database (TDB). Message-queue fields contain the selector of the associated TDB, and vice versa.
At startup, a program's message queue is created by the InitApp() routine. Memory for the application message queue comes from the global heap. You can obtain a handle to the current message queue via the undocumented GetTaskQueue() (USER.35), whose prototype is HANDLE FAR PASCAL GetTaskQueue(HANDLE hTask). If you pass it an hTask value of 0, you'll get the current task's queue.
Messages are placed in the application's queue via PostMessage(). Some internal Windows functions will also call PostMessage() behind the scenes--DefWindowProc() for instance.
The default size for an application message queue is eight messages, usually enough to contain all the messages actually posted to an application. Typically, more messages are sent directly to the window via SendMessage(). You can alter the size of the application queue with SetMessageQueue(). Call this function before any windows are created, because the old message queue gets deleted and a new one created, and this causes confusion if the original message queue is already in use. An alternative to using SetMessageQueue() is to modify the DefaultQueueSize setting in WIN.INI. This is an undocumented key, so you may have to add it if it's not present.
The Windows 3.1 application message-queue structure is in Listing One (page 100). The queue contains data for several purposes. One is to maintain a circular queue of messages. This queue, similar in concept to the ROM-BIOS keyboard buffer, contains read and write pointers which wrap back to the beginning when past the end of the buffer and indicate where the next message will be read from and written to.
The application message queue also supports SendMessage() between tasks by storing the parameters, return values, and current state of the transaction. The section used is not the one for posted messages because sent messages are guaranteed to be processed immediately, ahead of other waiting messages.
To illustrate how to access the contents of the message queue, I wrote a program, Queue.C, which is available electronically; see "Availability," page 5.
The System Message Queue
The system message queue is a kind of half-brother to the application message queue. The system message queue's job is to hold all hardware-input messages. This includes mouse, keyboard, and other input-device events.
In general, hardware events occur at a good clip. Moving your mouse across the screen causes dozens of WM_MOUSEMOVE messages. In order not to lose any of these messages, the system queue's capacity is larger than that of the application queue, containing by default 120 messages. (You can change this by modifying or adding the Type-Ahead entry in WIN.INI.)
The system queue is also allocated and initialized by USER.EXE. There's only one system queue for Windows. The format of the system queue is the same as the application queue, except for stored messages. But the only fields of the system queue actually used are those that implement the circular message buffer.
There's no API to obtain the handle of the system queue, but you can get its handle via a sneaky hack. The first WORD in the segment 0x2C of USER contains the system queue's handle. (In Windows 3.0, it's the WORD at offset 2 of segment 0x2B.) The GlobalEntryModule() function in ToolHelp provides a way to obtain a segment's selector handle, given its ordinal number in the module.
Messages in the system queue are not destined for a particular window because the processing of one system message can affect which window/task subsequent messages go to. For instance, a WM_LBUTTONDOWN message can cause a change of focus. Subsequent messages in the queue must then go to the new focus window rather than the previous one.
On the other hand, the system queue can be locked by a task, ensuring that no other task reads system queue messages until the locking task is done. For example, a double-click message is synthesized out of a series of button up/down messages. One task shouldn't steal messages in the middle of the process. The system queue is unlocked when no messages are left for a task, or when another task's message is found.
How do events get into the system queue? In USER.EXE, EnableInput() calls the mouse and keyboard drivers enable functions (ordinal entry #2). Their parameters are the addresses of the exported USER functions mouse_event() and keybd_event(), respectively; mouse_event() and keybd_event() are essentially interrupt-level functions. When the mouse is moved or a key is struck, a hardware interrupt is generated. The DOS-extender subsystem in Windows vectors control to the appropriate interrupt-handler function in the mouse or keyboard device driver (typically called MOUSE.DRV and KEYBOARD.DRV). The mouse and keyboard drivers then call mouse_event() and keybd_event() via the function pointers passed during the enablement process. Processing occurs inside mouse_event() and keybd_event() to place appropriate values in registers before calling SaveEvent().
SaveEvent() places the message in the system queue via a call to WriteSysMsg(), then attempts to coalesce multiple WM_KEYDOWN messages that result from autorepeating keys. Lastly, it calls WakeSomeone(), which determines the best application candidate to receive the message. When an application is found, flags are set in that app's message queue, and an event is posted to its TDB. The application wakes up and receives the message. Pseudocode for WakeSomeone() is in Listing Two, page 100.
In Listing Two, the test for hQCapture implements the Windows capture mechanism. When your application calls SetCapture(), hQCapture is set to the queue associated with the hwnd parameter to SetCapture(). If hQCapture is nonnull inside WakeSomeone(), the hQCapture queue receives the QS_MOUSE event instead of the queue which would ordinarily have received it. If Windows is in a system modal state, the hQSysModal queue is highest in the pecking order, ahead of the hQCapture queue.
WakeBits, WaitEvent, and the Scheduler
If no messages are waiting for processing inside GetMessage(), the system allows other programs to retrieve pending messages. Before describing how this happens, I'll define a few terms:
WakeBits. Bitfields located at offset 44 in the message queue that indicate that a particular kind of message (QS_PAINT, QS_TIMER, and so on) is available to the task. For instance, QS_PAINT means a paint message is waiting for the application, but hasn't been retrieved. Only QS_POSTEVENT messages exist in the application message queue; other message types imply messages synthesized by the system.
WakeMask. This value, at offset 46 in the message queue, is a mask of the QS_xxx message types that the application is actively waiting for. Typically, GetMessage() is called with wMsgFilterMin and wMsgFilterMax set to 0. This sets the WakeMask to include all the QS_xxx message types. If you specify an actual range of messages in the GetMessage() call, then an appropriate set of QS_xxx bits will be generated inside of GetMessage().
ChangeBits. This field, at offset 42 in the message queue, contains QS_xxx bits that have changed since the last call to GetQueueStatus(), GetMessage(), or PeekMessage().
Now look at Listing Three, page 100. GetMessage() calls SleepHq() to wait for a message, but still yields to other tasks if they have messages. The messaging system checks for sent messages in many places because these messages must be processed immediately. SleepHq() really wants to wait for a QS_POSTMESSAGE, or a QS_PAINT, or whatever; but if it sees a pending QS_SENDMESSAGE flag, it calls ReceiveMessage() to deal with it immediately, and then goes back to its normal business.
Because SendMessage() processing is dealt with inside SleepHq(), your application does not have to do anything special to receive sent messages--it comes free when you call GetMessage(). Your application cannot receive sent messages at any arbitrary time, only inside of GetMessage()/PeekMessage(), when you call SendMessage(), or when calling a function that uses SendMessage() (such as a dialog-box function). So if your program is crunching a long series of numbers, there's no worry that a sent message will unexpectedly arrive and disrupt processing.
The event-count field, located at offset 6 of the TDB, is like a flag on a mailbox. If it's up (contains a nonzero value), then there's a reason to switch to the task because something is waiting for it, as signified by the WakeBits in the message queue (see Listing Four, page 100). The scheduler doesn't know why the task should be awakened, just that it's necessary. WaitEvent() thus waits for the mailbox flag to pop up. SleepHq() is responsible for checking the mailbox, and either waiting some more for a desired QS_xxx letter, or returning when it finds what it wants. If it sees a QS_SENDMESSAGE in the mailbox, SleepHq() takes it out, deals with it promptly, and goes back to waiting for the desired QS_xxx letter. (For more information on the event-count field, see my article, "Inside the Windows Scheduler," DDJ, August 1992.)
Where do the QS_xxx bits come from? SetWakeBit2() is responsible for setting the WakeBits in the application's message queue, as well as ensuring that the program will be scheduled so that it can respond to the message. Pseudocode for SetWakeBit2() is in Listing Four. SetWakeBit2() is heavily used, and called by these USER routines:
WakeSomeone() sets the QS_MOUSE or QS_KEY bits; it's called by the hardware-event handlers when a message has been added to the system queue.
IncPaintCount() sets the QS_PAINT bit; it's called when a window region is invalidated.
SendMessage() sets the QS_SENDMESSAGE bits in the queue of the receiving task during an intertask SendMessage() so that the task will wake up and process the message.
ReceiveMessage() sets a bit not included in the previously defined QS_xxx bits when the receiving task is done processing the message during an intertask SendMessage() and needs to wake up the sending task to receive the result.
ScanTimers() sets the QS_TIMER bit if sufficient time has elapsed; it's called by the timer interrupt service routine.
WriteMessage() sets the QS_POSTMESSAGE bit. PostMessage() and PostAppMessage() call PostMessage2(), which uses WriteMessage() to put the message in the application's queue.
Bringing it All Together
GetMessage() and PeekMessage() are really front ends for a call to GetMessage2(), which does most of the actual work. The pseudocode for the GetMessage()/PeekMessage() front ends and for the workhorse GetMessage2() is in Listing Five, page 100. Listing Six (page 102) presents pseudocode for CheckForNewInput().
Here's how each of the five types of messages are dealt with in GetMessage()/PeekMessage():
QS_SENDMESSAGE. CheckForNewInput() is called several times in GetMessage2(). Its priority is checking for sent messages. If GetMessage2() ends up sleeping, via SleepHq(), sent messages are checked for in SleepHq() code.
QS_POSTMESSAGE. ReadMessage() extracts the message from the application message queue. The message fields are copied into the addresses specified in the GetMessage()/PeekMessage() call.
QS_MOUSE and QS_KEY. ScanSysQueue() extracts the message from the application message queue. The message fields are copied into the addresses specified in the GetMessage()/PeekMessage() call.
QS_PAINT. DoPaint() extracts the message from the system message queue. The message fields are copied into the addresses specified in the GetMessage()/PeekMessage() call.
QS_TIMER. DoTimer() writes the timer message into the application queue. GetMessage2() then starts at the beginning, and finds the timer message as if it were a normal PostMessage().
A couple of conclusions can be drawn from the code. First, GetMessage()/PeekMessage() will not yield to other applications if messages are waiting. Second, there's a definite pecking order of message priorities. Messages sent via SendMessage() always have top priority. This is necessary because the task that did the SendMessage() is cooling its heels, waiting for the reply. Next in priority are messages posted via PostMessage(). Messages from the input system (mouse and keyboard) come after that, and then WM_PAINT messages. WM_PAINT messages are handled after other messages because processing of other messages might generate additional paint operations. Processed at the very end, just before GetMessage2() gives up, goes to sleep, and yields to other tasks, are WM_TIMER messages.
How DispatchMessage Works
Once your application has retrieved a message, you're expected to deal with it--typically, by dispatching it to the appropriate window. Rather than requiring you to determine the address of the window procedure and call it directly, Windows provides DispatchMessage(); see Listing Seven, page 102.
DispatchMessage() is straightforward, except for a few things. At the start of the code, there's special handling for WM_TIMER and WM_SYSTIMER messages. If the lParam field of the message is nonzero, a user-supplied callback is called instead of the standard window procedure. The SDK documentation for SetTimer() describes how to use timers.
Also, DispatchMessage() handles "bad" programs that don't call BeginPaint() in their WM_PAINT handler. Apparently, Microsoft feels that it's enough of a problem that DispatchMessage() always checks if BeginPaint() was called by the app's message handler. If the program didn't call BeginPaint(), Dispatch Message() goes ahead and does some default painting to correct the situation (and whine at you with a debug message if you're running the debug version of Windows).
Lastly, you might notice that, before your program's window procedure is called, DS is set to the hInstance of the application. This compensates for applications that fail to export their callback functions. Under Windows 3.0, this may result in a GP fault (due to an invalid DS) when your window procedure gets called. With Windows 3.1, some people claim you no longer have to export functions or call MakeProcInstance(). This may or may not be sound advice, but Microsoft seems to feel that setting DS is a worthwhile activity for DispatchMessage().
Anatomy of a SendMessage Call
SendMessage() is one of the most frequently used Windows functions, yet perhaps the least understood. Many programmers mistakenly assume that SendMessage() just calls the appropriate window procedure. They forget that Sendmessage() needs to operate in two different task contexts when one application sends a message to another.
This situation can become rather complex. The receiver of a "sent message" might need to send a message to another task before it can respond to the original message, resulting in nested calls to SendMessage(). The processing of an intertask SendMessage() is shown in Listing Eight (page 102). Listing Nine presents pseudocode for ReceiveMessage(), and Listing Ten (page 103) is ReplyMessage().
As you can see from the pseudocode, handling the case where an application sends a message to itself is straightforward. The parameters are pushed on the stack, and the window procedure is called. The bulk of the code in SendMessage() is for handling situations in which the receiving window is in a different task. Within the intertask SendMessage() code and in ReceiveMessage() and ReplyMessage(), a large amount of code has to do with handling nested SendMessage() calls. As these calls pile up on top of each other, the system builds a linked list which specifies the message queues waiting for SendMessage() to return. The most recent queue is at the head of the list. As each message is replied to, the head of the list is removed, and the list shrinks.
Although not normally done, your application program can call ReplyMessage() (within a WH_CALLWND-PROC hook, for example) to prevent the window which ordinarily would get the message from actually receiving it. It's also useful to call ReplyMessage() when handling a message sent to you via SendMessage(). The sending program cannot execute until your program finishes processing the message. When handling the message, if your program calls a Windows function that yields control, such as MessageBox(), a potential deadlock situation can arise. A call to ReplyMessage() before this will avoid the deadlock.
--M.P.
Why's it So Hard to Write a GUI Debugger?
The fatal flaw in the Windows input system is that it is "single threaded." If your application fails to call GetMessage() or PeekMessage() in a timely manner, the system locks up. You can still move the mouse, and background processing in Enhanced-mode DOS boxes continues, but none of the Windows applications can respond to mouse or keyboard input because they aren't given a chance to run.
Say your database program gets a WM_COMMAND message, which it interprets to mean, "Go sort this database of 300,000 records," and dutifully conducts this 45-minute operation; during that time, all apps are locked out until your next call to GetMessage(). The polite thing is for your program to call PeekMessage() occasionally, thus yielding to other applications.
A quirk in the messaging system rears its head when you try to write a Windows-hosted debugger (also called a "GUI debugger"). A GUI debugger is a debugger for Windows programs that itself uses the Windows display mechanisms. What's the problem with that? Well, imagine the following scenario: A GUI debugger places a breakpoint inside of a Window procedure. Eventually, the debuggee program hits the breakpoint, and stops--and cannot call GetMessage() to yield control to other tasks! That means no other tasks--including the GUI debugger--can get their messages. The debugger can't even respond the mouse clicks that tell the debuggee to run again.
You may ask: But there are GUI debuggers available, so how do they deal with this?
Unfortunately, the answer is, "not extremely well." When the debuggee hits the breakpoint (or stops for any reason), the GUI debugger must take over the duties of calling GetMessage() and DispatchMessage() for the debuggee. The debugger must prevent any code in the debuggee process from running. To do this, the debugger needs to somehow intercept all messages that would normally go to the debuggee, and deal with them instead.
One way to accomplish this is by subclassing all of the debuggee's windows. The question then arises: How do you deal with all the messages originally intended for the debuggee? The debugger surely doesn't know how to paint the debuggees windows in response to a WM_PAINT message. Situations where message ordering is critical, such as DDE transactions, are even harder to deal with. Unfortunately, there's no perfect solution. GUI debugger designers deal with this as best they can. This explains why both Borland's Turbo Debugger for Windows and Microsoft's Codeview for Windows are text-mode debuggers. In Win32, the input mechanism has been redesigned (although by the same person who designed the Windows and OS/2 PM input systems). A major goal was to eliminate the input-system problem described above. Consequently, Win32 uses a separate input queues for each task. A thread in the Win32 subsystem continually assigns messages to the appropriate applications queue as input events occur. This lets programs deal with messages in their own sweet time, without adversely affecting the responsiveness of the system as a whole. Unfortunately, this improved functionality does not extend to Win32s applications. Under Win32s, the Windows 3.1 USER.EXE module is still in charge of the input system, thereby causing Win32s applications to be in the same boat as regular Windows programs.
--M.P.
_INSIDE THE WINDOWS MESSAGING SYSTEM_ by Matt Pietrek[LISTING ONE]
<a name="0091_000f"> 00h WORD Selector of next message queue, (implements linked list). 02h WORD hTask of task that owns this queue. 04h WORD Size of a message in this queue. (In Windows 3.1, this is 22). 06h WORD Number of messages waiting that have not been removed by a GetMessage() or PeekMessage(PM_REMOVE). 08h WORD Offset in the queue segment of next message to be retrieved. 0Ah WORD Offset in the queue segment where next message will be written. 0Ch WORD The length in bytes of the queue's segment. 0Eh DWORD DWORD value returned by GetMessageTime(). 12h DWORD DWORD value returned by GetMessagePos(). 16h WORD Unknown. Sometimes contains 1. 18h DWORD Information returned by GetMessageExtraInfo(). 1Ch WORD Unknown. 1Eh DWORD Contains the LPARAM of a SendMessage() to another task. 22h WORD Contains the WPARAM of a SendMessage() to another task. 24h WORD Contains the MSG of a SendMessage() to another task. 26h WORD Contains the HWND of a SendMessage() to another task. 28h WORD Contains the DWORD result from the SendMessage(). 2Ch WORD PostQuitMessage() has been called by this program. 2Eh WORD PostQuitMessage() exit code. 30h WORD Flags of some sort. 32h DWORD Unknown. 36h WORD Expected Windows version, from NE file. 38h WORD Queue handle of application that is sending a message to this app. 3Ah WORD Used for an intertask SendMessage(). 3Ch WORD Used for an intertask SendMessage(). 3Eh WORD Number of "paints" needed by this application. 40h WORD Number of timer events waiting for this application 42h WORD QS_xxx bits that have changed since the last call to GetMessage(), PeekMessage(), or GetQueueStatus(). 44h WORD QS_xxx bits indicating the kind of messages that are waiting for the application. 46h WORD Contains the QS_xxx bits that an application is currently waiting for. 48h WORD Used for intertask SendMessages(). 4Ah WORD Used for intertask SendMessages(). 4Ch WORD Used for intertask SendMessages(). 4Eh WORD Something having to do with hooks 50h BYTE[1Eh] Unknown. Possibly having to do with hooks. 6Eh WORD Start of the posted message storage area. The memory from here, to the end of the segment, can be thought of as an array of messages, each message being 22 bytes in length. <a name="0091_0010"> <a name="0091_0011">[LISTING TWO]
<a name="0091_0011"> // Global variables: hQCursor - The queue "associated" with the cursor // hQActive - The queue of the "active" window that has focus // hQCapture - The queue associated with the capture window // hQSysModal - The queue associated with the system modal window // Local variables: best_queue - contains the current "best guess" as to which // queue should be woken up to receive the message // wakebit - contains the QS_xxx message type (QS_MOUSEMOVE, // QS_MOUSEBUTTON, or QS_KEY) that will be placed in the WakeBits // of whatever queue is selected to receive the message. best_queue = hQCursor if ( message is a not a key message ) goto mouse_event wakebit = QS_KEY if ( hQActive != NULL ) best_queue = hQActive goto system_modal_check mouse_event: if ( message == WM_MOUSEMOVE ) wakebit = QS_MOUSEMOVE else wakebit = QS_MOUSEBUTTON if ( hQCapture != NULL ) best_queue = hQCapture system_modal_check: if ( hQSysModal != NULL ) best_queue = hQSysModal if ( best_queue != 0 ) goto wake_em_up iterate through queue linked list { if ( queues WakeMask includes wakebit determined previously ) { best_queue = current queue under examination goto wake_em_up } if ( at end of queues linked list ) return } wake_em_up: SetWakeBit2(); // Sets WakeBits, and posts event return <a name="0091_0012"> <a name="0091_0013">[LISTING THREE]
<a name="0091_0013"> // WakeMask contains QS_xxx OR'ed together. SleepHq() will not return until at // 1 of QS_xxx bits in the WakeMask parameter has been set in the ChangeBits. void SleepHq( unsigned WakeMask ) { HANDLE currQ SleepHq_check_flags: currQ = Get_current_task_queue // If already have a message then go get it if ( WakeMask & currQ.ChangeBits ) goto SleepHq_done // Check for SendMessages and deal with them if ( currQ.WakeBits & QS_SENDMESSAGE ) goto SleepHq_have_SendMessage // Always check for SendMessages currQ.WakeMask = WakeMask & QS_SENDMESSAGE if ( WakeMask & currQ.ChangeBits ) goto SleepHq_done WaitEvent() // Kernel routine that waits for an event goto SleepHq_check_flags: SleepHq_done: zero_out_currQ.WakeMask return SleepHq_have_SendMessage: zero_out_qWakeMask // Deal with the SendMessage(). Described in the section on SendMessage() ReceiveMessage() goto SleepHq_check_flags } <a name="0091_0014"> <a name="0091_0015">[LISTING FOUR]
<a name="0091_0015"> void SetWakeBit2(HANDLE hQueue, UINT WakeBit) { hQueue.ChangeBit |= WakeBit // Turn on the QS_xxx flags hQueue.WakeBit |= WakeBit // If we're setting a QS_xxx bit that the queue is waiting // for, then force the scheduler to schedule the task if ( WakeBit & hQueue.WakeMask ) { hQueue.WakeMask = 0 PostEvent() to hQueue's task } } <a name="0091_0016"> <a name="0091_0017">[LISTING FIVE]
<a name="0091_0017"> // "flags" are the "flags" parameter to PeekMessage(). "removeFlag" is a local // indicating whether a message will be read from the queue. "WakeMask" is a // local containing a QX_xxx mask of messages types GetMessage()/PeekMessage() // are waiting for. "WakeBits" is a local containing the the QS_xxx bits that // indicate which types of messages are waiting for this task. PeekMessage: Is_GetMessage_call = 0 goto GetMessage2 GetMessage: Is_GetMessage_call = 1 Insert a flags WORD in the stack frame so that the stack frame for GetMessage() is the same as for PeekMessage(). The flag is set to PM_REMOVE. GetMessage2: // This is where GetMessage() and PeekMessage() // start sharing their code if ( current task is locked ) set PM_NOYIELD in flags removeFlag = flags & PM_REMOVE Unlock the system queue if this task holds it. if ( (msgMin != 0) or (msgMax != 0) ) Call function to set up WakeMask for the specified message range else WakeMask = QS_MOUSE | QS_KEY | QS_POSTMESSAGE | QS_TIMER | QS_PAINT begin_looking_for_msgs: if ( !CheckForNewInput() ) goto wait_for_input if ( system queue not locked ) goto not_in_system_queue if ( system queue not locked by current queue ) goto not_in_system_queue if ( (QS_MOUSE | QS_KEY) set in WakeMask and WakeMask ) { if ( ScanSysQueue() ) goto GetMessage_have_msg } not_in_system_queue: if ( QS_POSTMESSAGE set in WakeBits and WakeMask ) if ( ReadMessage() ) goto GetMessage_have_msg if ( (QS_MOUSE or QS_KEY) set in WakeBits and WakeMask ) if ( ScanSysQueue() ) goto GetMessage_have_msg if ( !CheckForNewInput() ) goto wait_for_input if ( QS_PAINT set in WakeBits and WakeMask ) if ( DoPaint() ) goto GetMessage_have_msg if ( PM_NOYIELD set in flags ) goto check_for_timer_msg UserYield() if ( !CheckForNewInput() ) goto wait_for_input check_for_timer_msg: if ( QS_TIMER set in WakeBits and WakeMask ) if ( DoTimer() ) begin_looking_for_msgs wait_for_input: if ( FSHRINKGDI ) ShrinkGDIheap() ; Where is this defined??? // If not in GetMessage, we must be in PeekMessage if ( Is_GetMessage_call == 0 ) goto PeekMessage_exit SleepHq(wakemask) goto begin_looking_for_msgs GetMessage_have_message: if ( a WH_GETMESSAGE hook is installed ) call the hook function // If not in GetMessage, we must be in PeekMessage if ( Is_GetMessage_call ) return 1 if ( returning msg == WM_QUIT ) return 0 else return 1 PeekMessage_exit: if ( ! PM_NOYIELD ) UserYield() // Yield to any higher priority app return 0 <a name="0091_0018"> <a name="0091_0019">[LISTING SIX]
<a name="0091_0019"> // Returns Zero Flag set if no desired input flag is set. WakeMask & WakeBits // are in registers, and are same as WakeMask and WakeBits in GetMessage2(). top: Get handle of current queue if ( QS_SENDMESSAGE set in the queues wakebits ) { ReceiveMessage() goto top } // AND instruction sets the Zero flag if any bits match AND WakeMask, WakeBits together Return <a name="0091_001a"> <a name="0091_001b">[LISTING SEVEN]
<a name="0091_001b"> LPMSG lpMsg // ptr to passed-in message, used as scratch variable. if ( (msg != WM_TIMER) && (msg != WM_SYSTIMER) ) goto handle_normally if ( msg.lParam == 0 ) goto handle_normally GetTickCount() push msg parameters on stack lpMsg = msg.lParam // Timer function callback address AX = SS // Something with MakeProcInstance thunk??? goto call_function handle_normally: if ( msg.hwnd == 0 ) return; push msg parameters on stack if ( msg.msg == WM_PAINT ) set "paint" flag in WND structure lpMsg = Window proc address // stored in WND data structure; // pointed to by msg.hwnd AX = hInstance from WND structure // For use by MakeProcInstance() thunks call_function: ES = DS = SS // Set all segment registers to hInstance of application call [lpMsg] // Call the window proceedure (or timer callback fn). // lpMsg is now used to store the address of window // function (or timer callback function) to be called if ( msg.msg != WM_PAINT ) goto DispatchMessage_done // Check for destroyed window if ( ! IsWindow(msg.msg) ) goto DispatchMessage_done if ( "paint" flag in wnd structure still set ) goto No_BeginPaint DispatchMessage_done: return No_BeginPaint: Display debugging message "Missing BeginPaint..." Call DoSyncPaint() to handle the painting correctly goto DispatchMessage_done <a name="0091_001c"> <a name="0091_001d">[LISTING EIGHT]
<a name="0091_001d"> if ( receiving HWnd == -1 ) goto BroadcastMessage // Not included here Verify sending app has a message queue Get receiving apps queue from receiving hWnd // Are the sending and receiving queues the same??? Intertask = ( receivingHQueue == sendingHQueue ) Call any installed WH_CALLWNDPROC hooks if ( Intertask ) goto InterTaskSend // Next section deals with calling a window proceedure within same program // This is the simple case and is much easier than calling between two // different programs (below) Push address of the wndproc of the receiving WND structure on stack Push SendMessage params on stack Put hInstance into AX Load DS & ES from the SS register Call through the wndproc address in the window structure SendMessage_done: Return to caller SendMessage_error: // Common JMP location when errors occurr Put 0 in DX:AX Goto SendMessage_done // SendMessage()'s that go between different tasks come here. // This is where the code gets complex. InterTaskSend: if ( A task is locked ) { display a diagnostic in debugging version Goto SendMessage_Error } if ( sending task is terminating ) { display a diagnostic in debugging version Goto SendMessage_Error } if (SendMessage parameter area in sending app is already used) { display a diagnostic in debugging version Sleep until the parameter area is free // Uses SleepHq() } Grab parameter area in sending app Save the address where the result of the call will be stored Copy the SendMessage parameters off the stack into the sending hQueue Put the receiving queue at the head of the SendMessage() list // Set bits to wake up the receiving task SetWakeBit2( QS_SENDMESSAGE ) SendMessage_wakeup_receiving_task: if ( a previous SendMessage() has completed ) goto got_reply Turn off "have result" flags in sending queue Call DirectedYield() to force the child task to run next // When the DirectedYield() returns, the receiving task should have awoken // and called ReceiveMessage() and ReplyMessage(). Described below. Sleep until result is back from child // Uses SleepHq(). Probably redundant, because there already should be a // result available when the prior DirectedYield() returned. got_reply: Copy the return value to the "result" area on the stack Release parameter area in sending queue if ( Not replied to ) goto SendMessage_wakeup_receiving_task goto SendMessage_done <a name="0091_001e"> <a name="0091_001f">[LISTING NINE]
<a name="0091_001f"> Make sure there is a SendMessage waiting for us. Remove sending queue from SendMessage() list of queues. Clear QS_SENDMSG bit if the list of queues is empty. Save copies of the sending hQueue and pointer to area where results should be saved in the sending task. Free the the SMPARAMS area in the sending queue. Make sure target window is still valid. Copy the ExtraInfo data from sender to receiver. Call the target window proc. Call ReplyMessage. Return. <a name="0091_0020"> <a name="0091_0021">[LISTING TEN]
<a name="0091_0021"> // Reply message takes the value that should be returned to // the sender as a parameter. Here, it's called "return_value" ReplyMessage_start: If ( message has already been replied to, or if there is no sending queue ) return if ( QS_SENDMESSAGE bit set in receiving queue) { ReceiveMessage() Goto ReplyMessage_start } if ( result area in use ) { OldYield() Goto ReplyMessage_start } Copy return_value into sending hQueue Restore pointer to result area on stack in the sending hQueue Set AlreadyRepliedFlag SetWakeBit2( QS_SMRESULT ) DirectedYield(SendingTask) Return <a name="0091_0022"> //================================= // LISTING 1: QUEUE.C // // QUEUE, by Matt Pietrek, 1992 // //================================= #include <windows.h> #include <dos.h> #include "winio.h" // If your IMPORT.LIB or LIBW.LIB doesn't include // GetTaskQueue(), you'll have to add it to the IMPORTS section // of the .DEF file. The ordinal number is KERNEL.35 WORD FAR PASCAL GetTaskQueue(WORD hTask); typedef struct { DWORD extraInfo; HWND hwnd; WORD message; WORD wParam; DWORD lParam; DWORD time; POINT pt; } QUEUEMSG; typedef struct { WORD NextQueue; WORD OwningTask; WORD MessageSize; WORD NumMessages; WORD ReadPtr; WORD WritePtr; WORD Size; LONG MessageTime; POINT MessagePoint; WORD Unknown1; DWORD ExtraInfo; WORD Unknown2; LONG SendMessageLParam; WORD SendMessageWParam; WORD SendMessageMessage; HWND SendMessageHWnd; DWORD SendMessageResult; WORD QuitFlag; int ExitCode; WORD flags; DWORD Unknown3; WORD ExpWinVersion; WORD SendingHQ; WORD sendmsg_helper1; WORD sendmsg_helper2; WORD PaintCount; WORD TimersCount; WORD ChangeBits; WORD WakeBits; WORD WakeMask; WORD SendMessageResult1; WORD SendMessageResult2; WORD SendMessageResult3; WORD Hook; BYTE Hooks2[30]; BYTE MessageArrayStart; } QUEUE; // // Dumps selected fields of a message queue // void DumpQueueContents(QUEUE far *queue) { QUEUEMSG far *queuemsg; unsigned maxMessages, i; maxMessages = ( queue->Size - FP_OFF(&queue->MessageArrayStart)) / sizeof(QUEUEMSG); queuemsg = (QUEUEMSG far *) &queue->MessageArrayStart; printf("Messages: %u ReadPtr: %04X WritePtr: %04X\n", queue->NumMessages, queue->ReadPtr, queue->WritePtr); printf("WakeBits: "); if ( queue->WakeBits & QS_KEY ) printf("QS_KEY "); if ( queue->WakeBits & QS_MOUSE ) printf("QS_MOUSE "); if ( queue->WakeBits & QS_POSTMESSAGE ) printf("QS_POSTMESSAGE "); if ( queue->WakeBits & QS_TIMER ) printf("QS_TIMER "); if ( queue->WakeBits & QS_PAINT ) printf("QS_PAINT "); printf("\n"); for ( i=0; i < maxMessages; i++ ) { printf( "HWnd: %04X Msg: %04X WParam: %04X LParam: %08lX\n", queuemsg->hwnd, queuemsg->message, queuemsg->wParam, queuemsg->lParam ); queuemsg++; } printf("\n"); } // // Get a pointer to the application message queue. Then, puts // some messages into the queue, and retrieve them. We display // the contents of the queue at each state, so that we can see // the principles involved. // void ExamineQueue(void) { QUEUE far *queue; MSG msg; queue = MK_FP( GetTaskQueue(GetCurrentTask()), 0 ); if ( !queue ) { printf("Unable to find message queue\n"); return; } printf("Here we have an empty queue:\n\n"); DumpQueueContents(queue); printf( "We'll now call PostAppMessage() to put some messages in\n" "the queue. Note that the message count goes up, and that\n" "QS_POSTMESSAGE is now set:\n\n"); PostAppMessage(GetCurrentTask(), 0x1234, 0x5678, 0x12345678L); PostAppMessage(GetCurrentTask(), 0x2345, 0x6789, 0x12345678L); PostAppMessage(GetCurrentTask(), 0x3456, 0x789A, 0x12345678L); PostAppMessage(GetCurrentTask(), 0x4567, 0x89AB, 0x12345678L); DumpQueueContents(queue); printf( "We'll now call GetMessage() to remove a message. The\n" "message still appears in the message array, but the Read\n" "pointer has been incremented. We also print out the\n" "contents of the retrieved message to show that it matches\n" "what was in the queue:\n\n"); GetMessage(&msg, 0, 0, 0); DumpQueueContents(queue); printf( "The message retrieved into the MSG struct:\n" "HWnd: %04X Msg: %04X WParam: %04X LParam: %08lX\n\n", msg.hwnd, msg.message, msg.wParam, msg.lParam ); printf( "We now call GetMessage 3 more times to get rid of the\n" "remaining messages. Note that the Read and Write ptrs are\n" "equal, the QS_POSTMESSAGE flag is no longer set, and the\n" "message count field shows 0. Thus, the queue is considered\n" "to be empty:\n\n"); GetMessage(&msg, 0, 0, 0); GetMessage(&msg, 0, 0, 0); GetMessage(&msg, 0, 0, 0); DumpQueueContents(queue); } int main() { // This program uses the message queue format for Windows // 3.1. Abort if running under any other version. if ( LOWORD(GetVersion()) != 0x0A03 ) { winio_warn(FALSE, "QUEUE", "This program requires Windows 3.1"); return 1; } // Turn off repaints. If we don't do this, the WINIO library // will attempt to use the queue while we're in the process of // examining it. winio_setbusy(); winio_setpaint(winio_current(), FALSE); ExamineQueue(); // Turn the repaints back on. This allows WINIO to refresh // the display with all the output that was created in // ExamineQueue(). winio_setpaint(winio_current(), TRUE); winio_resetbusy(); winio_home(winio_current()); return 0; }
Copyright © 1993, Dr. Dobb's Journal