The Man Who Fell From The Wrong Side Of The Sky:2010年2月7日分

[最新版] [一覧] [前月] [今月] [翌月]

2010/2/7(Sun)

[NetBSD] libedit I18N への道(その3)

よっこい庄一、だいぶ時間が空いてしまいましたが第3回です。
今日のところは 前回予告した 1 から 3 は後回しにして、実際に read.c の修正をお粉ってみましょうか。
というかチート行為の話はともかく、多言語情報処理とか CSI の話をクソ真面目に書くのは結構大変なので
3行しか集中力の続かない私はめんどくさくなって放置してたのだったり。

@ mbtowc(3) は使うな

まずは私が source-changes-d@ に投げた 指摘の 4. についてすな。
mbtowc(3) をなぜ library function が使っちゃ駄目かの説明をば。

仕様には以下の一文があります。

For a state-dependent encoding, this function is placed into its initial state 
by a call for which its character pointer argument, s, is a null pointer.
Subsequent calls with s as other than a null pointer shall cause the internal 
state of the function to be altered as necessary.

そうです、eucJP や Shift_JIS そして UTF-8 のような stateless encoding だけを使ってると忘れがちなのですが
mbtowc(3) は内部にステート情報を持つ上「静的に確保」されるので、strtok(3) なんかと同様に再入可能じゃないのです。

お手元に NetBSD がございましたら烏賊のコードを試してみてください。
s0 は JIS X0208 の文字、s1 は US-ASCII の文字で、これらを交互に mbtowc(3) で変換します。

$ cat >iso2022.c
#include <limits.h>
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <wchar.h>

int
main(void)
{
	char *loc;
	char s0[] = "\x1b$B$\"$$$&$($*\x1b(B";
	char s1[] = "ABCD";
	wchar_t wc0, wc1;

	loc = setlocale(LC_CTYPE, "ja_JP.ISO2022-JP");
	if (loc != NULL) {
		wprintf(L"%s\n", loc);
		wprintf(L"%d\n", mbtowc(&wc0, &s0[0], sizeof(s0)));
		wprintf(L"%lc\n", wc0);
		wprintf(L"%d\n", mbtowc(&wc1, &s1[0], sizeof(s1)));
		wprintf(L"%lc\n", wc1);
	}
}
^D

さてさて、このコードの実行結果はどうなるでしょうか?

多くの人は s0 に対する mbtowc(3) の呼び出しで

そして s1 に対しては

という予想します、しかしこれは完全にマテガイです。

ロンよりウッドじゃなくて証拠、実行してみましょう。

$ make iso2022
cc -O2   -o iso2022 iso2022.c
$ ./iso2022
ja_JP.ISO2022-JP
5
あ
2
疎

s1 の変換結果が 2byte かつ「 A 」ではなく「疎」になってしまっています、どうしてこうなった(AA略)かというと

ということです。つまりはある文字列の変換をいちど始めたら、終端 '\0' まで変換して内部状態が初期化されるか
mbtowc(NULL, NULL, 0) とすることで明示的に初期化するまでは他の文字列の変換を試みてはならないちうこと。

よってライブラリ関数が内部で mbtowc(3) を呼んでしまうと、アプリも同様に使ってた場合には
内部状態を壊しあい宇宙になってしまいますやね、gkbr

といっても実際のところ locking-shift を持たない stateless encoding の場合には問題にならない上に
ISO2022-JP を実際にサポートしている libc 実装なんてーのは世の中 NetBSD と newlib の C-JIS locale だけですので
(DragonFlyBSD では MB_LEN_MAX の都合上、libISO2022 は無効になっている)発覚することはまずないんですけどね。
だからといって手を抜いちゃ駄目ですよ。

それにですね、そもそもこういう1バイトづつ読み込んで変換可能かを試みるってなコードの場合
mbtowc(3) を使うとバッファ管理が自前になるのでひじょーに生産性が悪いんですよ。

#include <errno.h>
#include <limits.h>
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <wchar.h>

