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

标签 c function-pointers c99

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

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

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 ,“无效指针转换”被视为错误。 为了解决这个问题,我需要转换函数指针,如下所示:

destructor d = (destructor)destroyFoo1;

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

这一切都很好,但是当我需要更改 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);
    }
}

感谢您的回复

最佳答案

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

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

我们通过利用 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拥有和 functionName用户传递到CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERS (例如, 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 。否则我们将抛出编译时错误。

关于检查将一个函数指针转换为另一个函数指针是否安全,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55595201/

相关文章:

c - 为什么我在 C 中的 for 循环可以正常工作?

MATLAB:我可以以某种方式使用函数作为第一类对象吗?

c++ - 在现代 C++ 中有没有什么好的方法来获取 arg 类型列表形式的函数指针?

c - 为什么 C 中的等式表达式中不允许使用结构?

c - 未知类型名称 'siginfo_t' 与使用 _POSIX_C_SOURCE 2 的 Clang,为什么?

c - 如何在函数中的结构体中 malloc 数组

c - 弹性/Bison : segmentation fault core dump

c - 将函数指针传递给静态函数是否可能/安全/理智?

c - 这段 C 代码不是在创建悬空指针吗?

c - 如何将链接目标添加到 Ruby mkmf?