Wednesday, July 9, 2008

C-style callbacks in C++ code

There have been a few questions about how to use BZFlag's callback pattern. We don't typically use either C++-style template callbacks or functors; we use C-style callbacks in C++ code. It's not an unusual pattern, so I thought I'd lay out how it works here so our SoC students and others can benefit.

Anatomy of a Callback
There are three major components on each side of a callback system:

On the callback ("user") side:
1. Registering function - this function (usually a constructor or initializer) is responsible for registering a callback. It calls the Registration Function, usually providing a pointer to a static member function of the same class with the callback prototype, and passing the 'this' pointer as the user data. There may be an accompanying deregistering function (for instance, an object's destructor) if the calling side has a deregistration function.

2. Primary callback function - this function is almost always a static member function. Its prototype matches the callback prototype provided by the calling side. If you passed 'this' as the user data when registering, you'll get the same pointer (i.e. an instance pointer to your class) in the void* user data when this callback is called.

3. Local callback function - since the primary callback function is static, there's frequently an instanced local callback function, invoked by the primary callback function - for instance, ((MyType*)data)->localCallbackHandler(...). This makes the code easier to follow and avoids access-specifier issues.

On the calling ("library") side:
1. Registration function - this function allows the user to register his desired callback. It stores the function pointer and user data in a table for lookup later. It is usually accompanied by a deregistration function to avoid crashing when the user data is dealloc'ed.

2. Callback prototype - this is a function prototype defined by a typedef that specifies how the callback function should look (parameters and return value). Minimally it includes the user data pointer (as a void*).

3. Calling function - this function iterates through the callback table and calls each one with its appropriate user data, plus any other data the calling side wishes to pass to the callback.

I've been told I need to use a callback - how do I?
First, familiarize yourself with the calling ("library") side. Find the callback prototype and find out what the calling function(s) are - this will tell you what data you have access to, and when the callback will be triggered. Also locate the registration and deregistration functions.

Second, add registration code to the object that you want to receive the callback. This will usually involve a call to the registration function in your constructor and the deregistration function in your destructor, though you may choose to put them elsewhere if your design calls for it.

Third, write your callback function. Follow the prototype you found earlier. Generally this will need to be a static member function. You may be able to do all your logic directly in the primary callback function, but usually you will need a local (instanced) callback also. In some cases you may want all your logic in the local callback, so all you'll need to do is cast the user data to your class type, and call its local callback member.

Example (the "user" side is at the end):

// this is the "additional information" we're going to
// pass back to our callback
typedef struct
{
// stuff
} callbackInfo;

// this is the callback prototype
typedef void (*cbfunc)(const callbackInfo*, void*);

// this is the "calling side" (library) class
class Calling
{
public:
// add a callback to the table
void registerCB(cbfunc function, void* data)
{
callbacks.push_back(
std::make_pair<cbfunc,void*>(function, data));
}

// remove a callback from the table
void deregisterCB(cbfunc function, void* data)
{
// left as an excercise for the reader
}

// this is the function that actually calls the callback
void trigger(void)
{
callbackInfo cbi; // fill this as appropriate
for (std::list<std::pair<cbfunc,void*> >::iterator
itr = callbacks.begin();
itr != callbacks.end(); ++itr)
{
(*itr).first(&cbi, (*itr).second);
}
}

private:
// callback table
std::list<std::pair<cbfunc,void*> > callbacks;
}

// we need an instance of the library class
Calling globalCaller;

// this is the "callback side" (user) class
class CallMe
{
public:
CallMe()
{
globalCaller.registerCB(&myCallback, this);
}
~CallMe()
{
globalCaller.deregisterCB(&myCallback, this);
}
static void myCallback(const callbackInfo* info, void* userData)
{
if (!userData) return;
((CallMe*)userData)->localCallback(info);
}
void localCallback(const callbackInfo* info)
{
// do stuff
}
}

No comments: