Type safe hooks in C

I sometimes envy the C++ guys for having simple APIs for the signal-slot mechanism. While the signal and slots were designed by the people doing GUI toolkits — with the main credit going to the Qt project — they can be useful as a general inter-module communication mechanism in any software project.

Signals provide loose module coupling, which is crucial for good design. All you need to declare are the arguments of the signal, and then any module is free to generate them and any other module is free to receive them. If the module sending signals is freed, no problem, the receiver will simply not receive any more signals. If the receiver module is freed, again no problem, the signals will be simply ignored. It’s that simple.

Another property of the signals is that they are type safe, meaning that if the type of the arguments of a signal changes and you fail to update all receivers, you will get compile time errors instead of weird hard-to-find run-time bugs. The type unsafeness for callbacks is too often ignored by C programmers, and it generates a good share of bugs.

The reason I said I envy C++ programmers is that the mechanism can’t be elegantly implemented in C, because C lacks generics and function overloading. But fear not, if we know what we aim for, we can get pretty close to anything in C. Here is how I do it: a hook is a list of pointers to callback functions. The receivers register their callbacks by adding them to the list. When the hook is called, i.e. the signal is generated, it will call all the callbacks from its list. Pretty simple so far, but how do you provide type safeness? you ask. With wrappers and macros.

Lets suppose the hook structure looks something like:

typedef struct hook_cb {
    void (*f)(void);
    struct hook *next;
} hook_cb_t;

typedef struct hook {
    struct hook *hooks;
    pthread_rwlock_t *lock;
} hook_t;

It’s basically the list of callbacks that I was talking about, protected by a read-write lock. We then need methods for registering and unregistering the callbacks. Here are the prototypes, you have the pleasure of doing the single-linked list insertion and removal on your own (or cheat with whatever library).

/**
 * Adds f to the hook->hooks_cb list.
 */
int hook_register_cb(hook_t *hook, void(*f)(void));

/**
 * Removes f from the hook->hooks_cb.
 */
int hook_unregister_cb(hook_t *hook, void(*f)(void));

So far everything is type unsafe. The type of the callback was just randomly chosen to look like a function. But the macro comes into play:

#define hook_call(hook, type, ... ) do { 
        hook_cb_t __cb; 
        pthread_rwlock_rdlock(&(hook)->lock); 
        for (__cb = (hook)->hooks_cb; __cb; __cb = __cb->next) 
                ((type*)__cb->f)(__VA_ARGS__); 
        pthread_rwlock_unlock(&(hook)->lock); 
} while(0)

This hook_call macro receives as arguments a pointer to the hook structure, the type of the callback, and a variable number of arguments. It iterates through the callback list, casts the function pointers to the provided type and calls them with the macro arguments using the variadic macro for that. If the arguments don’t match the given function type, the compiler will report an error, which is all we ever wanted, actually.

For example, lets suppose we have a hook with two integer arguments. The callback type is:

typedef void(my_callback_t)(int, int, float);

Then, the hook can be called with something like:

hooks_call(hook, my_callback_t, 3, 4);

As the careful reader will notice, this only solves half of the problem. If the receivers register callbacks of the wrong type, it will be called anyway and cause troubles. This is why when declaring hooks, I also create wrappers for the register routines. Extending the above example:

inline static int my_hook_register( hook_t *hook, my_callback_t *f ) {
        return hook_register(hook, (void*)f) ;
}

inline static int my_hook_unregister( hook_t *hook, my_callback_t *f ) {
        return hooks_unregister( hook, (void*)f );
}

These 7 lines are the actual cost of type safeness. A fair price if you ask me. Especially since you can have callbacks with any number of arguments having any type. No need for casts in the callback, no need for packing structures, no need for documenting the type of the arguments. Things get simpler because the glue is centralized in the hook declaration.

Finally, a wrapper for the calling macro might also come handy to simplify the signal generators.

inline static void my_hook_call(hook_t *hook, int a, int b) {
        hook_call(hook, my_callback_t, a, b );
}

Useful? Found an error? Something not clear? Leave a comment.

3 thoughts on “Type safe hooks in C

  1. Angelo

    Hi,
    thanks a lot for this page. I found it very useful. I am also developing an event handling system in C and I found lot of difficulties on applying the same concepts to the C language of the Signal/Slot used by QT.
    You said that “If the arguments don’t match the given function type, the compiler will report an error, which is all we ever wanted, actually.”. This is true if the file has extension .cpp. In case the file has a .c extension (meaning that it is compiled by the C compiler), I have only a warning and not an error. C is less strict than C++, and the use of function pointers is always risky.
    Thanks again for your work.
    Best Regards

    Reply
  2. tsg Post author

    Hi Angelo, thanks for the comment and I’m glad you found it useful. Indeed, in C the compiler can cast between pointer types, so you only get warnings. I try to keep my code warning free and use -Werror when building for production.

    Reply
  3. Angelo

    Hi TSG and thanks for the reply. In big projects or when porting the sw, it can be very complicated to eliminate all the warnings, so the -Werror option is not possible to use. Do you know if there is a workaround/trick to let the C compiler to return a compiler error when encountering a worng cast of a function pointer? Something like: “treat the warning returned in wrong function pointer casting as an error”? The funny think is that this is the behavior of a C++ compiler but not the C one.
    Thanks again in advance for any feedback and congratulation for you web page, a clear and competent source of information.
    Best,

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>