丁稚な日々

Rubyで遊んだ日々の記録。あくまで著者視点の私的な記録なので、正確さを求めないように。
Rubyと関係ない話題にはその旨注記しているはず。なので、一見関係無いように見える話題もどこかで関係あるのかもしれません。または、注記の書き忘れかもしれません...

[直前] [最新] [直後] [Top]

Jul.16,2013 (Tue)

Revision: 1.3 (Jul.16,2013 15:27)

小ネタ

_ 次のようなプログラムを考えてみよう。
とりあえずファイル名はconsole.cとでもしておく。

#include <windows.h>
#include <stdio.h>

int
WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    HANDLE h;
    CONSOLE_SCREEN_BUFFER_INFO buf;

    if (!AttachConsole(ATTACH_PARENT_PROCESS)) {
        MessageBox(NULL, "AttachConsole Error", "debug", MB_OK);
    }

    h = GetStdHandle(STD_OUTPUT_HANDLE);
    if (!h) {
        MessageBox(NULL, "GetStdHandle Error", "debug", MB_OK);
        return 1;
    }

    if (!GetConsoleScreenBufferInfo(h, &buf)) {
        MessageBox(NULL, "GetConsoleScreenBufferInfo Error", "debug", MB_OK);
        return 1;
    }

    MessageBox(NULL, "OK", "debug", MB_OK);

    return 0;
}

要するに、コマンドプロンプトから起動して、そのコンソールスクリーンバッファの情報を取得するだけのプログラムである。
ちょっと特殊なのは、コンソールアプリとして作られているわけではなく、GUIアプリとして作られていて(ウィンドウを一切作ってないけど)、親(*1)のコンソールにアタッチしていることくらいである。

_ では、お手元のVC2013あたりを使って試してみよう。
そんな新しいものがなければ、VC2008くらいでも、まあよい。

C:\> "C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\vcvarsall.bat"

C:\> cl console.c user32.lib
Microsoft(R) C/C++ Optimizing Compiler Version 17.00.50727.1 for x86
Copyright (C) Microsoft Corporation.  All rights reserved.

console.c
Microsoft (R) Incremental Linker Version 11.00.50727.1
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:console.exe
console.obj
user32.lib

C:\>

ふむ、コードに問題はないようである(user32.libとリンクするのはMessageBox()のため)。
では、できたconsole.exeをそのままそのプロンプトから実行してみると...

_ あれー、GetConsoleScreenBufferInfo Errorって出るよ?

_ ここで、おもむろにもう一つ新しいコマンドプロンプトを開いて、そこからconsole.exeを実行してみよう。
今度はOKというダイアログが表示される。
なんじゃこりゃー!!!

_ さて、ここではいったい何が起きているのだろうか?

_ それでは、検証のため、次のようなバッチファイルを用意してみよう。名前はtest.batとでもする。

call :end > nul 2>&1
:end

これは何をしてるかというと、標準出力をNULに、標準エラー出力を標準出力に(つまりNUL)にそれぞれリダイレクトした状態で、自分自身(=test.bat)のラベル:endcallする、というものである。
自己ラベルcall処理が気持ち悪い、と思う方もいるかもしれないが、そこは今回の主題ではないので、空のファイルfoo.batでも用意していただいて、:endcallする代わりにそのファイルfoo.batcallすることにしてもよい。

_ では、新しいコマンドプロンプトを開いてみよう。
念のため、もう一度先ほどのconsole.exeを実行しておく。
うん、OKというダイアログが表示されるはずだ。
その上で、test.batを実行してみる。
もちろん、特に何もせずに再びプロンプトに制御が戻ってくる。
そこでもう一度console.exeを実行すると... うわー、GetConsoleScreenBufferInfo Errorだー!

_ というわけで、実質的には何もしないはずのtest.bat実行の前後で、これとは関係ないはずのconsole.exeの挙動が変化してしまうわけである。

_ ま、ありていに言って、cmd.exeかなんかのバグである。

_ なお、先ほど、console.cをコンパイルしたコマンドプロンプトで失敗していたのは、実はVC2008以降のvcvarsall.bat、というか、そこから辿った先で実行されるvsvars32.batというバッチファイルの中で、test.bat相当の処理が実行されていたためである。
なお、VC2005まではこの処理がないので、test.bat相当の処理を別途実行するまでこの問題は発現しない。

_ バグの原因を考えると、要するにcallを実行したときにリダイレクトが絡むとそのためにcmd.exe内の出力先関係の状態が一時的に変更されるのだけど、それを戻すのが正しくない部分があって、標準出力ハンドルの指すものが失踪している状態になっているのではないかと思われる。

_ なお、回避策だが、console.cのGetStdHandle()の引数をSTD_ERROR_HANDLEに変更すればこの問題はおきない。
どうせSTD_OUTPUT_HANDLESTD_ERROR_HANDLEが指すはずのスクリーンバッファは普通は同じもののはずなので、この場合は上記のエラーが生じることを除けばどちらを使っても差異はない。

_ しかしまー、こんな問題を踏むの世界に何人くらいだろうかねえ :)

_ いやしかし、これ4,5年くらい(つまりVC2008以降を使い始めてから)ずっと悩んでたんだけど、ちょうどOSをVistaかなんかに切り替えた後からだったから、それで仕様が変わったせいだろうとずっと思ってて真面目に追ってなかったんだよねえ。
まさかvcvarsall.batがトリガーだなんて思わなかったよ。ははは。

付記

(*1)
この場合は起動元のコマンドプロンプトの、おそらくはcmd.exe。


被捕捉アンテナ類
[Ant] [Antenna-Julia] [Rabbit's Antenna] [Ruby hotlinks]