Document of c-modernization-kit (util) 1.0.0
Loading...
Searching...
No Matches
console-util.c
Go to the documentation of this file.
1
9
10#include <console-util.h>
11
12/* ===== Windows 実装 ===== */
13
14#ifdef _WIN32
15
16#include <windows.h>
17#include <io.h> /* _open_osfhandle, _dup, _dup2, _close, _fileno */
18#include <fcntl.h> /* _O_WRONLY, _O_BINARY */
19#include <stdio.h> /* FILE, setvbuf, fflush, stdout, stderr */
20#include <string.h> /* memmove */
21
22/* パイプ読み取りスレッドのバッファサイズ (バイト) */
23#define READ_BUF_SIZE 4096
24
25/* ストリームごとの内部状態 */
26typedef struct {
27 HANDLE orig_handle; /* DuplicateHandle で保存した元の Windows ハンドル */
28 HANDLE pipe_read; /* 匿名パイプの読み取り端 */
29 int orig_crt_fd; /* _dup で保存した元の CRT ファイルディスクリプタ */
30 HANDLE thread; /* 読み取りスレッドハンドル */
31 LONG active; /* 初期化済みフラグ (InterlockedExchange で操作) */
32} stream_state_t;
33
34/* グローバル状態 (ゼロ初期化) */
35static stream_state_t s_stdout_state;
36static stream_state_t s_stderr_state;
37
38/* ===== UTF-8 境界ユーティリティ ===== */
39
40/*
41 * buf[0..len) の末尾にある不完全な UTF-8 マルチバイトシーケンスを除いた
42 * 有効バイト数を返す。buf 全体が完全なシーケンスで終端している場合は len を返す。
43 *
44 * UTF-8 エンコーディング:
45 * 0xxxxxxx : 1 バイト (ASCII)
46 * 110xxxxx : 2 バイト先頭
47 * 1110xxxx : 3 バイト先頭
48 * 11110xxx : 4 バイト先頭
49 * 10xxxxxx : 継続バイト
50 */
51static int utf8_complete_length(const char *buf, int len)
52{
53 int i;
54 unsigned char lead;
55 int seq_len;
56
57 if (len == 0) return 0;
58
59 /* 末尾から最後の非継続バイト (リードバイトまたは ASCII) を探す */
60 i = len - 1;
61 while (i >= 0 && ((unsigned char)buf[i] & 0xC0) == 0x80) {
62 i--;
63 }
64 if (i < 0) return 0; /* 全バイトが継続バイト (異常データ): 安全のため 0 を返す */
65
66 lead = (unsigned char)buf[i];
67 if ((lead & 0x80) == 0x00) seq_len = 1; /* ASCII */
68 else if ((lead & 0xE0) == 0xC0) seq_len = 2;
69 else if ((lead & 0xF0) == 0xE0) seq_len = 3;
70 else if ((lead & 0xF8) == 0xF0) seq_len = 4;
71 else return len; /* 不正なリードバイト: 破損データとして境界を変更しない */
72
73 /* シーケンスが完結しているか確認 */
74 return (len - i >= seq_len) ? len : i;
75}
76
77/* ===== 読み取りスレッド ===== */
78
79/*
80 * パイプから UTF-8 バイト列を読み取り、出力先に応じて書き出す。
81 * - コンソール (TTY): WriteConsoleW で UTF-16 として出力。失敗時は WriteFile でフォールバック。
82 * - パイプ / ファイル: WriteFile で UTF-8 バイト列をそのまま転送。
83 * マルチバイト文字の途中で分割された場合は次回の読み取りまで末尾バイトを保留する。
84 */
85static DWORD WINAPI reader_thread_proc(LPVOID param)
86{
87 stream_state_t *s = (stream_state_t *)param;
88 char buf[READ_BUF_SIZE];
89 wchar_t wbuf[READ_BUF_SIZE]; /* UTF-16 変換バッファ (最大 READ_BUF_SIZE 文字) */
90 int pending_len = 0;
91 DWORD mode = 0;
92 BOOL is_console = (GetFileType(s->orig_handle) == FILE_TYPE_CHAR)
93 && GetConsoleMode(s->orig_handle, &mode);
94
95 for (;;) {
96 DWORD nread = 0;
97 BOOL ok = ReadFile(s->pipe_read,
98 buf + pending_len,
99 (DWORD)(READ_BUF_SIZE - pending_len),
100 &nread, NULL);
101 if (!ok || nread == 0) break; /* パイプが閉じた / エラー */
102
103 {
104 int total = pending_len + (int)nread;
105
106 if (is_console) {
107 /* 完全な UTF-8 シーケンスの境界を計算し、不完全な末尾を保留する */
108 int complete = utf8_complete_length(buf, total);
109 int new_pending = total - complete;
110
111 if (complete > 0) {
112 int wlen = MultiByteToWideChar(CP_UTF8, 0,
113 buf, complete,
114 wbuf, READ_BUF_SIZE);
115 if (wlen > 0) {
116 DWORD nw = 0;
117 if (!WriteConsoleW(s->orig_handle, wbuf, (DWORD)wlen, &nw, NULL)) {
118 /* WriteConsoleW 失敗: UTF-8 バイト列をそのまま書き戻す */
119 WriteFile(s->orig_handle, buf, (DWORD)complete, &nw, NULL);
120 }
121 }
122 }
123
124 /* 不完全な末尾シーケンスを buf 先頭に移動して次回へ持ち越す */
125 if (new_pending > 0) {
126 memmove(buf, buf + complete, (size_t)new_pending);
127 }
128 pending_len = new_pending;
129 } else {
130 /* パイプ / ファイル: UTF-8 バイト列をそのまま転送 */
131 DWORD nw = 0;
132 WriteFile(s->orig_handle, buf, (DWORD)total, &nw, NULL);
133 pending_len = 0;
134 }
135 }
136 }
137
138 return 0;
139}
140
141/* ===== ストリーム初期化 / 解放 ===== */
142
143/*
144 * 指定したストリームを匿名パイプに差し替え、読み取りスレッドを起動する。
145 *
146 * 処理順:
147 * 1. GetStdHandle → DuplicateHandle で元ハンドルを保存
148 * 2. CreatePipe で匿名パイプ (read, write) を作成
149 * 3. _dup で元 CRT FD を保存
150 * 4. _open_osfhandle + _dup2 で crt_stream を書き込み端に向ける
151 * 5. setvbuf で CRT バッファを無効化
152 * 6. CreateThread で読み取りスレッドを起動
153 */
154static int init_stream(stream_state_t *s, DWORD std_handle_id, FILE *crt_stream)
155{
156 HANDLE proc;
157 HANDLE orig;
158 HANDLE pipe_read;
159 HANDLE pipe_write;
160 SECURITY_ATTRIBUTES sa;
161 int new_fd;
162
163 orig = GetStdHandle(std_handle_id);
164 if (orig == NULL || orig == INVALID_HANDLE_VALUE) return -1;
165
166 /* 元ハンドルを安全に複製して保存 */
167 proc = GetCurrentProcess();
168 if (!DuplicateHandle(proc, orig, proc, &s->orig_handle,
169 0, FALSE, DUPLICATE_SAME_ACCESS)) return -1;
170
171 /* 匿名パイプを作成 (書き込み端は継承可能に設定) */
172 sa.nLength = sizeof(sa);
173 sa.lpSecurityDescriptor = NULL;
174 sa.bInheritHandle = TRUE;
175 if (!CreatePipe(&pipe_read, &pipe_write, &sa, 0)) {
176 CloseHandle(s->orig_handle);
177 return -1;
178 }
179 /* 読み取り端は子プロセスに継承しない */
180 SetHandleInformation(pipe_read, HANDLE_FLAG_INHERIT, 0);
181 s->pipe_read = pipe_read;
182
183 /* 元 CRT FD を保存 */
184 s->orig_crt_fd = _dup(_fileno(crt_stream));
185 if (s->orig_crt_fd == -1) {
186 CloseHandle(s->orig_handle);
187 CloseHandle(pipe_read);
188 CloseHandle(pipe_write);
189 return -1;
190 }
191
192 /* パイプ書き込み端から CRT FD を作成し、stdout / stderr を差し替える */
193 new_fd = _open_osfhandle((intptr_t)pipe_write, _O_WRONLY | _O_BINARY);
194 if (new_fd == -1) {
195 _close(s->orig_crt_fd);
196 CloseHandle(s->orig_handle);
197 CloseHandle(pipe_read);
198 CloseHandle(pipe_write);
199 return -1;
200 }
201
202 if (_dup2(new_fd, _fileno(crt_stream)) != 0) {
203 _close(new_fd); /* pipe_write を解放 (_open_osfhandle が所有) */
204 _close(s->orig_crt_fd);
205 CloseHandle(s->orig_handle);
206 CloseHandle(pipe_read);
207 return -1;
208 }
209 _close(new_fd); /* _dup2 済み。pipe_write の元ハンドルを解放。FD 1 は独立した複製を保持。 */
210 setvbuf(crt_stream, NULL, _IONBF, 0);
211
212 /* 読み取りスレッドを起動 */
213 s->thread = CreateThread(NULL, 0, reader_thread_proc, s, 0, NULL);
214 if (s->thread == NULL) {
215 /* 差し替えを元に戻す (_dup2 で書き込み端の複製も閉じる) */
216 _dup2(s->orig_crt_fd, _fileno(crt_stream));
217 _close(s->orig_crt_fd);
218 CloseHandle(s->orig_handle);
219 CloseHandle(pipe_read);
220 return -1;
221 }
222
223 InterlockedExchange(&s->active, 1);
224 return 0;
225}
226
227/*
228 * 指定したストリームを元のハンドルに戻し、読み取りスレッドを停止する。
229 * active が 0 の場合 (未初期化) は何もしない。
230 *
231 * 処理順:
232 * 1. fflush で CRT バッファを吐き出す
233 * 2. _dup2 で crt_stream を元の FD に戻す → パイプ書き込み端を閉じて EOF を送出
234 * 3. WaitForSingleObject でスレッド終了を待つ
235 * 4. ハンドルを解放する
236 */
237static void dispose_stream(stream_state_t *s, FILE *crt_stream)
238{
239 /* active を 1 → 0 に変更。戻り値が 0 なら元々 inactive なので何もしない。 */
240 if (!InterlockedCompareExchange(&s->active, 0, 1)) return;
241
242 fflush(crt_stream);
243
244 /* crt_stream を元の FD に戻す。これにより FD 1 / 2 の書き込み端複製が閉じ、
245 * スレッドの ReadFile が ERROR_BROKEN_PIPE で返る (EOF 相当)。 */
246 _dup2(s->orig_crt_fd, _fileno(crt_stream));
247 _close(s->orig_crt_fd);
248
249 /* スレッド終了を最大 5 秒待つ */
250 WaitForSingleObject(s->thread, 5000);
251
252 CloseHandle(s->pipe_read);
253 CloseHandle(s->orig_handle);
254 CloseHandle(s->thread);
255
256 s->orig_crt_fd = -1;
257 s->pipe_read = NULL;
258 s->orig_handle = NULL;
259 s->thread = NULL;
260}
261
262/* ===== 公開 API ===== */
263
265{
266 /* 二重初期化を防ぐ (マルチスレッド安全ではないが init はシングルスレッドで呼ぶ想定) */
267 if (s_stdout_state.active) return;
268
269 if (init_stream(&s_stdout_state, STD_OUTPUT_HANDLE, stdout) != 0)
270 {
271 fprintf(stderr, "console_init: stdout の初期化に失敗しました。\n");
272 return;
273 }
274
275 if (init_stream(&s_stderr_state, STD_ERROR_HANDLE, stderr) != 0) {
276 /* stderr の初期化に失敗した場合は stdout の差し替えをロールバックする */
277 dispose_stream(&s_stdout_state, stdout);
278 fprintf(stderr, "console_init: stderr の初期化に失敗しました。\n");
279 return;
280 }
281}
282
284{
285 /* stderr → stdout の順で解放する */
286 dispose_stream(&s_stderr_state, stderr);
287 dispose_stream(&s_stdout_state, stdout);
288}
289
290#else /* !_WIN32 */
291
292/* ===== Linux / 非 Windows 実装 (no-op) ===== */
293
296
297#endif /* _WIN32 */
void CONSOLE_UTIL_API console_dispose(void)
コンソールヘルパーを終了し、リソースを解放する。
void CONSOLE_UTIL_API console_init(void)
コンソールヘルパーを初期化する。
コンソール UTF-8 ヘルパー API。
#define CONSOLE_UTIL_API
呼び出し規約マクロ。