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

ただ、まだ入力の方に対応できていないので、茨の道はまだまだ続くのであった。

Windows の Gauche コンソールで UTF-8 対応 - satosystemsの日記

これはホントに茨の道だ。

なんだかまだよくわかってないし解決してないけどまとめておこう。

そもそも何で UTF-8 なの?

Gauche は内部の文字コードが UCS2 になっている。これは国際化を意識した設計なんだけど、ここで入出力を CP932 などにすると、本来は世界中の文字が扱えるはずなのに、日本語のみしか扱えない、という事態になってしまう。

ちなみに Windows NT 系も Java も、内部文字コードGauche と同じ UCS2 で管理しているけど、これらは割り切って CP932 で入出力を行う。Java が登場したころは Windows 9x 系をサポートしなければならないため、Windows は互換性を維持しなければならないため、致し方ないところだと思う。

UTF-8 で出力するかどうかに関しては、0.9 をリリースする際に議論されたようで、以下のようなログが残っています。

eyasuyuki@twitter
cmd.exeがUTF-8を通さないのはWindowsのバグなんだから、堂々とUTF-8で出荷するというのはどうでしょう。
2009/11/16 15:58:39 PST

shiro
0.9はそれで行くしかないでしょうね。将来的には、例えば出先のマシンでちょろっとgosh欲しいと思った時にさくっとダウンロードして使えると嬉しいので、それなりに便利にしたくはあります。

http://practical-scheme.net/chaton/gauche/a/2009/11/16

さて、僕も日常で使用するにあたり、便利にしたいのでがんばってはいるのですが・・・。

UTF-8 の出力を正しく表示する

これは比較的簡単でした。

Gauche はとにかく UTF-8 で出力を行うため、UTF-8 がコンソールのバッファに正しく書き込まれる必要があります。gosh が起動しているコンソールを、何らかの方法でコードページ 65001 にしてやればよいです。

方法は

  1. コマンドプロンプトで chcp 65001 をしてから gosh を起動する
  2. gosh の起動時に「gosh.exe" -i -e"(use gauche.termios)" -e"(sys-set-console-cp 65001)" -e"(sys-set-console-output-cp 65001)"」のような感じで起動する
  3. SetConsoleOutputCP(CP_UTF8) を呼び出す

などです。

これを怠ると、gosh は UTF-8 で出力しているのに、コンソールは CP932 だと思ってバイト列をコンソールバッファに書き込みます。これは 'λ' なら UTF-8 では 0xCE, 0xBB が書き込まれ、MultiByteToWideChar を使用して WCHAR に復元が可能ですが、'あ' の場合は UTF-8 では 0xE3, 0x81, 0x82 が出力されますが、コンソールは CP932 のマルチバイトは 2 バイトまでと思い込んで、最後の 0x82 が欠落します。

  • 入力を正しい OEM コードページで処理系に渡す

GaucheWindows 用の移植レイヤー win-compat.c の関数 mbs2wcs を見ると、CP_ACP としてマルチバイトを受け取る前提になっているようです。CP_ACP とはつまり日本語環境なら CP932 です。

ソースを見る前に、「gosh.exe" -i -e"(use gauche.termios)" -e"(sys-set-console-cp 65001)" -e"(sys-set-console-output-cp 65001)"」で起動した gosh に対して、マルチバイト文字を渡そうと四苦八苦していたのですが、コンソールの入力が UTF-8 なのに処理系では CP932 を期待されていたのではうまくいくわけがありません。もっと早くソースを確認すべきでした。

何故 CP_ACP に決め打ちしているのか、というのが疑問だったので、検証プログラム(テスト用のシェル、名付けて tesh)を作成して動作確認してみました。

僕の疑問(というか主張)は

  • ここは CP_ACP ではなくコンソールに設定されているコードページを取得して、そのコードページからマルチバイトに変換するのが本来の姿ではないだろうか

というものです。

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

#define PROMPT L"tesh> "

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];
  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 を使用)
    gets(buf);
    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);

    // 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);

    free(wcs);
    free(mbs);
  }

  return 0;
}

