2

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 
8
  • @ikegami yes, that was my concern. But a void* can be cast from any pointer and to any pointer? I cannot change the concrete functions. My only other solution is to have 10x10 wrapper functions which accept a void* cast it and call with a square*. In fact the compiler does not require the cast. Which is why I came up with the above. Other suggestions? Commented Dec 13, 2024 at 16:58
  • I see that... so alternatives..? Other than 100 wrapper functions (which is what I had).. I was under the impression that unlike integers etc, that pointers have the same "representation", they differ in what they access, eg how they increment... which would be fine, because by the time it gets to incrementing we are in code that knows it's a square*. Commented Dec 13, 2024 at 17:43
  • @ikegami also your example.. of derefencing.. by the time i'm in the function where any deference is happenng (or increment or anything type related), the code knows the type. Obviously it wouldn't work otherwise. Commented Dec 13, 2024 at 17:45
  • I did some reading up on when ptrs can have different memory representations. Apparently the only modern situatiions are embedded systems with different memory categories. So yes, it can happen, and it's outside the standard. Damn. Not that I need to support embedded architectures. This is only for a test suite. I normally write c++.. so 100 error prone copy pasted functions (or code generattion or macros) is pretty ugly to me. .. But C it has to be here. Commented Dec 13, 2024 at 18:14
  • @ikegami This is really the most important quote from the standard.. stackoverflow.com/a/1241314/1087626 Commented Dec 13, 2024 at 18:33

4 Answers 4

4

Ignoring variadic functions and old K&R style declarations, two functions pointers are compatible if they point to functions where:

  • Their return types are compatible
  • They have the same number of parameters
  • The types of the parameters are compatible

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; } 
Sign up to request clarification or add additional context in comments.

12 Comments

I am not passing function pointers where the processor expects void*? I am passing function pointers where the processor expects a different function pointer type.. using the quoted conversion. And I am casting to the gfp type excplicitly>? I cannot change the concrete functions. They are not under my control, and anyway they should be concrete.
@OliverSchönrock Yes, I missed the function type passing. That part is fine. For the concrete functions, you'll need to create wrapper functions.
Yes, I had wrapper functions accepting void* and calling the concrete functions. However they not even need a cast internally, because a void* can be passed directly to the concrete functions expecting a square*. It's 10x10=100 wrapper functions. I came up with the above to remove them.
That's correct. Just because an implicit conversion can happen doesn't mean they're compatible. Integer types of different sizes can be implicitly converted between one and the other but they're not compatible types either.
@OliverSchönrock Although void * and square * might not have the same representation, square * and circle * are guaranteed to have the same representation, but may have different alignment requirements. In general all pointers to struct types have the same representation as each other, and all pointers to union types have the same representation as each other. This is necessary because pointers to incomplete struct pointers are allowed, so they need to "smell alike", and the same for pointers to incomplete union types.
|
1

What does "not compatible" mean in this context?

I will leave other aspects of the posted question to dbush’s answer, at least for now, and speak to this specific question.

In the C standard, “compatible” essentially means two types can be completed (or are complete) to be the same type. (There are some complications such as structures declared in separate translation units that I will not cover here.) So “array of 3 int” is compatible with “array of unspecified number of int”, since the latter type can be completed to be the same as the former.

To be compatible, two types have to be the same except for their incomplete parts. They cannot just be similar, even if they have exactly the same size and representation. An int and a long that are both 32 bits in some C implementation are not compatible. A const int and an int are not compatible. The C standard’s use of “compatible” is entirely about the type system, not about how things are represented in memory. You can reinterpret the bytes of an int as a long (using memcpy or a union) or vice-versa, and all the represented values will be identical across reinterpretation, but they are not compatible types.

There can be some differences in how compatible types are declared. After typedef int T;, T and int are compatible because T is not a different type; it is just a different name for the type int. A function declaration with an array parameter is compatible with the same function declaration with the corresponding parameter declared as a corresponding pointer, because the function type is the type resulting after parameter adjustment.

So, if there are any differences in two function types other than their incomplete parts or the cosmetic differences of declaration form, the types are not compatible.

In C 2024, the behavior calling a function using an incompatible type is not defined. Prior to C 2024, allowances were made for calling a function defined without a prototype using a type that had a prototype or vice-versa. Function declarations without prototypes were removed in C 2024, so those allowances are gone. Since, in your situation, the functions are defined with prototypes and are called with types with prototypes, those allowances do not apply; even in earlier versions of the C standard, the behavior will not be defined because the types are not compatible.

1 Comment

