0

In my code I'm trying to use dummy objects to perform modularity in C.

At the moment I specify important function useful for every objects via function pointers, like destructors, toString, equals as follows:

typedef void (*destructor)(const void* obj);
typedef void (*to_string)(void* obj, int bufferSize, const char* buffer);
typedef bool (*equals)(void* obj, const void* context);

In my code base then I use function pointer compatible to the given typedef to abstractly handle objects, for example:

struct Foo {
    int a;
} Foo;

void destroyFoo1(const Foo* p) {
   free((void*)p);
}

int main() {
    //...
    Foo* object_to_remove_from_heap = //instance of foo
    destructor d = destroyFoo1;
    //somewhere else
    d(object_to_remove_from_heap, context);
}

The code compiles and normally it would generate only a warning (destructor first parameter should be a const void* but instead it is a const Foo*).

However, since I've enabled -Werror, the "invalid pointer cast" is treated as an error. To solve this issue, I need to cast the function pointer, as follows:

destructor d = (destructor)destroyFoo1;

I know per standard const void* and const Foo* may have different memory size, but I assume the platform where the code is deployed const void* and const Foo* are allocated in the same memory space and have the same size. In general I assume the cast of function pointer where at least one pointer argument is changed into some other pointer is a safe casting.

This is all good but the approach shows its weakness when, for example, I need to change the signature of destructor type, for example by adding a new const void* context parameter. Now the interesing warning is silenced and the number of parameters in the function pointer call mismatch:

//now destructor is
typedef void (*destructor)(const void* obj, const void* context);

void destroyFoo1(const Foo* p) {
   free((void*)p);
}

destructor d = (destructor)destroyFoo1; //SILCENCED ERROR!!destroyFoo1 has invalid parameters number!!!!
//somewhere else
d(object_to_remove_from_heap, context); //may mess the stack

My question is: is there a way to check if a function pointer can indeed be safely casted into another (and generating a compile error if not)?, something like:

destructor d = CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS(destroyFoo1);

Something that if we pass destroyFoo1 everything is fine but if we pass destroyFoo2 the compiler complains.

Below a code that summarizes the problem

typedef void (*destructor)(const void* obj, const void* context);

typedef struct Foo {
    int a;
} Foo;

void destroyFoo1(const Foo* p, const void* context) {
   free((void*)p);
   if (*((int*)context) == 0) {
       printf("hello world\n");
   }
}

void destroyFoo2(const Foo* p) {
    free((void*)p);
}

int main() {
    //this is(in my case) safe
    destructor destructor = (destructor) destroyFoo1;
    //this is really a severe error!
    //destructor destructor = (destructor) destroyFoo2;

    Foo* a = (Foo*) malloc(sizeof(Foo));
    a->a = 3;
    int context = 5;
    if (a != NULL) {
        //call a destructor: if destructor is destroyFoo2 this is a SEVERE ERROR!
        //calling a function accepting a single parameter with 2 parameters!
        destructor(a, &context);
    }
}

Thanks for any kind of reply

