Click here to Skip to main content
15,881,248 members
Articles / Programming Languages / C++
Article

Using class methods as callbacks

Rate me:
Please Sign up or sign in to vote.
4.00/5 (17 votes)
17 Apr 20046 min read 118.3K   2.6K   37   17
A neat trick to get the address of class methods and actually use them.

Introduction

This article presents a neat trick allowing you to use a method of any class as a callback.

The need

In many cases, one layer of your SW needs to get services from higher layers. In order to not hurt the layering concept, the "callback" mechanism was invented: the layer which needs the service only knows (and actually declares) the prototype of the service required, not the actual service provider itself. The address of the function which will actually perform the required service is supplied at run-time, via some registration mechanism.

This has been used extensively in C, and even many Win32 APIs get such callback functions as parameters (e.g. ::EnumChildWindows()). The problem presents itself when C++ class methods are the functions you would like to perform the job, not "plain" functions as is the usual case.

Theory

The basic reason this won't work is the fact that the this parameter in C++ is "hidden" -- it is not explicitly passed as any other "regular" argument. You cannot thus masquerade a C++ method as a plain C function.

Let's look at the type of a plain C function returning an int and receiving two ints:

typedef int (*FuncType)( int, int );

The C++ type of of a method in class CMyClass doing the same is:

typedef int (*CMyClass::MethodType)( int, int );

For the reason stated above, FuncType and MethodType are not compatible. The compiler's type-checking system will yell at you for trying to convert between the two, and no kind of cast will help. None. No, not even reinterpret_cast. In fact, some crazy attempts at static casting to "solve" this problem have been know to crash the VC++ compiler and cause internal errors.

Of course, the compiler is right about this: it should not be allowed, since even if you could pass the address of a method to where a regular function is needed, the caller will not be able to specify this to the called method.

What can be done

Several approaches exist. The easiest is simply to avoid the problem altogether: this is sometimes even considered "pretty" C++ programming. For example: when an object wants three callbacks, it will declare an interface (a pure-abstract class with no members) and ask for an interface pointer instead of asking for the three callbacks. It is up to the implementer to implement the three methods, usually with the class-inside-class idiom, and sometimes using multiple inheritance and inheriting from the said interface (as is done in ATL, with all the fooImpl template classes).

Another approach, which I like best, calls for writing small "wrapper" functions for the methods you want to pass as callbacks and passing the wrapper functions themselves as the callback. The wrapper functions are static members of the same class as the method you originally wanted to pass as callback. This requires that the prototype for the callback function gets, on top all the application-specific parameters, some kind of "Context" or "UserData" (X11/the Motif name) parameter, so the code registering the wrapper method can also register the value of this to be later used by the static wrapper when calling the method.

Since these are important ideas (both the static wrapper function and the notion of "UserData" and "Context"), I intend to elaborate on these in future versions of this article should it prove popular enough (more than 0 people read it :)).

Finally, there's the "dirty" approach. I assume there's more than one way, but following is how I do it.

The idea

Two things need to be resolved: the first is how to get the address of the method into a void* (or for that matter: any other type which does not have the word CMyClass:: in it). The second thing is how to call the method (remember the this issue?) once we do have the address.

The first problem is solved with a trick: no cast will work, because all of them do some sort of type-checking. What is the C/C++ construct which actually screams "no type checking done here"? 2 points to anyone who guessed "Ellipses". The compiler will not do any type checking on the actual parameters passed for an ellipses argument. This way, a really small function can be made to cast practically anything into a void*.

The second problem, of how to call the method once we have its address, is solved by using a "caller thunk": a function whose job it is to call the method pointed to by a void*, given the pointer and a value for this. Most methods' calling convention is __stdcall, which means amongst other things, that this is passed in the ECX register and that parameters are pushed on the stack from right to left. Using this knowledge, we can write a small function (in assembly) to do just that: invoke __stdcall methods itself.

Things to note

The implementation of the "caller thunk" is not portable: it should be changed for every compiler, since not all compilers pass this in ECX. (Heck, not all machines in the world even have ECX :)). That's not too bad, though: I already have versions of this thunk for the ARM C++ compiler, for the TeakLite C++ compiler, and for the GNU ARM C++ compiler.

You need to bear in mind that the approach works for __stdcall methods. If your method is explicitly declared to not be __stdcall, or if it has a variable number of arguments (ellipses), the trick will not work. You will simply need to replace the caller thunk to something appropriate. Again -- not too hard.

