将函数指针转换为另一种类型

发布于 2024-12-06 01:38:24 字数 620 浏览 1 评论 0 原文

假设我有一个接受 void (*)(void*) 函数指针用作回调的函数:

void do_stuff(void (*callback_fp)(void*), void* callback_arg);

现在,如果我有这样的函数:

void my_callback_function(struct my_struct* arg);

我可以安全地执行此操作吗?

do_stuff((void (*)(void*)) &my_callback_function, NULL);

我看过这个问题并且我看过一些C标准这说你可以转换为“兼容函数指针”,但我找不到“兼容函数指针”含义的定义。

Let's say I have a function that accepts a void (*)(void*) function pointer for use as a callback:

void do_stuff(void (*callback_fp)(void*), void* callback_arg);

Now, if I have a function like this:

void my_callback_function(struct my_struct* arg);

Can I do this safely?

do_stuff((void (*)(void*)) &my_callback_function, NULL);

I've looked at this question and I've looked at some C standards which say you can cast to "compatible function pointers", but I cannot find a definition of what "compatible function pointer" means.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

赏烟花じ飞满天 2024-12-13 01:38:25

如果您考虑一下函数调用在 C/C++ 中的工作方式,它们会将某些项目压入堆栈,跳转到新的代码位置,执行,然后在返回时弹出堆栈。如果您的函数指针描述具有相同返回类型和相同数量/大小的参数的函数,那么应该没问题。

因此,我认为您应该能够安全地这样做。

If you think about the way function calls work in C/C++, they push certain items on the stack, jump to the new code location, execute, then pop the stack on return. If your function pointers describe functions with the same return type and the same number/size of arguments, you should be okay.

Thus, I think you should be able to do so safely.

執念 2024-12-13 01:38:25

空指针与其他类型的指针兼容。它是 malloc 和 mem 函数(memcpymemcmp)工作原理的支柱。通常,在 C(而不是 C++)中 NULL 是定义为 ((void *)0) 的宏。

看看C99中的6.3.2.3(第1项):

指向 void 的指针可以与指向任何不完整或对象类型的指针相互转换

Void pointers are compatible with other types of pointer. It's the backbone of how malloc and the mem functions (memcpy, memcmp) work. Typically, in C (Rather than C++) NULL is a macro defined as ((void *)0).

Look at 6.3.2.3 (Item 1) in C99:

A pointer to void may be converted to or from a pointer to any incomplete or object type

最笨的告白 2024-12-13 01:38:24

就 C 标准而言,如果将函数指针强制转换为不同类型的函数指针,然后调用它,则这是未定义行为。参见附件 J.2(资料性):

在以下情况下该行为未定义:

  • 指针用于调用与所指向的类型不兼容的函数
    类型(6.3.2.3)。

第 6.3.2.3 节第 8 段内容如下:

指向一种类型函数的指针可以转换为指向另一种类型函数的指针
键入并再次返回;结果应等于原始指针。如果转换成
指针用于调用类型与所指向类型不兼容的函数,
行为未定义。

换句话说,您可以将函数指针转换为不同的函数指针类型,再次将其转换回来,然后调用它,事情就会起作用。

兼容的定义有些复杂。可以在第 6.7.5.3 节第 15 段中找到:

要使两个函数类型兼容,两者都应指定兼容的返回类型127

此外,参数类型列表(如果两者都存在)应在数量上一致
参数以及省略号终止符的使用;相应的参数应有
兼容类型。如果一种类型具有参数类型列表,而另一种类型由
函数声明符不是函数定义的一部分并且包含空
标识符列表,参数列表不应有省略号终止符以及每个参数的类型
参数应与应用程序产生的类型兼容
默认参数促销。如果一种类型有参数类型列表,另一种类型是
由包含(可能为空)标识符列表的函数定义指定,两者都应
参数个数一致,每个原型参数的类型为
与应用默认参数所产生的类型兼容
晋升为相应标识符的类型。 (在确定类型时
兼容性和复合类型,每个参数都用函数或数组声明
type 被视为具有调整后的类型,并且每个参数都使用限定类型声明
被视为具有其声明类型的非限定版本。)

