Windows の Gauche コンソールで UTF-8 対応 #3

注意:この記事は書きかけのまま、本業等が忙しく、何週間も経ってしまったため、何を書いていたのか自分でもわからなくなったので、未完という形にすることにしました。調査中にわかったことがいくつかあるので、それはまた次回の記事で記したいと思います。

      • -

今日は早起きして本件の調査。

僕の持論は

  1. 入出力のコードページは、コンソールに設定されているコードページにあわせるのが上策だと思う
    1. MultiByteToWideChar() に指定するコードページは GetConsoleCP() で取得したコードページ、WideCharToMultiByte() に指定するコードページは GetConsoleOutputCP() で取得したコードページ、という具合に動的に切り替える
  2. あるいは入力と出力のコードページは統一して、固定する
    1. 入出力ともの CP932 に統一、または UTF-8 に統一、など
  3. CygwinUTF-8 の入出力ができてるんだから、ckw でも不可能ではないはず

という感じ。


前回 Gauche の VC でのビルドに失敗したので、じゃあ MinGW ではビルドが成功するか、というのが最初の課題。

GaucheMinGW でビルド


GaucheMinGW でビルドするために、sourceforge.net からソースコードをチェックアウト。説明によると ./DIST mingw でうまくいくはずなんだけど、

checking build system type... i686-pc-mingw32
checking host system type... i686-pc-mingw32
checking target system type... i686-pc-mingw32
checking slib... /usr/share/slib
checking for gcc... gcc
checking whether the C compiler works... no
configure: error: in `/x/works/gauche/trunk':
configure: error: C compiler cannot create executables
See `config.log' for more details.
No VERSION; something wrong?

という感じでエラーが出てしまう。

conftest.c から conftest.exe が生成されないのが原因なんだけど、config.log や configure を見てみても、根本原因がわからず。

目先を変えてみる

目先を変えて Mosh をビルドしてみようと思ったけど、こちらもビルドが成功せず。

Ypsilon がビルドできなければどうしよう、と思っていたところ、Ypsilon はあっけなくビルド成功。警告ひとつすら出ない。すばらしい。

さあ、検証だ

Ypsilon のソースコードを MultiByteToWideChar/WideCharToMultiByte で検索すると、Windows ポーティング層が出てきた。コードページ 932 で変換している箇所があるので、ここを GetConsoleCP()/GetConsoleOutputCP() で取得した値に置き換えるだけだ。

さあ、修正してリビルド。検証だ。

コンソールのコードページを 65001 にすると、IME が起動できないので、クリップボードからペーストする。上のキャプチャは「'あ」をペーストしたところ。

この後エンターキーを押すと、Ypsilon が終了してしまう。

終了時にエラーが出ているわけでもなく、まるで (exit) を入力したかのような正常な終了の仕方をする。

追求する

この終了の仕方は、自分で作成した検証プログラムと似ている。というか同じだ。自分のコードがおかしいのかなと思っていたが、どうやらそうではないようだ。

いったいどうなっているのか確認してみた。

コンソールからの入力を取得するのに gets を使用していたんだけど、gets がデフォルトのコードページと異なる入力を受け付けた場合にうまく機能しないのかも知れない。

ということで、以下のようにコードを修正してみる。

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

#define PROMPT L"tesh> "

/**
 * エラーメッセージを表示する。
 *
 * 直前に発生したエラーメッセージをメッセージボックスに表示する。
 * エラーが発生しなかった場合は、処置完了メッセージを表示する。
 *
 * @param [in] title メッセージボックスのタイトル
 * @param [in] msgid メッセージ ID
 */
static void trace_last_error(DWORD msgid = 0, LPWSTR append = NULL) {
	LPWSTR lpBuffer;
	if (msgid == 0) msgid = GetLastError();
	FormatMessageW(
		FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,	// 入力元と処理方法のオプション
		NULL,															// メッセージの入力元
		msgid,															// メッセージ識別子
		LANG_USER_DEFAULT,												// 言語識別子
		(LPWSTR)&lpBuffer,												// メッセージバッファ
		0,																// メッセージバッファの最大サイズ
		NULL															// 複数のメッセージ挿入シーケンスからなる配列
	);
	if (append != NULL) {
		size_t len = wcslen(append) + wcslen(lpBuffer) + 2;
		LPWSTR buff = (LPWSTR)malloc(len * sizeof(WCHAR));
		wcscpy_s(buff, len, lpBuffer);
		wcscat_s(buff, len, append);
		OutputDebugStringW(buff);
		free(buff);
	} else {
		OutputDebugStringW(lpBuffer);
	}
	LocalFree(lpBuffer);
}


