Window Procedures as Class Member Functions

 

Written by Oleg Pudeyev

In this article, I will examine several ways of using a class member function to process Windows messages. I will not discuss how to map particular messages to member functions; in all examples, one window procedure will process all messages, as well as call DefWindowProc for default processing. The techniques are roughly arranged from more simple to more complicated. If you want to jump to a particular section of this document, use the following links:

Base Code Obvious Solution Global Variables Per-Window Data CBT Hooks Global Handle Map MFC Approach ATL Approach

Base Code

I will use a simple Win32 program as a base for the following enhancements. This program will open a window and print "Hello, World!" as a response to WM_PAINT message. See it here: basecode.cpp

Obvious Solution

The obvious, and incorrect, way to place a window procedure in a class is to just do it, like so:

class Window
{
	LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp);
	// ...
};

LRESULT CALLBACK Window::WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
	// Access member variables here
}

// ...
WNDCLASS wc = {
	// ...
	Window::WndProc,
	// ...
};
// ...
See the complete code in obvious.cpp. The compile fails with the following error message on MSVC7:
obvious.cpp(81) : error C2440: 'initializing' : cannot convert from
				'LRESULT (__stdcall Window::* )(HWND,UINT,WPARAM,LPARAM)' to
				'WNDPROC'
        None of the functions with this name in scope match the target type
MSVC6 produces the following error:
obvious.cpp(82) : error C2440: 'initializing' : cannot convert from
				'long (__stdcall Window::*)(struct HWND__ *,unsigned int,unsigned int,long)' to
				'long (__stdcall *)(struct HWND__ *,unsigned int,unsigned int,long)'
        There is no context in which this conversion is possible
The error is generated because member functions are not the same as global functions. The former have a hidden this parameter, referring to the instance of the class on which a particular member function has been called. Out-of-class functions don't have such parameter, and are incompatible with member functions.

 

Simple Fix for Obvious Solution

The error message can be fixed by making the member WndProc static, as the following line shows:

class Window
{
	// ...
	static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp);
	// ...
};
This will allow you to specify Window::WndProc in WNDCLASS initialization. Unfortunately, since WndProc is static, it will not be able to access any of the instance variables, in particular, the HelloString variable. So this is not really a solution.

 

Global Variables

The simplest way to deal with the issue at hand is to maintain a global instance of the class that will be handling messages and create a helper message handler function that will redirect messages to the WndProc of that global class, as the following code snippet shows:

Window w;
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
	return w.WndProc(hwnd, msg, wp, lp);
}
This method is straightforward and reliable, and suitable for cases when there is only one instance of the window class ('class' as in c++ class). In particular, if your application has only one window, this method may be a good choice. The downside of it is that it does not support instantiating multiple windows whose messages are to be handled by different instances of the same class. See the code here: global.cpp

 

Per-Window Data

You can associate a block of memory with an instance of every window that is created by Windows. That block, along with variables pointed to by pointers stored in that block, is called Per-Window Data. You can specify the size of this data area by providing appropriate value in cbWndExtra member of WNDCLASS structure, read it by using GetWindowLong or GetWindowLongPtr, and write it with SetWindowLong or SetWindowLongPtr. The use of Ptr versions are recommended because they are compatible with 64-bit Windows, and you should use them if you're interested in upward compatibility.

We want to associate a given Window object with a particular window by storing pointer to the object in the window's per-window data area. In other words, at some point we will call