127) 如果两个函数类型都是“旧式”,则不会比较参数类型。

确定两种类型是否兼容的规则在第 6.2.7 节中描述,由于它们相当冗长,我不会在这里引用它们,但您可以在 ""> open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf" rel="noreferrer">C99 标准草案 (PDF)

相关规则见第 6.7.5.1 节第 2 段:

为了使两个指针类型兼容,两者都应具有相同的限定,并且两者都应是指向兼容类型的指针。

因此,由于 void* struct my_struct* 不兼容void (*)(void*) 类型的函数指针与 void (*)(void*) 类型的函数指针不兼容void (*)(结构体my_struct*),因此函数指针的这种转换在技术上是未定义的行为。

但实际上,在某些情况下,您可以安全地摆脱函数指针的转换。在 x86 调用约定中,参数被压入堆栈,并且所有指针的大小相同(x86 中为 4 字节,x86_64 中为 8 字节)。调用函数指针归结为将参数压入堆栈并间接跳转到函数指针目标,并且在机器代码级别显然没有类型的概念。

你绝对不能做的事情:

  • 在不同调用约定的函数指针之间进行转换。你会弄乱堆栈,最好的情况是崩溃,最坏的情况是,通过一个巨大的安全漏洞默默地成功。在 Windows 编程中,经常传递函数指针。 Win32 期望所有回调函数都使用 stdcall 调用约定(其中宏 CALLBACKPASCALWINAPI 所有扩展到)。如果传递使用标准 C 调用约定 (cdecl) 的函数指针,则会导致错误。
  • 在 C++ 中,类成员函数指针和常规函数指针之间的转换。这常常会困扰 C++ 新手。类成员函数有一个隐藏的 this 参数,如果将成员函数转换为常规函数,则没有 this 对象可供使用,同样,会导致很多问题。

另一个有时可能有效但也是未定义行为的坏主意:

  • 在函数指针和常规指针之间进行转换(例如将 void (*)(void) 转换为 void*) 。函数指针的大小不一定与常规指针相同,因为在某些体系结构上它们可能包含额外的上下文信息。这在 x86 上可能可以正常工作,但请记住这是未定义的行为。

As far as the C standard is concerned, if you cast a function pointer to a function pointer of a different type and then call that, it is undefined behavior. See Annex J.2 (informative):

The behavior is undefined in the following circumstances:

  • A pointer is used to call a function whose type is not compatible with the pointed-to
    type (6.3.2.3).

Section 6.3.2.3, paragraph 8 reads:

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 pointed-to type,
the behavior is undefined.

So in other words, you can cast a function pointer to a different function pointer type, cast it back again, and call it, and things will work.

The definition of compatible is somewhat complicated. It can be found in section 6.7.5.3, paragraph 15:

For two function types to be compatible, both shall specify compatible return types127.

Moreover, the parameter type lists, if both are present, shall agree in the number of
parameters and in use of the ellipsis terminator; corresponding parameters shall have
compatible types. If one type has a parameter type list and the other type is specified by a
function declarator that is not part of a function definition and that contains an empty
identifier list, the parameter list shall not have an ellipsis terminator and the type of each
parameter shall be compatible with the type that results from the application of the
default argument promotions. If one type has a parameter type list and the other type is
specified by a function definition that contains a (possibly empty) identifier list, both shall
agree in the number of parameters, and the type of each prototype parameter shall be
compatible with the type that results from the application of the default argument
promotions to the type of the corresponding identifier. (In the determination of type
compatibility and of a composite type, each parameter declared with function or array
type is taken as having the adjusted type and each parameter declared with qualified type
is taken as having the unqualified version of its declared type.)

127) If both function types are ‘‘old style’’, parameter types are not compared.

The rules for determining whether two types are compatible are described in section 6.2.7, and I won't quote them here since they're rather lengthy, but you can read them on the draft of the C99 standard (PDF).

The relevant rule here is in section 6.7.5.1, paragraph 2:

For two pointer types to be compatible, both shall be identically qualified and both shall be pointers to compatible types.

Hence, since a void* is not compatible with a struct my_struct*, a function pointer of type void (*)(void*) is not compatible with a function pointer of type void (*)(struct my_struct*), so this casting of function pointers is technically undefined behavior.

In practice, though, you can safely get away with casting function pointers in some cases. In the x86 calling convention, arguments are pushed on the stack, and all pointers are the same size (4 bytes in x86 or 8 bytes in x86_64). Calling a function pointer boils down to pushing the arguments on the stack and doing an indirect jump to the function pointer target, and there's obviously no notion of types at the machine code level.

Things you definitely can't do:

  • Cast between function pointers of different calling conventions. You will mess up the stack and at best, crash, at worst, succeed silently with a huge gaping security hole. In Windows programming, you often pass function pointers around. Win32 expects all callback functions to use the stdcall calling convention (which the macros CALLBACK, PASCAL, and WINAPI all expand to). If you pass a function pointer that uses the standard C calling convention (cdecl), badness will result.
  • In C++, cast between class member function pointers and regular function pointers. This often trips up C++ newbies. Class member functions have a hidden this parameter, and if you cast a member function to a regular function, there's no this object to use, and again, much badness will result.

Another bad idea that might sometimes work but is also undefined behavior:

  • Casting between function pointers and regular pointers (e.g. casting a void (*)(void) to a void*). Function pointers aren't necessarily the same size as regular pointers, since on some architectures they might contain extra contextual information. This will probably work ok on x86, but remember that it's undefined behavior.
熊抱啵儿 2024-12-13 01:38:24

我最近询问了有关 GLib 中某些代码的完全相同的问题。 (GLib 是 GNOME 项目的核心库,用 C 语言编写。)我被告知整个 slot'n'signals 框架都依赖于它。

在整个代码中,有许多从类型 (1) 到类型 (2) 的转换实例:

  1. typedef int (*CompareFunc) (const void *a,
    const void *b)
  2. typedef int (*CompareDataFunc) (const void *b,
    常量无效*b,
    void *user_data)

使用这样的调用进行链通是很常见的:

int stuff_equal (GStuff      *a,
                 GStuff      *b,
                 CompareFunc  compare_func)
{
    return stuff_equal_with_data(a, b, (CompareDataFunc) compare_func, NULL);
}

int stuff_equal_with_data (GStuff          *a,
                           GStuff          *b,
                           CompareDataFunc  compare_func,
                           void            *user_data)
{
    int result;
    /* do some work here */
    result = compare_func (data1, data2, user_data);
    return result;
}

g_array_sort() 中亲自查看:http://git.gnome.org/browse/glib/tree/glib/garray.c

上面的答案很详细可能是正确的—— 如果您是标准委员会的成员。亚当和约翰尼斯的研究深入,值得赞扬。然而,在野外,您会发现这段代码运行得很好。有争议的?是的。考虑一下:GLib 使用各种编译器/链接器/内核加载器 (GCC/CLang/MSVC) 在大量平台 (Linux/Solaris/Windows/OS X) 上进行编译/工作/测试。我想,标准该死。

我花了一些时间思考这些答案。这是我的结论:

  1. 如果您正在编写回调库,这可能没问题。买者自负——使用风险自负。
  2. 否则,不要这样做。

写完这个回复后,深入思考一下,如果 C 编译器的代码使用同样的技巧,我不会感到惊讶。由于(大多数/全部?)现代 C 编译器是自举的,这意味着该技巧是安全的。

一个更重要的研究问题:有人能找到一个平台/编译器/链接器/加载器,这个技巧在其中不起作用吗?对此的主要印象分。我敢打赌有一些嵌入式处理器/系统不喜欢它。然而,对于桌面计算(可能还有移动/平板电脑),这个技巧可能仍然有效。

I asked about this exact same issue regarding some code in GLib recently. (GLib is a core library for the GNOME project and written in C.) I was told the entire slots'n'signals framework depends upon it.