Finally, please note what you are getting: you are taking the address of a method at link time, not at run-time. This means that late binding techniques (virtual function tables for virtual methods, for example) will not work. If you take the address of CMyClass::f1(), and some class derives from CMyClass and overrides f1(), you will still call CMyClass::f1().

If you think about it for a second, you will see that this is fine: you are losing nothing here. Since you gave the class name at compile time, it's the same as if you could directly call CMyClass::f1() -- you don't expect that to be late-bound, now do you?

Using the "caller thunk" to call static methods and "regular" functions shouldn't work. Or should it? There's no real reason it shouldn't -- all it does is a regular call plus setting of ECX. Since such functions don't use ECX to get this, there's no harm in setting it. Thus, all we need to make sure of is that these functions (unlike the default!) will use the __stdcall calling convention. This is shown in the example.

Using the code

There is only one source file, demonstrating the trick. It contains a plethora of functions/methods which will be used as callbacks, a caller thunk for __stdcall, a function to convert everything into a void*, and the main driver.

I demonstrate a non-virtual method (CMyClass::f1), two virtual methods (CMyClass::f2 and CAnotherClass::f2), and a regular function (MyFunc) as callback functions.

The function to convert everything into a void* is ToVoidStar(). The caller thunk is CallThunk(). See if you can figure out why it's fine that it does not return any value! (Spoiler: it's fine since the return value is kept in EAX by the called callback, and is not changed until we get to CallThunk's caller).

History

None so far :)

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


Written By
Web Developer
Israel Israel
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionWorks fine! Pin
fpaut10-Jul-19 0:33
fpaut10-Jul-19 0:33 
GeneralToVoidStar() ITS COOOOOOOOOL Pin
radioman.lt3-Sep-06 11:14
radioman.lt3-Sep-06 11:14 
GeneralI also used this trick Pin
chief editor6-May-06 10:43
chief editor6-May-06 10:43 
I'm glad to see that someone else implemented the same technique as myself Wink | ;-) . In fact, there is no need to have the 'void* This' parameter in the CallThunk, if you dare to use even more dirty things than described - I mean that a pointer to a member function contains itself the this, and the ToVoidStar can pull it from the stack and pass to the CallThunk (of course, if we assume that only non-static member functions are called via thunk, that in turn should be the case, because static member functions and global functions do not require thunk at all).

As for applicability of the technique, I must agree that it is not so necessary for callbacks, but it is the only method that I could think of then we need ad-hoc to call an arbitrary member function of an arbitrary class from a single line of code.

If I'm wrong, please correct me.

GeneralNicely done but what about... Pin
a1ring25-Oct-04 6:40
a1ring25-Oct-04 6:40 
QuestionWhy not pass pointer to class? Pin
ravenspoint16-Jun-04 18:03
ravenspoint16-Jun-04 18:03 
GeneralGreat article Pin
shlomim18-May-04 5:36
shlomim18-May-04 5:36 
Generalmake it generic Pin
sclavel7-May-04 5:14
sclavel7-May-04 5:14 
GeneralMemories... Pin
HalfWayMan24-Apr-04 1:28
HalfWayMan24-Apr-04 1:28 
GeneralFunctors instead of Function Pointers Pin
DaveAH22-Apr-04 22:29
DaveAH22-Apr-04 22:29 
GeneralNice trick, but... Pin
dCp30321-Apr-04 4:30
dCp30321-Apr-04 4:30 
GeneralRe: Nice trick, but... Pin
Nitzan Shaked21-Apr-04 5:25
Nitzan Shaked21-Apr-04 5:25 
GeneralThis code frightens me, but it's interesting all the same Pin
Don Clugston20-Apr-04 15:22
Don Clugston20-Apr-04 15:22 
GeneralRe: This code frightens me, but it's interesting all the same Pin
Nitzan Shaked20-Apr-04 18:48
Nitzan Shaked20-Apr-04 18:48 
GeneralRe: ARM thunk Pin
Nitzan Shaked20-Apr-04 20:37
Nitzan Shaked20-Apr-04 20:37 
GeneralGreat article - very interesting Pin
mier20-Apr-04 5:13
mier20-Apr-04 5:13 
GeneralUse at your own peril! Pin
MattyT19-Apr-04 14:38
MattyT19-Apr-04 14:38 
GeneralBasically right, but. Pin
Nitzan Shaked19-Apr-04 18:51
Nitzan Shaked19-Apr-04 18:51 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.