SetWindowLong(hwnd, GWL_USERDATA, this);
to establish the association, and at a later time we will use
Window *w = (Window *) GetWindowLong(hwnd, GWL_USERDATA);
to get our Window pointer from the window. Our global WndProc will thus be:
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
	Window *w = (Window *) GetWindowLong(hwnd, GWL_USERDATA);
	if (w)
		return w->WndProc(hwnd, msg, wp, lp);
	else
		return DefWindowProc(hwnd, msg, wp, lp);
}
The if statement is required to handle the case when WndProc is called before the association is established; more on this later. When do we establish the association, that is, call SetWindowLong? It may be tempting to do so right after CreateWindow call, as the following code snippet shows:
Window w;
HWND hwnd = CreateWindow(TEXT("BaseWnd"), TEXT("Hello, World!"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, 0, 0, hinst, 0);
if (!hwnd)
	return -1;
SetWindowLong(hwnd, GWL_USERDATA, &w);
Unfortunately, this will leave many messages unprocessed, including the important WM_CREATE message, because for them WndProc will be called before CreateWindow returns. It would be nice to establish the association earlier.

 

WM_NCCREATE comes to the rescue. As MSDN says, it is sent before WM_CREATE, and it comes with a pointer to CREATESTRUCT that we will use to pass this pointer. Note that CreateWindow has a parameter called lpParam; this same parameter is lpCreateParams member of CREATESTRUCT. By passing a pointer to our Window object, w, in lpParam, we can retrieve it later through lpCreateParams. Having acquired the pointer, we can call SetWindowLong in WM_NCCREATE handler. When WM_CREATE comes around, the pointer will be retrieved via GetWindowLong and the member handler function will be called. See the complete source code here: windowdata.cpp

There is one disadvantage of this approach, and that is the fact that WM_NCCREATE is not the first message received by WndProc. If you are lucky and don't check whether the value retrieved by GetWindowLong was non-null and access member variables when your this pointer is NULL, your program will crash. More precisely, window procedure recieves a WM_GETMINMAXINFO message before it receives WM_NCCREATE.

Optimizing per-window data solution

It is easy to note that the static WndProc will always evaluate the if statement, although once the this pointer is set, we know precisely which code path will be taken. Using two global WndProcs will allow our code to run slightly faster. See it here: windowdata2.cpp

Global temporary this pointer storage

This method allows member WndProc to be called for all messages that the global WndProc receives. The idea belongs to Magmai Kai Holmlor (see this GameDev.net thread). We can store the this pointer in a temporary global variable so that the first time global WndProc is invoked, it can perform the SetWindowLong call. This solution is better than the Global Variable solution above because it uses the global only for a short time, and thus many windows can be created, each using the same global. See gwd.cpp.

The simple global, unfortunately, is not thread-safe. If two threads are lucky enough to create windows simultaneously, it will likely be corrupted and the program will crash. To avoid this, we can use a critical section to protect the global. See gwd2.cpp. Still, the code is not perfect. The following scenario will cause a deadlock between two threads:

  • Thread A creates a window.
  • In WM_CREATE handler for that window, thread A spawns thread B and waits for an event to be set by thread B.
  • Thread B creates another window, sets the event, and continues to do something else.
When thread B attempts to create a window, it will try to enter a critical section already held by thread A, causing a deadlock. The problem can be fixed in two ways, both of which ultimately reduce the time for which each thread holds a lock of the global critical section.
  1. Leave critical section in the message handler once the first message is received. This method requires few changes to existing code, however, the required changes consist of adding a flag to the member data of the window object (gwd2m.cpp) to ensure that the critical section is only released once, regardless of whether window creation succeeded or failed. This looks convoluted and isn't very maintainable. The prospect of requiring instance data in window classes isn't particularly great either.
  2. This solution, implemented in gwd2c.cpp, makes use of a helper container that synchronizes access to a map of thread identifiers to window object pointers. This approach puts synchronization code into a well-defined scope and minimizes the time a critical section lock is held - it is only held for the duration of lookups and inserts in the map. Another positive side effect of this implementation is that it does not use thread-local storage at all.

 

Alternatively, we can use __declspec(thread) modifier to duplicate the global pointer for every active thread, like so:

__declspec(thread) Window *g_pWindow;
Unfortunately, you cannot use __declspec(thread) for data stored in a dll, making this method applicable only to code that is to be placed in executables.

CBT Hooks

CBT hooks present an alternative way to set the window's per-window data pointer to point to a Window object. The CBT hook is called before any messages are passed to the window procedure, making it possible for the hook to perform all work related to associating window object pointer with a window handle. Window procedure only needs to retrieve the pointer.

Before a window is created, set the hook with

	HHOOK hHook = SetWindowsHookEx(WH_CBT, CBTProc, 0, GetCurrentThreadId());
Again, this operation needs to be only done once per thread lifetime, not every time a window is created, but in the interests of keeping the code simple, the latter will be done. Call CreateWindow, and CBTProc will be invoked. In CBTProc, retrieve this pointer using global data, thread-local storage, or another mechanism, and call SetWindowLong. hook.cpp illustrates the use of this method. It builds on the code in gwd2c.cpp, which uses a helper map class for managing thread to window object mapping.

Global Handle Map

Another (non-scalable) possibility is to use a global handle-to-pointer map for all windows. An entry consisting of HWND and a corresponding Window pointer is added each time a Window object is created, and destroyed when the Window object is destroyed. The class can then provide a way to get a Window pointer given a HWND, much like CPtrMap in gwd2c.cpp does. Implementation is pretty straightforward, but potentially inefficient because looking up a pointer, something that has to be done for every processed message, can take linear time in the number of windows opened by the application if you use a vector or a list, or logarithmic time if you use a map. Hash map can perform look up in constant time, but it has other overhead. You can optimize access time by maintaining separate maps for every thread, like MFC does. Note that a global handle map intended for user-level handle-to-pointer resolution (as in MFC's FromHandle and FromHandlePermanent functions) can be maintained alongside with another solution for mapping handles to pointers internally by window management code. Also note that providing such resolution for the user may not be necessary; in fact, ATL lives just fine without it. Finally, note that this solution doesn't actually work by itself as well as other ones do: someone has to establish the handle-to-pointer association, and for that it needs both a handle and the corresponding pointer. Thus, a temporary window procedure, hook, or some other method must be used to establish said association. For simple scenarios, however, one can create the map entry right after calling CreateWindow; if handling of messages that are dispatched during CreateWindow is not required, this might be an acceptable solution.

To see how MFC associates window handles to objects, I used the following minimalistic MFC program: mfc.cpp. It's not pretty when you run it, but it gets the job done. You'll need to execute the program under a debugger, as we'll venture deep into MFC dlls (I don't believe into statically linking code that is available in dlls. Share code, people, and keep your exe sizes down.) If you execute the debug build of mfc.cpp, MFC will assert a bunch of times about null resource identifier I passed to LoadFrame; you can safely ignore these assertions.

The first point of interest is AfxHookWindowCreate, found in wincore.cpp. It's called when a window is created from CWnd::CreateEx function and hooks the window object into MFC. You can see that MFC is using CBT hooks to hook window creation on the current thread; this should look familiar. MFC uses thread state block to store the hook handle, because it maintains the hook for the duration of thread execution. Take note of how MFC exchanges data between its parts: each thread has an associated per-thread state structure (_AFX_THREAD_STATE, afxstat_.h), storing all kinds of thread-specific MFC data, including a window whose creation is being currently processed. The thread state structure itself is allocated from a dynamic array of pointers, and the pointer to the array is stored and retrieved via TlsXXX functions. Look around afxtls_.h and afxtls.cpp if you want to know more about AFX thread-local storage implementation. Getting back to hooks and data exchange, CreateEx stores pointer to the CWnd object it operates on in the thread state, and _AfxCbtFilterHook retrieves it from the thread state.

The hook now has the CWnd pointer it has retrieved from the thread state block, and HWND it corresponds to. It calls CWnd::Attach to bind CWnd to HWND and add a HWND to CWnd association to the per-thread handle map. (Note per-thread; this is why you cannot share CWnd and other windows resource wrapper objects across multiple threads - one thread's CWnd is not present in other threads' HWND-to-CWnd maps. Code that does a permanent CWnd lookup on HWND will fail if called on a thread different than the one which created CWnd object. Note that you can still pass raw HWND and other handles around threads and call CWnd::Attach to bind CWnd to HWNDs; but the CWnd objects will be different ones for different threads.) MFC then subclasses the window and sets the per-thread "window being created" pointer to null. CWnd is now ready to process messages.

When a window receives a message, it arrives at AfxWndProcBase, which routes it to AfxWndProc. The latter looks up CWnd given the passed HWND using the window handle map via CWnd::FromHandlePermanent. This map is thread-specific, thus only the thread that created a window can process messages posted to said window. AfxWndProc then calls AfxCallWndProc, whose only difference from the former is that it takes a CWnd pointer instead of a HWND handle. AfxCallWndProc then routes the message to CWnd::WindowProc, which calls CWnd::OnWndMsg. The latter finds the handler in the message map for a specified window, decodes message parameters, and calls the handler, in our case OnPaint.

ATL Approach

The atl.cpp program is a minimalistic ATL program that I used to investigate ATL message handling architecture. Fire up the debugger and step into CWindowImpl::Create call (atlwin.h). Some time later we arrive at AtlWinModuleAddCreateWndData (atlbase.h) which inserts a pointer to _AtlCreateWndData structure into a list. This structure list stores window object pointers so that they can be accessed from ATL's window procedure before it gets WM_NCCREATE message. Each said structure has a thread identifier and an associated window object pointer. In contrast with MFC, which maintains a reference to the window-being-created for every thread, regardless of whether it actually creates any windows, ATL has one list of structures with that information for the entire application, which should save some memory if you have many worker threads and one or few user-interface threads.

The control now arrives at a static member function CWindowImplBaseT::StartWndProc (atlwin.h). It's the message handler that is called for the first message dispatched to the current window, and its job is to set up CWindow association so that current and future messages can be routed to the member window procedure. Without further delay it extracts this pointer from the list of create-window-data structures using AtlWinModuleExtractCreateWndData (atlbase.h). The latter function looks through the list, attempting to match structures' thread identifier to the current thread identifier. Once a match is found, associated window object pointer is returned. Only one window object pointer is ever be associated with a particular thread, because it's retrieved before any other ATL or user code is run. This allows CreateWindow to be called from the first retrieved message (or at least ATL does not prevent such use). ATL uses a critical section to protect the list from simultaneous access by multiple threads. Note that ATL only acquires locks for the duration of addition and extraction of the create window data, not for the duration of the entire window creation process, preventing unnecessary thread waits.

Now that CWindowImplBaseT::StartWindowProc has the this pointer, it retrieves the actual window procedure for the window object using CWindowImplBaseT::GetWindowProc. Then it creates a thunk whose purpose is to convert HWND handle to a CWindow pointer. Here's how this is accomplished.

  • StartWindowProc initializes a piece of memory, which is part of window object's instance data, to some code. That code acts as an intermediate window procedure, mapping incoming HWND to outgoing CWindow pointer; more on this later. Then the window procedure is set (via a call to SetWindowLong) to point to the beginning of that memory block.
  • To Windows, that piece of memory is just something that stores a window procedure with its usual prototype:
    	 LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
    
  • When a window message arrives at that procedure, the code modifies hwnd parameter to point to a CWindow object. The address of the CWindow object is stored in that same memory block.
  • Then the message procedure performs a jump to the actual static member procedure. It has the same signature as the WindowProc above, except hwnd parameter is not really a window handle, but a CWindow pointer. The function performs a cast to get the pointer:
    	CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)hWnd;
    
