目的
- stdout / stderr をライブラリ内で差し替え、Windows コンソール (TTY) に対しては UTF-16 (WriteConsoleW) で出力し、パイプやファイルへは UTF-8 バイト列をそのまま書き戻す。
- stdin は差し替えない。UTF-8 マニフェスト (activeCodePage = UTF-8) により、CRT の入力関数 (fgets / scanf / getchar) がコンソールから UTF-8 をネイティブに読み取れるため、ライブラリの介入は不要とする。
- Linux 環境では console_init / console_dispose は何もしない no-op として動作し、呼び出し側で #ifdef _WIN32 ガードを行う必要がない。
前提
Linux / Windows 双方で利用可能な文字コード安全なコンソールアプリケーションを作成するにあたり、 Windows では、マニフェストによるコードページの固定だけでは、コンソール出力やパイプ送受信で文字化けの懸念がある。
Windows では、stdout / stderr をフックすることで、コンソール出力やパイプ送受信でユーザーに意識させずに 正しく UTF-8 で入出力できるようにする。
アプリケーションは初期化関数を一度呼び出す必要があるが、 呼び出し後は Linux と同じコードで文字コード安全なコンソールアプリケーションとすることができる。
既存プロジェクトの対応方式
本ライブラリの設計にあたり、Git / Rust / Node.js (libuv) の Windows コンソール UTF-8 対応方式を調査した。
出力方向 (stdout / stderr)
| プロジェクト | 方式 | 出力 API | パイプ差し替え |
| Git (compat/winansi.c) | パイプ差し替え + 専用スレッド + ANSI エスケープ処理 | WriteConsoleW | する |
| Rust (library/std/src/sys/stdio/windows.rs) | CRT バイパス、直接 Windows API を使用 | WriteConsoleW | しない |
| Node.js (deps/uv/src/win/tty.c) | CRT バイパス、直接 Windows API を使用 | WriteConsoleW | しない |
- 全プロジェクトが WriteConsoleW を使用。
- Git のみパイプ差し替え方式を採用しており、本ライブラリと同様のアプローチである。Rust / Node.js は CRT を使わない独自 I/O レイヤーを提供するため、パイプ差し替えは不要。
- 本ライブラリは CRT の printf / fprintf をそのまま使う設計であるため、Git と同じパイプ差し替え方式が適切である。
入力方向 (stdin)
| プロジェクト | 方式 | 通常モード API | Raw モード API | stdin パイプ差し替え | 独自 Line Discipline |
| Git | stdin に一切触れない | CRT 標準 | N/A | しない | 不要 |
| Rust | CRT バイパス、直接 API | ReadConsoleW | N/A | しない | 不要 |
| Node.js | CRT バイパス、モード切替 | ReadConsoleW | ReadConsoleInputW | しない | 不要 |
- 3 プロジェクトいずれも stdin をパイプに差し替えていない。
- 独自の行規律 (Line Discipline) を実装しているプロジェクトもない。コンソールの ENABLE_LINE_INPUT による行編集機能をそのまま利用する。
- Git は stdin に一切介入しない (最もシンプル)。Rust は ReadConsoleW を直接使用するが CRT をバイパスする独自 I/O のため。Node.js は通常モード (ReadConsoleW) と Raw モード (ReadConsoleInputW) を SetConsoleMode で切り替える。
本ライブラリへの適用
- 出力: Git と同じパイプ差し替え + WriteConsoleW 方式を採用する。CRT の printf / fprintf をそのまま使うため最適。
- 入力: Git と同じく stdin に介入しない方式を採用する。UTF-8 マニフェストにより CRT の入力関数が UTF-8 をネイティブに扱えるため、パイプ差し替えや独自行規律は不要。
API 仕様
公開関数
console_init
CONSOLE_UTIL_EXPORT void CONSOLE_UTIL_API console_init(void)
コンソールヘルパーを初期化する。
#define CONSOLE_UTIL_API
呼び出し規約マクロ。
#define CONSOLE_UTIL_EXPORT
DLL エクスポート/インポート制御マクロ。
コンソールヘルパーを初期化する。
| 項目 | 説明 |
| Windows 動作 | stdout と stderr を内部パイプに差し替え、バックグラウンドスレッドを起動する。スレッドはパイプから UTF-8 バイト列を受け取り、出力先がコンソール (TTY) の場合は WriteConsoleW で UTF-16 として書き出す。パイプやファイルへは UTF-8 バイト列をそのまま転送する。stdin には触れない。初期化に失敗した場合は stderr に警告を出力し、何もせずに返る。 |
| Linux 動作 | 何もしない (no-op)。 |
| 呼び出し制約 | プログラム開始時に一度だけ呼び出すことを想定。二重呼び出し時は何もしない。 |
| 後処理 | 呼び出し後は console_dispose でリソースを解放すること。 |
console_dispose
CONSOLE_UTIL_EXPORT void CONSOLE_UTIL_API console_dispose(void)
コンソールヘルパーを終了し、リソースを解放する。
コンソールヘルパーを終了し、リソースを解放する。
| 項目 | 説明 |
| Windows 動作 | バックグラウンドスレッドを停止し、stdout / stderr を元のハンドルに戻す。stderr → stdout の順で解放する。 |
| Linux 動作 | 何もしない (no-op)。 |
| 安全性 | console_init を呼び出していない場合も安全に呼び出せる。複数回呼び出しても安全。 |
DLL エクスポート / インポート制御マクロ
| マクロ | 説明 |
| CONSOLE_UTIL_EXPORT | DLL エクスポート / インポートを制御する。条件に応じて以下の値を取る。 |
| CONSOLE_UTIL_API | 呼び出し規約を指定する。Windows では __stdcall、Linux では空。 |
CONSOLE_UTIL_EXPORT の展開ルール:
| 条件 | 値 |
| Linux (非 Windows) | (空) |
| Windows / __INTELLISENSE__ 定義時 | (空) |
| Windows / CONSOLE_UTIL_STATIC 定義時 (静的リンク) | (空) |
| Windows / CONSOLE_UTIL_EXPORTS 定義時 (DLL ビルド) | __declspec(dllexport) |
| Windows / CONSOLE_UTIL_EXPORTS 未定義時 (DLL 利用側) | __declspec(dllimport) |
内部設計
本セクションでは console-util.c の Windows 実装の詳細を説明する。
ストリーム状態構造体
ストリーム (stdout / stderr) ごとに以下の内部状態を保持する。
typedef struct {
HANDLE orig_handle;
HANDLE pipe_read;
int orig_crt_fd;
HANDLE thread;
LONG active;
} stream_state_t;
グローバル状態としてゼロ初期化された s_stdout_state と s_stderr_state を使用する。
出力方向の差し替えフロー (init_stream)
- GetStdHandle(STD_OUTPUT_HANDLE / STD_ERROR_HANDLE) で元ハンドルを取得
- DuplicateHandle で元ハンドルを安全に複製して保存
- CreatePipe で匿名パイプ (read, write) を作成
- 書き込み端は子プロセスに継承可能 (bInheritHandle = TRUE)
- 読み取り端は SetHandleInformation で継承を無効化
- _dup で元の CRT ファイルディスクリプタを保存
- _open_osfhandle でパイプ書き込み端から CRT FD を作成 (_O_WRONLY | _O_BINARY)
- _dup2 で stdout / stderr を書き込み端に向ける
- setvbuf(crt_stream, NULL, _IONBF, 0) で CRT バッファを無効化 (即座にフラッシュ)
- CreateThread で読み取りスレッドを起動
- InterlockedExchange(&s->active, 1) で初期化完了フラグを設定
各ステップで失敗した場合は、それまでに確保したリソースをすべてロールバックする。console_init は失敗を検知すると stderr に警告メッセージを出力し、何もせずに返る。
読み取りスレッド (reader_thread_proc)
パイプから UTF-8 バイト列を読み取り、出力先に応じて書き出す。
コンソール判定 (スレッド起動時に一度だけ実行):
- GetFileType(orig_handle) == FILE_TYPE_CHAR かつ GetConsoleMode(orig_handle, &mode) が成功 → コンソール
コンソール出力時:
- utf8_complete_length() で完全な UTF-8 シーケンスの境界を計算
- MultiByteToWideChar(CP_UTF8, 0, buf, complete, wbuf, READ_BUF_SIZE) で UTF-16 に変換
- WriteConsoleW(orig_handle, wbuf, wlen, &nw, NULL) でコンソールに出力
- WriteConsoleW 失敗時は WriteFile で UTF-8 バイト列をそのまま書き戻す (フォールバック)
- 不完全な UTF-8 末尾バイトは memmove でバッファ先頭に移動し、次回読み取りまで保留
パイプ / ファイル出力時:
- WriteFile(orig_handle, buf, total, &nw, NULL) で UTF-8 バイト列をそのまま転送
終了条件: ReadFile が失敗 (ERROR_BROKEN_PIPE) または nread == 0 でループ終了。
UTF-8 境界検出 (utf8_complete_length)
バッファ末尾にある不完全な UTF-8 マルチバイトシーケンスを検出し、完全なバイト数を返す。
UTF-8 エンコーディング:
0xxxxxxx : 1 バイト (ASCII)
110xxxxx : 2 バイト先頭
1110xxxx : 3 バイト先頭
11110xxx : 4 バイト先頭
10xxxxxx : 継続バイト
- 末尾から最後の非継続バイト (リードバイト or ASCII) を探索
- リードバイトから予想されるシーケンス長を算出
- 残りバイト数がシーケンス長に満たない場合 → 不完全として除外
- 全バイトが継続バイトの場合 (異常データ) → 安全のため 0 を返す
- 不正なリードバイトの場合 → 境界変更せず len を返す (破損データとして通過)
ストリーム解放フロー (dispose_stream)
- InterlockedCompareExchange(&s->active, 0, 1) で active を 1 → 0 に変更。元が 0 なら何もしない。
- fflush(crt_stream) で CRT バッファを吐き出す
- _dup2(s->orig_crt_fd, _fileno(crt_stream)) で元の FD に戻す → パイプ書き込み端が閉じ、スレッドの ReadFile が ERROR_BROKEN_PIPE で返る
- _close(s->orig_crt_fd) で保存した FD を解放
- WaitForSingleObject(s->thread, 5000) でスレッド終了を最大 5 秒待つ
- ハンドル (pipe_read, orig_handle, thread) を CloseHandle で解放
- 状態をクリア
入力方向 (stdin)
stdin はライブラリの差し替え対象外とする。
- コンソール入力: UTF-8 マニフェスト (Windows 10 1903+ の activeCodePage = UTF-8) により、CRT の入力関数 (fgets / scanf / getchar 等) がコンソールから UTF-8 をネイティブに読み取る。ENABLE_LINE_INPUT によるコンソールの行編集機能 (Backspace、カーソル移動、コマンド履歴等) もそのまま動作する。
- パイプ入力: パイプからの入力は生のバイト列 (UTF-8 前提) であり、CRT がそのまま読み取る。変換は不要。
- SetConsoleMode: stdin はコンソールハンドルのままであるため、アプリケーションから SetConsoleMode で ENABLE_LINE_INPUT / ENABLE_ECHO_INPUT 等のフラグを自由に変更できる。
- _getch() / _getwch(): stdin を差し替えないため、CRT の _getch() / _getwch() も通常どおり動作する。入力バッファの競合は発生しない。
stdin を差し替えない設計とした根拠:
- 主要プロジェクトとの整合性: Git / Rust / Node.js いずれも stdin をパイプに差し替えていない。
- 行編集の維持: stdin をパイプに差し替えると、コンソールの行エディタ (ENABLE_LINE_INPUT) がバイパスされ、Backspace やカーソル移動が動作しなくなる。独自の行規律 (Line Discipline) を実装する必要が生じるが、コンソール固有機能 (doskey 連携、マウス選択等) の完全な再現は困難である。
- 透過性: SetConsoleMode / _getch() がそのまま動作するため、既存プログラムの変更が不要。
- UTF-8 マニフェストの活用: Windows 10 1903+ では UTF-8 マニフェストにより CRT の入力関数が UTF-8 をネイティブに扱えるため、エンコーディング変換のためのフックは不要。
クロスプラットフォーム対応
Linux (非 Windows) 環境では console_init / console_dispose はいずれも no-op として実装されている。 console_init / console_dispose はいずれも何もしない。
呼び出し側は #ifdef _WIN32 ガード不要で、以下のように統一的に記述できる:
int main(void) {
return 0;
}
テスト
test/util/src/libutilTest/consoleTest/consoleTest.cc に以下のテストケースが実装されている。
共通テスト (Windows / Linux 両方)
| テストケース | 検証内容 |
| test_init_succeeds | console_init() がクラッシュしないことを確認 |
| test_dispose_after_init | init → dispose がクラッシュしないことを確認 |
| test_dispose_without_init | init なしで dispose を呼んでも安全なことを確認 |
| test_double_dispose | dispose を 2 回呼んでもクラッシュしないことを確認 |
| test_write_after_init | init 後に printf / fprintf を呼んでもクラッシュしないことを確認 |
Linux 専用テスト
| テストケース | 検証内容 |
| test_nop_stdout_fd_unchanged | no-op 実装が stdout の FD に一切影響を与えないことを確認 |
| test_nop_stderr_fd_unchanged | no-op 実装が stderr の FD に一切影響を与えないことを確認 |
テストは mock_libc をリンクし、Google Test フレームワーク上で実行される。
実装ファイル一覧
互換性と注意点
- CRT の setvbuf / _setmode の設定がある場合、差し替え後も影響を受ける可能性がある。
- Windows Terminal や ConHost、パイプ先のツールの挙動差に留意する。
- SetConsoleCP(CP_UTF8) / SetConsoleOutputCP(CP_UTF8) によるコードページ変更は Windows 10 1607 以降で利用できる簡易的な代替手段だが、パイプ経由の出力には効果がなく、本ライブラリは出力方向についてコードページ設定に依存しない方式を採用する。
- 入力方向は UTF-8 マニフェストに依存する。Windows 10 1903 未満の環境では UTF-8 マニフェストが動作しないため、CRT の入力関数がシステムコードページでの読み取りとなる。この場合は SetConsoleCP(CP_UTF8) を呼び出す等のアプリケーション側の対応が必要であり、本ライブラリのスコープ外とする。
コード例
マニフェストは makefw/docs-src/windows-utf8-console.md によって埋め込まれている前提。
#include <stdio.h>
int main(void) {
printf("こんにちは\n");
fprintf(stderr, "警告\n");
char buf[256];
fgets(buf, sizeof(buf), stdin);
return 0;
}