例えば構造体にひとつ要素を追加すると,無関係な API でバグ疑惑が発生するという話
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 |
- wxWidgets 2.8.7 は SystemParametersInfo(SPI_GETNONCLIENTMETRICS, 0, &nm, 0); の戻り値をチェックしていなかったため,API 呼出しが失敗していることに気付かず,後続の GetTextExtentPoint32 が失敗したところで問題が表面化した.
- SystemParametersInfo が失敗するようになって初めて,戻り値のチェックを行っていない問題が表面化した.
- SystemParametersInfo が失敗したとき,WINVER が 0x0600 以上だったら,nm.cbSize から sizeof(int) 引いてリトライというパッチ が投げられた.
#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)")); } }
-
- WINVER >= 0x0600 という条件式はまずそうな.
- ヒント:Windows 7 で NONCLIENTMETRICS に 8 byte ほど要素が足されるとどうなるか?
- コピペされないことを祈る.
- エラーチェックするようになったのは前進.
- いずれにせよ,WINVER 0x0600 でビルドしたバイナリが,Windows XP で動くと期待して配布しているのなら,それは辞めるべき.
- 既知の,あるメンバまで存在することを期待するなら,CCSIZEOF_STRUCT マクロを使う
- 参考1: 構造体が拡張されたときの動作 - 糸且之入Eヨ言己
- 参考2: 構造体のサイズの取得 (CCSIZEOF_STRUCT) - Windows SDK - 社本@ネオニート Blog
- 便利なのに活用事例少なそうな.もったいない.wine の人たちがちゃんと活用してるっぽいのは流石.
- WINVER >= 0x0600 という条件式はまずそうな.
- それはそれとして,Visual C++ 2008 (含む Express Edition) で WINVER を設定しないと 0x0600 (Windows Vista 以降専用) になるのは酷いかも.
966 名前: デフォルトの名無しさん [sage] 投稿日: 2007/10/27(土) 21:20:43
967 名前: デフォルトの名無しさん [sage] 投稿日: 2007/10/27(土) 22:09:12
968 名前: デフォルトの名無しさん [sage] 投稿日: 2007/10/28(日) 17:03:32966だけど、ちょっとわかったのでメモッとく。
VC9は_WIN32_WINNTを定義しないと、WINVERが0x0600になる。
↓
ヘッダの一部がvista用のメンバを持って構造体のサイズが増える。
XPのランタイムが理解できずに失敗する。
ということらしい。
更新履歴
- 2008年3月4日
- 『構造体が拡張されたときの動作 - 糸且之入Eヨ言己』を参考に,CCSIZEOF_STRUCT マクロへの言及を追加.
- Visual C++ 2008 でのデフォルト WINVER の話,誤解のないように追記.