Koldar
  • 1,317
  • 15
  • 35
  • Possible duplicate of [Function pointer cast to different signature](https://stackoverflow.com/questions/188839/function-pointer-cast-to-different-signature) – Nellie Danielyan Apr 09 '19 at 15:10
  • Try `-Wbad-function-cast` option – Nellie Danielyan Apr 09 '19 at 15:23
  • @NellieDanielyan I've look at the question but I don't understand how it would solve the issue. From what I understood, `CALL_MAYBE` macro objective is to automatically check the nullness of the function pointer, not deal with the correct/wrong number of parameters. – Koldar Apr 09 '19 at 15:42
  • @NellieDanielyan as for `-Wbad-function-cast`: I've tried on the MWE I've provided and it doesn't generate an error (the warning generated is `-Wincompatible-pointer-types`) – Koldar Apr 09 '19 at 15:45
  • You can remove the cast and use [pragma directives](https://gcc.gnu.org/onlinedocs/gcc/Diagnostic-Pragmas.html) to silence that specific error for that file – Nellie Danielyan Apr 09 '19 at 15:51
  • Wouldn't be really unsafe? It's highly possible that I still need the `incompatible-pointer-types` warning in the same file for totally unrelated code (e.g., `float* b = 5; int* a = b`). – Koldar Apr 09 '19 at 16:47
  • you can still have it as a warning instead of an error while leaving -Werror enabled for the rest. `#pragma GCC diagnostic warning "Wincompatible-pointer-types"` – Nellie Danielyan Apr 09 '19 at 17:01

2 Answers2

0

Ok, I think I've figure it out, but it's not straightforward.

First of all the problem is that CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS needs to compile-time compare 2 signatures: an input one (given from an input function pointer, e.g., destroyFoo1) and a base one (i.e., signature of destructor type): if we implement a method that does that, we can check if 2 signatures are "compliant" or not.

We do that by exploiting C preprocessor. The main idea is that each function we would like to use as destructor has a macro defined. CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS will be a macro as well that simply generates a macro name based on the type signature of destructor: if the macro name generated in CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS exists, than we assume the functionPointer is compliant with destructor and we cast to it. We throw a compilation error otherwise. Since we need a macro definition per function we want to use as destructor, this may be a costly solution in huge codebases.

Note: The implementation is GCC dependent(it uses a variant of ## and _Pragma, but I think it can be easily ported to some other compilers as well).

So, for example:

#define FUNCTION_POINTER_destructor_void_destroyFoo1_voidConstPtr_voidConstPtr 1
void destroyFoo1(const Foo* p, const void* context);

The macro value is just a constant number. What is important is the name of the macro which has a well defined structure. The convention you use is irrelevant, just choose and stick with one. Here I've used the following convention:

//macro (1)
"FUNCTION_POINTER_" typdefName "_" returnType "_" functionName "_" typeparam1 "_" typeparam2 ...

Now we're going to define a macro that checks if 2 signatures are the same. To help us, we're using P99 project. We are going to use few macros from the project, so you can implement such macros on your own if you don't want to rely on it:

#define CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS(functionName) \
    _ENSURE_FUNCTION_POINTER(1, destructor, void, functionName, voidConstPtr, voidConstPtr)

#define _ENSURE_FUNCTION_POINTER(valueToCheck, castTo, expectedReturnValue, functionName, ...) \
        P99_IF_EQ(valueToCheck, _GET_FUNCTION_POINTER_MACRO(castTo, expectedReturnValue, functionName, ## __VA_ARGS__)) \
            ((castTo)(functionName)) \
            (COMPILE_ERROR())

#define COMPILE_ERROR() _Pragma("GCC error \"function pointer casting error!\"")

The input of the macro is the macro value of (1) to check (i.e., 1 in this case, the value from the function macro), the typedef we want to check against (castTo), the return type we expect functionName to have and the functionName the user passed to CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS (e.g., destroyFoo1 or destroyFoo2). The variadic are the types of each parameters.These parameters needs to be same as in (1): we write voidConstPtr because we cannote have const void* within a macro name.

_GET_FUNCTION_POINTER_MACRO generates the macro associated to the signature we expect functionName to have:

#define _DEFINE_FUNCTION_POINTER_OP(CONTEXT, INDEX, CURRENT, NEXT) P99_PASTE(CURRENT, NEXT)
#define _DEFINE_FUNCTION_POINTER_FUNC(CONTEXT, CURRENT, INDEX) P99_PASTE(_, CURRENT)

#define _GET_FUNCTION_POINTER_MACRO(functionPointerType, returnValue, functionName, ...) \
    P99_PASTE(FUNCTION_POINTER, _, functionPointerType, _, returnValue, _, functionName, P99_FOR(, P99_NARG(__VA_ARGS__), _DEFINE_FUNCTION_POINTER_OP, _DEFINE_FUNCTION_POINTER_FUNC, ## __VA_ARGS__))

//example
_GET_FUNCTION_POINTER_MACRO(destructor, void, destroyFoo2, voidConstPtr, voidConstPtr)
//it generates
FUNCTION_POINTER_destructor_void_destroyFoo2_voidConstPtr_voidConstPtr

So, for example:

#define FUNCTION_POINTER_destructor_void_destroyFoo1_voidConstPtr_voidConstPtr 1
void destroyFoo1(const Foo* p, const void* context) 
{
   free((void*)p);
   if (*((int*)context) == 0) {
       printf("hello world\n");
   }
}

void destroyFoo2(const Foo* p) 
{
    free((void*)p);
}
int main(void)
{
    //this will work:
    //FUNCTION_POINTER_destructor_void_destroyFoo1_voidConstPtr_voidConstPtr 
    //macro exist and is equal to 1
    destructor destructor1 =  CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS(destroyFoo1);

    //this raise a compile error:
    //FUNCTION_POINTER_destructor_void_destroyFoo2_voidConstPtr_voidConstPtr
    //does not exist (or exists but its value is not 1)
    destructor destructor2 = CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS(destroyFoo2);
}

Important Notes

actually voidConstPtr or even void in the macro name are just strings. Everything would have worked even if you replaced void with helloWorld. They just follow a convention.

The last bit of understanding is the condition implemented by P99_IF_EQ in _ENSURE_FUNCTION_POINTER: if the output of _GET_FUNCTION_POINTER_MACRO is an existing macro, the preprocessor will automatically replace it with its value, otherwise the macro name will remain the same; if the macro is replaced with 1 (macro generated _GET_FUNCTION_POINTER_MACRO existing and equal to 1) we will assume the only one this is achieved is because the developer defined macro (1) and we will assume functionName complies with destructor. We will throw a compile time error otherwise.

Koldar
  • 1,317
  • 15
  • 35
-1

It's been a while, but shouldn't the code for function pointer assignment be:

//this is okay
destructor destructor1 = &destructorFoo1;

//this should throw a compilation error!
destructor destructor2 = &destructorFoo2;

EDIT:

Okay I've gone away and had a closer look at this.

If I change the declaration of the function pointer to use const Foo* p rather than const void* obj so that we're not relying on a cast to hide the incompatibility between void* and Foo* then I get a warning with default compiler settings.

Then by casting destroyFoo2 to (destructor) you then hide this warning, by forcing the compiler to treat the function as that type.

I guess that this highlights the pitfalls of casting.

I checked this using the following code:

typedef struct Foo
{
    int a;
} Foo;

typedef void (*destructor)(const Foo* p, const void* context);


void destroyFoo1(const Foo* p, const void* context);
void destroyFoo1(const Foo* p, const void* context) 
{
   free((void*)p);
   if (*((int*)context) == 0) {
       printf("hello world\n");
   }
}
void destroyFoo2(const Foo* p);
void destroyFoo2(const Foo* p) 
{
    free((void*)p);
}
int main(void)
{
    //this is okay
    destructor destructor1 =  destroyFoo1;
    //this triggers a warning
    destructor destructor2 = destroyFoo2;
    //This doesn't generate a warning
    destructor destructor3 = (destructor)destroyFoo2;

}
ChrisBD
  • 9,104
  • 3
  • 22
  • 35
  • 1
    I don't think it makes a difference: AFAIK they are already function pointers. Just to be sure, I've just tried the example (with valgrind) and it changes nothing. And even if it changed something, sadly it doesn't solve the issue. – Koldar Apr 09 '19 at 14:58
  • in C, the name of a function "decays" into a pointer to that function. Hence it can be assigned to a variable of the right function type without needed the addressof operator. – David Jones Apr 09 '19 at 15:02
  • After the EDIT: I think you misunderstood the question (sorry my fault for not being clear): the issue is not removing the warning (the cast itself can solve the problem) but to detect if the cast is safe or if it will lead to severe UB. with your `destructor3` you're not solving the issue because after assigning `destructor3` the program will then go to `destructor3(a, &context);`: there we will attempt to call `destructor3` on 2 parameters while `destroyFoo2` has only 2 of them (severe UB happening without the compiler altering you!). – Koldar Apr 09 '19 at 16:41
  • Modifying the type of the first parameter of `destructor` cannot be done as well: while `Foo` is just an example, there are several different structs in the code base (`Bar`, `Baz` and so on). Each of them has its own destructor. The only possible first parameter type of `destructor` hence is `void*` – Koldar Apr 09 '19 at 16:42
  • Okay. Perhaps I have misunderstood. I don't think that there is a safe way of automatically checking whether it is safe to cast or not using the compiler, as casting bypasses the type checking that is emplaced by using the typedef in the first place. – ChrisBD Apr 10 '19 at 07:08