I'm currently trying to write a somewhat generic FSM in C.
The problem I ran into is that I want to call a function of a specific state (e.g. the start function of the state) via a function pointer, which has a generic type.
I set up a simple example which illustrates my problem:
typedef struct test
{
int dummy;
} test_t;
void foo(test_t * test)
{
}
typedef void (fooFunction)(test_t * test);
typedef void (genericFooFunction)(void * test);
void bar()
{
test_t test = {45};
fooFunction * fpFoo = &foo;
genericFooFunction * fpGenericFoo = (genericFooFunction *) &foo;
fooFunction * fpGenericFooRecast = (fooFunction *) fpGenericFoo;
fpFoo(&test); //OK
fpGenericFooRecast(&test); //OK
fpGenericFoo(&test); //not OK?
}
I think I know that the C Standard allows the first two function calls, but not the last. I wonder why this is the case. (This code works in my environment (Ubuntu 20.04 in WSL with gcc)). I know that there can be differences in how different machines represent pointers or call functions, but I can't think of a case where the last function call would fail or mess up the stack.
This is because:
- The return type of the function and the generic pointer are equal, so no problem here.
- The number of arguments matches.
- The types of the arguments aren't equal, but both are pointers to an object, which should have the same representation.
Where am I wrong?
CodePudding user response:
Assumption 3 is incorrect. From C17 6.2.5/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.
Conversions from one function pointer type to another are OK, but using a function pointer to call an incompatible function is not OK. From C17 6.3.2.3/8:
A pointer to a function of one type may be converted to a pointer to a function of another type and back again; the result shall compare equal to the original pointer. If a converted pointer is used to call a function whose type is not compatible with the referenced type, the behavior is undefined.
In your code sample, the function call fpGenericFoo(&test);
results in undefined behavior because the called function foo
is not compatible with the function pointer fpGenericFoo
.
CodePudding user response:
I think I know that the c standard allows the first two function calls, but not the last.
Correct. C17 6.3.2.3/8:
A pointer to a function of one type may be converted to a pointer to a function of another type and back again; the result shall compare equal to the original pointer. If a converted pointer is used to call a function whose type is not compatible with the referenced type, the behavior is undefined.
I wonder why this is the case.
The C standard generally doesn't enforce or assume anything at all about pointer implementations. As soon as you do wild function/object pointer conversions and then de-reference, you are pretty much on your own, out of scope of any guarantees by the standard.
First of all, it obviously always matters that different function prototypes may have different calling conventions in a given ABI. Similarly, object pointer conversions obviously need to have the same alignment in cases where it matters.
Apart from those obvious cases, different object pointers may have different formats. The C standard guarantees the conversion itself between pointer types, but not that they have the same representation. The conversion of object pointers is guaranteed by C17 6.3.2.3/1:
A pointer to
void
may be converted to or from a pointer to any object type. A pointer to any object type may be converted to a pointer tovoid
and back again; the result shall compare equal to the original pointer.
Of course this means that it would be very impractical for a compiler to use different internal representations of different object pointer types, though in theory it could (DeathStation 9000 compilers). So while there is no support by the standard for your example 3, it will very likely work in practice.
Various unusual systems do exist: some with non-linear address maps, some with negative addresses and some different address sizes. However, such systems have traditionally always dealt with their exotic features by non-standard extensions, most commonly the near
and far
qualifiers for pointers.
None of these real world, semi-exotic systems I'm aware of make a difference in pointer format depending on what pointer type that's used, but rather depending on where the pointed-at data is stored.