読者です 読者をやめる 読者になる 読者になる

例えば構造体にひとつ要素を追加すると,無関係な API でバグ疑惑が発生するという話

Vista

Visual C++ 2008 (バージョン9) とWindows SDK

以下は動作から推測した話ですが、あながち外していないと思います。

Visual C++ 2008は、Windows Vista対応のPlatform SDK(改名されてWindows SDKらしい)を搭載しています。Express Editionにも、必須ライブラリが含まれています。

この新しいWindows SDKに含まれるライブラリは、どうやら古いPlatform SDKとは異なるもののようです。つまり、同じ名前のlibファイルの中身が違うのです。おそらく、Windows Vista(GDI+以上の何かすごいAPIを持ってたり、CrearTypeフォントが使えたり)を考慮して、もうgdi32.dllを直接インポートするライブラリではなくなったのでしょう。

このために起きる弊害として、Visual C++ 2008 Express Editionは、wxWidgets2.8を正しくmakeできませんでした。ほとんどの機能は動作しますが、ただひとつ、オーナードロー処理中に、GetTextExtentPoint32(デバイスコンテキストの現時点における文字列の幅と高さを知るAPI)が、起きるはずのない失敗を起こします。メニューにアイコンを添えて表示しようとしたとき、サイズとしてメモリ上の残骸が返された結果をもとにして動作するため、メニュー全体が壊れます。

挙動が異なるなら、明示的に異なるライブラリをリンクさせればいいのに。もし最新コンパイラで、直接gdi32.dllをリンクする古いアプリケーションのコンパイル結果を再現したいときは、どうすればいいのでしょうか?

いくつか私の推測が間違っていました。すみません。私が一番誤解していたのは、最初に問題が起こっている箇所です。それは、gdi32.dllではなく、user32.dllのSystemParametersInfoを用いた、画面のメトリクス設定の取得でした。

これの原因は「wxWidgetsのパッチにフォーカス - the technote」でお書きになっていますが、そもそもなぜWINVERが0x600なのかというとwxWidgetsのwrapwin.hの

#if !defined(__VISUALC__) || (__VISUALC__ >= 1300)
    #define WINVER 0x0600
#endif

ここで定義しています。WINVERが0x600ということはVista以降でのみ動作するという指示なわけですが、wxWidgets 2.6.4だとファイルのタイムスタンプが2006/03(Vista発売前)です。つまり最新版でいいやという適当な定義だったのが、実際にVistaが発表されてみたら不都合が出たということでしょう。これはwxWidgetsが悪いです。

不具合マニアの私としても気になる話題だったのでメモ.

要点

とりあえず私なりにこの件の要点をまとめると,

  • wxWidgets 2.8.7ソースコードでは,wrapwin.h に WINVER 0x0600 と定義されている
  • その結果,Visual C++ 2008 Express Edition で wxWidgets 2.8.7 をビルドすると,Windows XP 等で意図通り動作しなくなった
  • この問題の 修正パッチ をコピペしてはいけない
  • サイズ拡張によるバージョニングが行われる構造体では,CCSIZEOF_STRUCT マクロを活用する.

Windows XP 等で意図通り動作しなくなった直接の理由

  • Windows Vista で NONCLIENTMETRICS 構造体末端に要素が追加された.
    • ちなみにこの要素が追加された理由も互換性絡みだったり*1
typedef struct tagNONCLIENTMETRICSW
{
    UINT    cbSize;
    int     iBorderWidth;
    int     iScrollWidth;
    int     iScrollHeight;
    int     iCaptionWidth;
    int     iCaptionHeight;
    LOGFONTW lfCaptionFont;
    int     iSmCaptionWidth;
    int     iSmCaptionHeight;
    LOGFONTW lfSmCaptionFont;
    int     iMenuWidth;
    int     iMenuHeight;
    LOGFONTW lfMenuFont;
    LOGFONTW lfStatusFont;
    LOGFONTW lfMessageFont;
#if(WINVER >= 0x0600)
    int     iPaddedBorderWidth;
#endif /* WINVER >= 0x0600 */
}   NONCLIENTMETRICSW, *PNONCLIENTMETRICSW, FAR* LPNONCLIENTMETRICSW;
  • Visual C++ 2008 (含む Express Edition) には Windows SDK が付属し,デフォルトではこれをビルドに使用する.
    • wrapwin.h に WINVER 0x0600 と定義されているため,新しい SDK (Windows SDK) では NONCLIENTMETRICS::cbSize に格納される値が従来の値と異なる.古い SDK 使用時には,この現象は表面化しない.
    • 結果として,Windows Vista より前の OS で SystemParametersInfo API が失敗する.