The following is the thunk code, found in _stdcallthunk in atlbase.h:
	 mov dword ptr[esp+4], pThis
	 jmp WndProc
pThis is passed as a parameter to Init; it is the pointer to CWindow object for the current window. WndPro c is also passed as a parameter; it is the address of the actual WndProc to use, which is CWindowImplBaseT::WindowProc (atlwin.h) under most circumstances. Init builds the above code at runtime given these two pieces of information. The code is stored in a memory block that is part of the CWindow object, for two reasons:
  1. Code segment is read-only, and modifying code in memory sounds very virus-like.
  2. Different windows can have different window procedures, and they most definitely have different corresponding CWindow pointers; thus, one piece of code cannot be used to handle different windows, and the code must be created or duplicated at runtime.
What the above code does is it replaces the hwnd parameter to the window procedure with a pointer to the object to which the thunk belongs (and that is CWindowImplRoot, atlwin.h). It then performs a jump to the static member WndProc whose first parameter is a CWindow pointer disguised as a HWND to keep the compiler happy. ATL implementation is significantly more efficient than that of MFC as far as getting to the member window procedure is concerned. Not only does ATL code for window object lookup execute in constant time, while MFC code takes linear in the number of windows time, ATL code is about as efficient as it can be made - one move and one jump.

 

