Why do member functions need to be “static” to be used as a callback?

As we learned yesterday, nonstatic member functions take a secret "this" parameter, which makes them incompatible with the function signature required by Win32 callbacks. Fortunately, nearly all callbacks provide some way of providing context. You can shove the "this" pointer into the context so you can reconstruct the source object. Here's an example:

class SomeClass {
 static DWORD CALLBACK s_ThreadProc(LPVOID lpParameter)
  return ((SomeClass*)lpParameter)->ThreadProc();
 DWORD ThreadProc()
  ... fun stuff ...

Some callback function signatures place the context parameter (also known as "reference data") as the first parameter. How convenient, for the secret "this" parameter is also the first parameter. Looking at the various calling conventions available to us, it sure looks like the __stdcall calling convention for member functions matches our desired stack layout rather well. Let's take WAITORTIMERCALLBACK for example:

__stdcall callback __stdcall method call thiscall method call
.. rest of stack .. .. rest of stack .. .. rest of stack ..
TimerOrWaitFired TimerOrWaitFired TimerOrWaitFired <- ESP
lpParameter <- ESP this <- ESP

Well, "thiscall" doesn't match, but the two "__stdcall"s do. Fortunately the compiler is smart enough to recognize this and can optimize the s_ThreadProc static method to nothing if you just give it enough of a nudge:

class SomeClass {
 static DWORD CALLBACK s_ThreadProc(LPVOID lpParameter)
  return ((SomeClass*)lpParameter)->ThreadProc();
 DWORD __stdcall ThreadProc()
  ... fun stuff ...

If you look at the code generation for the s_ThreadProc function, you'll see that has been reduced to nothing but a jump instruction, since the compiler has realized that the two calling conventions coincide here so there is no actual translation to do.

?s_ThreadProc@SomeClass@@SGKPAX@Z PROC NEAR
  jmp     ?ThreadProc@SomeClass@@QAGKXZ
?s_ThreadProc@SomeClass@@SGKPAX@Z ENDP

Now some people would take this one step further and just cast the second parameter to CreateThread to LPTHREAD_START_ROUTINE and get rid of the helper s_ThreadProc function entirely. I strongly advise against this. I have seen too many people cause trouble by miscasting function pointers; more on this in a future entry.

Although we took advantage above of a coincidence between the two __stdcall calling conventions, we did not rely on it. If the coincidence in calling conventions fails to occur, the code is still correct. This is important when it comes time to port this code to another architecture, one where the coincidence may longer be true!

Comments (17)
  1. Doug says:

    I would smack any fool around that tried to depend on how the assembly works. Just BEGGING for trouble.

    The good thing is, there are now very, very few programmers who understand assembly to this level, and they are all old enough to know better. Or one would hope.


    Now, passing a non-static member function in for a callback, I’ve seen fools try that. But it never makes it out of programmer testing, because nothing works…..

    (Duh, why am I getting a visit from the Doctor?)

    Course, passing objects in WM_ type stuff can cause other problems with deleted objects. Those can get amusing to prevent.

    What if there is already a message in the queue with that object reference as an LPARAM when you clean things up in the destructor? Can you say trap on shutdown?

    You really need to have some form of validation for the "this" pointer.

  2. Hello Raymon,

    You shouldn’t disregard our precious software in public.


  3. Steve Sheppard says:

    Stupid and illiterate too!


    You seem to have a real problem with names, both RaymonDs and your own -> Willaim != William.

    Thanks for playing,


  4. asdf says:

    Woah, wtf? A new BOOLEAN type? What happened to BOOL?

  5. Most of the callbacks seems to allow user-data, but not the Service-API, which is too bad because when you put several services in one exe, it would be nice to have a class for each.

    Do you know why the Service-API lacks this (no pun intended), is there a good reason or was it just "missed"?

  6. Raymond Chen says:

    BOOLEAN, like UNICODE_STRING, dates back to the days when Windows NT was called "OS/2 3.0 NT".

    Andreas: I don’t know what the deal is with the service APIs. You can always fake it be instancing your callbacks. There are some other callbacks which frustratingly fail to pass reference data, like AbortProc, all the Windows hooks…

  7. Mike says:

    Andreas: Service callbacks take user-data. Put your this pointer in the lpContext.

    DWORD WINAPI HandlerEx(

    DWORD dwControl,

    DWORD dwEventType,

    LPVOID lpEventData,

    LPVOID lpContext


  8. Florian says:

    What if SomeClass was derived from SomeParentClass and ThreadProc was a virtual member function? Would the compiler still do the stack optimization?

  9. Raymond Chen says:

    Try it and see! (and see if you can explain the codegen)

  10. Henk Devos says:

    I’m wondering what’s happened to this in Win64.

    Have all user data parameters been widened to 64 bit?

  11. Ebbe Kristensen says:

    return ((SomeClass*)lpParameter)->ThreadProc();

    really should be:

    return reinterpret_cast<SomeClass*>(lpParameter)->ThreadProc();

    Using new-style casts makes the code safer. Also it is easier to find casts in the source using a simple search.

    See also B. Stroustrup, The C++ Programming Language 3rd edition, pg 819. No, C-style casts are not deprecated – yet…

  12. ??????,??????????? ?????????? calling convention: The history of calling conventions, part 1 The history of calling conventions, part 2 The history of calling conventions, part 3 The history of calling conventions, part 4: ia64 Why do member functions need to be…

  13. Alex says:

    Isn’t this passed in ebx?

  14. Raymond Chen says:

    Try it and see. My remarks applied to the Microsoft Visual C++ compiler – other compilers will probably do things differently.

  15. Thereby ensuring that you match the calling convention.

Comments are closed.

Skip to main content