構造体が拡張されたときの動作

  • 以下,UNICODE ビルドで試した SystemParametersInfo/SPI_GETNONCLIENTMETRICS のパラメータと,Windows XP SP2 及び Windows Vista SP1 での API 成否の関係.uiParam が無視されていて,NONCLIENTMETRICS::cbSize のみで互換性判定を行っているらしいことが分かる.
    • だからといってドキュメントを無視すべきではない.
uiParam cbSize XP SP2 (x86) Vista SP1 (x86)
0 500 success success
500 500 success success
504 500 success success
0 504 fail success
500 504 fail success
504 504 fail success
504 0 fail fail
500 0 fail fail
例えば構造体にひとつ要素を追加すると,無関係な API でバグ疑惑が発生するという話 - NyaRuRuの日記

なるほど。疑問に思ったら試してみることが大切ですね。
構造体が拡張された前例としてWindows 95→98でのMENUITEMINFOがあります。SetMenuItemInfoでテストしてみました。

cbSize 95 OSR2 98 SE NT4.0 SP6 2000 SP4 XP SP2 Vista 備考
40 x x o x x x 不正な値
44 o o o o o o 旧仕様
48 x o o o o o _WIN32_WINNT >= 0x0500

NT4.0のSetMenuItemInfoはサイズをチェックしていないようです。ゼロを含め、任意の値で呼び出しに成功しました。(95もOSR2以前のバージョンではチェックしていないかもしれません)

SystemParametersInfo(SPI_GETNONCLIENTMETRICS)でもやってみました。

cbSize 95 OSR2 98 SE NT4.0 SP6 2000 SP4 XP SP2 Vista 備考
336 x x x x x x 不正な値
340 o o o o o o 旧仕様
344 x x x x x o _WIN32_WINNT >= 0x0600

全てのOSで、サポートしているサイズ以外では失敗しています。なのでNT4.0のSetMenuItemInfoは例外と捉えるべきですね。

ところでバージョン毎に構造体のサイズが異なるものの代表例といえばShell APIコモンコントロールです。ヘッダを見てみると、PlatformSDK以降では構造体のサイズを取得するCCSIZEOF_STRUCTというマクロがあり、

#define TTTOOLINFOA_V1_SIZE CCSIZEOF_STRUCT(TTTOOLINFOA, lpszText)
#define TTTOOLINFOA_V2_SIZE CCSIZEOF_STRUCT(TTTOOLINFOA, lParam)
#define TTTOOLINFOA_V3_SIZE CCSIZEOF_STRUCT(TTTOOLINFOA, lpReserved)

というように複数のバージョンを定義しています。
そんなわけで下記の件ですが、

さてこの対処方法ですが、理想的には、まだユーザ数の少ないSDKにパッチをあてて、NONCLIENTMETRICSを従来どおり32ビットで数えて500バイトのままにしておき、Vista専用のNONCLIENTMETRICS_AEROか何かの別の504バイトの構造体を設けることが、最良の策だと思います。どうでしょうMicrosoftさん(^^。どうせSystemParametersInfoの第三引数はPVOID型なんだし、何のポインタを入れても、cbSizeが正しければOKみたいだし。「Vistaに特化して増えた画面設定の値を使いたいけど、古いSDKになじんでいて新しい構造体の名前を知らない」、なんて人はほとんどいないと言ってよくて、むしろ、多くのアプリケーションでは、XP以下との互換を考慮して、増えたフィールドを使うことはないでしょうし。

とはいえ、実際には、#if WINVER >= 0x0600 のときだけ、NONCLIENTMETRICSを-sizeof(int)して使うしかないです。別のプログラムで発生する同じような問題で困っている人がググったとき、このエントリが少しでも役に立てば幸いです。

wxWidgetsのパッチにフォーカス - the technote

NyaRuRuさんがお書きになった通り、「-sizeof(int)」という値はいまいちですね。
MS流に書くのであればこんな感じでしょうか。

#define NONCLIENTMETRICSA_V1_SIZE \
    CCSIZEOF_STRUCT(NONCLIENTMETRICSA, lfMessageFont)
#define NONCLIENTMETRICSW_V1_SIZE \
    CCSIZEOF_STRUCT(NONCLIENTMETRICSW, lfMessageFont)
#define NONCLIENTMETRICSA_V6_SIZE \
    CCSIZEOF_STRUCT(NONCLIENTMETRICSA, iPaddedBorderWidth)
#define NONCLIENTMETRICSW_V6_SIZE \
    CCSIZEOF_STRUCT(NONCLIENTMETRICSW, iPaddedBorderWidth)

...

#if defined(__WXMSW__) && defined(__WIN32__) && defined(SM_CXMENUCHECK)
        NONCLIENTMETRICS nm;
        nm.cbSize = sizeof(NONCLIENTMETRICS);
        if ( !::SystemParametersInfo(SPI_GETNONCLIENTMETRICS,0,&nm,0) )
        {
#if WINVER >= 0x0600
            // fallback to legacy size
#ifdef UNICODE
            nm.cbSize = NONCLIENTMETRICSW_V1_SIZE;
#else
            nm.cbSize = NONCLIENTMETRICSA_V1_SIZE;
#endif // UNICODE
            if ( !::SystemParametersInfo(SPI_GETNONCLIENTMETRICS, 0, &nm, 0) )
#endif // WINVER >= 0x0600
            {
                // maybe we should initialize the struct with some defaults?
                wxLogLastError(_T("SystemParametersInfo(SPI_GETNONCLIENTMETRICS)"));
            }
        }

結論: MicrosoftがCCSIZEOF_STRUCTをShell APIコモンコントロール以外にも適用すれば解決。

  • それはそれとして,Visual C++ 2008 (含む Express Edition) で WINVER を設定しないと Windows Vista 以降専用になるのは酷いかも.
    • Windows SDK のヘッダ内での _WIN32_WINNT と WINVER のデフォルト値は 0x0600.
    • サンプルコードを公開する人は気をつけましょう.
例えば構造体にひとつ要素を追加すると,無関係な API でバグ疑惑が発生するという話 - NyaRuRuの日記

手元の環境で試したら以下のような結果でした。

バージョン WINVER _WIN32_WINDOWS _WIN32_WINNT
VC++ 6.0 0x400 undefined undefined
VC++ 6.0 + Platform SDK 0x501 0x500 undefined
VC++ 6.0 + Windows SDK 0x600 undefined 0x600
VC++ 2003 0x501 undefined undefined
VC++ 2005 0x501 undefined undefined
VC++ 2008 0x600 undefined 0x600

MS的には、未定義ならその時の最新の値を定義するのはまあ仕方ないという気はします。
VC++2003の時点で標準環境としてXPが想定されているわけで、これも旧環境では動かないものが生成されるわけです。
結論としては「Windowsアプリケーションの開発者はWINVERか_WIN32_WINNTを必ず定義せよ」ということになるかと思います。