Сообщений 6    Оценка 30        Оценить  
Система Orphus

Обработка исключений в WTL

Автор: Люкшин Иван Станиславович
Источник: RSDN Magazine #1-2010
Опубликовано: 17.08.2010
Исправлено: 10.12.2016
Версия текста: 1.1
Введение
Проблема №1. Обработка необработанных исключений
Особенности очистки объекта при удалении окна в ATL
Проблема №2. Корректное удаление объектов
Описание демонстрационного проекта
Заключение
Ссылки

Демонстрационный проект

Введение

Наряду с несомненными достоинствами, библиотека WTL имеет некоторые недостатки, которые могут портить нервы разработчикам. Одним из таких недостатков является то, что библиотека не дает достаточной безопасности при возникновении исключений в коде пользователя. Однако многие из проблем безопасности по отношению к исключениям можно решить тем или иным способом. Данная статья предлагает вашему вниманию некоторые из этих способов. Мысль о написании этой статьи навеяна ветками форума http://rsdn.ru/forum/atl/687404.1.aspx и http://rsdn.ru/forum/atl/829771.aspx. Подразумевается, что читатель, как минимум, знаком с библиотекой WTL и знаком с принципами организации цикла сообщений для оконного приложения в рамках этой библиотеки. Если нет, то лучше начните с изучения статей об основах WTL, например Использование WTL.

Проблема №1. Обработка необработанных исключений

Как известно, обработка ошибок это существенная часть любых проектов, обойти которую не удается практически никому из разработчиков. Простейший способ обработать возникающие в коде исключения – установить обработчик в функции, которая является точкой входа. В случае с проектом WTL – данный подход не позволяет корректно удалить окна и связанные с ними экземпляры классов. Почему? Ответ кроется в том, как устроено оконное приложение: большинство действий с окнами производятся в рамках цикла обработки сообщений. Ниже приведен пример такого цикла для WTL:

      int Run()
  {
    BOOL bDoIdle = TRUE;
    int nIdleCount = 0;
    BOOL bRet;

    for(;;)
    {
      bRet = ::GetMessage(&m_msg, NULL, 0, 0);

      if(!PreTranslateMessage(&m_msg))
      {
        ::TranslateMessage(&m_msg);
        ::DispatchMessage(&m_msg);
      }
    }
    return (int)m_msg.wParam;
  }

В данном примере представлен кусок функции Run для класса WTL::CMessageLoop. Из нее убраны проверки на ошибки и дополнительная обработка. Полную версию можно легко найти в файле atlapp.h. Чтобы можно было нормально удалить объекты классов, наследуемых от ATL::CWindowImplBaseT необходимо, чтобы цикл продолжал работу (объект освобождает ресурсы при получении WM_NCDESTROY – подробности см. ниже). Модифицируем цикл, и будем обрабатывать исключения в нем. Реализуем для этого класс SafeMessageMap:

      class SafeMessageLoop : public CMessageLoop
{
public:
    SafeMessageLoop(CMainFrame* mainFrame);
    int Run();

private:
    CMainFrame* m_mainFrame;
};

В этом классе мы перекрываем функцию Run() своей, аналогичной по функциональности, с секций catch(…) для перехвата необработанных исключений. Выглядит она следующим образом:

      int SafeMessageLoop::Run()
{
    BOOL bDoIdle = TRUE;
    int nIdleCount = 0;
    BOOL bRet;
    bool failure = false;

    for(;;)
    {
        try
        {
            if(failure && !::PeekMessage(&m_msg, NULL, 0, 0, PM_NOREMOVE))
                PostQuitMessage(-1);

            bRet = ::GetMessage(&m_msg, NULL, 0, 0);

            if(!PreTranslateMessage(&m_msg))
            {
                ::TranslateMessage(&m_msg);
                ::DispatchMessage(&m_msg);
            }
        }
        catch(...)
        {
            if(!failure)
            {
                failure = true;
                ::MessageBox(0,
L"Необработанное исключение!\nОкно приложения будет закрыто", L"Ошибка",
MB_OK);
                if(m_mainFrame && m_mainFrame->IsWindow())
                    m_mainFrame->PostMessage(WM_CLOSE);
            }
        }
    }

    return (int)m_msg.wParam;
}

Как видно из этого фрагмента, функция не претерпела каких либо существенных изменений, добавлен только блок try-catch. Если ограничиться данным подходом, то, теоретически, мы получим возможность корректно закрыть окно. Однако на практике этого недостаточно. Если, например, исключение произойдет в обработчике любого сообщения главного окна приложения, то в деструкторе CWindowImplRoot главного окна сработает assert, который укажет нам, что экземпляр класса не был должным образом деинициализирован. Таким образом, мы подошли к первой проблеме: если исключение происходит в рамках оконной процедуры (обработка любого сообщения, адресованного этому окну) корректной очистки экземпляра класса не происходит.

