2014年4月16日水曜日

CCSIZEOF_STRUCT がバグっている

Windows では、構造体の先頭のメンバで*1、その構造体のサイズを表すようになっているものが多いです。
API を呼び出す側で構造体のサイズを設定することにより、将来 Windows の機能追加で構造体にメンバが追加されても、古いプログラムの互換性が保てるようになっています。
例えば LVGROUP という構造体は、以下のように定義されています。

typedef struct tagLVGROUP
{
    UINT    cbSize;
    UINT    mask;
    LPWSTR  pszHeader;
    int     cchHeader;
    LPWSTR  pszFooter;
    int     cchFooter;
    int     iGroupId;
    UINT    stateMask;
    UINT    state;
    UINT    uAlign;
#if _WIN32_WINNT >= 0x0600
    LPWSTR  pszSubtitle;
    UINT    cchSubtitle;
    LPWSTR  pszTask;
    UINT    cchTask;
    LPWSTR  pszDescriptionTop;
    UINT    cchDescriptionTop;
    LPWSTR  pszDescriptionBottom;
    UINT    cchDescriptionBottom;
    int     iTitleImage;
    int     iExtendedImage;
    int     iFirstItem;
    UINT    cItems;
    LPWSTR  pszSubsetTitle;
    UINT    cchSubsetTitle;
#endif
} LVGROUP, *PLVGROUP;

_WIN32_WINNT >= 0x0600 ということは、pszSubtitle 以降のメンバは Windows Vista で追加されたということになります。
これを以下のように利用すると、

LVGROUP lvg;
lvg.cbSize = sizeof(LVGROUP);

_WIN32_WINNT が 0x0600 以上の場合、Vista より前の XP などでは動作しなくなります。
そういう時に困らないようにということで、過去のバージョンの構造体のサイズを表す定数が定義されています。
LVGROUP の場合、LVGROUP_V5_SIZE として Vista で追加されたメンバを除いたサイズが、以下のように定義されています。

#define LVGROUP_V5_SIZE CCSIZEOF_STRUCT(LVGROUP, uAlign)

CCSIZEOF_STRUCT というのは、構造体のあるメンバまでのサイズを求めるマクロで、意味としては以下のような計算を行うようになっています。

offsetof(struct, member) + sizeof(struct.member)

しかし、実はこれでは問題があるのです。それは、構造体のパディングが考慮されないという点です。
ある構造体 A があったとして、sizeof(A) は A のメンバのサイズの合計になるでしょうか。
答えは「なる場合もあれば、ならない場合もある」です。
なぜなら、構造体のメンバ間や末尾には、CPU の都合のいいようにパディングと呼ばれる余分な領域が追加されることがあるからです。

これにより、x64 ビルドで _WIN32_WINNT の値を 0x0600 未満に定義した場合(つまり Vista 以降のメンバを除外した場合)、sizeof(LVGROUP) は 56 ですが、 _WIN32_WINNT を 0x0600 以上に定義した場合の LVGROUP_V5_SIZE は 52 となり、サイズが食い違ってしまいます。

これは Windows のヘッダのバグでしょう。
私はこのバグにはまってしまい、x86 ビルドだと動くが x64 ビルドだとなぜか動かないという問題が出て、原因の解明に時間を掛けてしまいました。

構造体とサイズを表すメンバに関係して、微妙な問題が他にもあるので、次に続きます。

*1: サイズを表すメンバが、構造体の途中にあるものも一部存在します。

0 件のコメント:

コメントを投稿