web-dev-qa-db-ja.com

voidポインターへのC関数ポインターのキャスト

次のプログラムを実行しようとしていますが、奇妙なエラーが発生します。

ファイル1.c:

typedef unsigned long (*FN_GET_VAL)(void);

FN_GET_VAL gfnPtr;

void setCallback(const void *fnPointer)
{
    gfnPtr = *((FN_GET_VAL*) (&fnPointer));
}

ファイル2.c:

extern FN_GET_VAL gfnPtr;

unsigned long myfunc(void)
{
    return 0;
}

main()
{
   setCallback((void*)myfunc);
   gfnPtr(); /* Crashing as value was not properly 
                assigned in setCallback function */
}

ここで、gccでコンパイルすると、64ビットのsuse linuxでgfnPtr()がクラッシュします。しかし、gfnPtr()VC6とSunOSを正常に呼び出しました。

しかし、以下のように機能を変更すると、正常に動作しています。

void setCallback(const void *fnPointer)
{
    int i; // put any statement here
    gfnPtr = *((FN_GET_VAL*) (&fnPointer));
}

問題の原因を手伝ってください。ありがとう。

22
Manoj

C標準では、関数ポインタをvoid*にキャストすることはできません。別の関数ポインタ型にのみキャストできます。 C11標準、6.3.2.3§8

あるタイプの関数へのポインタを別のタイプの関数へのポインタに変換して、元に戻すことができます。

重要なのは、ポインタを使用して関数を呼び出す前に、元の型にキャストバックする必要があることです(技術的には、互換性のある型にキャストします。 6.2.7 での「互換性」の定義)。

POSIX標準は、使用されるコンテキストのために多くの(すべてではない)Cコンパイラーも従わなければならないため、関数ポインターをvoid*に変換したり戻したりできることが義務付けられていることに注意してください。これは、一部のシステム機能(dlsymなど)に必要です。

38
Pascal Cuoq

残念ながら、POSIXやその他のプラットフォームではそのようなキャストが必要ですが、標準ではデータポインタと関数ポインタ間のキャストは許可されていません(一部の非常にあいまいなプラットフォームでは意味がない場合があるため)。回避策の1つは、ポインターをキャストするのではなく、ポインターをポインターにキャストすることです(これはコンパイラーによって問題なく、すべての通常のプラットフォームでジョブを実行します)。

typedef void (*FPtr)(void); // Hide the ugliness
FPtr f = someFunc;          // Function pointer to convert
void* ptr = *(void**)(&f);  // Data pointer
FPtr f2 = *(FPtr*)(&ptr);   // Function pointer restored
14
Tronic

データポインタとコードポインタに関しては、次の3つの経験則があります。

  • 行うないデータポインタとコードポインタを混在させる
  • しないでくださいmixデータポインタとコードポインタ
  • everデータポインタとコードポインタを混在させないでください!

次の関数で:

void setCallback(const void *fnPointer)
{
    gfnPtr = *((FN_GET_VAL*) (&fnPointer));
}

データポインタが関数ポインタの場合です。 (言うまでもなく、これを行うには、最初にポインター自体のアドレスを取得し、それを逆参照する前に、ポインターへのポインターにキャストします)。

次のように書き直してみてください。

void setCallback(FN_GET_VAL fnPointer)
{
     gfnPtr = fnPointer;
}

また、ポインタを設定するときにキャストをドロップすることができます(またはドロップする必要があります)。

main()
{
   setCallback(myfunc);
   gfnPtr();
}

追加のボーナスとして、コンパイラーによって実行される通常の型チェックを使用できるようになりました。

9
Lindydancer

可能な部分的な説明を提案します。

@Manoj両方のコンパイラによって生成されたSetCallbackのアセンブリリストを調べる(または提供できる)場合、決定的な答えを得ることができます。

まず、Pascal Couqのステートメントは正しく、Lindydancerはコールバックを正しく設定する方法を示しています。私の答えは、実際の問題を説明する試みにすぎません。

この問題は、Linuxと他のプラットフォームが異なる64ビットモデルを使用しているという事実に起因していると思います( ウィキペディアの64ビットモデル を参照)。 LinuxはLP64(intは32ビット)を使用することに注意してください。他のプラットフォームについては、さらに詳細が必要です。 SPARC64の場合、ILP64(intは64ビット)を使用します。

ご存知のとおり、この問題はLinuxでのみ観察され、intローカル変数を導入すると解消されました。最適化をオフまたはオンにしてこれを試しましたか?ほとんどの場合、このハックは最適化をオンにしても有益な効果はありません。

両方の64ビットモデルでは、ポインターがコードを指しているかデータを指しているかに関係なく、ポインターは64ビットである必要があります。ただし、これが当てはまらない可能性があります(セグメント化されたメモリモデルなど)。したがって、パスカルとリンディダンサーの忠告。

ポインタが同じサイズの場合、残っているのはスタック配置の問題の可能性です。ローカルint(Linuxでは32ビット)を導入すると、配置が変更される可能性があります。これは、void *と関数ポインタの配置要件が異なる場合にのみ効果があります。疑わしいシナリオ。

それにもかかわらず、異なる64ビットメモリモデルが、観察したことの原因である可能性が最も高いです。アセンブリのリストを提供して、分析できるようにしてください。

2
Kevin A. Naudé