How do I `std::bind` a non-static class member to a Win32 callback function `WNDPROC`?
Asked Answered
R

2

12

I'm trying to bind a non-static class member to a standard WNDPROC function. I know I can simply do this by making the class member static. But, as a C++11 STL learner, I'm very interested in doing it by using the tools under the <functional> header.

My code is as follows.

class MainWindow
{
    public:
        void Create()
        {
            WNDCLASSEXW WindowClass;
            WindowClass.cbSize          = sizeof(WNDCLASSEX);
            WindowClass.style           = m_ClassStyles;
            WindowClass.lpfnWndProc     = std::function<LRESULT(HWND, UINT, WPARAM, LPARAM)>
                                            (   std::bind(&MainWindow::WindowProc, 
                                                *this,
                                                std::placeholders::_1,
                                                std::placeholders::_2,
                                                std::placeholders::_3,
                                                std::placeholders::_4));
            WindowClass.cbClsExtra      = 0;
            WindowClass.cbWndExtra      = 0;
            WindowClass.hInstance       = m_hInstance;
            WindowClass.hIcon           = LoadIconW(m_hInstance, MAKEINTRESOURCEW(IDI_WINDOW));
            WindowClass.hCursor         = LoadCursor(NULL, IDC_ARROW);
            WindowClass.hbrBackground   = (HBRUSH) COLOR_WINDOW;
            WindowClass.lpszMenuName    = MAKEINTRESOURCEW(IDR_MENU);
            WindowClass.lpszClassName   = m_ClassName.c_str();
            WindowClass.hIconSm         = LoadIconW(m_hInstance, MAKEINTRESOURCEW(IDI_WINDOW_SMALL));
            RegisterClassExW(&WindowClass);
            m_hWnd = CreateWindowEx(/*_In_      DWORD*/     ExtendedStyles,
                                    /*_In_opt_  LPCTSTR*/   m_ClassName.c_str(),
                                    /*_In_opt_  LPCTSTR*/   m_WindowTitle.c_str(),
                                    /*_In_      DWORD*/     m_Styles,
                                    /*_In_      int*/       m_x,
                                    /*_In_      int*/       m_y,
                                    /*_In_      int*/       m_Width,
                                    /*_In_      int*/       m_Height,
                                    /*_In_opt_  HWND*/      HWND_DESKTOP,
                                    /*_In_opt_  HMENU*/     NULL,
                                    /*_In_opt_  HINSTANCE*/ WindowClass.hInstance,
                                    /*_In_opt_  LPVOID*/    NULL);

        }

    private:
        LRESULT CALLBACK WindowProc(_In_ HWND hwnd,
                                    _In_ UINT uMsg,
                                    _In_ WPARAM wParam,
                                    _In_ LPARAM lParam)
        {
            return DefWindowProc(hwnd, uMsg, wParam, lParam);
        }
};

When I run it as is, it gives the error message:

Error: no suitable conversion function from "std::function<LRESULT(HWND, UINT, WPARAM, LPARAM)>" to "WNDPROC".
Readily answered 10/8, 2013 at 12:4 Comment(0)
U
15

While JohnB already explained the details why this is not possible, here is a common solution to the problem you are trying to solve: Granting class instance access to a static class member.

The guiding principle to the solution is that an instance pointer must be stored in a way that is accessible to the static class member. When dealing with windows the extra window memory is a good place to store this information. The requested space of extra window memory is specified through WNDCLASSEXW::cbWndExtra while data access is provided through SetWindowLongPtr and GetWindowLongPtr.

  1. Store an instance pointer in the window extra data area after construction:

    void Create()
    {
        WNDCLASSEXW WindowClass;
        // ...
        // Assign the static WindowProc
        WindowClass.lpfnWndProc = &MainWindow::StaticWindowProc;
        // Reserve space to store the instance pointer
        WindowClass.cbWndExtra  = sizeof(MainWindow*);
        // ...
        RegisterClassExW(&WindowClass);
        m_hWnd = CreateWindowEx( /* ... */ );
    
        // Store instance pointer
        SetWindowLongPtrW(m_hWnd, 0, reinterpret_cast<LONG_PTR>(this));
    }
    
  2. Retrieve the instance pointer from the static window procedure and call into the window procedure member function:

    static LRESULT CALLBACK StaticWindowProc( _In_ HWND hwnd,
                                              _In_ UINT uMsg,
                                              _In_ WPARAM wParam,
                                              _In_ LPARAM lParam )
    {
        // Retrieve instance pointer
        MainWindow* pWnd = reinterpret_cast<MainWindow*>(GetWindowLongPtrW(hwnd, 0));
        if ( pWnd != NULL )  // See Note 1 below
            // Call member function if instance is available
            return pWnd->WindowProc(hwnd, uMsg, wParam, lParam);
        else
            // Otherwise perform default message handling
            return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }
    

    The signature of the class member WindowProc is the same as in the code you provided.

