在WM_NCDESTROY上解除分配是否安全?

时间:2016-12-28 12:46:44

标签: c++ winapi

请考虑以下代码段:

// MyWindow.h
struct MyWindow
{
    LRESULT CALLBACK myWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
    static LRESULT CALLBACK myWindowProcWrapper(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
};
extern MyWindow *windowPtr; // windowPtr is initialized on startup using raw new

// MyWindow.cpp
MyWindow *windowPtr = 0;

LRESULT CALLBACK MyWindow::myWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_NCDESTROY:
        delete windowPtr;
        break;
    }
    return DefWindowProc(hwnd, msg, wParam, lParam);
}

LRESULT CALLBACK MyWindow::myWindowProcWrapper(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    return windowPtr->myWindowProc(hwnd, msg, wParam, lParam);
}

问题是给定的代码片段是否像写的那样安全。

基本上,MyWindow是使用WinAPI创建的窗口的类。当窗口被破坏时,我需要做一些最后的清理工作。

请注意,MyWindowwindowPtr的实例是使用原始new创建的。我必须在成员函数中的某处删除实例,所以我从成员函数中删除对象本身的引用。

代码依赖于WM_NCDESTROY是该窗口收到的最后一条消息的假设。

所以问题如下:

  • 是否可以安全地假设WM_NCDESTROY始终是窗口收到的最后一条消息并在那里执行最终清理?
  • 列出的代码是否安全?如果不是,它会在什么条件下破裂?

备注:我只对代码在技术上是否安全感兴趣,而不是使用原始新变量和/或全局变量是好的做法。我有一些很好的理由来实现这个。

3 个答案:

答案 0 :(得分:4)

没有明确记录,WM_NCDESTROY是窗口收到的最终消息。如果您在两行之间阅读,您可以推断出这些信息。

WM_NCDESTROY的文档包含以下注释:

  

此消息释放内部为窗口分配的所有内存。

Window Features: Window Destruction概述了这种后果:

  

当窗口被销毁时,系统会删除与窗口关联的所有内部数据。 这会使窗口句柄无效,应用程序将无法再使用它。

将它们放在一起,破坏窗口会使窗口句柄无效。一旦WM_NCDESTROY消息处理程序运行完成,窗口句柄就不再有效。无效的窗口句柄不再接收任何消息。

您的实施是安全的。

令人怀疑的是,这些规则中的任何规则将来都会发生变化(有很多应用程序依赖WM_NCDESTROY作为最终消息),但如果您想做好准备,可能需要考虑放置{ {1}}之后的{1}}语句。这样做可以确保您的应用程序以可预测的方式失败,以防在windowPtr = nullptr;实例被处置后收到消息。

答案 1 :(得分:0)

是。自我解除分配在WM_NCDESTROY中是安全的。 但是为了确保你的程序不会在Wine下失败,例如,我建议在删除后将windowPtr设置为null,并在调用windowPtr-> myWindowProc之前检查它。 而且,因为它必须只是那个wndclass的窗口。

答案 2 :(得分:0)

是的,WM_NCDESTROY是发送给WindowProc的最后一条消息(现在最低限度)。

但在delete this中直接,无条件地调用myWindowProc在复杂情况下可能不安全并且会产生非常糟糕的错误(因为很难找到)

您不会考虑可以递归调用myWindowProc

让我们首先考虑一个简单的版本:

    case WM_KEYDOWN:
        if (wParam == VK_ESCAPE)
        {
            DestroyWindow(hwnd);
        }

所以我们销毁ESCAPE上的窗口。在这种情况下myWindowProcWM_NCDESTROY递归调用,我们delete this,然后我们返回上一级myWindowProcWM_KEYDOWN)。但此时this已被破坏且无效。所以我们不能再访问任何成员字段或虚函数。如果我们在自己DefWindowProc的末尾直接调用WindowProc,那就太好了。但是,如果说我们的类实现并调用virtual LRESULT DefWinProc(..)允许覆盖DefWindowProc行为,该怎么办? (对DefMDIChildProc说)?

现在更复杂的情况 - 假设您实现了一些子控件。基于WM_SOMETHING_1您将WM_NOTIFY_SOMETHING_1设置为父级(通过SendMessage)。和父母在处理此通知时决定调用DestrowWindow(对于自己,以及所有孩子的结果)。所以在内部通话delete this,当您从SendMessage(..WM_NOTIFY_SOMETHING_1..)返回时,您的this已经被删除,但您甚至都不知道。

