What goes on/what's going on:2021年04月20日分

2021/04/20(Tue)

[プログラミング] printf(3)の桁区切り(位取り)はどのように実装されているのか(その2)

過去回で「%eは指数を最低2桁表示する」って話したけど、なんでかなーと思ったけどこれ電卓の指数表示が2桁だった時代の名残ちゅうことか、おのれヒューレットとパッカード! *1

はい今回は予告通りprintf(3)から少し離れてPOSIX localeとは何ぞやを説明するよ。

@チンPOSIX localeとはなんぞや

なんか収まりが悪いと直したくなりますよね定位置を *2、右か左かなんてチンポジの話だけにして欲しいよね…

まぁそれはおいといて(ゴソゴソ)、POSIX localeってのはボブ・ディラン級の言語能力を駆使して簡潔にまとめると

  • ソフトウェアの国際化(インターナショナライゼーション略してi18n)のためのフレームワーク
  • ひとたびPOSIX localeで定めるルールに従ってプログラムを書けば、とある地域・言語向けに地域化(ローカライゼーション略してl10n)するために
    • プログラムをいちいち修正して再コンパイルすることなく
    • 地域・言語ごとに別のプログラムとすることなく(シングルバイナリという)
    • 環境変数LANGあるいはLC_*で地域・言語を指定するだけでローカライズ済のアプリケーションが起動する

という魔法のこと、もちろん魔法というのは人類に扱いきれないのが世の常なのでJavaのWrite Once, Run Anywareくらい信用ならん自滅系の詠唱かもしれない。

なお通例POSIX localeと呼ばれるのでUnix方面だけのものと思われがちだけど、これはれっきとしたISO-Cの機能でありC90の頃から基本機能は存在し、C95(C90AMD1)/C99で大幅に機能が強化され実用になり、POSIX:2008及びC11でまるで冗談のような筋の悪い拡張がされてエンガチョと化した、だいたい元RedhatのUrlich Drepperのせいだし一番悪いのはC++がstd::localeでろくでもねえ拡張入れたらその尻ぬぐいなんだけど。

@まずは基本的な使い方

このPOSIX localeというものを使うには、<locale.h>に存在するsetlocale(3)を呼び出すことからはじめる。

#include <locale.h>
int
main(int argc, char *argv[])
{
	setlocale(LC_ALL, "");
}

引数を2つとるのだが、第一引数はLC_ALL、第二引数は""でいい余計な事は何も考えるな、戻り値のエラーチェックもせんでいい。

より高度なフレームワークをお使いであればそいつ自身が独自の国際化機能を持ち、内部でsetlocale(3)を勝手に呼んどいてくれるケースもあるのでその場合は明示的に呼ばんでもいい。 例えばX ToolkitのXtSetLanguageProc()とかがそれ。

int
main(int argc, char *argv[])
{
	Widget toplevel;

	XtSetLanguageProc(NULL, (XtLanguageProc)NULL, NULL);

	toplevel = XtOpenApplication(...);
}

他にも *3GTK+とかgtk_init()が内部的に呼んでたよなと記憶してたが、いつの間にやら明示的にsetlocale(3)呼べに変わってたので以下略。

このsetlocale(3)の第一引数で指定するLC_*(Locale Categoryの略だな)には

  • LC_CTYPE … マルチバイト/ワイド文字変換などに関するAPI(mbrtowc/wcrtombなど)
  • LC_COLLATE … 文字照合順序に関するAPI(strcoll/strxfrmなど)
  • LC_MESSAGES … yes/noなどの応答メッセージに関するデータ
  • LC_MONETARY … 通貨記号および金額表示方法に関するデータとAPI(strfmon)
  • LC_NUMERIC … 一般的な数値の表示方法に関するデータとAPI(printfなど)
  • LC_TIME … 年月日および時刻の表示などに関するデータとAPI(strftime/strptimeなど)

の6つ *4があり、LC_ALLはこれらすべてを同時に同じ値にセットするという意味。99%のケースではLC_ALLを指定しておけばOKだ。

そして第二引数、ここにはlocale -aで表示される地域・言語名を指定するのだけど、""を指定しておけば環境変数LANGおよびLC_*から値を検索するようになる。 明示的に指定しちゃうとその地域・言語固定になってしまうので意味がないからな。

