четверг, 2 сентября 2010 г.

Когда зацикливают обычные сообщения удаления окна

Это перевод When the normal window destruction messages are thrown for a loop. Автор: Реймонд Чен.

В последний раз я сослался на "странности", которые могут привести к тому, что обычный порядок доставки сообщений оказывается нарушен, и сообщения приходят в беспорядке.

Комментатор Adrian заметил, что сообщение WM_GETMINMAXINFO приходит до WM_NCCREATE для top-level окон. Это, в самом деле, неудачное поведение, но (баг это или нет) так было вот уже пятнадцать лет, и "исправление" этого имеет большие риски по обратной совместимости.

Но это не та странность, что я имел ввиду.

Когда-то я помогал отладить проблему в приложении, которая использовала элемент управления ListView. Проблема была отслежена до части, где программа создавала sub-класс элемента управления ListView и, через сложную цепочку объектов, почему-то пыталась уничтожить ListView, который уже находился в процессе удаления.

Давайте возьмём пустое приложение и продемонстрируем происходящее в более очевидной манере (прим.пер.: мне очень лениво переводить код):
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;
  ...
}
Мы добавили немного отладочной трассировки, чтобы было проще наблюдать за тем, что происходит. Запустите программу, затем закройте её и наблюдайте за тем, что произойдёт:
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 ??               ???
Ой! Что же произошло?

Когда вы нажали на кнопку "X", это нажатие запустило процессу удаления окна. Как и ожидалось, окно получило сообщение WM_DESTROY, но в ответ программа попыталась уничтожить окно снова. Заметьте, что IsWindow сообщит вам, что окно в этот момент всё ещё существует. Это правда: окно всё ещё существует, хотя и находится в процессе уничтожения. В исходном сценарии код, который удалял окно, делал примерно следующее:
if (IsWindow(hwndToDestroy)) {
 DestroyWindow(hwndToDestroy);
}
В любом случае, рекурсивный вызов DestroyWindow приведёт к новому циклу уничтожения окна, вложенному в первый. Это создаст новое сообщение WM_DESTROY, за которым последует и WM_NCDESTROY (заметьте, что окно при этом получит два сообщения WM_DESTROY!). Наш блестящий код делает ещё один рекурсивный вызов DestroyWindow, что запускает третий цикл уничтожения окна. Окно получает своё третье сообщение, затем второе WM_NCDESTROY - и в этот момент второй рекурсивный вызов DestroyWindow возвращает управление. К этому моменту окно больше не существует: DestroyWindow удалила окно.

Вот почему мы вылетаем. Базовый оконный класс обрабатывает сообщение WM_NCDESTROY уничтожением переменных, ассоциированных с окном. Поэтому, когда самый вложенный вызов DestroyWindow возвращает управление, то эти переменные уже удалены. Выполнение продолжается с обработчика WM_NCDESTROY базового класса, который пытается получить доступ к этим переменным и получает мусор, а затем делает ещё большее "нет-нет": освобождая память, которая уже освобождена, портя, таким образом, кучу процесса. Тут-то мы и вылетаем - пытаясь вызвать виртуальный деструктор на уже удалённом объекте.

Мораль истории: разбирайтесь в жизненном цикле ваших окон и не удаляйте окно, которое уже находится в процессе уничтожения.

Комментариев нет:

Отправить комментарий

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.