NONCLIENTMETRICS nm;
nm.cbSize = sizeof(NONCLIENTMETRICS);
const BOOL ret = SystemParametersInfo(SPI_GETNONCLIENTMETRICS, uiParam, &nm, 0);
    • ここで,wxWidgets 2.8.7 は uiParam に 0 を渡しているが,ドキュメントによれば これは誤り.
    • ただし,伝統的に Windows は SPI_GETNONCLIENTMETRICS の uiParam を無視している (無視せざるを得ない) ようである.よって,実は uiParam の値は本件とは関係ない.
    • 以下,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
#if defined(__WXMSW__) && defined(__WIN32__) && defined(SM_CXMENUCHECK)
        NONCLIENTMETRICS nm;
        nm.cbSize = sizeof(NONCLIENTMETRICS);
        if ( !::SystemParametersInfo(SPI_GETNONCLIENTMETRICS,0,&nm,0) )
        {
#if WINVER >= 0x0600
            // a new field has been added to NONCLIENTMETRICS under Vista, so
            // the call to SystemParametersInfo() fails if we use the struct
            // size incorporating this new value on an older system -- retry
            // without it
            nm.cbSize -= sizeof(int);
            if ( !::SystemParametersInfo(SPI_GETNONCLIENTMETRICS, 0, &nm, 0) )
#endif // WINVER >= 0x0600
            {
                // maybe we should initialize the struct with some defaults?
                wxLogLastError(_T("SystemParametersInfo(SPI_GETNONCLIENTMETRICS)"));
            }
        }
  • それはそれとして,Visual C++ 2008 (含む Express Edition) で WINVER を設定しないと 0x0600 (Windows Vista 以降専用) になるのは酷いかも.
    • Windows SDK のヘッダ内での _WIN32_WINNT と WINVER のデフォルト値は 0x0600.
    • XP で動かしたら必ずクラッシュするとかじゃなくて,気付かないうちに互換性を失っちゃうこともあるとかそういう話.
    • もちろん Win32 API を使わないようなアプリケーションには関係ない.
    • サンプルコードを公開する人は気をつけましょう.
966 名前: デフォルトの名無しさん [sage] 投稿日: 2007/10/27(土) 21:20:43
ベータ2安定して使えてる?

付属のプラットフォームSDKだと、SystemParameterInfoの
挙動が変。既出だったらスマソ。

967 名前: デフォルトの名無しさん [sage] 投稿日: 2007/10/27(土) 22:09:12
ベータ2 + Windows SDKの最新(v6.1)使っているけど、安定しているよ。
でも、SQL 2005は、VS2008のIDEを使ってくれないんだね...

968 名前: デフォルトの名無しさん [sage] 投稿日: 2007/10/28(日) 17:03:32
966だけど、ちょっとわかったのでメモッとく。

VC9は_WIN32_WINNTを定義しないと、WINVERが0x0600になる。

ヘッダの一部がvista用のメンバを持って構造体のサイズが増える。

XPのランタイムが理解できずに失敗する。

ということらしい。

更新履歴

*1:『[http://shellrevealed.com/blogs/shellblog/archive/2006/10/12/Frequently-asked-questions-about-the-Aero-Basic-window-frame.aspx:title=Frequently asked questions about the Aero Basic window frame - Shell Blog]』.Vista 向け GUI アプリを書いている人は割と必読かも.