Особенности очистки объекта при удалении окна в ATL

Корни обозначенной выше проблемы находятся в механизме работы оконной процедуры в рамках архитектуры окон АTL, лежащей, как известно, в основе WTL. Давайте взглянем на функцию оконной процедуры (приведен укороченный вариант):

        template <class TBase, class TWinTraits>
LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::WindowProc(
  HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  CWindowImplBaseT< TBase, TWinTraits >* pThis =
    (CWindowImplBaseT< TBase, TWinTraits >*)hWnd;
  // set a ptr to this message and save the old value// устанавливаем указатель на текущее сообщение, сохраняя предыдущее
  _ATL_MSG msg(pThis->m_hWnd, uMsg, wParam, lParam);
  const _ATL_MSG* pOldMsg = pThis->m_pCurrentMsg;
  pThis->m_pCurrentMsg = &msg;
  // pass to the message map to process  // отдаем сообщение на обработку в карту сообщений
  LRESULT lRes;
  BOOL bRet =
    pThis->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam, lRes, 0);

  // do the default processing if message was not handled  // выполняем обработку по умолчанию если сообщение не было обработаноif(!bRet)
  {
    if(uMsg != WM_NCDESTROY)
      lRes = pThis->DefWindowProc(uMsg, wParam, lParam);
    else
    {
      // mark window as destryed      // помечаем окно уничтоженным
      pThis->m_dwState |= WINSTATE_DESTROYED;
    }
  }
  if((pThis->m_dwState & WINSTATE_DESTROYED) && pOldMsg == NULL)
  {
    // clear out window handle    // очищаем хэндл окна
    HWND hWndThis = pThis->m_hWnd;
    pThis->m_hWnd = NULL;
    pThis->m_dwState &= ~WINSTATE_DESTROYED;
    // clean up after window is destroyed    // очистка после уничтожения окна
    pThis->m_pCurrentMsg = pOldMsg;
    pThis->OnFinalMessage(hWndThis);
  }else {
    pThis->m_pCurrentMsg = pOldMsg;
  }
  return lRes;
}

В первой части функции производится вызов оконной процедуры наследников – ProcessWindowMessage. Это вызов пользовательского кода (обычно функция ProcessWindowMessage реализуется в наследниках с помощью макросов BEGIN_MSG_MAP(class)/END_MSG_MAP() или аналогичных им), который может привести к генерации исключения. Во второй части функции выполняется обработка сообщений по умолчанию и код деинициализации. Как видно, функция реагирует на сообщение WM_NCDESTROY: производит отмену сабклассинга (тут опущено для краткости) и выставляет флаг "окно уничтожено". В конце функции имеется блок очистки экземпляра, выполняющийся, если флаг m_dwState был установлен в WINSTATE_DESTROYED, и вызов не является обработкой вложенного сообщения, т.е. pOldMsg == NULL..Проверка вложенности вызовов работает следующим образом: в начале функции запоминается указатель на предыдущее сообщение, если оно было (только для вложенных сообщений); затем в m_pCurrentMsg записывается указатель на текущее сообщение и вызываются пользовательские обработчики. После завершения обработки сообщения и обработки по умолчанию сохраненное сообщение восстанавливается. Работа данной функции основана на том, что у нее ровно один выход. В случае возникновения исключения это не так. Как результат – объект остается в несогласованном состоянии, т.к. текущее сообщение не было восстановлено, последний блок очистки не выполняется и возникает assert в деструкторе базового класса.

Решение проблемы могло бы быть следующим: не допускать, чтобы исключения выходили за пределы карты сообщений конкретного класса пользователя. Но это неудобно. Есть более гибкое решение – заменить оконную процедуру своей, которая лишена такого дефекта. Выглядеть она могла бы так:

        struct MsgRestorator 
{
    const _ATL_MSG* old;
    const _ATL_MSG** msg;
    MsgRestorator(const _ATL_MSG** m) : msg(m), old(*m) {}
    ~MsgRestorator() { *msg = old; }
};

