例えば構造体にひとつ要素を追加すると,無関係な 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
#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 アプリを書いている人は割と必読かも.

クエリ式で総当たり

発端

  1. C# の yield return の使い道 - u_1rohのカタチ』にコメントを書く.
  2. 久しぶりに yhara くんの『Ruby勉強会@関西-16「30分でわかるcallccの使い方」- Greenbear Diar』を読み直す.
  3. oxy くんの『Non Determinism - Rubyのある風景』を思い出す.
  4. そういや C# 3.0 なら書けるなぁ.

とまあそんな感じで.

リストモナドで非決定性計算

以上のリストモナドの性質を使うと、総当たりのプログラム、格好よくいうと非決定性の計算を行うことができます。 SICPから次のような問題を借りることにします。

Baker, Cooper, Fletcher, MillerとSmithは五階建てアパートの異なる階に住んでいる。Bakerは最上階に住むのではない。Cooperは最下階に住むのではない。Fletcherは最上階にも最下階にも住むのではない。MillerはCooperより上の階に住んでいる。SmithはFletcherの隣の階に住むのではない。FletcherはCooperの隣の階に住むのではない。それぞれはどの階に住んでいるか。

さて、この問題を解くことにしましょう。次に示すコードを見ると、まるで問題の条件を並べているだけのように見えるかもしれませんが、実はこれで可能な解をちゃんと求めることができます。

import Control.Monad.List

solve = do baker <- [1, 2, 3, 4, 5]
           cooper <- [1, 2, 3, 4, 5]
           fletcher <- [1, 2, 3, 4, 5]
           miller <- [1, 2, 3, 4, 5]
           smith <- [1, 2, 3, 4, 5]
           guard $ distinct [baker, cooper, fletcher, miller, smith]
           guard $ baker /= 5
           guard $ cooper /= 1
           guard $ fletcher /= 1 && fletcher /= 5
           guard $ miller > cooper
           guard $ abs (smith - fletcher) /= 1
           guard $ abs (fletcher - cooper) /= 1
           [baker, cooper, fletcher, miller, smith]

distinct :: Eq a => [a] -> Bool
distinct [] = True
distinct (x:xs) = all (/=x) xs && distinct xs

main :: IO()
main = print solve
実行結果: [3,2,4,5,1]
Non Determinism - Rubyのある風景

これの C# 版を書いてみました.ほとんど直訳で行けますな.

using System;
using System.Linq;

class Program
{
    static void Main(string[] args)
    {
        var answers =
            from baker in Enumerable.Range(1, 5)
            from cooper in Enumerable.Range(1, 5)
            from fletcher in Enumerable.Range(1, 5)
            from miller in Enumerable.Range(1, 5)
            from smith in Enumerable.Range(1, 5)
            where new []{ baker, cooper, fletcher, miller, smith }.Distinct().Count() == 5
            where baker != 5
            where cooper != 1
            where fletcher != 1 && fletcher != 5
            where miller > cooper
            where Math.Abs(checked(smith - fletcher)) != 1
            where Math.Abs(checked(fletcher - cooper)) != 1
            select new { baker, cooper, fletcher, miller, smith };

        foreach (var answer in answers)
            Console.WriteLine(answer);
    }
}
{ baker = 3, cooper = 2, fletcher = 4, miller = 5, smith = 1 }

STL の std::next_permutation みたいなのを先に作るのもありかもしれません.

余談

昨年だったか一昨年だったか,MVP Summit でレドモンドに遊びに行ったとき,波村さんの鞄の中に入っていたのが SICP と Hibernate 本でした.なんというか,この 2 冊を悪魔合体させたら LINQ ができることもあるかもなぁと妙に納得した憶えがあります.