Throughout the code, there are numerous instances of casting from type (1) to (2):

  1. typedef int (*CompareFunc) (const void *a,
    const void *b)
  2. typedef int (*CompareDataFunc) (const void *b,
    const void *b,
    void *user_data)

It is common to chain-thru with calls like this:

int stuff_equal (GStuff      *a,
                 GStuff      *b,
                 CompareFunc  compare_func)
{
    return stuff_equal_with_data(a, b, (CompareDataFunc) compare_func, NULL);
}

int stuff_equal_with_data (GStuff          *a,
                           GStuff          *b,
                           CompareDataFunc  compare_func,
                           void            *user_data)
{
    int result;
    /* do some work here */
    result = compare_func (data1, data2, user_data);
    return result;
}

See for yourself here in g_array_sort(): http://git.gnome.org/browse/glib/tree/glib/garray.c

The answers above are detailed and likely correct -- if you sit on the standards committee. Adam and Johannes deserve credit for their well-researched responses. However, out in the wild, you will find this code works just fine. Controversial? Yes. Consider this: GLib compiles/works/tests on a large number of platforms (Linux/Solaris/Windows/OS X) with a wide variety of compilers/linkers/kernel loaders (GCC/CLang/MSVC). Standards be damned, I guess.

I spent some time thinking about these answers. Here is my conclusion:

  1. If you are writing a callback library, this might be OK. Caveat emptor -- use at your own risk.
  2. Else, don't do it.

Thinking deeper after writing this response, I would not be surprised if the code for C compilers uses this same trick. And since (most/all?) modern C compilers are bootstrapped, this would imply the trick is safe.

A more important question to research: Can someone find a platform/compiler/linker/loader where this trick does not work? Major brownie points for that one. I bet there are some embedded processors/systems that don't like it. However, for desktop computing (and probably mobile/tablet), this trick probably still works.

你与清晨阳光 2024-12-13 01:38:24

关键不在于你是否可以。简单的解决方案是,

void my_callback_function(struct my_struct* arg);
void my_callback_helper(void* pv)
{
    my_callback_function((struct my_struct*)pv);
}
do_stuff(&my_callback_helper);

一个好的编译器只会在确实需要时为 my_callback_helper 生成代码,在这种情况下,您会很高兴它这样做了。

The point really isn't whether you can. The trivial solution is

void my_callback_function(struct my_struct* arg);
void my_callback_helper(void* pv)
{
    my_callback_function((struct my_struct*)pv);
}
do_stuff(&my_callback_helper);

A good compiler will only generate code for my_callback_helper if it's really needed, in which case you'd be glad it did.

对你而言 2024-12-13 01:38:24

如果返回类型和参数类型兼容,那么您就有一个兼容的函数类型 - 基本上(实际上更复杂:))。兼容性与“相同类型”相同,只是更宽松,允许有不同的类型,但仍然有某种形式的说法“这些类型几乎相同”。例如,在 C89 中,如果两个结构在其他方面相同但只是名称不同,则它们是兼容的。 C99 似乎改变了这一点。引用c 基本原理文档(强烈推荐阅读,顺便说一句!):

两个不同翻译单元中的结构体、联合或枚举类型声明不会正式声明相同的类型,即使这些声明的文本来自同一包含文件,因为翻译单元本身是不相交的。因此,该标准为此类类型指定了额外的兼容性规则,因此,如果两个此类声明足够相似,那么它们就是兼容的。

也就是说 - 是的,严格来说这是未定义的行为,因为您的 do_stuff 函数或其他人将使用以 void* 作为参数的函数指针调用您的函数,但您的函数有一个不兼容的参数。但尽管如此,我希望所有编译器都能毫无抱怨地编译和运行它。但是您可以通过让另一个函数采用 void* (并将其注册为回调函数)来做到更干净,然后该函数将只调用您的实际函数。