int
main(void)
{
	FILE *fp;
	char buf[MB_LEN_MAX + 1];
	size_t n = 0;
	int ret;
	wchar_t wc;

	setlocale(LC_CTYPE, "");

	fp = ...

	mbtowc(NULL, NULL, 0);
	for (;;) {
		if (n == MB_CUR_MAX)
			abort(); /* too long escape sequence */
		buf[n++] = (unsigned char)fgetc(fp);
		if (ferror(fp))
			abort(); /* file read error */
		if (feof(fp))
			break; /* end of file */
		errno = 0;
		ret = mbtowc(&wc, &buf[0], n);
		if (ret == (size_t)-1) {
			if (errno == EILSEQ)
				abort(); /* illegal byte sequence */
		} else {
			if (wc == L'\0')
				break; /* NUL terminator */
			wprintf(L"%lc", wc);
			n = 0;
		}
	}
}

とてもめんどくさいです >< これを mbrtowc(3) を使うように書き直すバヤイ、こっちの関数は変換途中の
マルチバイトの欠片を mbstate_t 中に保持してくれるので、MB_*_MAX やらバッファ管理を意識する必要がなくなり

#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <wchar.h>

int
main(void)
{
	FILE *fp;
	mbstate_t st;
	size_t n = 0, ret;
	char ch
	wchar_t wc;

	setlocale(LC_CTYPE, "");

	fp = ...

	mbrtowc(NULL, NULL, 0, &st);
	for (;;) {
		ch = (unsigned char)fgetc(fp);
		if (ferror(fp))
			abort(); /* file read error */
		if (feof(fp))
			break; /* end of file */
		ret = mbrtowc(&wc, &ch, 1, &st);
		if (ret == (size_t)-1)
			abort(); /* illegal byte sequence */
		if (ret != (size_t)-2) {
			if (wc == L'\0')
				break; /* NUL terminator */
			wprintf(L"%lc", wc);
		}
	}
}

とコードが若干スッキリします、まぁ前回も書いたけど fgetwc(3) 使えばもっと簡単に

#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <wchar.h>

int
main(void)
{
	FILE *fp;
	wchar_t wc;

	setlocale(LC_CTYPE, "");

	fp = ...

	for (;;) {
		wc = (wchar_t)fgetwc(fp);
		if (ferror(fp))
			abort(); /* file read error */
		if (feof(fp) || wc == L'\0')
			break; /* end of file or NUL terminator */
		wprintf(L"%lc", wc);
	}
}

なんですけどね、まぁ今回は低水準 FILE IO が絡むのでいたし方が無い。

おまけ、read_char.c 中のコメントに茶々入れてみるテスト。

344 			if (cbp >= MB_LEN_MAX) { /* "shouldn't happen" */
345 				*cp = '\0';
346 				return (-1);
347 			}

shouldn't happen (起きないだろうけど)とありますが、ダウト。
stateful encoding は冗長な escape sequence が発生する可能性があるのでよゆーでMB_LEN_MAX を超えます。
glibc2 の mbtowc(3) の man には MB_CUR_MAX を超えることはあるとまでは書いてあるんですが
なかなかそれなら MB_LEN_MAX だって超えるやん!とは気づかないもんでしてねぇ。
まぁでもちゃんと対策コードが入ってるのはエライ。

このへんの MB_LEN_MAX については 以前のエントリを参考にしてください。
ちなみに MB_LEN_MAX の最適値は 42 から 72 に訂正されました、 72 とは
オブイェークト。

@ 謎フラグ

さてさて、お次は read_char() の最後の方で出現する IGNORE_EXTCHARS という flag bit はなんなんだしょうか。

354 	if ((el->el_flags & IGNORE_EXTCHARS) && bytes > 1) {
355 		cbp = 0; /* skip this character */
356 		goto again;
357 	}

ソースを grep(1) してみると eln.cの以下の箇所で使われているようです。

