When the normal window destruction messages are thrown for a loop


Last time, I alluded to weirdness that can result in the normal cycle of destruction messages being thrown out of kilter.

Commenter Adrian noted that the WM_GETMINMAXINFO message arrives before WM_NCCREATE for top-level windows. This is indeed unfortunate but (mistake or not) it's been that way for over a decade and changing it now would introduce serious compatibility risk.

But that's not the weirdness I had in mind.

Some time ago I was helping to debug a problem with a program that was using the ListView control, and the problem was traced to the program subclassing the ListView control and, through a complicated chain of C++ objects, ending up attempting to destroy the ListView control while it was already in the process of being destroyed.

Let's take our new scratch program and illustrate what happens in a more obvious manner.

class RootWindow : public Window
{
public:
 RootWindow() : m_cRecurse(0) { }
 ...
private:
 void CheckWindow(LPCTSTR pszMessage) {
  OutputDebugString(pszMessage);
  if (IsWindow(m_hwnd)) {
   OutputDebugString(TEXT(" - window still exists\r\n"));
  } else {
   OutputDebugString(TEXT(" - window no longer exists\r\n"));
  }
 }
private:
 HWND m_hwndChild;
 UINT m_cRecurse;
 ...
};

LRESULT RootWindow::HandleMessage(
                          UINT uMsg, WPARAM wParam, LPARAM lParam)
{
 ...
  case WM_NCDESTROY:
   CheckWindow(TEXT("WM_NCDESTROY received"));
   if (m_cRecurse < 2) {
    m_cRecurse++;
    CheckWindow(TEXT("WM_NCDESTROY recursing"));
    DestroyWindow(m_hwnd);
    CheckWindow(TEXT("WM_NCDESTROY recursion returned"));
   }
   PostQuitMessage(0);
   break;

  case WM_DESTROY:
   CheckWindow(TEXT("WM_DESTROY received"));
   if (m_cRecurse < 1) {
    m_cRecurse++;
    CheckWindow(TEXT("WM_DESTROY recursing"));
    DestroyWindow(m_hwnd);
    CheckWindow(TEXT("WM_DESTROY recursion returned"));
   }
   break;
  ...
}

We add some debug traces to make it easier to see what is going on. Run the program, then close it, and watch what happens.

WM_DESTROY received - window still exists
WM_DESTROY recursing - window still exists
WM_DESTROY received - window still exists
WM_NCDESTROY received - window still exists
WM_NCDESTROY recursing - window still exists
WM_DESTROY received - window still exists
WM_NCDESTROY received - window still exists
WM_NCDESTROY recursion returned - window no longer exists
Access violation - code c0000005
eax=00267160 ebx=00000000 ecx=00263f40 edx=7c90eb94 esi=00263f40 edi=00000000
eip=0003008f esp=0006f72c ebp=0006f73c iopl=0         nv up ei ng nz na pe cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000283
0003008f ??               ???

Yikes! What happened?

When you clicked the "X" button, this started the window destruction process. As is to be expected, the window received a WM_DESTROY message, but the program responds to this by attempting to destroy the window again. Notice that IsWindow reported that the window still exists at this point. This is true: The window does still exist, although it happens to be in the process of being destroyed. In the original scenario, the code that destroyed the window went something like

if (IsWindow(hwndToDestroy)) {
 DestroyWindow(hwndToDestroy);
}

At any rate, the recursive call to DestroyWindow caused a new window destruction cycle to begin, nested inside the first one. This generates a new WM_DESTROY message, followed by a WM_NCDESTROY message. (Notice that this window has now received two WM_DESTROY messages!) Our bizarro code then makes yet another recursive call to DestroyWindow, which starts a third window destruction cycle. The window gets its third WM_DESTROY message, then its second WM_NCDESTROY message, at which point the second recursive call to DestroyWindow returns. At this point, the window no longer exists: DestroyWindow has destroyed the window.

And that's why we crash. The base Window class handles the WM_NCDESTROY message by destroying the instance variables associated with the window. Therefore, when the innermost DestroyWindow returns, the instance variables have been thrown away. Execution then resumes with the base class's WM_NCDESTROY handler, which tries to access the instance variables and gets heap garbage, and then makes the even worse no-no of freeing memory that is already freed, thereby corrupting the heap. It is here that we crash, attempting to call the virtual destructor on an already-destructed object.

I intentionally chose to use the new scratch program (which uses C++ objects) instead of the classic scratch program (which uses global variables) to highlight the fact that after the recursive DestroyWindow call, all the instance variables are gone and you are operating on freed memory.

Moral of the story: Understand your window lifetimes and don't destroy a window that you know already to be in the process of destruction.