You have a compatible function type if the return type and parameter types are compatible - basically (it's more complicated in reality :)). Compatibility is the same as "same type" just more lax to allow to have different types but still have some form of saying "these types are almost the same". In C89, for example, two structs were compatible if they were otherwise identical but just their name was different. C99 seem to have changed that. Quoting from the c rationale document (highly recommended reading, btw!):

Structure, union, or enumeration type declarations in two different translation units do not formally declare the same type, even if the text of these declarations come from the same include file, since the translation units are themselves disjoint. The Standard thus specifies additional compatibility rules for such types, so that if two such declarations are sufficiently similar they are compatible.

That said - yeah strictly this is undefined behavior, because your do_stuff function or someone else will call your function with a function pointer having void* as parameter, but your function has an incompatible parameter. But nevertheless, i expect all compilers to compile and run it without moaning. But you can do cleaner by having another function taking a void* (and registering that as callback function) which will just call your actual function then.

木落 2024-12-13 01:38:24

由于 C 代码编译为根本不关心指针类型的指令,因此使用您提到的代码是很好的。当您使用回调函数运行 do_stuff 并指向其他内容然后将 my_struct 结构作为参数时,您会遇到问题。

我希望我可以通过显示什么不起作用来使其更清楚:

int my_number = 14;
do_stuff((void (*)(void*)) &my_callback_function, &my_number);
// my_callback_function will try to access int as struct my_struct
// and go nuts

或者...

void another_callback_function(struct my_struct* arg, int arg2) { something }
do_stuff((void (*)(void*)) &another_callback_function, NULL);
// another_callback_function will look for non-existing second argument
// on the stack and go nuts

基本上,只要数据在运行时继续有意义,您就可以将指针强制转换为您喜欢的任何内容。

As C code compiles to instruction which do not care at all about pointer types, it's quite fine to use the code you mention. You'd run into problems when you'd run do_stuff with your callback function and pointer to something else then my_struct structure as argument.

I hope I can make it clearer by showing what would not work:

int my_number = 14;
do_stuff((void (*)(void*)) &my_callback_function, &my_number);
// my_callback_function will try to access int as struct my_struct
// and go nuts

or...

void another_callback_function(struct my_struct* arg, int arg2) { something }
do_stuff((void (*)(void*)) &another_callback_function, NULL);
// another_callback_function will look for non-existing second argument
// on the stack and go nuts

Basically, you can cast pointers to whatever you like, as long as the data continue to make sense at run-time.

谁的年少不轻狂 2024-12-13 01:38:24

好吧,除非我理解错误,否则你可以用这种方式转换函数指针。

void print_data(void *data)
{
    // ...
}


((void (*)(char *)) &print_data)("hello");

更简洁的方法是创建一个函数 typedef。

typedef void(*t_print_str)(char *);
((t_print_str) &print_data)("hello");

Well, unless I understood the question wrong, you can just cast a function pointer this way.

void print_data(void *data)
{
    // ...
}


((void (*)(char *)) &print_data)("hello");

A cleaner way would be to create a function typedef.

typedef void(*t_print_str)(char *);
((t_print_str) &print_data)("hello");
忱杏 2024-12-13 01:38:24

正如其他一些答案/评论所解释的那样,这是未定义的行为。

那么它将是明确定义的

void my_callback_function(void *arg)
{
   struct my_struct* p = arg;
   ... // use p
}

但是,如果您将 void my_callback_function(struct my_struct* arg) 替换为假设您在通过函数指针调用函数时确实将指向 my_struct 对象的指针作为参数传递,

。确实,指向任何对象的指针都可以转换为 void,然后转换回原始类型,而不会改变其值,但这只能保证像我上面建议的函数有效,而不是 void my_callback_function( struct my_struct* arg) 人们倾向于相信它们实际上是相同的。

It's undefined behaviour as some other answers/comments have explained.

But it would be well-defined if you replace void my_callback_function(struct my_struct* arg) with

void my_callback_function(void *arg)
{
   struct my_struct* p = arg;
   ... // use p
}

assuming that you really passed a pointer to a my_struct object as an argument when calling the function by the function pointer.

It is true that a pointer to any object can be cast to void and then cast back to the original type without any change in its value, but this can only guarantee that a function like I suggested above works, not void my_callback_function(struct my_struct* arg) People tend to believe that they are actually the same.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文