47 public int
48 el_getc(EditLine *el, char *cp)
49 {
...
52
53 	el->el_flags |= IGNORE_EXTCHARS;
54 	num_read = el_wgetc (el, &wc);
55 	el->el_flags &= ~IGNORE_EXTCHARS;
...
60 }
...
72 public const char *
73 el_gets(EditLine *el, int *nread)
74 {
...
77 	el->el_flags |= IGNORE_EXTCHARS;
78 	tmp = el_wgets(el, nread);
79 	el->el_flags &= ~IGNORE_EXTCHARS;
...

さぁてもうお分かりですやね、editline(3) には

という API が定義されているのですが、前者は後者の wrapper として実装しているわけです。

元々 el_get{c,s} は read.c に実装が ありました

...
 public int
-el_getc(EditLine *el, char *cp)
+FUN(el,getc)(EditLine *el, Char *cp)
...
-public const char *
-el_gets(EditLine *el, int *nread)
+public const Char *
+FUN(el,gets)(EditLine *el, int *nread)
...

ですが今回のうん国際化対応において chartype.h にある

...
 72 #define FUN(prefix,rest)        prefix ## _w ## rest
...
121 #define FUN(prefix,rest)        prefix ## _ ## rest

というマクロを使ったテンプレート技で

と compile されるように変更してしまったのです。
そのため後者の場合失われることになる el_getc(3) を eln.cで

.if ${WIDECHAR} == "yes"
OSRCS += eln.c
SRCS += tokenizern.c historyn.c
CLEANFILES+=tokenizern.c.tmp tokenizern.c historyn.c.tmp historyn.c
CPPFLAGS+=-DWIDECHAR
.endif

として 補なったわけです、にゃるほど。

んでは IGNORE_EXTCHARS の flag bit が立ってた場合のコードを見直してみましょう。

354 	if ((el->el_flags & IGNORE_EXTCHARS) && bytes > 1) {
355 		cbp = 0; /* skip this character */
356 		goto again;
357 	}

つまりは前段の mbtowc(3) で変換したワイド文字がシングルバイト文字でない場合は
無視して最初っからやり直しをしてるというわけです。

えっ

えっ

これ明らかに挙動不審すね、例えば eucJP locale で { 0xA4, 0xA2, 0x0 } という入力があった場合
昔の el_getc(3) であれば 0xA1 を返してたのに、L'あ'に変換される 0xA4 + 0xA2 を読み飛ばして
いきなり 0x0 が帰ってきてしまうことに、それに

という問題もありますな。

さてこれどうしたもんですかね。アイデアとしてすぐにパッと思いつくのが
そもそも -DWIDECHAR を CPPFLAGS で与えなければ、旧来の el_getc(3) のままなんだから
wread.c として以下のようなファイルを追加するってー案その1。

#define WIDECHAR
#include "read.c"

すでにこの手の手法の法は bcopy(3) と memset(3) の実装なんかでやってます。

でも read.c には残念なことに FUN() マクロでテンプレート化されてないけど、public な API も含まれます。
よってこの方法では重複するシンボルを別ファイルに追い出すリファクタリングが必要になってしまいます。
ですが私のポリシーは「動いているものは触るな(何故ならそれは奇跡だから)」ですので お 断 り だ !

ここはもう変更箇所を最小限にすべくread_char()の中で

private int
read_char(EditLine *el, Char *cp)
{
	if (el->el_flags & IGNORE_EXTCHARS) {

		// el_get{c,s}向けの処理

	} else {

		// el_wget{c,s}向けの処理

	}
	...

と大きめに if〜else してしまうことでお茶を濁す方針ですかね。
リファクタリングは後回し(そして実際に対策することは無い…かも)ということで。

@ ちゅうわけで次回

ここまでを対策した patch が こちら、まだやることはいっぱいあるので参考までに。

お筆先様による自動筆記なので予定は未定ですが、以前から予告してる UTF-8 cheat と
glibc2 の話を書きたいっすね。

今日

@

むー、いつの間にかmbsnrtowcsなんかも POSIX入りして炭化、 Extended API Set, Part 1ねぇ。

ということでfmemopen(3)とopen_memstream(3)は昔書いたのを突っ込むか、ってかんじだけど
open_wmemstream(3)はそもそも仕様そのものがバグじゃねーかという気がしてならない。

@

錯乱坊フイタ、そいや 伝説のユンボでライブ会場破壊とか 写真あったのね。

@

Ceremony by Bad Lieutenant 歌い継がれるのか。

HaciendaFAC251 Factorymust be built.
テーブル代金で殴りあったTony WilsonもRob Grettonももう今はいないけど
観光に行くことがあったら行ってみたい。


[ホームへ] [ページトップへ]