This is one way to implement the desired behavior. Remy Lebeau suggested a variation to this which has the benefit of getting all messages routed through the class member WindowProc:

  1. Allocate space in the window extra data (same as above):

    void Create()
    {
        WNDCLASSEXW WindowClass;
        // ...
        // Assign the static WindowProc
        WindowClass.lpfnWndProc = &MainWindow::StaticWindowProc;
        // Reserve space to store the instance pointer
        WindowClass.cbWndExtra  = sizeof(MainWindow*);
        // ...
    
  2. Pass instance pointer to CreateWindowExW:

        m_hWnd = CreateWindowEx( /* ... */,
                                 static_cast<LPVOID>(this) );
        // SetWindowLongPtrW is called from the message handler
    }
    
  3. Extract instance pointer and store it in the window extra data area when the first message (WM_NCCREATE) is sent to the window:

    static LRESULT CALLBACK StaticWindowProc( _In_ HWND hwnd,
                                              _In_ UINT uMsg,
                                              _In_ WPARAM wParam,
                                              _In_ LPARAM lParam )
    {
        // Store instance pointer while handling the first message
        if ( uMsg == WM_NCCREATE )
        {
            CREATESTRUCT* pCS = reinterpret_cast<CREATESTRUCT*>(lParam);
            LPVOID pThis = pCS->lpCreateParams;
            SetWindowLongPtrW(hwnd, 0, reinterpret_cast<LONG_PTR>(pThis));
        }
    
        // At this point the instance pointer will always be available
        MainWindow* pWnd = reinterpret_cast<MainWindow*>(GetWindowLongPtrW(hwnd, 0));
        // see Note 1a below
        return pWnd->WindowProc(hwnd, uMsg, wParam, lParam);
    }
    

Note 1: The instance pointer is stored into the window extra data area after the window has been created while the lpfnWndProc is set prior to creation. This means that StaticWindowProc will be called while the instance pointer is not yet available. As a consequence the if-statement inside StaticWindowProc is required so that messages during creation (like WM_CREATE) do get properly handled.

Note 1a: The restrictions stated under Note 1 do not apply to the alternative implementation. The instance pointer will be available going forward from the first message and the class member WindowProc will consequently be called for all messages.

Note 2: If you want to destroy the C++ class instance when the underlying HWND is destroyed, WM_NCDESTROY is the place to do so; it is the final message sent to any window.

Undercroft answered 10/8, 2013 at 14:43 Comment(11)
Instead of calling SetWindowLongPtr() after CreateWindowEx(), you can pass the instance pointer to CreateWindowEx() itself and then call SetWindowLongPtr() in the WM_NCCREATE message handler.Essential
@Remy Thanks for pointing out an alternative implementation. I updated the answer to include your suggestion.Undercroft
If you use GWLP_USERDATA with SetWindowLongPtr() then you don't need to set the window's cbWndExtra at all.Essential
@Remy That's true, however, GWLP_USERDATA is basically useless today, since everyone and their dog (ab)use it to store their precious data (e.g. when subclassing). Whenever you control window class registration it is more reliable to use that 'private' area instead. Technically, there's no difference - both areas are accessible to anyone. However, the extra window memory is not as much perceived as public domain as the GWLP_USERDATA is.Undercroft
You are wrong. It is possible. I suppose you didn't hear about thunking.Moyer
@Moyer Read the question. Read my answer in context. Comprehend. Don't turn this into a dick-waving contest.Undercroft
@IInspectable: You referred to the other answer where the proof is flawed, the space can be created by creating executable code at runtime. So my point stands. By the way, I made a question specifically because of you, about the const stuff. Knock yourself out.Moyer
@Moyer Why don't you comment on that other answer then, and downvote that other answer instead of mine? Can you see the flaw? Also, creating executable code at runtime is going to upset a few antivirus applications.Undercroft
The suggested way with static_cast<LPVOID>(this) does not work for me - app crashingDonate
@IhorBaklykov You need to debug the application to find out where and why it is crashing. Set up the debugger to break on unhandled SEH exceptions and you'll get right to the point of the crash with a full call stack available for analysis.Undercroft
@Undercroft yeah, sorryl, already found. It was my fault. Code worksDonate
E
1

Guess you cannot do that, since WNDPROC stands for a function pointer. Every function pointer can be converted to a std::function, yet not every std::function represents a function pointer.

Proof of impossibility of your plan: Technically, WNDPROC represents only the address of the function in memory which is to be called. Hence a variable of type WNDPROC does not contain "space" to store information about bound parameters.

Its the same problem as in the following example:

typedef void (* callbackFn) ();

struct CallingObject {
    callbackFn _callback;

    CallingObject (callbackFn theCallback) : _callback (theCallback) {
    }

    void Do () {
       _callback ();
    }
};

void f () { std::cout << "f called"; }
void g () { std::cout << "g called"; }
void h (int i) { std::cout << "h called with parameter " << i; }

int main () {
    CallingObject objF (f); objF.Do (); // ok
    CallingObject objG (g); objG.Do (); // ok

}

Yet in order to call h from a CallingObject with some parameter value determined at runtime, you must store the parameter value in a static variable and then write a wrapper function calling h with this value as argument.

That's the reason callback functions usually take an argument of type void *, where you can pass arbitrary data needed for the calculation.

Elyot answered 10/8, 2013 at 12:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.