このプログラムは常に UTF-8 で出力を行い、入力はコンソールに指定されているものを使用する、というものです。入力された内容はいったん UCS2 に変換されてから、UTF-8 として出力されます。gosh で

gosh> 'こんにちは

とした場合とほぼ同じ動作になっていると思いますなっていればいいな。

上図は CP932 として入力を受け取ったものです。「hello」、「こんにちは」、「你好」を入力しています。

上図は UTF-8 を入力として受け取ったものです。

「hello」は両方のケースでもちろん文字化けせずに通りますが、CP932 で「こんにちは」が文字化けしてないのが、コンソールの入力を CP932 で受け取るべきである何よりの証拠です。「你好」の「你」は CP932 には含まれないので文字化けするのは仕方がありません。

なお、入力のエコーバックが文字化けしているのは私の ckw のせいで、ここでは本質的な問題ではありません。

ということで、コンソールから OEM コードページを受け取るというのは正しい処理のような気がしますが、やっぱり違うような気もします。

以下をご覧ください。

入力と出力の文字コードが異なるケース

ということで MinGWGauche は、出力は UTF-8 で、SetConsoleOutputCP(CP_UTF) 相当が必須、一方入力は CP_ACP であるため、困ったことが発生します。

入力のエコーバックが文字化けしてしまうんです。

コンソールのバッファはひとつしかなく、そこに入力も出力も格納されるわけですが、入力(とりわけマルチバイト文字)は CP932 で格納し、出力は UCS2 で格納するということをしています。

出力コードページがも CP932 であれば、上記の方法で文字化けは発生しないのですが、出力コードページが UTF-8 になっていると文字化けしてしまうのです。

ということは、入力も UTF-8 にすれば文字化けしなくなるのではないか、と考えるわけですが、そうすると今度は処理系に入力文字列を渡すことができません。

なんというか、心が折れそうです。

Cygterm なんかはどうしてるのかな、と思ったら Cygwin 側に TELNET サーバを立てて、その TELNET サーバがシェルの出力と Tera Term の入力を橋渡ししている感じで、Tera Term の本来の機能を十二分に活用したうまい方法です。

どうなってるのか調べてみたい

tesh であらかた動作を確認したわけですが、所詮は擬似コードなので、Gauche を使用して本当の動作を確認してみたいと思い、ビルドを試みました。

まず、0.9 のソースコードGauche-0.9.tgz)をダウンロードして Cygwin 上でビルド。

その後 Cygwin 上の gosh を使用して、Cygwin 用にビルドしたものとは別のソースツリーに対してプリプロセスをかけます。

それが終わったら gauche.sln を開いてソリューションのビルドを行えばよいのですが、以下のようなエラーが出てビルドできません。

1>------ ビルド開始: プロジェクト: libgauche, 構成: Debug Win32 ------
2>------ ビルド開始: プロジェクト: gauche-config, 構成: Debug Win32 ------
1>Generate gauche/config.h
2>コンパイルしています...
1>Microsoft (R) Windows Script Host Version 5.8
1>Copyright (C) Microsoft Corporation 1996-2001. All rights reserved.
1>コンパイルしています...
2>gauche-config.c
1>autoloads.c
1>c:\program files\microsoft visual studio 8\vc\include\process.h(54) : error C2059: 構文エラー : '{'
1>c:\program files\microsoft visual studio 8\vc\include\process.h(54) : error C2059: 構文エラー : '型'
1>c:\program files\microsoft visual studio 8\vc\include\process.h(59) : warning C4273: 'GC_beginthreadex' : dll リンクが一貫していません。
1>        x:\works\gauche-0.9\gc\include\gc.h(1049) : 'GC_beginthreadex' の前の定義を確認してください
1>c:\program files\microsoft visual studio 8\vc\include\process.h(60) : warning C4273: 'GC_endthreadex' : dll リンクが一貫していません。
1>        x:\works\gauche-0.9\gc\include\gc.h(1054) : 'GC_endthreadex' の前の定義を確認してください

