Program Club

함수 포인터를 다른 유형으로 캐스팅

proclub 2020. 10. 9. 12:34
반응형

함수 포인터를 다른 유형으로 캐스팅


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 표준을 보았지만 '호환 가능한 함수 포인터'가 무엇을 의미하는지에 대한 정의를 찾을 수 없습니다.


C 표준에 관한 한, 함수 포인터를 다른 유형의 함수 포인터로 캐스팅 한 다음이를 호출하면 정의되지 않은 동작 입니다. 부록 J.2 (정보) 참조 :

다음 상황에서는 동작이 정의되지 않습니다.

  • 포인터는 유형이 가리키는 유형 (6.3.2.3)과 호환되지 않는 함수를 호출하는 데 사용됩니다.

섹션 6.3.2.3, 단락 8은 다음과 같습니다.

한 유형의 함수에 대한 포인터는 다른 유형의 함수에 대한 포인터로 변환되었다가 다시 돌아갈 수 있습니다. 결과는 원래 포인터와 동일하게 비교됩니다. 변환 된 포인터를 사용하여 형식이 가리키는 형식과 호환되지 않는 함수를 호출하면 동작이 정의되지 않습니다.

즉, 함수 포인터를 다른 함수 포인터 유형으로 캐스트하고 다시 캐스트하고 호출하면 모든 것이 작동합니다.

호환 의 정의 는 다소 복잡합니다. 섹션 6.7.5.3, 단락 15에서 찾을 수 있습니다.

두 함수 유형이 호환 되려면 둘 다 호환 가능한 반환 유형 127을 지정해야 합니다.

또한 매개 변수 유형 목록이 둘 다있는 경우 매개 변수 수와 생략 부호 종결자를 사용하는 데 동의해야합니다. 해당 매개 변수는 호환 가능한 유형을 가져야합니다. 한 유형에 매개 변수 유형 목록이 있고 다른 유형이 함수 정의의 일부가 아니고 빈 식별자 목록을 포함하는 함수 선언자에 의해 지정된 경우 매개 변수 목록에는 줄임표 종결자가 없어야하며 각 매개 변수의 유형은 다음과 같아야합니다. 기본 인수 승격을 적용한 결과 유형과 호환되어야합니다. 한 유형에 매개 변수 유형 목록이 있고 다른 유형이 식별자 목록 (비어있을 수 있음)을 포함하는 함수 정의에 의해 지정되는 경우, 둘 다 매개 변수 수에 동의해야합니다. 각 프로토 타입 매개 변수의 유형은 기본 인수 승격을 해당 식별자 유형에 적용한 결과 유형과 호환되어야합니다. (유형 호환성 및 복합 유형의 결정에서 함수 또는 배열 유형으로 선언 된 각 매개 변수는 조정 된 유형을 갖는 것으로 간주되고 규정 된 유형으로 선언 된 각 매개 변수는 선언 된 유형의 규정되지 않은 버전을 갖는 것으로 간주됩니다.

127) 두 함수 유형이``이전 스타일 ''이면 매개 변수 유형이 비교되지 않습니다.

두 유형이 호환되는지 여부를 결정하는 규칙은 섹션 6.2.7에 설명되어 있으며 길이가 길기 때문에 여기에서 인용하지 않겠지 만 C99 표준 (PDF) 초안 에서 읽을 수 있습니다 .

여기서 관련 규칙은 섹션 6.7.5.1, 단락 2에 있습니다.

두 포인터 유형이 호환되기 위해서는 둘 다 동일하게 규정되어야하고 둘 다 호환 가능한 유형에 대한 포인터 여야합니다.

A는 이후 따라서, void* 호환되지 않는 A를 struct my_struct*, 형의 함수 포인터 void (*)(void*)타입의 함수 포인터와 호환되지 않습니다 void (*)(struct my_struct*)함수 포인터의 캐스팅은 기술적으로 행동을 정의되지 않습니다 그래서.

그러나 실제로는 경우에 따라 함수 포인터를 캐스팅하여 안전하게 벗어날 수 있습니다. x86 호출 규칙에서 인수는 스택에 푸시되고 모든 포인터는 동일한 크기입니다 (x86에서는 4 바이트, x86_64에서는 8 바이트). 함수 포인터를 호출하는 것은 스택의 인수를 푸시하고 함수 포인터 대상으로 간접 점프하는 것으로 요약되며 기계 코드 수준에서 유형에 대한 개념은 분명히 없습니다.

확실히 할 수없는 일 :

  • 다른 호출 규칙의 함수 포인터간에 캐스트합니다. 스택을 엉망으로 만들고 최악의 경우 크래시가 발생하고 거대한 보안 허점으로 조용히 성공합니다. Windows 프로그래밍에서는 종종 함수 포인터를 전달합니다. Win32에서 모든 콜백 함수가 사용할 것으로 예상 stdcall호출 규칙을 (이 매크로가 CALLBACK, PASCAL그리고 WINAPI모두에 확장). 표준 C 호출 규칙 ( cdecl) 을 사용하는 함수 포인터를 전달하면 불량이 발생합니다.
  • C ++에서 클래스 멤버 함수 포인터와 일반 함수 포인터간에 캐스트합니다. 이것은 종종 C ++ 초보자를 괴롭 힙니다. 클래스 멤버 함수에는 숨겨진 this매개 변수가 있으며 멤버 함수를 일반 함수로 캐스트하면 this사용할 개체가 없으며 다시 많은 나쁜 결과가 발생합니다.

때로는 작동하지만 정의되지 않은 동작이기도 한 또 다른 나쁜 아이디어 :

  • 함수 포인터와 일반 포인터 간의 캐스팅 (예 : a void (*)(void)를 a로 캐스팅 void*). 일부 아키텍처에서는 추가 컨텍스트 정보를 포함 할 수 있기 때문에 함수 포인터는 일반 포인터와 크기가 반드시 같지는 않습니다. 이것은 아마도 x86에서 잘 작동하지만 정의되지 않은 동작이라는 것을 기억하십시오.

최근 GLib의 일부 코드와 관련하여 정확히 동일한 문제에 대해 질문했습니다. (GLib은 GNOME 프로젝트의 핵심 라이브러리이며 C로 작성되었습니다.) 전체 slots'n'signals 프레임 워크가 그것에 의존한다고 들었습니다.

코드 전체에서 유형 (1)에서 (2)로 캐스트하는 수많은 인스턴스가 있습니다.

  1. typedef int (*CompareFunc) (const void *a, const void *b)
  2. typedef int (*CompareDataFunc) (const void *b, const void *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;
}

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.


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.


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.


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.


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.


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

참고URL : https://stackoverflow.com/questions/559581/casting-a-function-pointer-to-another-type

반응형