Hazards of cross-process messages
In addition to all of the previous issues, posting messages across processes
have additional hazards. For example, you cannot under any circumstances pass
an address across process boundaries.
This is true even if the address is in a shared memory area, such as shared
DLL data segment or a memory-mapped files. Just because an address is valid in
one process does not guarantee it is valid in a different process. And if you
have evidence that says this is not so, that you've seen that you can pass a
shared memory pointer from one process to another and it is valid, be aware that
the phenomenon you are seeing is transient and accidental. While Win32 makes
a sincere attempt to map shared memory into the same location in each process,
it is important to emphasis that it does not guarantee this. Depending
on this opens you to serious problems with the delivered product, which might
not show up for months. But that's an essay for some other time.
In addition, you can hit nasty deadlock situations, where a SendMessage
does not complete, and hangs forever, or appears to.
Here's a typical example, and we've actually seen this in practice:
My application wants to be the one-and-only-copy running. One of the many detection
mechanisms for this is to search for a window of the same class (which is a Bad
Idea, since the class names are invented by MFC), the same caption (which is
a Bad Idea since the captions may vary depending on which MDI child is active),
etc. One of the better ways is to send a Registered Window Message to each window
and look for a specific response. It used to be easy; it is now somewhat harder.
The naive approach is to do something like shown below. What I show here is the
handler for the EnumWindows call:
BOOL CMyApp::myEnumProc(HWND hWnd, LPARAM lParam)
{
CMyApp * me = (CMyApp *)lParam;
if(::SendMessage(hWnd, UWM_ARE_YOU_ME))
{ /* found duplicate */
// ... do something here, e.g.,
me->foundPrevious = TRUE;
return FALSE; // stop enumerating
} /* found duplicate */
return TRUE; // keep enumerating
}
If I have a handler in my main window
LRESULT CMainFrame::OnAreYouMe(WPARAM, LPARAM)
{
return TRUE;
}
then in principle any message sent to any window that doesn't recognize the
Registered Window Message will pass it on to DefWindowProc, which, because
it does not recognize the message, will return 0, which means that only
my window will respond. This is also a technique for locating a client or server
window of an affiliated application.
So if I initially set foundPrevious to FALSE and call EnumWindows(myEnumProc,
(LPARAM)this) then when I return I'll know if I found a previous instance.
There are three major flaws in this code, none of which are evident.
First, while this is a fine technique for recognizing an affiliated window
such as a client window or server window (and in that case, the message would
probably pass a window handle as WPARAM to tell the affiliated window
who sent the message), it won't work for finding a duplicate instance
of the application. That's the topic of a different
essay.
Second, it can deadlock on a failed or non-responsive application, leaving
you hanging.
Third, Microsoft has gone out of their way to make life totally miserable by
installing, on Windows 98, as part of the FrontPage product, a window which ALWAYS
responds with TRUE to any message!!!! While it is hard to believe that
anyone would have been stupid enough to do this, they have done it, and we all
suffer.
Let's look at these problems in detail:
The first flaw is very evident when you have a desktop configured to launch
an application on a single click. A user who is accustomed to double-clicking
to launch an application will double-click, and this will launch TWO copies of
the application. The first one does the EnumWindows loop and sees that
it is the only instance running. It then proceeds to create its main window and
continue on. The second instance comes up, and does the EnumWindows loop
as well. Because we are in a preemptive multithreaded environment, it actually
manages to complete its EnumWindows loop while the first application is
still creating its main window! So the first application's main window isn't
up yet, and the second application doesn't find it, and thinks that IT is the
one-and-only application. So we have two copies running, which is not what we
wanted.
We can't avoid this. Come up with any scenario (such as not calling the EnumWindows
until after the main window is created) and you still get the same race condition.
So we rule this out for finding duplicate copies
of the application. But let's look at the issue of polling all the windows
to find a client or server. In this case, I might do
HWND target = ::SendMessage(hWnd, UWM_HERE_I_AM,
(WPARAM)m_hWnd);
which is handled by the response:
LRESULT CTheOtherApp::OnHereIAm(WPARAM wParam, LPARAM)
{
other = new CWnd;
other.Attach((HWND)wParam);
return (LRESULT)m_hWnd;
}
which looks like a pretty good exchange of window handles. If you don't know
what Attach does, read my essay on Attach/Detach.
Now, somewhere on the system I've got a wedged application. Perhaps it is a
copy of IE waiting for an infinitely-long Web page timeout, and is blocked somewhere
in the network handler. Perhaps it is some poorly-designed program that is doing
a convolution algorithm on a terabit bitmap, and is doing it in the one-and-only
main thread, and isn't allowing any messages to come in (it will be finished
in a week or two). Perhaps it is one of your own programs you're debugging that
is lost in a loop. Whatever. The key is that there is very long, possibly infinite
delay, in an application you may not have even heard of. And guess what? Your
innocent application hangs.
The solution to this is to use ::SendMessageTimeout, so if one of these
applications would block you, you will not hang. You should select a small timeout
interval, such as 100ms, otherwise, you'll have a simply very long startup.
Now to the nasty part, in which Microsoft really does us in. There is some
application which has a window whose class is "Front Page Message Sink",
and which exhibits the pathological and unsociable behavior that it returns a
nonzero value for EVERY message it receives. This is colossally stupid. It is
undocumented, it violates all known Windows standards, and it will be fatal to
you. This only shows up if you have Personal Web Server running. But it is absolutely
inexcusable to have done this.
All I know is that I see it return the value 1, consistently, for any kind
of Registered Window Message it receives. Perhaps this is the only value it returns.
I don't know, and nobody at Microsoft responded to my bug report with anything
explaining what is going on.
Thus testing the result of the ::SendMessage or the DWORD result
of ::SendMessageTimeout against 0 is not informative. Thus far, if you
see 0 or 1, you can't depend on the value having any meaning. With any luck,
Microsoft will fix this (hah!), and ideally they will not introduce other gratuitous
examples of such insanity in the future.
I worked around this by modifying the receiving handler to return a non-zero,
non-one value. For example, I actually use ::RegisterWindowMessage to
get a value, although I suppose I could have used ::GlobalAddAtom. At
least I know that ::RegisterWindowMessage will return a non-zero, non-one
value.
// ... in a header file shared by both applications
#define MY_TOKEN T("Self token-{E32F4800-8180-11d3-BC36-006067709674}")
// ... rewrite the handler as
LRESULT CMainFrame::OnAreYouMe(WPARAM, LPARAM)
{
return ::RegisterWindowMessage(MY_TOKEN);
}
// In the calling module, do something like
UINT token = ::RegisterWindowmessage(MY_TOKEN);
// ... and recode the caller, the EnumWndProc, as
DWORD result
BOOL ok = ::SendMessageTimeout(hWnd,
(WPARAM)m_hWnd, // WPARAM
0, // LPARAM
SMTO_ABORTIFHUNG |
SMTO_NORMAL,
TIMEOUT_INTERVAL,
&result));
if(!ok)
return TRUE; // failed, keep iterating
if(result == token)
{ /* found it */
// ... do whatever you want here
return FALSE; // done iterating
} /* found it */
If you are trying to pass a handle back, instead of just a designated value,
you would have to make the test
if(result != 0 &&
result != 1) // avoid Microsoft problem
{ /* found it */
target = new CWnd;
target->Attach(hWnd);
// ... do whatever you want here
return FALSE; // done iterating
} /* found it */
Note that if you choose to use ::FindWindow, it does an ::EnumWindows
internally, and consequently can be subject to the same permanent hang. Using
::FindWindow is extremely hazardous, since it assumes that in fact every
::SendMessage will complete. Note that ::FindWindow does a ::SendMessage(hWnd,
WM_GETTEXT, ...) to get the window text to search for.