... snip ...

1>allchblk.c
1>x:\works\gauche-0.9\gc\include\private\gc_locks.h(30) : fatal error C1083: include ファイルを開けません。'atomic_ops.h': No such file or directory
1>alloc.c
1>x:\works\gauche-0.9\gc\include\private\gc_locks.h(30) : fatal error C1083: include ファイルを開けません。'atomic_ops.h': No such file or directory

... snip ...

4>fcntl.c
6>gauche-hook-lib.c
6>c1 : fatal error C1083: ソース ファイルを開けません。'..\ext\gauche\gauche-hook-lib.c': No such file or directory
3>uvector.c
5>gauche-parameter-lib.c
5>c1 : fatal error C1083: ソース ファイルを開けません。'..\ext\gauche\gauche-parameter-lib.c': No such file or directory

... snip ...

========== ビルド: 1 正常終了、33 失敗、0 更新、0 スキップ ==========

何か情報がないかと調べてみたところ以下が見つかりました。

process.h の include を止めたら直った。
しかし test\concurrent.scm が通らない。

2009-06-21

Bohem GCWindows のスレッド周りの問題なのかな。

上記のとおりに Gauche でも process.h の include をやめてみたんだけど、ビルドは通りません。


次はこちら。

> それとprocess.hはGC.hより先にインクルードしないとダメでした。 VCはVCでもバージョンによって微妙に違ったりするので、そのあたりが関係あるのかなぁ。 少なくとも自分の環境では再現しませんでした。ちょっと分かりません。すいません。

VS2005 SP1,svn HEAD gosh(cygwin)で./DIST winvcしたものでコンパイルで再現しました。
C:\Program Files\Microsoft Visual Studio 8\VC\include\process.h(54) : error C2059: 構文エラー : '{'
C:\Program Files\Microsoft Visual Studio 8\VC\include\process.h(54) : error C2059: 構文エラー : '型'
C:\Program Files\Microsoft Visual Studio 8\VC\include\process.h(59) : warning C4273: 'GC_beginthreadex' : dll リンクが一貫していません。
c:\work\Project\gauche\Gauche-vs2005\gc\include\gc.h(1049) : 'GC_beginthreadex' の前の定義を確認してください
C:\Program Files\Microsoft Visual Studio 8\VC\include\process.h(60) : warning C4273: 'GC_endthreadex' : dll リンクが一貫していません。
c:\work\Project\gauche\Gauche-vs2005\gc\include\gc.h(1054) : 'GC_endthreadex' の前の定義を確認してください

http://practical-scheme.net/wiliki/wiliki.cgi?Gauche:Windows/VC%2B%2B

ということで win-compat.h の process.h の include をコメントアウトして、GC.h の直前に追記することで、ちょっと進展がありました。

------ ビルド開始: プロジェクト: libgauche, 構成: Debug Win32 ------

... snip ...

compile.c
compile.scm(405) : error C2143: syntax error : missing ';' before 'type'
compile.scm(405) : error C2275: 'ScmObj' : illegal use of this type as an expression
        x:\works\gauche-0.9\src\gauche.h(161) : see declaration of 'ScmObj'
compile.scm(405) : error C2146: syntax error : missing ';' before identifier 'fp'
compile.scm(405) : error C2065: 'fp' : undeclared identifier
compile.scm(407) : warning C4047: '=' : 'int' differs in levels of indirection from 'ScmObj'
compile.scm(407) : warning C4047: '=' : 'int' differs in levels of indirection from 'ScmObj'
compile.scm(408) : warning C4047: '==' : 'ScmObj' differs in levels of indirection from 'int'
compile.scm(414) : error C2065: 'cise__1037' : undeclared identifier
compile.scm(414) : warning C4047: '=' : 'int' differs in levels of indirection from 'ScmObj'
compile.scm(414) : warning C4047: '=' : 'int' differs in levels of indirection from 'ScmObj'
compile.scm(432) : error C2143: syntax error : missing ';' before 'type'
compile.scm(432) : error C2065: 'cise__1038' : undeclared identifier
compile.scm(432) : warning C4047: '=' : 'int' differs in levels of indirection from 'ScmObj'
compile.scm(432) : warning C4047: '=' : 'int' differs in levels of indirection from 'ScmObj'

