EDIT2:
It seems that the standard is not exactly clear, and appears to at least partially contradict itself in different parts on the usage of void* or struct* when casting function pointers.
The lack of clarity is such that major security projects have found themselves using very similar techniques as those described in this question, only to then have LLVM release a version of UBSAN in clang 17 (2023) which interprets these techniques as UB and throws runtime warnings.
While it is a shame that there is not more clarity on such fundamental issues, I have concluded that it is not worth the potential aggravation and have resorted to "the dark arts of macro thunking". See my second self answer below
EDIT1: I learned a lot from the answers, and they are correct. The code as shown is not well defined, because void*
is "not compatible" with square*
, specifically because pointers to struct and non struct types may have different representations, as quoted here.
https://stackoverflow.com/a/1241314/1087626
However, as stated in above quote, and confirmed below, all pointers to struct types are guaranteed by the standard to have the same size and representation. So all I need to do, is to change void*
to obj_*
where obj_
is a dummy struct, and the code is supported by the standard. Full code shown in self answer below.
Original question:
In my mock example below, I am trying to construct a generic_processor
interface, where the caller passes the object pointers into a void*
param in the processor, and the caller explicitly casts the "actions" to void(*gfp)(void)
(a generic function pointer), which is what the generic_processor
expects.
The "concrete actions" have almost identical signatures for each action across the types, in that they take an object pointer, maybe some other params and return an int
. Note that the concrete actions take object pointers of the concrete type.
Here is the key step, which I have doubts about: The generic_processor
casts the gfp
params to a function pointer typedef
with a signature which matches the concrete action functions except that the object pointer is now a void*
.
As quoted in this answer:
https://stackoverflow.com/a/189126/1087626
- 766 A pointer to a function of one type may be converted to a pointer to a function of another type and back again;
- 767 the result shall compare equal to the original pointer.
- 768 If a converted pointer is used to call a function whose type is not compatible with the pointed-to type, the behavior is undefined.
So, the core of the question is: What does "not compatible" mean in this context?
In the generic_processor
I am casting to a function pointer signature which is the same as the original concrete signature, except that it has a void*
as the first parameter, instead of circle*
or square*
.
Is this well defined?
Mock example:
#include <stdio.h>
// types
typedef struct {
int a;
double d;
} circle;
typedef struct {
int b;
float f;
} square;
// ... 10 types,.. different sizeof()
// concrete API
int open_circle(circle* c) {
printf("opening circle: %d: %f\n", c->a, c->d);
return c->a;
}
int open_square(square* s) {
printf("opening square: %d: %f\n", s->b, s->f);
return s->b;
}
int send_circle(circle* c, const char* msg) {
printf("sending circle: %d: %f: %s\n", c->a, c->d, msg);
return -c->a;
}
int send_square(square* s, const char* msg) {
printf("sending square: %d: %f: %s\n", s->b, s->f, msg);
return -s->b;
}
// ten more operations for each type
// "genericised" function pointer types (note the void* params!!)
typedef int (*open_fpt)(void* o);
typedef int (*send_fpt)(void* o, const char*);
typedef void (*gfp)(void); // generic function pointer
int generic_processor(void* obj, gfp open, gfp send) {
int sum = 0;
sum += ((open_fpt)open)(obj);
sum += ((send_fpt)send)(obj, "generically sent");
return sum;
}
int main() {
circle c = {2, 22.2};
square s = {3, 33.3F};
int net = 0;
net += generic_processor(&c, (gfp)open_circle, (gfp)send_circle);
net += generic_processor(&s, (gfp)open_square, (gfp)send_square);
printf("net %d\n", net);
return 0;
}
compiled with gcc 13.2: works fine, no warnings:
gcc -std=c99 -g -o gfp tests/gfp.c -Wall -Wextra -pedantic -fsanitize=address,leak,undefined && ./gfp
opening circle: 2: 22.200000
sending circle: 2: 22.200000: generically sent
opening square: 3: 33.299999
sending square: 3: 33.299999: generically sent
net 0
Ignoring variadic functions and old K&R style declarations, two functions pointers are compatible if they point to functions where:
This is spelled out in section 6.7.6.3p15 of the C standard:
For two function types to be compatible, both shall specify compatible return types. Moreover, the parameter type lists, if both are present, shall agree in the number of parameters and in use of the ellipsis terminator; corresponding parameters shall have compatible types.
In your case, none of your functions are compatible with the function pointers open_fpt
or send_fpt
because they take a single void *
parameter and the functions in question take a single pointer to differing structure types, and a void *
is not compatible with a struct pointer. So this conversion is invalid and results in undefined behavior.
What you need to do is change your functions to take a void *
parameter, then convert the parameter to the proper type. For example:
int open_circle(void *p) {
circle* c = p;
printf("opening circle: %d: %f\n", c->a, c->d);
return c->a;
}
Or, if you can't change the above function definition, create a wrapper function:
int open_circle_wrap(void *p) {
return open_circle(p);
}
And in generic_processor
you can remove the casts:
int generic_processor(void* obj, open_fpt open, send_fpt send) {
int sum = 0;
sum += open(obj);
sum += send(obj);
return sum;
}