Hi, my name is Pat Brenner and I’m a software design engineer on the Visual C++ libraries team. I recently rejoined the Visual C++ team after almost 10 years in the Visual Studio environment team. I am also the owner of Spy++. While I did not originally write Spy++, I owned it from around 1993 until around 2003, and now that I am back on the Visual C++ team it has been returned to me. Today I’m going to spend some time talking about Spy++ internals.
Spy++ is made to be an observer (and not a modifier) of the system around it. While it can do other things as well, the main purpose of Spy++ is to log messages that are being passed around in Windows. Spy++ accomplishes this by the use of three global message hooks: a WH_GETMESSAGE hook, which hooks a message posted to a window (via PostMessage); a WH_CALLWNDPROC hook, which hooks a message sent to a window (via SendMessage); and a WH_CALLWNDPROCRET hook, which hooks the return of a message sent to a window (via SendMessage).
Because the hooks must be global (so that they can hook messages to any window in the system) they must reside in a DLL. This DLL is loaded into every running process as soon as the hooks are set (via SetWindowsHookEx), and remains loaded in the processes until the hooks are unhooked (via UnhookWindowsHookEx).
The hook DLL communicates the message information with the Spy++ application via a circular queue, so there are a number of synchronization requirements.
· Since the hook DLL is loaded into every process, the circular queue resides in a shared data section, and there is a pair of mutexes that controls access to the queue. A writer mutex is used to synchronize write accesses between the loaded copies of the hook DLL, and an access mutex is used to synchronize access between the writers and the Spy++ application itself, which is reading from the queue.
· Because the queue is circular, there needs to be synchronization between the reading and the writing, so there are two events used for this purpose. A read event is signaled when the Spy++ application reads a packet of message data from the queue, and a written event is signaled when a copy of the hook DLL writes a packet of message data to the queue.
· Synchronization is also required with regard to the current read and write locations in the queue, so there are offsets maintained (as shared data) which specify the next read location and the next write location in the queue. There is also a count maintained (in shared data) which is the number of packets that have been written but not yet read.
So the writers (in the hook DLL), when a message comes through one of their hooks, do the following:
· Take the writer mutex.
· Take the access mutex.
· Obtain a message packet (section of shared queue) large enough to store the data for the message.
· Copy the message data to the packet.
· Increment the count of message packets in the queue.
· Increment the write offset by the size of the message packet taken.
· Set the written event.
· Release the access mutex.
· Release the writer mutex.
The reader, which resides in a loop in a thread in the Spy++ application, does the following.
· Check the written event. Once it is set:
· Take the access mutex.
· Copy the message data from the message packet in the shared queue.
· Decrement the count of message packets in the queue.
· If the message packet count is now zero, reset the written event.
· Release the access mutex.
· Set the read event.
· Sends the copied packet data in a message to a hidden window owned by the main thread.
The hidden window processes the message by looping through the current message loggers (since there may be more than one message logger open) and calling each active logger. The logger will then apply any filtering (since the message may have been for a window or message that the logger is not interested in) and if necessary will display the message in its view.
Once Spy++ starts hooking the messages in the system, we want the writers to stay ahead of the reader in the queue, but we don’t want the writers to overtake and pass the reader. So if obtaining a new packet for a writer would overtake the reader, the writer has to loop, doing the following:
· Reset the read event.
· Wait for the read event to be set.
· Check for either of the following conditions:
- The message packet count has returned to zero (the reader is fully caught up).
- The read offset is far enough ahead (write offset plus new packet size does not pass read offset).
That pretty much covers what I wanted to talk about today. There are a couple of other things worth mentioning.
· You may notice that Spy++ does not log any messages for its own windows. Obviously, this is because this would cause an infinite set of messages to be sent throughout the system. So Spy++ knows its own process and thread identifiers and filters out any messages sent to windows owned by them.
· All the data that Spy++ displays is a copy of the original data. Since much of the data passed in messages may only be valid for the lifetime of the message, Spy++ copies any data that it needs to display into the queue, and then copies it again when the message is placed in the message log, since the data in the queue will be overwritten as well.
· Ever since Spy++ was first written, using it has occasionally resulted in the hanging of the system, with the only recourse being to cold-boot the computer. After a recent conversation with a co-worker, I (finally!) realized that this was because all the waits (for mutexes or events) were infinite, rather than using timeouts. This could easily cause a hang, particularly if the Spy++ application crashed while it held the access mutex, since then all the copies of the hook DLL would block in their hooks waiting for the access mutex. So I have made a fix for this (by using timeouts on the waits) which should substantially reduce the trouble that Spy++ can cause in the system. This also made Spy++ much easier for me to debug, since I no longer have to resort to powering off/on my computer when a bug is encountered in the message processing.
Thanks, and I hope you found this interesting. I welcome any questions you might have about Spy++. I welcome feature requests for future versions as well, but keep in mind the “Spy++ is an observer, not a modifier” statement I made earlier.
Visual C++ Libraries Team