#define GETS 0
#define READFILE 0
#define READCONSOLEINPUT 1

int wmain(int argc, wchar_t *argv[]) {
	// コードページが指定されていたら設定
	if (argc > 1) {
		UINT cp = (UINT)_wtoi(argv[1]);
		SetConsoleOutputCP(cp);
		if (argc > 2) {
			cp = (UINT)_wtoi(argv[2]);
			SetConsoleCP(cp);
		}
	}

	// REPL のつもり
	char buf[1024];
#if READCONSOLEINPUT
	wchar_t bufW[1024];
#endif
	while (true) {
		// プロンプトを出す処理
		DWORD size = WideCharToMultiByte(CP_UTF8, 0, PROMPT, -1, NULL, 0, NULL, NULL);
		char *mbs = (char *)malloc(size + 1);
		WideCharToMultiByte(CP_UTF8, 0, PROMPT, -1, mbs, size, NULL, NULL);
		mbs[size - 1] = '\0';
		printf(mbs);
		free(mbs);

		// 現在のコードページを取得
		UINT codepage = GetConsoleCP();
		sprintf(buf, "codepage: %d\n", codepage);
		OutputDebugStringA(buf);
		memset(buf, 0, sizeof(buf));

		// コンソール入力を取得(簡単のため gets を使用)
#if GETS
		gets(buf);
		printf("size: %d\n", strlen(buf));
#endif

		// ReadFile でもだめ
#if READFILE
		HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE);
		DWORD numberOfBytesToRead;
		if (ReadFile(hStdIn,
			buf,
			sizeof(buf) / sizeof(buf[0]), 
			&numberOfBytesToRead, 
			NULL) == 0) {
			trace_last_error();
		}
		printf("size: %d\n", strlen(buf));
#endif

#if READCONSOLEINPUT
		HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE);
		INPUT_RECORD buffer[1024];
		DWORD numberOfEventsRead;
		if (ReadConsoleInputW(hStdIn, buffer, sizeof(buffer) / sizeof(buffer[0]), &numberOfEventsRead) == 0) {
			trace_last_error();
		} else {
			DWORD j = 0;
			for (DWORD i = 0; i < numberOfEventsRead; i++) {
				if (buffer[i].EventType == KEY_EVENT) {
					bufW[j++] = buffer[i].Event.KeyEvent.uChar.UnicodeChar;
				}
			}
			if (j == 0) {
				continue;
			}
			bufW[j] = '\0';
			OutputDebugStringW(bufW);
		}
		wchar_t *wcs = bufW;
#endif

#if GETS + READFILE
		OutputDebugStringA(buf);

		// 入力文字列を UCS2 に変換
		size = MultiByteToWideChar(codepage, 0, buf, -1, NULL, 0);
		wchar_t *wcs = (wchar_t *)malloc(size * sizeof(buf));
		MultiByteToWideChar(codepage, 0, buf, -1, wcs, size);
		OutputDebugStringW(wcs);
#endif

		// UTF-8 でエコーバック
		size = WideCharToMultiByte(CP_UTF8, 0, wcs, -1, NULL, 0, NULL, NULL);
		mbs = (char *)malloc(size + 1);
		WideCharToMultiByte(CP_UTF8, 0, wcs, -1, mbs, size, NULL, NULL);
		mbs[size - 1] = '\0';
		puts(mbs);

#if GETS
		free(wcs);
#endif
		free(mbs);
	}

	return 0;
}

簡単に言うと、gets の代わりに ReadFile と ReadConsoleInputW 試してみよう、というものだ。

まず、コンソールが UTF-8 入出力時に gets がどういう振る舞いをするかというと、シングルバイト文字は問題なく、マルチバイト文字は取得できない。正確は、空白文字が取得される。

ReadFile はどうなるかというと・・・、結論から言うと ReadFile は駄目(どう駄目だったのか忘れてしまいました)。

ReadConsoleInputW だと、マルチバイトが期待通り取得できる。ただし、コンソールをむやみに 65001 にしたりしないほうがよさそうだ。



(この後も検証結果を書く予定だったのですが、何を書くか忘れてしまいました。とりあえず今回はここまでとします)