Unfortunately you typically have to forgo some of this compile-time safety if you're using C. You might get a warning at best, but if you have a design that is uniformly casting function pointers this way, you're likely to ignore or outright disable them. Instead you want to place your emphasis on achieving safe coding standards. What you can't guarantee by force, you can encourage strongly by policy.
I would suggest, if you can afford it, to start by casting arguments and return values rather than whole function pointers. A flexible representation is like so:
typedef void* GenericFunction(int argc, void** args);
This emulates the ability to have variadic callbacks, and you can uniformly do runtime safety checks in debug builds, e.g., to make sure that the number of arguments matches the assumptions:
void* MyCallback(int argc, void** args)
{
assert(argc == 2);
...
return 0;
}
If you need more safety than this for the individual arguments being passed and can afford a typically-small cost of an extra pointer per argument with a slightly bulky structured solution, you can do something like this:
struct Variant
{
void* ptr;
const char* type_name;
};
struct Variant to_variant(void* ptr, const char* type_name)
{
struct Variant new_var;
new_var.ptr = ptr;
new_var.type_name = type_name;
return new_var;
}
void* from_variant(struct Variant* var, const char* type_name)
{
assert(strcmp(var->type_name, type_name) == 0 && "Type mismatch!");
return var->ptr;
}
void* pop_variant(struct Variant** args, const char* type_name)
{
struct Variant* var = *args;
assert(var->ptr && "Trying to pop off the end of the argument stack!");
assert(strcmp(var->type_name, type_name) == 0 && "Type mismatch!");
++*args;
return var->ptr;
}
With macros like so:
#define TO_VARIANT(val, type) to_variant(&val, #type);
#define FROM_VARIANT(var, type) *(type*)from_variant(&var, #type);
#define POP_VARIANT(args, type) *(type*)pop_variant(&args, #type);
typedef struct Variant* GenericFunction(struct Variant* args);
Example callback:
struct Variant* MyCallback(struct Variant* args)
{
// `args` is null-terminated.
int arg1 = POP_VARIANT(args, int);
float arg2 = POP_VARIANT(args, float);
...
return 0;
}
A side benefit is what you can see in your debugger when you trace into MyCallback
through those type_name
fields.
This kind of thing can be useful if your codebase supports callbacks into dynamically-typed scripting languages, since scripting languages should not be doing type casts in their code (typically they're meant to be a bit on the safer side). The type names can then be used to automatically convert the arguments into the scripting language's native types dynamically using those type_name
fields.