template <class T, class TBase, class TWinTraits>
LRESULT CALLBACK SafeWindowImpl<T, TBase, TWinTraits >::WindowProcSafe
(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    CWindowImplBaseT< TBase, TWinTraits >* pThis =
        (CWindowImplBaseT< TBase, TWinTraits >*)hWnd;
    // set a ptr to this message and save the old value    // устанавливаем указатель на текущее сообщение, сохраняя предыдущее
    _ATL_MSG msg(pThis->m_hWnd, uMsg, wParam, lParam);
    MsgRestorator savedMsg(&pThis->m_pCurrentMsg);
    pThis->m_pCurrentMsg = &msg;
    // pass to the message map to process    // отдаем сообщение на обработку в карту сообщений
    LRESULT lRes;
    BOOL bRet = pThis->ProcessWindowMessage(
    pThis->m_hWnd, uMsg, wParam, lParam, lRes, 0);
    ATLASSERT(pThis->m_pCurrentMsg == &msg);

    // do the default processing if message was not handled    // выполняем обработку по умолчанию если сообщение не было обработаноif(!bRet)
    {
        if(uMsg != WM_NCDESTROY)
            lRes = pThis->DefWindowProc(uMsg, wParam, lParam);
        else
        {
            // unsubclass, if needed            // отменяем сабклассинг, если необходимо
            LONG_PTR pfnWndProc = ::GetWindowLongPtr(
                pThis->m_hWnd, GWLP_WNDPROC);
            lRes = pThis->DefWindowProc(uMsg, wParam, lParam);
            if(pThis->m_pfnSuperWindowProc != ::DefWindowProc && 
            ::GetWindowLongPtr(pThis->m_hWnd, GWLP_WNDPROC) == pfnWndProc)
                ::SetWindowLongPtr(pThis->m_hWnd, GWLP_WNDPROC,
                    (LONG_PTR)pThis->m_pfnSuperWindowProc);
            // mark window as destryed            // помечаем окно уничтоженным
            pThis->m_dwState |= WINSTATE_DESTROYED;
        }
    }
    if((pThis->m_dwState & WINSTATE_DESTROYED) && savedMsg.old == NULL)
    {
        // clear out window handle        // очищаем хэндл окна
        HWND hWndThis = pThis->m_hWnd;
        pThis->m_hWnd = NULL;
        pThis->m_dwState &= ~WINSTATE_DESTROYED;
        // clean up after window is destroyed        // очистка после уничтожения окна
        pThis->m_pCurrentMsg = savedMsg.old;
        pThis->OnFinalMessage(hWndThis);
    }
    return lRes;
}

В таком виде функция всегда будет восстанавливать текущее сообщение. Нужно сказать пару слов о классе SafeWindowImpl. Этот класс предназначен в качестве замены стандартному CWindowImpl, который является базовым для большинства окон. Поскольку оконные процедуры для диалогов устроены аналогичным образом, то и для диалогов можно изменить базовый класс с CDialogImpl на SafeDialogImpl Эти классы можно найти в демонстрационном проекте, в файле atlsafe.h. Такая замена возможна благодаря тому, что разработчики снабдили класс CWindowImplBaseT виртуальной функцией GetWindowProc(), за что им большое спасибо.

ПРЕДУПРЕЖДЕНИЕ

Класс CContainedWindowT такой функции не имеет. И подменить оконную процедуру для него не получится, не изменив файла atlwin.h, который является частью библиотеки ATL. Поэтому старайтесь не допускать выхода исключений за пределы карты сообщений его наследников. Это не всегда фатально, но в определенных случаях может приводить к обращению к уже освобожденной памяти. Стабильным такое поведение назвать нельзя.

Для тех классов ATL/WTL, что уже используют в качестве базовых классов CWindowImplBaseT или CDialogImplBaseT, необходимо сделать обертки, аналогичные приведенному выше классу. Например, это потребуется для класса CFrameWindowImpl. Пример можно найти в демонстрационном проекте.

Проблема №2. Корректное удаление объектов

Как известно, окна в WTL можно создавать динамически. Но если создание такого окна не вызывает вопросов, удаление экземпляра класса может вызывать определенные трудности. Разработчики предлагают удалять экземпляр в виртуальной функции OnFinalMessage(), которая вызывается, как было показано выше, из оконной процедуры. Однако если какой то объект сохранит у себя указатель на удаляемый экземпляр класса окна, то возникает потенциально опасная ситуация. То же спраедливо и для обратной ситуации, когда другой объект решит удалить экземпляр, а окно еще не удалено. Решить эту проблему можно с помощью счетчика ссылок и boost::intrusive_ptr. Для начала определим класс счетчика ссылок:

      class RefCounted
{
public:
    RefCounted() : m_refcount(0) {}

protected:
    void AddRef() { ++m_refcount; }
    void Release()
    {
        if(m_refcount)
        {
            --m_refcount;
            if(!m_refcount)
                OnDelete();
        }
    }
    unsignedint UseCount() const { return m_refcount; }

