Function pointer cast to different signature
Asked Answered
S

7

20

I use a structure of function pointers to implement an interface for different backends. The signatures are very different, but the return values are almost all void, void * or int.


struct my_interface {
    void  (*func_a)(int i);
    void *(*func_b)(const char *bla);
    ...
    int   (*func_z)(char foo);
};

But it is not required that a backends supports functions for every interface function. So I have two possibilities, first option is to check before every call if the pointer is unequal NULL. I don't like that very much, because of the readability and because I fear the performance impacts (I haven't measured it, however). The other option is to have a dummy function, for the rare cases an interface function doesn't exist.

Therefore I'd need a dummy function for every signature, I wonder if it is possible to have only one for the different return values. And cast it to the given signature.


#include <stdio.h>

int nothing(void) {return 0;}

typedef int (*cb_t)(int);

int main(void)
{
    cb_t func;
    int i;

    func = (cb_t) nothing;
    i = func(1);

    printf("%d\n", i);

    return 0;
}

I tested this code with gcc and it works. But is it sane? Or can it corrupt the stack or can it cause other problems?

EDIT: Thanks to all the answers, I learned now much about calling conventions, after a bit of further reading. And have now a much better understanding of what happens under the hood.

Serotine answered 9/10, 2008 at 19:33 Comment(0)
S
17

By the C specification, casting a function pointer results in undefined behavior. In fact, for a while, GCC 4.3 prereleases would return NULL whenever you casted a function pointer, perfectly valid by the spec, but they backed out that change before release because it broke lots of programs.

Assuming GCC continues doing what it does now, it will work fine with the default x86 calling convention (and most calling conventions on most architectures), but I wouldn't depend on it. Testing the function pointer against NULL at every callsite isn't much more expensive than a function call. If you really want, you may write a macro:

#define CALL_MAYBE(func, args...) do {if (func) (func)(## args);} while (0)

Or you could have a different dummy function for every signature, but I can understand that you'd like to avoid that.

Edit

Charles Bailey called me out on this, so I went and looked up the details (instead of relying on my holey memory). The C specification says

766 A pointer to a function of one type may be converted to a pointer to a function of another type and back again;
767 the result shall compare equal to the original pointer.
768 If a converted pointer is used to call a function whose type is not compatible with the pointed-to type, the behavior is undefined.

and GCC 4.2 prereleases (this was settled way before 4.3) was following these rules: the cast of a function pointer did not result in NULL, as I wrote, but attempting to call a function through a incompatible type, i.e.

func = (cb_t)nothing;
func(1);

from your example, would result in an abort. They changed back to the 4.1 behavior (allow but warn), partly because this change broke OpenSSL, but OpenSSL has been fixed in the meantime, and this is undefined behavior which the compiler is free to change at any time.

OpenSSL was only casting functions pointers to other function types taking and returning the same number of values of the same exact sizes, and this (assuming you're not dealing with floating-point) happens to be safe across all the platforms and calling conventions I know of. However, anything else is potentially unsafe.

Substituent answered 9/10, 2008 at 20:42 Comment(3)
By the way, the do{}while(0) trick is to avoid unexpected behavior if you write something like: if (foo) CALL_MAYBE(func, arg); else {do_something;} Only works as a statement; if you need it as an expression, GCC supports statement expressions: ({if (func) ...; 0;})Substituent
It sounds like the pre-release GCC behaviour was wrong. The current C standard says that a function pointer may be converted to a function pointer of a different type and back again and must convert equal to the original type. Calling a function through a pointer of incompatible type is illegal.Tannatannage
The opening paragraph is incorrect. Casting the function pointer is fine. Calling a function through a function pointer of the wrong type is undefined.Maskanonge
P
3

I suspect you will get an undefined behaviour.

You can assign (with the proper cast) a pointer to function to another pointer to function with a different signature, but when you call it weird things may happen.

Your nothing() function takes no arguments, to the compiler this may mean that he can optimize the usage of the stack as there will be no arguments there. But here you call it with an argument, that is an unexpected situation and it may crash.

I can't find the proper point in the standard but I remember it says that you can cast function pointers but when you call the resulting function you have to do with the right prototype otherwise the behaviour is undefined.

As a side note, you should not compare a function pointer with a data pointer (like NULL) as thee pointers may belong to separate address spaces. There's an appendix in the C99 standard that allows this specific case but I don't think it's widely implemented. That said, on architecture where there is only one address space casting a function pointer to a data pointer or comparing it with NULL, will usually work.

Putscher answered 9/10, 2008 at 19:48 Comment(1)
BS ISO/IEC 9899:1999 6.3.2.3 para 8 And while you can't compare data and function pointer types, 0, (void*)0 and NULL can all legitimately be converted to a function pointer type and result in an appropriate null pointer of that type.Tannatannage
E
1

You do run the risk of causing stack corruption. Having said that, if you declare the functions with extern "C" linkage (and/or __cdecl depending on your compiler), you may be able to get away with this. It would be similar then to the way a function such as printf() can take a variable number of arguments at the caller's discretion.

Whether this works or not in your current situation may also depend on the exact compiler options you are using. If you're using MSVC, then debug vs. release compile options may make a big difference.

Episcopacy answered 9/10, 2008 at 19:39 Comment(0)
S
0

It should be fine. Since the caller is responsible for cleaning up the stack after a call, it shouldn't leave anything extra on the stack. The callee (nothing() in this case) is ok since it wont try to use any parameters on the stack.

EDIT: this does assume cdecl calling conventions, which is usually the default for C.

Suspect answered 9/10, 2008 at 19:37 Comment(0)
R
0

As long as you can guarantee that you're making a call using a method that has the caller balance the stack rather than the callee (__cdecl). If you don't have a calling convention specified the global convention could be set to something else. (__stdcall or __fastcall) Both of which could lead to stack corruption.

Rosco answered 9/10, 2008 at 19:49 Comment(0)
F
0

This won't work unless you use implementation-specific/platform-specific stuff to force the correct calling convention. For some calling conventions the called function is responsible for cleaning up the stack, so they must know what's been pushed on.

I'd go for the check for NULL then call - I can't imagine it would have any impact on performance.

Computers can check for NULL about as fast as anything they do.

Foliar answered 9/10, 2008 at 21:0 Comment(0)
C
-1

Casting a function pointer to NULL is explicitly not supported by the C standard. You're at the mercy of the compiler writer. It works OK on a lot of compilers.

It is one of the great annoyances of C that there is no equivalent of NULL or void* for function pointers.

If you really want your code to be bulletproof, you can declare your own nulls, but you need one for each function type. For example,

void void_int_NULL(int n) { (void)n; abort(); }

and then you can test

if (my_thing->func_a != void_int_NULL) my_thing->func_a(99);

Ugly, innit?

Cadena answered 6/12, 2008 at 1:20 Comment(1)
There is a NULL for pointers to function and that is NULL. It is also the value that function pointers are initialized to by 0 or if inside a struct with {0}. Check 6.5.9 for comparison of pointers with NULL and 6.5.15.1 for assignment of pointers to NULL, where it says pointer and would have said pointer to object if it didn't include function pointers.Studdard

© 2022 - 2024 — McMap. All rights reserved.