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

时间:2019-04-09 14:28:18

标签: c function-pointers c99

在我的代码中,我试图使用伪对象在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,因此将“无效的指针强制转换”视为错误。 要解决此问题,我需要按如下所示强制转换函数指针:

destructor d = (destructor)destroyFoo1;

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

这一切都很好,但是,例如当我需要更改destructor类型的签名(例如通过添加新的const void* context参数)时,该方法就会显示出其弱点。现在,interesing警告被消除,并且函数指针中的参数数量调用不匹配:

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

感谢您的回复

2 个答案:

答案 0 :(得分:0)

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

首先,问题是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的{​​{1}}(例如CHECK_IF_FUNCTION_RETURNS_VOID_AND_REQUIRE_2_VOID_POINTERSdestroyFoo1) 。可变参数是每个参数的类型。这些参数必须与(1)中的参数相同:我们写destroyFoo2是因为宏名中不能包含voidConstPtr。< / p>

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,也一切都会正常进行。他们只是遵循惯例。

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

答案 1 :(得分:-1)

已经有一段时间了,但是功能指针分配的代码不应为:

//this is okay
destructor destructor1 = &destructorFoo1;

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

编辑:

好的,我走了,仔细看了一下。

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

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

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

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

Foo*