Introduction
In this article I'll show you how to make a callback to a function within a class that is NOT static. This will make use of functions, pointers and some assembly. It's not possible to my knowledge to do this without the ASM. However, this guide does not require you to know any ASM; instead it will explain what it does. This guide is not intended to teach out ASM, however. There's plenty of other good guides out there on the web for that. This is an advanced technique, however, but even beginners should be able to make use of it.
What do we need? A compiler that can do ASM and a class! In this article I will also show you how I used this technique to forward messages through a window proc. Let's get started then!
First then, we need a base-class. I use MFC in this sample to show you how this technique works. So if you want to follow in this sample, use MFC to create a dialog window. The class is the dialog class of your main window.
class CMyTestDlg
{
public:
...
CProcEdit m_MyEdit;
protected:
LRESULT WindowProc(CWnd* pCtrl, UINT message, WPARAM wParam, LPARAM lParam);
};
Perhaps you noticed the WindowProc
isn't virtual plus an extra argument: pCtrl
. This is because, in this example, I intend to make this a "global" WindowProc
. All controls will forward messages to this one, kinda like the original window proc (non-MFC).
Now that we have this, we need to create the whole forwarding function. How? It's quite simple. We simply derive the controls you want to forward messages from with a new class. Let's define this one, too...
typedef LRESULT (WndProcPtr)(UINT,WPARAM,LPARAM);
class CProcEdit: public CEdit
{
public:
struct CallbackPtr
{
CWnd* pThisPtr;
WndProcPtr* pWndProcPtr;
} pCallbackPtr;
protected:
virtual LRESULT WindowProc(UINT message, WPARAM wParam, LPARAM lParam);
};
We don't need any more complex class. It inherits everything from MFCs CEdit
class, their edit class. In any case, this class' purpose is only to forward its messages. That's why we override the WindowProc
. You're probably also wondering why the struct
and two arguments? A function pointer is only one variable. Yes... but we need two arguments: one for the address to the function and one pointing to the class itself that contains this function. Got that? Good. As to why we need the second... let me get back to that later.
Now then, to make it able to do a callback, we must set these variables when initializing (or creating) the dialog or class. So we do this in the InitDialog
of our main dialog. Now also comes the first part where we must use assembly. Let me show you the code first and then discuss what it does...
BOOL CMyTestDlg::OnInitDialog()
{
WndProcPtr* pCallback;
__asm
{
mov eax, WindowProc;
mov pCallback, eax;
};
m_MyEdit.pCallbackPtr.pThisPtr = this;
m_MyEdit.pCallbackPtr.pWndProcPtr = pCallback;
};
That's all. Now let's see what this code does... and why the ASM? ...and what does it do, the ASM? Let me explain... if you're experienced with this, you should know that...
pCallback = WindowProc;
...will give a compile error. That's why we must use assembly! There is no such restriction there. The "mov
" opcode moves database from or to memory. So first, we move the offset of the WindowProc
function into the eax
register. And then we move the contents of the eax
register into out pCallback
variable. Note that we cannot move the contents directly into the variable due to restrictions of the ASM language. After this, the pCallback
variable contains the address (or offset) of our function. So then we set the two variables of the edit class to point to our dialog class and the function. Okay, with me so far? I hope you are!
Now that we've initialized the class, we must make sure the edit class forwards out messages. This might be a little tricky... Anyway, here's the code:
LRESULT CProcEdit::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
if (pWndProc)
{
__asm
{
mov eax, [lParam];
push eax;
mov eax, [wParam];
push eax;
mov eax, [message];
push eax;
mov eax, this;
push eax;
mov eax, this;
add eax, pThisPtr;
mov ecx, [eax];
mov eax, pWndProc;
add eax, this;
mov eax, [eax];
call eax;
}
}
return CEdit::WindowProc(message, wParam, lParam);
}
Whew, that was sure a lot of assembly code. What does it do? Why do we need ASM to call a function pointer? Well, it's not as easy as you think... first, let me tell you why a normal call wouldn't work. As you know, in classes there is a "this
" pointer. The "this
" pointer is stored in the ecx
register. The ecx
register must point to the offset where the class is located. What happens then, if we call the function normally? The offset placed in ecx
is the offset of the current class - or in other words the CProcEdit
class! When this happens, the function we call won't be able to access its member correctly, possibly even creating a access violation. Why does this happen? The functions are in the classes, so... why can't you just call them and it works?
Good question... unfortunately, it doesn't work that way... did you ever export a function in a DLL that is part of a class? It works. But when you call them, the this
pointer is NULL
! Why? That's because functions are not stored in the classes! Look at this class...
class CAnotherClass
{
int arg1;
int arg2;
void myfunc() { }
};
A sizeof
on this class will return 8! Because there are two int
arguments. The functions are NOT counted! So... when calling a function within a class, the function must know where the class exists in order to reach its data. That's why there is a this
pointer, which is stored in the ecx
register.
That is why we need to assembly. We need to make sure, we fill the ecx
register properly. Hence, why we also took a pointer to the class itself. Okay then, what does the code do? Let's see here...
First, we push the arguments onto the stack. To push something onto the stack, we need to put them into a register first. All arguments are passed from right to left, so there. Second, we must fill in the ecx
with the address (or offset) of its class! And thirdly, we call the function. How all this works, I'm not going to explain more detailed... if you wish to know, then go find an ASM course.
Lastly, we call the base class' windowproc
function so that it may further handle the messages. This is all we need to do! There is one note on this example I wish to pass on, however. In the called windowproc
of CMyTestDlg
, do not call the base class's window proc function! Why? Because the messages are not intended for that class. If you do pass them along, you'll probably get weird painting on the form and other strange things. Instead, return 0 or some other value; it doesn't matter since we do not return the value returned by this window proc.
...And that's all folks! Now you can probably understand why callback to class functions aren't possible. And if not... I guess I can spoil it to you. It's simple. The compiler does not know to which class the function belongs.
Happy programming!