MFC vs ATL

Note the following differences between ATL and MFC implementations as described above:

  • ATL does not use any thread-local storage, and therefore does not require any special initialization. If you use MFC, you have to initialize it with AfxWinInit. ATL works straight out of the box with your existing code. Additionally, accessing TLS is done in amortized constant time due to dynamic array resizes, which slows MFC down by another bit.
  • ATL's mappings are not thread-specific. You can use CWindow objects from all threads, not just the creator one. MFC binds CWnd and other objects to the thread that created them. Also note that in general, MFC passes pointers to its classes around (CWnd *), while ATL passes raw handles around (HWND) as function parameters and the such.
  • MFC's WndProc has more overhead due to additional indirections; ATL code is faster.
  • MFC provides a linear time lookup of CWnd given a HWND, which is not the fastest thing if you've got a bunch of windows floating around. ATL doesn't provide equivalent functionality, but it doesn't use it in WndProc either, as MFC does. In other words: MFC's WndProc gets slower as the number of windows belonging to your application increases, while ATL's WndProc executes at the same high speed regardless of how many windows you have.

The End

These are the different ways you can use to have your window procedure as a member function. I tried to make this document practical and easy to follow, and I will appreciate it if you let me know if I succeeded. Want a different code sample? Know of a technique that isn't described here? Have a fix or a suggestion? Email me at olegpb@hotmail.com or talk to me over MSN (same email) or over AIM (blownupcomputer).

MFC Approach

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值