... snip ...

symbol.c
x:\works\gauche-0.9\src\symbol.c(106) : error C2146: syntax error : missing ')' before identifier 'PRIdPTR'
x:\works\gauche-0.9\src\symbol.c(106) : error C2059: syntax error : ')'
syslib.c

... snip ...

allchblk.c
x:\works\gauche-0.9\gc\include\private\gc_locks.h(30) : fatal error C1083: Cannot open include file: 'atomic_ops.h': No such file or directory

... snip ...

uvseq.c
c1 : fatal error C1083: Cannot open source file: '..\ext\uvector\uvseq.c': No such file or directory

... snip ...

========== ビルド: 1 正常終了、33 失敗、0 更新、0 スキップ ==========

プリプロセスに失敗してるのかな、というようなエラーが出ています。

道のりは長いなー。

他の処理系の REPL

他の処理系の Windows 上の REPL で多言語対応がどのようになっているか調べてみました。

実験の方法は、以下を REPL で入力してみる、という簡単なものです。

(integer->char 955)
(integer->char #x3042)
'こんにちは
'你好
'안녕하세요

「你」は簡体字中国語で Shift_JIS(CP932)には含まれていません。ハングル文字ももちろん含まれていません。

Ypsilon (0.9.6)

公式サイトによると、日本語 Windos のコンソール上では入出力を CP932 にしているとのことで、日本語の扱いは問題ないですが、簡体字中国語とハングル文字が出力時に ? に化けてしまいます。これは仕方がないですね。

mosh (0.2.5)

出力の状態が Ypsilon とまったく同じなので、おそらく入出力を CP932 にしているのだと思います。

Gauche (0.9)

文字化けも起こっているのですが、それ以上の問題が発生しています。コマンドプロンプトでマルチバイト文字を扱うのは、現時点ではやめておいたほうがよさそうです。

Gauche on Cygwin (0.9)

おっと!これは限りなく正解に近い。簡体字中国語もしっかり表示できてます。ハングル文字はフォントを調整したら表示できそうな気がします。

というか、Unix 環境ならこれが普通なんですよね、きっと。

なお、Cygwin 版はソースコードからビルドする必要があります。

Gauchebox (0.9)

Windows のコンソールでマルチバイトを扱うのが厳しい状況を省みて、MeadowGauche のフロントエンドにする Gauchebox というものが Gauche とは別に配布されているので、これも試してみました。

CP932 の範囲は扱えている感じです。簡体字中国語やハングル文字が入力時点で化けているのは、Meadowクリップボードから文字列を取り出すときに UCS2 として取り出してないからだと思います。

MzScheme (4.2.5)

国外の処理系の代表として MzScheme をゲストに迎えました。

出力結果が Gauche と似ているので、おそらく出力は UTF-8 だと思います。

DrScheme (4.2.5)

DrScheme というのは MzScheme のフロントエンドです。Gauchebox の Meadow みたいな感じのものです。

すべての文字を化けさせずに表示できています。きっと Unix 上の Emacs はこんな感じなんでしょう。うらやましい。

今後の展望

GaucheCygwin 上で本当の多言語化を実現できているので、きっとコマンドプロンプト上でもできるはず。また Cygwin 上の Gauche は入出力を UTF-8 で行っているので、コマンドプロンプト版も入力を UTF-8 で受けなければならないのではないか、というわけです。

というのも、Cygwin 上で MinGW 版の gosh を動作させると、やはり同じエラーが発生するからです。

VC 版の Gauche をビルドできるようにするか、MinGW 版の Gauche のビルド方法を調べるかして、入力を UTF-8 で受け取るバージョンを作成して試してみたいです。

VC 版の Gauche のビルドはかなり難しいということがわかったので、MinGW 版の Gauche をビルドして確かめてみたいですね。