Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Class function callback

0.00/5 (No votes)
3 May 2005 1  
Shows how to make callback to functions within classes that are non-static.

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
    { 
      // Pass arguments (right to left)

      mov eax, [lParam];
      push eax;
      mov eax, [wParam];
      push eax;
      mov eax, [message];
      push eax;
      mov eax, this;
      push eax;

      // Fill ecx with the class placement

      mov eax, this;
      add eax, pThisPtr;
      mov ecx, [eax];

     // Call function

     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!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here