See self answer below stackoverflow.com/a/79280121/1087626 The footnote to 6.2.5#28 port70.net/%7Ensz/c/c11/n1570.html#note48 specifically says: "48) The same representation and alignment requirements are meant to imply interchangeability as arguments to functions, return values from functions, and members of unions."
1

I learned a lot from the answers, and they are correct. Thanks to those contributing.

The code as shown in question above is not well defined, because void* is "not compatible" with square*, specifically because pointers to struct and non struct types may have different size and representations.

However, all pointers to struct types are guaranteed by the standard to have the same size and representation.

https://port70.net/%7Ensz/c/c11/n1570.html#6.2.5p28

28 A pointer to void shall have the same representation and alignment requirements as a pointer to a character type.48) Similarly, pointers to qualified or unqualified versions of compatible types shall have the same representation and alignment requirements. All pointers to structure types shall have the same representation and alignment requirements as each other. All pointers to union types shall have the same representation and alignment requirements as each other. Pointers to other types need not have the same representation or alignment requirements.

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.

This falls into the "alternative solutions" category. It's just one line extra for the dummy struct and 2 lines changed for void* => obj_* and in IMO, this is much preferable to my previous solution, which was also discussed in comments above, of having 10x10 wrapper functions to cast the pointers.

Full code shown below:

 #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 typedef struct { int dummy_; } obj_; // "genericised" function pointer types, now using `obj_*` as a generic proxy typedef int (*open_fpt)(obj_* o); typedef int (*send_fpt)(obj_* 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; } 

19 Comments

Re “all struct types are guaranteed by the standard to have the same size and representation”: No, pointers to structure types are required to have the same size and representation. Structure types can and must vary.
It is a bad idea to use this method. The standard’s assertion that the requirement of same size and representation is meant to imply interchangeability is in a non-normative footnote, is vague, and may not be well supported in compilers. Since good supported solutions have been given, this method should be avoided.
The facts are: 1. All compilers on target systems do support it, 2. 6.2.5 #28 is already sufficient, the footnote is just for clarity. 3. I have not tried an AVR microcontroller, which is one of the only platforms where pointers do apparently vary across types at all, but even if the implementors of that (non-target) platform did try to vary pointers representations between types, they would be in violation of 6.2.5#28 if they varied between struct* and struct* 4. this is for a test suite.
@EricPostpischil Re “all struct types are guaranteed by the standard to have the same size and representation”... I fixed the typo. Thanks. Clearly structs are not all the same. ;-)
What are the "good proposed solutions"... a wall of 100s of hard to maintain, and growing, wrapper functions?
|
1

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 the 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. Not for runtime compatibility reasons but due to "potential CFI problems".

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".

The solution below is conformant without doubt, as all contributors to this question agree, and clang-18's UBSAN is also happy. It also avoids highly verbose, hard to maintain and error prone manual thunks. I have tried to make the macros quite easy to follow.

The upside over the suggested technique in my OG question is that you get somewhat more IDE help and type checking when using concrete, macro generated thunks.

Full code below...

#include <stdio.h> // types typedef struct { int a; double d; } circle; typedef struct { int b; float f; } square; int circle_open(circle* c) { printf("opening circle: %d: %f\n", c->a, c->d); return c->a; } int circle_send(circle* c, const char* msg) { printf("sending circle: %d: %f: %s\n", c->a, c->d, msg); return -c->a; } int square_open(square* s) { printf("opening square: %d: %f\n", s->b, s->f); return s->b; } int square_send(square* s, const char* msg) { printf("sending square: %d: %f: %s\n", s->b, s->f, msg); return -s->b; } #define FNAM(type, action) type##_##action #define GFNAM(type, action) gen_##type##_##action #define F1(t, a, rt, t1, p1) rt GFNAM(t, a)(t1 p1) { return FNAM(t, a)(p1); } #define F2(t, a, rt, t1, p1, t2, p2) rt GFNAM(t, a)(t1 p1, t2 p2) { return FNAM(t, a)(p1, p2); } #define GEN_THUNKS(ftype) \ F1(ftype, open, int, void*, obj) \ F2(ftype, send, int, void*, obj, const char*, msg) \ // ... 10 more actions GEN_THUNKS(circle) GEN_THUNKS(square) // ...10 more types int generic_processor(void* obj, int (*open)(void* obj), int (*send)(void* obj, const char*)) { int sum = 0; sum += open(obj); sum += send(obj, "generically sent"); return sum; } int main(void) { circle c = {2, 22.2}; square s = {3, 33.3F}; int net = 0; net += generic_processor(&c, gen_circle_open, gen_circle_send); net += generic_processor(&s, gen_square_open, gen_square_send); printf("net %d\n", net); return 0; } 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.