前回の記事では、主に相手がファイルの場合を扱った。今回は、 Windows のコンソールに対して MSVCRT の入出力関数を使う場合を考える。
目次:
- Windows のコンソールについて
- I/O 関数の種類と各モードの関係
- ストリームの各モードにおけるコンソール入出力
- おまけ:ストリームの指す先がコンソールかどうかを確かめる方法(?)
- 例1:コンソールのコードページによらず、 Unicode 文字を出力したい
- 例2:UTF-8 の文字列をマルチバイトの関数で出力したい
- 例3:悪用例:UTF-8 のコード値をワイド文字版の関数で出力する
- 例4:自分でコンソールデバイスを開いた場合
Windows のコンソールについて
ファイルの場合は、プログラムが C のライブラリ関数を通してワイド文字を読み書きしたと思っていても、最終的にファイルに読み書きされるのはバイト列であった。
同様に、コンソールにもバイト列を読み書きすることができる。バイト列はコンソールのコードページによって解釈され、画面に表示される。コンソールのコードページの値は、chcp コマンドや、 Get/SetConsoleCP, Get/SetConsoleOutputCP などの API によって取得、設定できる。(MSVCRT のロケールとは違い、コンソールのコードページには UTF-8 (65001) を指定できる)【MSDN: Console Code Pages】
一方で、コンソールの場合は、然るべき API (ReadConsoleW, WriteConsoleW) を使えば Unicode 文字列を直接読み書きすることもできる。
では、MSVCRT はコンソールで入出力を行う際に、バイト列と Unicode のどちらを使うのか。
I/O 関数の種類と各モードの関係
byte I/O function / wide character I/O function と各モード (binary mode / text mode / Unicode mode) のストリームとの間の変換の仕様は、前の記事に書いたファイル入出力の場合と同じである。
(…という話は尤もらしいし、これを疑っていたら検証すべき事項が倍増するので、認めることにする。)
ストリームの各モードにおけるコンソール入出力
先に、図を載せておく:
一応断っておくが、この記事に書いていることは、いくつかのテストプログラムの実行結果を元に筆者が推測したものであり、正確ではないかもしれない。また、 MSVCRT や Windows のバージョンによっても細かい挙動は変わるかもしれない。
ストリームが binary mode の場合
コンソールへはバイト列として出力され、結果として、コンソールのコードページに従って表示される。
ストリームが text mode の場合
ロケールが “C” の場合は、プログラムが書き出したバイト列はそのままコンソールのコードページに従って解釈される。
ロケールが “C” でない場合は、プログラムが読み書きするバイト列と、コンソールに対して読み書きするバイト列の間で、マルチバイト対マルチバイトの変換が行われる。文字コードは、プログラムのロケール (LC_CTYPE) とコンソールのコードページに従う。
コンソールが文字を表示する際に、コンソールのコードページに変換せずに直接 Unicode で出力すればいいじゃないかという気がするが、何故かこうなっている。この動作は、実際に表示される文字がコンソールのコードページの制約を受けることから推測される(CP932 の場合は、 CP932 にない文字は表示されない。例えば、プログラムのロケールが欧文のコードページであっても、欧文のアクセント記号は消える)。
ストリームが Unicode mode の場合
直接、コンソールにワイド文字列を読み書きする。
プログラムのロケールや、コンソールのコードページの影響は受けない。
おまけ:ストリームの指す先がコンソールかどうかを確かめる方法(?)
ファイルポインタ (FILE *
) として与えられたストリームの出力先がコンソールであるか確かめるには、Windows API の GetConsoleMode 関数を使うという手が考えられる:
#include <stdio.h> #include <io.h> #include <windows.h> BOOL is_stream_console(FILE *stream) { DWORD mode; return !!GetConsoleMode((HANDLE)_get_osfhandle(_fileno(stream)), &mode); }
この関数を実際に試した結果は例4を参照されたい。
例1:コンソールのコードページによらず、 Unicode 文字を出力したい
ストリームを Unicode mode にして、ワイド文字版の入出力関数を使う。従来の printf のようなバイト文字版の関数は使えない。
実行例:
>chcp 932 現在のコード ページ: 932 >cl -nologo console-unicode.c console-unicode.c >console-unicode Café au lait 猫 >chcp 1252 Active code page: 1252 >console-unicode Café au lait 猫
コンソールのコードページに左右されずに、 “é” や “猫” のような文字を出力できている。
例2:UTF-8 の文字列をマルチバイトの関数で出力したい
コンソールのコードページを 65001 に設定する。その上で、ストリームを binary mode にするか、もしくは LC_CTYPE を “C” に設定した上で text mode を使う。
実行例:
>chcp 65001 Active code page: 65001 >cl -nologo console-utf8.c console-utf8.c >console-utf8 Console output code page is 65001. (should be 65001) Café au lait 猫
実行前に chcp 65001 によりコンソールのコードページを UTF-8 にしておく必要がある。そうでない場合は文字化けする。
例3:悪用例:UTF-8 のコード値をワイド文字版の関数で出力する
“C” ロケールではマルチバイト変換の際に8ビット値が素通りすること、 “C” ロケールの場合は text mode でもバイト列が直接コンソールに出力されることを考えると、ワイド文字版の関数で UTF-8 のコードユニット値を出力するということが可能である。
筆者が試したところ、何もしないで fputws を使うと、1文字(UTF-8 の1バイト)ずつ出力しようとして変換に失敗(REPLACEMENT CHARACTER に化ける)した。setvbuf 関数によりバッファリングを明示的に有効にすれば、想定通りに変換された。
実行例:
>chcp 65001 Active code page: 65001 >cl -nologo console-utf8t.c console-utf8t.c >console-utf8t Console output code page is 65001. (should be 65001) Café au lait 猫 Café au lait 猫
前半は例2と同じことをテキストモードでしただけである。
興味深いのは後半で、ワイド文字として与えたはずの入力が最終的に UTF-8 として解釈されて表示されている。
例4:自分でコンソールデバイスを開いた場合
標準入出力ストリーム (stdio, stdout, stderr) がコンソールを指していた場合に Unicode に関して特別扱いが行われるのは分かったが、自分で作ったストリームに対しても同様の措置は行われるのか?
つまり、 fopen に “CON” や “CONOUT$” を指定して作ったストリームを Unicode モードにしたときに、ロケールやコンソールのコードページに依存しない Unicode 出力ができるのか?
実行例:
>chcp 932 現在のコード ページ: 932 >cl -nologo console-dev.c console-dev.c >console-dev stdout is a tty. stdout is a console. stdout is a console output. Café au lait 猫 fopen(CON, w) is a tty. fopen(CON, w) is a console. fopen(CON, w) is a console output. Cafテゥ au lait 迪ォ fopen(CON, w+) failed. fopen(CONOUT$, w) is a tty. fopen(CONOUT$, w) is a console. fopen(CONOUT$, w) is a console output. Cafテゥ au lait 迪ォ fopen(CONOUT$, w+) is a tty. fopen(CONOUT$, w+) is a console. fopen(CONOUT$, w+) is a console output. Café au lait 猫
「Unicode mode での特別扱い」が行われるか確かめるために、 UTF-8 (65001) 以外のコードページを指定しておく。
“CON” デバイスを書き込みモード (“w”) で開いたストリームは、特別扱いが行われない。
“CON” デバイスを読み書きモード “w+” で開くことはできない。この挙動は CreateFile 関数のドキュメントを読めば見当がつく。【MSDN: CreateFile 関数】
“CONOUT$” デバイスの場合は、読み書きモード “w+” で開いた場合にのみ特別扱いが行われる。
おまけ:リダイレクトした場合
標準出力をリダイレクトした場合はどうなるか。
>console-file > NUL stdout is a tty. stdout is not a console. stdout is not a console output. fopen(CON, w) is a tty. fopen(CON, w) is not a console. fopen(CON, w) is not a console output. Cafテゥ au lait 迪ォ fopen(CON, w+) failed. fopen(CONOUT$, w) is a tty. fopen(CONOUT$, w) is not a console. fopen(CONOUT$, w) is not a console output. Cafテゥ au lait 迪ォ fopen(CONOUT$, w+) is a tty. fopen(CONOUT$, w+) is not a console. fopen(CONOUT$, w+) is not a console output. Café au lait 猫
NUL へリダイレクトした場合、 _isatty 関数の結果と自作の is_stream_console 関数の結果が異なる。【MSDN: _isatty 関数】
>console-file > CON stdout is a tty. stdout is not a console. stdout is a console output. Cafテゥ au lait 迪ォ fopen(CON, w) is a tty. fopen(CON, w) is not a console. fopen(CON, w) is a console output. Cafテゥ au lait 迪ォ fopen(CON, w+) failed. fopen(CONOUT$, w) is a tty. fopen(CONOUT$, w) is not a console. fopen(CONOUT$, w) is a console output. Cafテゥ au lait 迪ォ fopen(CONOUT$, w+) is a tty. fopen(CONOUT$, w+) is not a console. fopen(CONOUT$, w+) is a console output. Café au lait 猫
標準出力を CON へリダイレクトした場合、 is_stream_console 関数(GetConsoleMode 関数)と is_stream_console_output 関数(WriteConsole 関数を使用)の結果が異なる。
結局、自作の is_stream_console 関数も is_stream_console_output 関数も「MSVCRT がストリームをコンソールと判定する条件」の再現に至っていない。この辺はもうちょっと調査が必要である。