なお環境変数で指定した地域・言語のサポートが無い場合、setlocale(3)はNULLを返すがこれは無視していい。どうせC localeに勝手にfallbackするので。

じゃちょっとコード書いてみようか、setlocale(3)呼ぶだけでlibcに含まれる様々な関数は指定した地域・言語に対応した振る舞いをするようになる、例えばLC_TIMEのstrftime(3)なんかが判りやすいのでサンプルコード。

$ cat >unko.c
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int
main(void)
{
	char buf[BUFSIZ];
	time_t now;
	struct tm *t;

	setlocale(LC_ALL, "");
	now = time(NULL);
	t = localtime(&now);
	strftime(&buf[0], sizeof(buf), "%c", t);
	puts(buf);
}
^D
$ make unko
$ for i in C ja_JP zh_CN ko_KR; do LANG=$i.UTF-8 ./unko; done
Tue Apr 20 00:18:11 2021
2021年4月20日  0時18分11秒
2021年4月20日  0时18分11秒
2021년4월20일  0시18분11초

環境変数変えるだけで再コンパイルの必要も無く複数の地域・言語による日付表示ができてますやね、これがシングルバイナリでの国際化、POSIX localeの魔法よ。 表示がされないのであれば

  • 地域・言語データが未インストールであるか
  • お前の使ってるOSの実装がへちょい

が原因と思われるので、パッケージ導入するか窓から投げ捨ててどうぞ。

@地域・言語データとはなんぞや

さっきの魔法だけどもなにも無から日本語/簡体中文/ハングルが捻り出された本物の魔法ではなく、ちゃんと種も仕掛けも存在する。 たとえばNでは/usr/share/locale以下、Linuxなら/usr/lib/locale以下に地域・言語そしてカテゴリ毎にデータベースが置かれ、libcはsetlocale(3)が呼ばれるとこのデータベースを読込んでlibc内部の定数などを上書きするのだ。

このデータベースをどう提供するかはC言語は定める立場に無いので実装依存なんだけど、SUS(Single UNIX Specification)ではUNIX(TM)を名乗るならlocaledef(1)というコマンドに定義ファイルを喰わせて生成することが求められている。 まぁNはlocaledef(1)ではなくもっとショボいmklocale(1)というコマンドを使ってるんだけどね。

このlocaledef(1)の仕様についてはドイツのUNIX User Groupが主体になって取りまとめて ISO/IEC TR14652を出したのだけど、そもそもが複雑怪奇な仕様で実装クソめんどくさい上に一部の文字コードの扱いに難があるので、結局TR止まりで撤回(withdrawn)されたっきりなのよね。 やっぱりジャガイモ喰ってるようなのはダメだな。

そんでlocaledefに喰わせる定義ファイルを取りまとめてた作業部会も、流行りモノ大好きなミーハー連中なのでフォーマットを時代の寵児XMLにしようぜ!文字集合もUnicodeで統一な!とCLDR(Common Locale Data Repository)に流れて行ってしまい解散してしまったというね。

まぁそんなオワコン状態ではあるけど、いまだlocaledef(1)を使ってるOSが多いのでそれ前提で話を進める。mklocale(1)とかいまだに使ってるOSは隅で腹筋でもしてろ。

@次回予告

ということでlocaledef(1)コマンドに喰わせる定義ファイルの内容を、重要なとこだけかいつまんで説明するよ。

*1:そういえばワイがRPN電卓なんてものの存在を知ったきっかけのカメラ・レンズ設計者の安原伸氏が去年お亡くなりになってたようで驚いた、「安原製作所回顧録」も枻出版社が今年に入って民事再生で復刊も無いだろうなぁ。
*2:なんかPOSIX原理主義を名乗って無意味な縛りプログラミングを他人に強要するアレなのが跋扈してると聞くので最近はチンポジックスと呼んでいる。
*3:ん?Qt?WxWidget?知らんし勝手に調べてどうぞ、さぁ僕とOSF/Motifの美について語ろうか…
*4:実装によってはもっと沢山のカテゴリ(カレンダーや用紙サイズ、住所表記とか)あるのだが移植性無いしそもそも公開API無いので無視してよろし。