Technical

Do C Callbacks Like This. Not Like That.

Let’s say you’re creating a module that uses callbacks. Being a good person, you’ve allowed the caller to register a context that will be passed to the callback when it’s invoked (this is super helpful for C/C++ interoperability, BTW…but that’s a different blog post):

typedef void (*callback_type)(void *context, int notification_flag);
void register_notification(callback_type callback, void *context);

Now, when you’re using this module, you might be tempted to do this:

struct my_context {
  int foo;
};

void notification_callback(struct my_context *context, int notification_flag) {
  context->foo++;
}

...
register_notification((callback_type)notification_callback, &a_context);
...

That cast in register_notification is a pretty blunt tool. It’s required because the signature of callback_type and notification_callback don’t match. We’re casting because we know that the only difference is the my_context pointer vs. the void pointer in the function signatures. We know these are compatible, and we know that our callback will be handed a my_context pointer when it’s called. So, check this out:

...
register_notification((callback_type)printf, &a_context);
...

That’ll compile just fine because the cast forces the compiler to assume that printf is a callback_type. While nobody might be dumb enough to pass printf as our callback, what happens if we change our module? Let’s say we decide to add another parameter to the callback:

typedef void (*callback_type)(void *context, int notification_flag, bool *should_continue);

Now, instead of getting a bunch of compilation errors everywhere we register an old-style callback function that’s missing this new parameter, we have to carefully hunt down all registrations and ensure that we update the callbacks to match the new signature. If we miss one, the compiler happily compiles the old callback thanks to our casts, and we’ll probably be hunting down a crash. Instead, it’s better to make our callback function signature match the callback type declaration exactly and perform a smaller cast inside the callback:

struct my_context {
  int foo;
};

void notification_callback(void *context, int notification_flag) {
  struct my_context *mycontext = (struct my_context *)context;
  mycontext->foo++;
}

...
register_notification(notification_callback, &a_context);
...

That cast is “smaller” because it’s only casting one parameter as opposed to the entire function signature. This way, if the callback signature changes, the compiler will help you by producing errors everywhere an old function is registered. Now, of course you can still screw up and cast the void pointer to the wrong type–but, the registration and the callback likely appear in the same module, so that shouldn’t be too hard to spot. As a more general tip, get used to seeing casts (and, by association, void pointers) as Dangerous Things. Like knives, gasoline, and nerve gas, they are super useful tools, but you shouldn’t be cavalier about using them. When reading code, they should jump out at you as indicators of areas that you should study carefully. Keeping their scope contained will help you write change-tolerant–and thus, maintainable–code. The compiler is your friend. It yells at you because it cares. Don’t tell it to shut up, get it to yell louder.