访问this之后,它将在复杂的Windows案例中被删除,如果我们修改一些成员数据 - 可能不是直接崩溃(这对于检测有利),而是堆损坏,这将在以后显示这很难察觉。

然而,对于这个存在100%正确的解决方案,即使WM_NCDESTROY不是窗口的最终消息,它也将起作用。

我们不得使用全局MyWindow *windowPtr,而是通过GWLP_USERDATA将其分配给窗口数据,并在WM_NCDESTROY上删除。仅在myWindowProc不返回0时调用GetWindowLongPtr(hwnd, GWLP_USERDATA)。在这种情况下,即使WM_NCDESTROY - 静态myWindowProcWrapper之后的某些WM_ *消息没有调用myWindowProc,我们也必须使用MyWindow类的引用计数:

  1. AddRef();
  2. 之前致电SetWindowLongPtrW(hwnd, GWLP_USERDATA,(LONG_PTR)windowPtr);
  3. Release();
  4. 之后致电SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0);
  5. 包裹windowPtr->myWindowProc(hwnd, msg, wParam, lParam) AddRef(); Release();
  6. 仅从delete this致电Release并制作~MyWindow() private:
  7. 使用此规则,我们可以在任何时间和任何情况下绝对安全地访问myWindowProc中的此指针。并且不依赖于WM_NCDESTROY是最后一条消息

    struct MyWindow
    {
    private:
        PSTR _somedata;
        LONG _dwRef;
    
    public:
    
        MyWindow() : _dwRef(1), _somedata(0)
        {
        }
    
        void AddRef()
        {
            InterlockedIncrement(&_dwRef);
        }
    
        void Release()
        {
            if (!InterlockedDecrement(&_dwRef)) delete this;
        }
    
        static LRESULT CALLBACK myWindowProcWrapper(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
        {
            MyWindow *windowPtr;
    
            if (msg == WM_NCCREATE)
            {
                windowPtr = (MyWindow *)((LPCREATESTRUCT)lParam)->lpCreateParams;
    
                windowPtr->AddRef();
                SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR)windowPtr);
            }
            else
            {
                windowPtr = (MyWindow *)GetWindowLongPtr(hwnd, GWLP_USERDATA);
            }
    
            LRESULT lr;
    
            if (windowPtr)
            {
                windowPtr->AddRef();
                lr = windowPtr->myWindowProc(hwnd, msg, wParam, lParam);
                windowPtr->Release();
            }
            else
            {
                lr = DefWindowProc(hwnd, msg, wParam, lParam);
            }
    
            if (msg == WM_NCDESTROY && windowPtr)
            {
                SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0);
                windowPtr->Release();
            }
    
            return lr;
        }
    
    protected:
    
        LRESULT CALLBACK myWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
        {
            switch (msg)
            {
            case WM_NCDESTROY:
                PostQuitMessage(0);
                break;
    
            case WM_KEYDOWN:
                if (wParam == VK_ESCAPE)
                {
                    DestroyWindow(hwnd);
                    DbgPrint("%s\n", _somedata);// bug can be here if not use ref semantic
                }
                break;
    
            case WM_CREATE:
                if (_somedata = new CHAR[64])
                {
                    strcpy(_somedata, "1234567890");
                }
                break;
            }
            // bug can be here if not use ref semantic, because myDefWinProc virtual
            return myDefWinProc(hwnd, msg, wParam, lParam);
            //return DefWindowProc(hwnd, uMsg, wParam, lParam);
        }
    
        // for demo only here, not need in simply case
        virtual LRESULT myDefWinProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
        {
            return DefWindowProc(hwnd, uMsg, wParam, lParam);
        }
    
    private:
    
        ~MyWindow()
        {
            if (_somedata) delete _somedata;
        }
    };
    
    
        if (MyWindow* p = new MyWindow)
        {
            CreateWindowEx(0, L"lpszClassName", L"lpWindowName", WS_OVERLAPPEDWINDOW|WS_VISIBLE,
                CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, HWND_DESKTOP, 0, 0, p);
            p->Release();
        }