.ISO C locale は一応ステートフルエンコーディングを扱えるということにはなっていて、実際 Citrus でも itojun さん謹製の ISO2022 モジュールをベースにした ja_JP.ISO-2022-JP locale が用意されてはいる。でも、一つだけ問題がある。 .それは、ISO-2022 が状態遷移にエスケープシーケンスを使っているので、エスケープシーケンスを利用している他の機能 --- 一番メジャーな所では ISO 6429 のいわゆる ANSI 端末エスケープシーケンスおよびその眷属 --- との相性が良くない、ということなんだな。 .一つ例を考えてみよう。ISO 6429 の画面消去のエスケープシーケンスは ESC [ 2 J (0x1B 0x5B 0x32 0x4A) という 4 バイトの列で表される。以下、現在の locale において、ASCII のコード範囲の文字は制御文字も含めて wchar_t にはそのままゼロ拡張された値が入る実装になっているとすれば、これはワイド文字列では wchar_t wc[4] = { 0x1B, 0x5B, 0x32, 0x4A };という 4 つのワイド文字に変換されないといけない。 .マルチバイト文字をワイド文字に変換する手段はいくつかあるけれど、一文字ずつ変換するには mbrtowc() という関数を使う。普通は、あるまとまった長さのバイト数のマルチバイト文字列を渡して、そこから一文字を取り出すという使い方をするのだけれども、一バイトずつちまちまと渡していっても間違った使い方ではない、ということになっている。 .ISO-2022 な locale において、さっきのエスケープシーケンスを一バイトずつちまちまと mbrtowc() に渡して行ってみよう: char mb[1]; wchar_t wc; size_t ret; mbstate_t state; // mbrtowc(NULL, NULL, 0, &state); // state init // mb[0] = 0x1B; ret = mbrtowc(&wc, mb, 1, &state); // (1) mb[0] = 0x5B; ret = mbrtowc(&wc, mb, 1, &state); // (2) mb[0] = 0x32; ret = mbrtowc(&wc, mb, 1, &state); // (3) mb[0] = 0x4A; ret = mbrtowc(&wc, mb, 1, &state); // (4) .ここで、(1) における mbrtowc の振る舞いは、
.(2) あたりから雲行きがおかしくなってくる。mbrtowc は次のように振る舞うしかない:
.……はて、最後の 0x4A はどうやって取り出せばいいんだろう。こんな方法を考えるかもしれない: ret = mbrtowc(&wc, NULL, 0, &state); // (5) ret = mbrtowc(&wc, "", 0, &state); // (6)これらはいずれも意図した動作にはならない。 .(5) のように、第二引数に NULL を渡すと、これは単純に state の再初期化として処理されて、第一引数と第三引数は無視される。 .(6) は一見よさそうだが、mbrtowc() としては返すべき適切な値がない。mbrtowc() の返り値は基本的に食ったバイト数を返すことになっているので、1 バイトも食っていないのだから 0 を返せばいいようにも見えるけれども、これではいつ停止すればいいのか判然としない(一般的には保留されてる文字が 1 文字とは限らないし、何文字保留されてるかを知る方法がない)。それに、規格上 0 を返すというのは「ヌル文字を食った」という意味として定められているので、wc には L'\0' が格納されていないといけない。なんでヌル文字を食った時に 0 を返す必要があるのか、私にはその理由がいまいち判然としないのだけれども、いずれにしてもそう決まっている。 .1 バイトずつ渡すという方法ではうまく行きそうにない。 .他に何か良い方法はないのか。実はあるんですね。 .つまり、「ひょっとして、mbrtowc に 4 バイト一気に渡せば、先読みできるからちゃんと処理できるんじゃない?」という考え方ができる。これは限定的にはうまくいく可能性があるし、実際 Citrus の ja_JP.ISO-2022-JP locale は次の mbrtowc() 呼び出しに対して 1 を返して wc に 0x1b を格納する: ret = mbrtowc(&wc, "\x1B\x5B\x32\x4A", 4, &state);このとき、state は初期状態に戻っているので、適切に入力バッファを進めることによって残りの 3 バイトも正しく処理できる。 .ただ、どうもこの動作は本当に「良い」のか分からない。1 を返しているくせにこっそり 2 バイト目以降も見ている,というのが気にくわないし、ライブラリ作成者としてはユーザがどんな使い方をしてくるか予断を挟むべきではないから、続く呼び出しではまったく違うバッファを渡されないとも限らないし、それをされると正しくない動作になる可能性もある。 .それに、何バイトまとめて渡せばいいのかよく分からない。「MB_LEN_MAX溜めてから渡せば?」という考え方もあるかもしれないけれど、そんなに溜まるのを待ってられない、というケースもある。そういう場合には state の保存とリストアをうまく使って処理するしかない: char buf[MB_LEN_MAX]; wchar_t wc; int n = 0; size_t ret; mbstate_t state, saved_state; // mbrtowc(NULL, NULL, 0, &state); // retry: if (n >= MB_LEN_MAX) // error return -1; buf[n++] = getchar(); memcpy(&saved_state, &state, sizeof (mbstate_t)); errno = 0; ret = mbrtowc(&wc, buf, n, &state); if (ret == (size_t)-1) // error return -1; else if (ret == (size_t)-2) { // incomplete memcpy(&state, &saved_state, sizeof (mbstate_t)); goto retry; } // doneせっかくの -2 が台無し、という気もするが、ここまでの帰着により、ISO-2022 を正しく処理できる可能性を担保するにはこうしないといけない。まあ、しかしながら、別のもっと根本的な問題として、ISO-2022 のステートシフトは無限に伸びる可能性があるので、上のコードでは n が MB_LEN_MAX を超えてしまう可能性がある。これは ISO C locale としてはもはや打つ手が無い。 .もっと別の方法としては、mbsrtowcs() を使うという手もある。ただ、こっちも 1 バイトずつ渡すという芸当はできなくて(-2 を返してはくれない)、結局は何バイトかまとめて渡さないといけないから、上のコードと大差が無くなる。 .まとめ: ISO C locale 面倒くさいね |