    friendvoid intrusive_ptr_add_ref(RefCounted* p);
    friendvoid intrusive_ptr_release(RefCounted* p);

    virtualvoid OnDelete() = 0;

    ~RefCounted() {}
private:
    unsignedint m_refcount;
};
ПРИМЕЧАНИЕ

Я использовал здесь boost в демонстрационных целях – чтобы показать подход к решению проблемы. Если не хочется использовать boost, можно заменить boost::intrusive_ptr любым другим аналогичным решением, вплоть до собственноручно написанного класса.

Затем нужно научить boost::intrusive_ptr работать с этим классом. Сделать это можно следующим образом:

      void intrusive_ptr_add_ref(RefCounted* p)
{
    p->AddRef();
}

void intrusive_ptr_release(RefCounted* p)
{
    p->Release();
}

Теперь можно модифицировать уже созданный класс SafeWindowImpl, чтобы он поддерживал работу с boost::intrusive_ptr. В результате он приобретет вид:

      template <class T,
 class TBase = CWindow,
 class TWinTraits = CControlWinTraits>
class ATL_NO_VTABLE SafeWindowImpl :
 public CWindowImpl< T, TBase, TWinTraits >, public RefCounted
{
public:

    HWND Create(HWND hWndParent, _U_RECT rect = NULL,
 LPCTSTR szWindowName = NULL, DWORD dwStyle = 0, DWORD dwExStyle = 0,
        _U_MENUorID MenuOrID = 0U, LPVOID lpCreateParam = NULL)
    {
        HWND hWnd = CWindowImpl< T, TBase, TWinTraits >::Create(
hWndParent, rect, szWindowName, dwStyle,
dwExStyle, MenuOrID, lpCreateParam);
        if(UseCount())
            AddRef();
        return hWnd;
    }

    virtual WNDPROC GetWindowProc()
    {
        return WindowProcSafe;
    }

    static LRESULT CALLBACK WindowProcSafe(
HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

    virtualvoid OnDelete()
    {
        T* pT = static_cast<T*>(this);
        delete pT;
    }
    virtualvoid OnFinalMessage(HWND /*hWnd*/)
    {
        Release();
    }
};

Аналогично поддержка работы с boost::intrusive_ptr добавляется в базовые классы для диалога главного окна. Проверка UseCount() необходима для того, чтобы не произошло уничтожения окна, которое не было сохранено в boost::intrusive_ptr. Уничтожение такого окна полностью ложится на плечи создавшего его. Также, благодаря этой проверке объекты, унаследованные от SafeWindowImpl, можно безопасно включать в другие классы.

Описание демонстрационного проекта

В демонстрационной программе проиллюстрированы подходы, описанные в данной статье. Пример написан в Visual Studio 9 с использованием библиотеки WTL 8.1 и boost 1.41 (подойдут и более ранние версии).

Чтобы пример имел хоть какую-то ценность, за основу был взят шаблон приложения Multi Threaded SDI, в котором обработка исключений позволяет окнам из других потоков не закрываться при возникновении необработанного исключения. Программа умеет создавать новые окна (пункт меню New Window) и падать: при двойном щелчке внутри клиентской области (падение в дочернем окне), при отправке команды New (падение во вложенном сообщении WM_USER), после закрытия диалога About (падение в обычном сообщении) и в плавающем окне аналогично с рабочей областью окна. В примере сделаны минимальные изменения – вместо агрегации клиентского окна вида используется хранение этого окошка с помощью boost::intrusive_ptr в демонстрационных целях. Падение внутри диалога ничем принципиальным от падения в обычном окне не отличается, поэтому в демонстрационном проекте оно не представлено. Данный пример ни в коем случае не претендует на полноту и всеобщий охват. Он всего лишь показывает, как можно использовать описанные выше техники.

ПРЕДУПРЕЖДЕНИЕ

В примере используется реализация по умолчанию реакции на WM_DESTROY. Это означает, что удалятся корректно только те окна, которые принадлежат главному окну потока. Если в вашем приложении имеются окна, не принадлежащие главному окну, то необходимо вызывать PostQuitMessage() самостоятельно в цикле сообщений после уничтожения всех окошек потока, чтобы гарантировать корректную деинициализацию всех оконных объектов.

Заключение

В этой статье рассмотрено решение некоторых типичных задач, возникающих при работе с WTL. Надеюсь, кому-то она сэкономит немного времени. Спасибо за внимание!

Ссылки

  1. Использование WTL Часть 1
  2. Использование WTL Часть 2


Эта статья опубликована в журнале RSDN Magazine #1-2010. Информацию о журнале можно найти здесь
    Сообщений 6    Оценка 30        Оценить