Comments (17)
  1. jwf says:

    Why can’t DestroyWindow assert in this recursive case to give the developer more clues?

  2. binaryc says:

    I’ve seen so many people call DestroyWindow in WM_DESTROY. It really makes me wonder where they are getting the idea that this is the right thing to do. MSDN has some incorrect information, but I really doubt it suggests doing that.

    Maybe people think WM_DESTROY means "please destroy your window" rather than "your window is being destroyed"

  3. Crash Cash says:

    Maybe people think WM_DESTROY means "please destroy your window" rather than "your window is being destroyed"

    It could be they’re coming from X Windows, where that’s exactly the case. Still doesn’t give them an excuse though.

  4. Fox Cutter says:

    In a perfect world it would be nice if windows would see that’s already in the destroy window phase and just ignore the command, but I have the suspicion that such a thing might break things for some applications.

    Still it’s interesting to know, it’s something I’ve done inadvertently done before. I didn’t understand it at the time, but this does explains a lot.

  5. Simon Cooke says:

    : Maybe people think WM_DESTROY means "please

    : destroy your window" rather than "your

    : window is being destroyed"

    That would be silly, because WM_CLOSE means that. ;-)

  6. Jack Mathews says:

    Well, the old old Windows messages don’t have a distinction between notification messages and window messages.

    Looking at WM_DESTROY and WM_CLOSE, and thinking of a "message" being a command, both are telling a window to close or destroy. They’re pretty much synonymous on the surface. Now if WM_DESTROY were named WN_DESTROYING, then that would be a lot easier to see it as "oh, I’m being notified that I’m being destroyed."

    I guess I’m just saying it’s just unfortunate all around and not terribly clear.

  7. Tim Smith says:

    I agree with Jack. I am sure I have done this myself. With general window messages, it can be hard to tell which are active/request messages and which are notifications.

  8. Simon Cooke says:

    Jack wrote:

    : Looking at WM_DESTROY and WM_CLOSE, and

    : thinking of a "message" being a command,

    : both are telling a window to close or

    : destroy. They’re pretty much synonymous on

    : the surface. Now if WM_DESTROY were named

    : WN_DESTROYING, then that would be a lot

    : easier to see it as "oh, I’m being notified

    : that I’m being destroyed."

    Well, WM_CLOSE is a command telling you to get rid of that window. WM_DESTROY is a command telling you that the window is being destroyed, kill the resources it owns.

    That’s the problem; you can view it as both…

  9. Norman Diamond says:

    These two surprise me:

    > WM_NCDESTROY received – window still exists

    > WM_NCDESTROY recursing – window still exists

    I thought that WM_NCCREATE came before the window existed (even if it’s not the first message) and WM_NCDESTROY came after the window stopped existing? Sure the recursion is due to the program’s bug, but why was the window still in existence either of these times?

  10. Everest says:

    Indeed, the whole thing will be more complex if the WH_CBT hook was installed. Who is the very first message the window received depends upon the hook procedures.

  11. Ben Hutchings says:

    Isn’t any use of IsWindow highly problematic? I mean, just because a window handle is valid now, doesn’t mean (a) it will be valid in a moment’s time, or (b) it refers to the window it used to a while ago.

  12. carlso says:

    I’ve seen a lot of WM_DESTROY handling code which goes through considerable amount of trouble to call DestroyWindow() for each child window of the window being destroyed.

    This is, of course, unnecessary as Windows will automatically delete the child windows for you. But it looks like Windows recognizes this case and doesn’t call DestroyWindow() on a child window if you have already done so (i.e., I only see one WM_DESTROY message on a child window).

  13. binaryc says:

    I suppose it’s not terribly clear if you never bother to read the documentation. From MSDN:

    "The WM_DESTROY message is sent when a window is being destroyed."

    "The WM_CLOSE message is sent as a signal that a window or an application should terminate."

    "By default, the DefWindowProc function [for WM_CLOSE] calls the DestroyWindow function to destroy the window."

    "The function [DestroyWindow] sends WM_DESTROY and WM_NCDESTROY messages to the window"

    "DestroyWindow automatically destroys the associated child or owned windows when it destroys the parent or owner window."

    Seems pretty clear to me. The sentence about DestroyWindow sending WM_DESTROY is particularly helpful, as it implies that it would be silly to call DestroyWindow inside a WM_DESTROY.

  14. Stewart Tootill says:

    Isn’t any use of IsWindow highly problematic?

    > I mean, just because a window handle is valid

    > now, doesn’t mean (a) it will be valid in a

    > moment’s time, or (b) it refers to the window

    > it used to a while ago.

    I don’t think this is a problem. It would only be a problem if you were on another thread, but since this example is checking on the thread that created the window, the window exists up until the first complete call to destroy window (which is them most deeply nested one) returns, so it is perfectly safe.

    I don’t even think you can call DestroyWindow for a window that was created on another thread (well, that is to say you can, but it won’t work)

  15. carlso says:

    "I suppose it’s not terribly clear if you never bother to read the documentation."

    Yes, it is clearly documented now. The MSDN documentation has really improved over the years (I believe Raymond deserves thanks for this). But the early documentation wasn’t as clear and there’s been a lot of code written whose authors didn’t have a full understanding of what was going on. And, as such, often times you’re left scratching your head while looking at old code wondering what the heck they were thinking ;-)

  16. anonymous says:

    It seems like hwnd is only valid in the GUI thread that created that hwnd. Any other thread cannot safely use that hwnd. So what is the correct way to send information to a window in a GUI thread that may or may not exist?

  17. fengzi_shen says:

    WM_DESTROY 和 WM_NCDESTROY 消息之间有什么区别?

Comments are closed.