The Man Who Fell From The Wrong Side Of The Sky:2008年5月16日分

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

2008/5/16(Fri)

[i18n] setlocale(3) で指定するロケール名の移植性

kbkさんとこ。
setlocale(3)でどうやって

を文字列にして指定するかは、 仕様では決まってないのですよ。

The locale argument is a pointer to a character string containing the required
setting of category. The contents of this string are implementation-defined.

とある通り、"C", "POSIX", "", NULL以外は実装依存です。

まあUnix方面だと

言語 + "_" + 地域 + "." 文字符号化手法 ( + "@" おまけ)

というフォーマットで

となってる実装が多いのですが
これHTTPのAccept-Languageのように、 RFC4646 Tags for Identifying Languages
の仕様で縛られてる訳ではないです(そもそも言語タグだと"_"じゃなくて"-"ですな)。
混同されてる方は要注意。

さらに文字符号化手法については

と、まあ表記の揺れが激しいわけです、glibc2ではloose matchingとか導入したくらいで。

HTTPのAccept-CharsetはいちおIANA charset registry縛りがあるのだったっけ?
あれはPOSIX localeには何の強制もないですし、そもそも

setlocale(LC_ALL, "ja_JP.Extended_UNIX_Code_Packed_Format_for_Japanese");

なんて嫌すぐる *1

それと、複数のカテゴリをいっぺんにsetlocale(3)に指定する場合も
やりかたは実装によってバラバラです、いくつか確認してみましょ。

Solarisの場合

$ cat >hoge.c
#include <locale.h>
#include <stdio.h>
#if defined(_WIN32)
#define JPLOCALE "Japanese_Japan.932"
#elif defined(__linux__)
#define JPLOCALE "ja_JP.EUC-JP"
#else
#define JPLOCALE "ja_JP.eucJP"
#endif
main(void)
{
	setlocale(LC_ALL, "C");
	setlocale(LC_CTYPE, JPLOCALE);
	printf("%s\n", setlocale(LC_ALL, NULL));	
}
^D
$ make hoge
cc -o hoge hoge.c
$ ./hoge
/ja_JP.eucJP/C/C/C/C/C

各カテゴリをスラッシュ区切りで表示します(順序はLC_*の定数値の小さい方から)。
なのですが、"/"ではじまる場合はpathnameと解釈するちゅう仕様に反してる気が。
つかそもそもスラッシュ区切りだと、LC_ALLの場合pathnameが使えなくなりますな。

/usr/local/share/locale/ww_WW.wwwWW/LC_COLLATE;/usr/local/share/locale/ww_WW.wwwWW/LC_CTYPE;...

とか、せめて;とか:なんかにしとけばよかったのに。

glibc2の場合は

$ ./hoge
LC_CTYPE=ja_JP.EUC-JP;LC_NUMERIC=C;...

となります、これはこれでウザい(注:単に個人差です)。

われらがNetBSDは↓になってます、FreeBSDが4.4BSD runeを拡張したコードが元ネタ。

$ ./hoge
C/ja_JP.eucJP/C/C/C/C

Solarisと似てますが、先頭が"/"ではじまる場合はpathnameと解釈するPOSIX仕様とはいちお整合性あります。
まあLC_ALLの場合pathname使えないのはSolarisと一緒です(汗)。

んで最後、MSVCはglibc2と一緒っすね。

C:\> hoge.exe
LC_COLLATE=C;LC_CTYPE=Japanese_Japan.932;...

んで気づいてしまったのだけど これ、将来的にカテゴリを追加する( ISO/IEC TR 14652にはある)
場合を考えると、カテゴリが足りない場合エラーになる今のNetBSD setlocale(3)の
実装では glibc2のLC_*_MASK問題と同様に、やっぱりバイナリの後方互換性失われるのよなぁ。
仕様にopaqueとは書かれてないので、移植性を諦めさえすれば
ハードコードすることは別に禁じられてないし…うーむ頭痛い。
どーせ互換性無くなるならglibc2/MSVC風の方がキモイけどリーズナブル。

この問題の別解として

setlocale(LC_ALL, "ja_JP.eucJP");

として、LC_ALLを指定した場合は全部同じロケールをセットすること以外は
禁止してしまえばいいようにも思えるのだけど

char *tmp, *s;

setlocale(LC_ALL, "C");
setlocale(LC_CTYPE, "ja_JP.eucJP");
...
tmp = setlocale(LC_ALL, NULL);
s = strdup(tmp); /* save current locale */
...
setlocale(LC_ALL, s); /* restore previous locale */
free(s);

というcurrent localeのsave/restoreを行うコードが動作しなくなっちまうよな。

まあアプリ側はそもそも この通り

そもそも、/ で区切る指定ができるということを POSIX その他は
全く規定していないですし、そういうことをやりたい人は LC_xxx で
明示的に指定するべきでしょう。

なのよな。

つーわけでパラノイアなアプリ実装者は、とことん安全側に倒して

char *tmp, *collate, *ctype, ...;

setlocale(LC_ALL, "C");
setlocale(LC_CTYPE, "ja_JP.eucJP");
...
tmp = setlocale(LC_COLLATE, NULL);
collate = strdup(tmp); /* save current LC_COLLATE */
tmp = setlocale(LC_CTYPE, NULL);
ctype = strdup(tmp); /* save current LC_CTYPE */
...

setlocale(LC_COLLATE, collate); /* restore previous LC_COLLATE */
setlocale(LC_CTYPE, ctype); /* restore previous LC_CTYPE */
free(collate);
free(ctype);
...

とした方がいいかもしれない、save/restore目的にはLC_ALLは使うなと。
ああ、やっぱりPOSIX localeは設計が腐(以下略

あと XFree86/xorg のlocale.aliasにある

POSIX-UTF2                                      C

なんじゃこれ(UTF2ってのはUTF-8の古い名前ね)。

いちお ではC/POSIX localeのnl_langinfo(CODESET)がUTF-8でも構わない *2のだけど
XFree86/xorgがこのaliasをつけちゃうのは、あんまりよろしくない。


*1:NetBSDの場合、実装の都合上ロケール名は32byteまでだったり。
*2:これ、えんがちょの理由はUTF-8だからではなく、わざわざUCS2までの限定サポートにしてること。

[NetBSD] multi-locale その9

uselocale(3)、Thread Local Storage無しでもerrnoみたいにlibpthread側で実装すりゃいいのだが
そもそもC++(=libstdc++)のmulti-localeを使いもんになるようにしよーぜ
ちゅー消極的な目的の為だけににlibpthreadなんぞ弄りたくないんだよな。
でもsrc/gnu/dist/gcc4/libstdc++-v3/config/locale/gnu以下をgrepするとワラワラと。
よく読んでない(読みたくない)けど、__gnu_cxx::__uselocale使えば逃げられるのかな。

それにしてもC++0xはもはやなにがなんだかワカンネ。
0xは200x年の意味じゃなくて、 エクストリームな気がしてきた。


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