Best method for storing this pointer for use in Wn

2019-01-07 07:15发布

问题:

I'm interested to know the best / common way of storing a this pointer for use in the WndProc. I know of several approaches, but each as I understand it have their own drawbacks. My questions are:

What different ways are there of producing this kind of code:

CWindow::WndProc(UINT msg, WPARAM wParam, LPARAM)
{
  this->DoSomething();
}

I can think of Thunks, HashMaps, Thread Local Storage and the Window User Data struct.

What are the pros / cons of each of these approaches?

Points awarded for code examples and recommendations.

This is purely for curiosities sake. After using MFC I've just been wondering how that works and then got to thinking about ATL etc.

Edit: What is the earliest place I can validly use the HWND in the window proc? It is documented as WM_NCCREATE - but if you actually experiment, that's not the first message to be sent to a window.

Edit: ATL uses a thunk for accessing the this pointer. MFC uses a hashtable lookup of HWNDs.

回答1:

In your constructor, call CreateWindowEx with "this" as the lpParam argument.

Then, on WM_NCCREATE, call the following code:

SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR) ((CREATESTRUCT*)lParam)->lpCreateParams);
SetWindowPos(hwnd, 0, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER);

Then, at the top of your window procedure you could do the following:

MyWindowClass *wndptr = (MyWindowClass*) GetWindowLongPtr(hwnd, GWL_USERDATA);

Which allows you to do this:

wndptr->DoSomething();

Of course, you could use the same technique to call something like your function above:

wndptr->WndProc(msg, wparam, lparam);

... which can then use its "this" pointer as expected.



回答2:

While using the SetWindowLongPtr and GetWindowLongPtr to access the GWL_USERDATA might sound like a good idea, I would strongly recommend not using this approach.

This is the exactly the approached used by the Zeus editor and in recent years it has caused nothing but pain.

I think what happens is third party windows messages are sent to Zeus that also have their GWL_USERDATA value set. One application in particular was a Microsoft tool that provied an alternative way to enter Asian characters in any windows application (i.e. some sort of software keyboard utility).

The problem is Zeus always assumes the GWL_USERDATA data was set by it and tries to use the data as a this pointer, which then results in a crash.

If I was to do it all again with, what I know now, I would go for a cached hash lookup approach where the window handle is used as the key.



回答3:

You should use GetWindowLongPtr()/SetWindowLongPtr() (or the deprecated GetWindowLong()/SetWindowLong()). They are fast and do exactly what you want to do. The only tricky part is figuring out when to call SetWindowLongPtr() - You need to do this when the first window message is sent, which is WM_NCCREATE.
See this article for sample code and a more in-depth discussion.

Thread-local storage is a bad idea, since you may have multiple windows running in one thread.

A hash map would also work, but computing the hash function for every window message (and there are a LOT) can get expensive.

I'm not sure how you mean to use thunks; how are you passing around the thunks?



回答4:

I've used SetProp/GetProp to store a pointer to data with the window itself. I'm not sure how it stacks up to the other items you mentioned.



回答5:

You can use GetWindowLongPtr and SetWindowLongPtr; use GWLP_USERDATA to attach the pointer to the window. However, if you are writing a custom control I would suggest to use extra window bytes to get the job done. While registering the window class set the WNDCLASS::cbWndExtra to the size of the data like this, wc.cbWndExtra = sizeof(Ctrl*);.

You can get and set the value using GetWindowLongPtr and SetWindowLongPtr with nIndex parameter set to 0. This method can save GWLP_USERDATA for other purposes.

The disadvantage with GetProp and SetProp, there will be a string comparison to get/set a property.



回答6:

With regard to SetWindowLong() / GetWindowLong() security, according to Microsoft:

The SetWindowLong function fails if the window specified by the hWnd parameter does not belong to the same process as the calling thread.

Unfortunately, until the release of a Security Update on October 12, 2004, Windows would not enforce this rule, allowing an application to set any other application's GWL_USERDATA. Therefore, applications running on unpatched systems are vulnerable to attack through calls to SetWindowLong().



回答7:

In the past I've used the lpParam parameter of CreateWindowEx:

lpParam [in, optional] Type: LPVOID

Pointer to a value to be passed to the window through the CREATESTRUCT structure (lpCreateParams member) pointed to by the lParam param of the WM_CREATE message. This message is sent to the created window by this function before it returns. If an application calls CreateWindow to create a MDI client window, lpParam should point to a CLIENTCREATESTRUCT structure. If an MDI client window calls CreateWindow to create an MDI child window, lpParam should point to a MDICREATESTRUCT structure. lpParam may be NULL if no additional data is needed.

The trick here is to have a static std::map of HWND to class instance pointers. Its possible that the std::map::find might be more performant than the SetWindowLongPtr method. Its certainly easier to write test code using this method though.

Btw if you are using a win32 dialog then you'll need to use the DialogBoxParam function.



回答8:

I recommend setting a thread_local variable just before calling CreateWindow, and reading it in your WindowProc to find out the this variable (I presume you have control over WindowProc).

This way you'll have the this/HWND association on the very first message sent to you window.

With the other approaches suggested here chances are you'll miss on some messages: those sent before WM_CREATE / WM_NCCREATE / WM_GETMINMAXINFO.

class Window
{
    // ...
    static thread_local Window* _windowBeingCreated;
    static thread_local std::unordered_map<HWND, Window*> _hwndMap;
    // ...
    HWND _hwnd;
    // ...
    // all error checking omitted
    // ...
    void Create (HWND parentHWnd, UINT nID, HINSTANCE hinstance)
    {
        // ...
        _windowBeingCreated = this;
        ::CreateWindow (YourWndClassName, L"", WS_CHILD | WS_VISIBLE, x, y, w, h, parentHWnd, (HMENU) nID, hinstance, NULL);
    }

    static LRESULT CALLBACK Window::WindowProcStatic (HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
    {
        Window* _this;
        if (_windowBeingCreated != nullptr)
        {
            _hwndMap[hwnd] = _windowBeingCreated;
            _windowBeingCreated->_hwnd = hwnd;
            _this = _windowBeingCreated;
            windowBeingCreated = NULL;
        }
        else
        {
            auto existing = _hwndMap.find (hwnd);
            _this = existing->second;
        }

        return _this->WindowProc (msg, wparam, lparam);
    }

    LRESULT Window::WindowProc (UINT msg, WPARAM wparam, LPARAM lparam)
    {
        switch (msg)
        {
            // ....


回答9:

ATL's thunk is the most efficent. the thunk executes once and replaces the callback function for the WINPROC to the classes own message processing member function. subsiquent messages are passed by a direct call to the classes member function by windows. it doesnt get any faster than that.