检查将函数指针转换为另一个函数指针是否安全

Checking if it's safe to cast a function pointer into another one

在我的代码中,我尝试使用虚拟对象在 C 语言中执行模块化。

目前我通过函数指针指定对每个对象有用的重要函数,如析构函数,toStringequals如下:

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);

然后在我的代码库中使用与给定 typedef 兼容的函数指针来抽象地处理对象,例如:

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);
}

代码编译,通常它只会生成一个警告(析构函数的第一个参数应该是 const void* 但它是 const Foo*)。

然而, 因为我启用了 -Werror,所以 "invalid pointer cast" 被视为错误。 为了解决这个问题,我需要转换函数指针,如下:

destructor d = (destructor)destroyFoo1;

我知道每个标准 const void*const Foo* 可能有不同的内存大小,但我假设部署代码的平台 const void*const Foo* 分配在相同的内存 space 并且具有相同的大小。一般来说,我假设至少将一个指针参数更改为其他指针的函数指针的转换是安全的转换。

这一切都很好,但是当我需要更改 destructor 类型的签名时,例如通过添加新的 const void* context 参数,这种方法就显示出它的弱点。现在有趣的警告消失了,函数指针调用中的参数数量不匹配:

//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

我的问题是:有没有办法检查函数指针是否确实可以安全地转换为另一个函数指针(如果不能则生成编译错误)?,类似于:

destructor d = CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS(destroyFoo1);

如果我们传递 destroyFoo1 一切都很好,但如果我们传递 destroyFoo2 编译器会报错。

下面是总结问题的代码

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);
    }
}

感谢您的任何回复

好久不见,函数指针赋值的代码不应该是:

//this is okay
destructor destructor1 = &destructorFoo1;

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

编辑:

好的,我走了,仔细看看这个。

如果我将函数指针的声明更改为使用 const Foo* p 而不是 const void* obj,这样我们就不会依赖强制转换来隐藏 void*Foo* 然后我收到默认编译器设置的警告。

然后通过将 destroyFoo2 转换为 (destructor),然后通过强制编译器将该函数视为该类型来隐藏此警告。

我想这突出了铸造的陷阱。

我使用以下代码进行了检查:

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;

}

好的,我想我已经弄清楚了,但这并不简单。

首先,问题是 CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS 需要在编译时比较 2 个签名:一个输入签名(从输入函数指针给出,例如 destroyFoo1)和一个基本签名(即,destructor 类型的签名):如果我们实现一个这样做的方法,我们可以检查 2 个签名是否为 "compliant"。

我们通过利用 C 预处理器来做到这一点。主要思想是我们想用作 destructor 的每个函数都定义了一个宏。 CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS 也将是一个宏,它只是根据 destructor 的类型签名生成宏名称:如果 CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS 中生成的宏名称存在,那么我们假设 functionPointer 符合destructor 然后我们投射到它。否则我们抛出一个编译错误。由于我们需要为每个要用作析构函数的函数定义一个宏,这在庞大的代码库中可能是一个代价高昂的解决方案。

注意:该实现依赖于 GCC(它使用 ##_Pragma 的变体,但我认为它也可以很容易地移植到其他一些编译器)。

因此,例如:

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

宏值只是一个常量。重要的是具有明确定义结构的宏的名称。您使用的约定无关紧要,只需选择并坚持使用一个约定即可。在这里,我使用了以下约定:

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

现在我们要定义一个宏来检查两个签名是否相同。为了帮助我们,我们使用 P99 project。我们将使用项目中的一些宏,所以如果你不想依赖它,你可以自己实现这些宏:

#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!\"")

宏的输入是要检查的(1)的宏值(即本例中的1,函数宏的值),我们要检查的typedef针对 (castTo),我们期望 functionName 具有的 return 类型和用户传递给 CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERSfunctionName(例如,destroyFoo1destroyFoo2)。可变参数是每个参数的类型。这些参数需要与 (1) 中的相同:我们写 voidConstPtr 因为我们不能在宏中包含 const void*名字.

_GET_FUNCTION_POINTER_MACRO 生成与我们期望 functionName 具有的签名关联的宏:

#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

因此,例如:

#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);
}

重要提示

实际上宏名称中的voidConstPtr甚至void都只是字符串。即使您将 void 替换为 helloWorld,一切都会正常进行。他们只是遵循惯例。

最后一点理解是P99_IF_EQ_ENSURE_FUNCTION_POINTER中实现的条件:如果_GET_FUNCTION_POINTER_MACRO的输出是一个存在的宏,预处理器会自动用它的值替换它,否则宏名称将保持不变;如果用 1 替换宏(生成的宏 _GET_FUNCTION_POINTER_MACRO 存在且等于 1),我们将假设唯一实现的是因为开发人员定义了宏 (1),我们将假设 functionName 符合 destructor。否则我们将抛出编译时错误。