パソコンでプログラミングしよう ウィンドウズC++プログラミング環境の構築
1.6.3.6(15)
ソースコード(国際化機能とロケール)

コンソールアプリケーションのGNU gettextによる国際化機能でのロケールの扱いについてソースコードで確認する。

mingw-w64実装でGNU gettext libintlライブラリによる翻訳文字列をコマンドプロンプトへ出力する方法を検討する。

...
bindtextdomain("Console1",...); // ドメインパスの設定
textdomain("Console1"); // デフォルトドメインの設定
std::cout << _("Hello world!") << std::endl; // _マクロはgettext関数に#define
...

これを正しく翻訳文字列として出力するにはロケールを事前設定する。選択される翻訳ファイルは[ドメインパス]\[ロケール]\[カテゴリ]\[ドメイン].moで、例えば[ドメインパス]\ja_JP\LC_MESSAGES\Console1.moとなる。ロケール名はウィンドウズ準拠でなくPOSIX準拠を採る。

ドキュメント(GNU gettext utilities 4.2 Triggering gettext Operations)はsetlocale(LC_ALL,"")でC言語ロケールを設定せよとするが、これはロケールをLANGなどの環境変数から取得するPOSIX互換環境を前提とする。ウィンドウズもLANGなどに加えてOUTPUT_CHARSET環境変数にコマンドプロンプトのコードページ(整数値)に対応する文字コード名(例えば932に対して"CP932")を与えれば良いとするが(GNU gettext utilities 2.4 Obtaining good output in a Windows console)、mingw-w64では日本語の翻訳文字列(シフトJIS)の出力がうまく行かない。そもそもウィンドウズネイティブのロケールはスタートメニュー[設定|時刻と言語|地域|...]が設定するもの(ウィンドウズ設定ロケール)で環境変数の利用は好ましくない。

本項目はC言語ロケール、ウィンドウズ設定ロケール、翻訳文字列出力コードページ、コマンドプロンプトコードページの四つを出力に影響する要素として検討する。翻訳文字列のコマンドプロンプト出力は以下の3ステップとなる。翻訳ファイル文字コードで記述された翻訳文字列はコマンドプロンプト出力に至るまで一回または二回コードページ(文字コード)変換される。コードページ変換はC言語ロケールLC_CTYPEカテゴリー(LC_CTYPEロケール)に従う。

  1. ウィンドウズ設定ロケール(ユーザーデフォルトロケールまたはシステムロケール)に従い翻訳ファイルを選択して翻訳する。
  2. 翻訳文字列を出力コードページへ変換する。
    • LC_CTYPEロケールが"C"以外なら出力コードページはLC_CTYPEロケールから取得する。
    • LC_CTYPEロケールが"C"なら出力コードページはシステムロケールから取得する。
  3. 翻訳文字列をコマンドプロンプトへ出力する。
    • LC_CTYPEロケールが"C"以外ならコマンドプロンプトコードページに変換して出力する。
    • LC_CTYPEロケールが"C"ならそのまま出力する。

第一ステップで翻訳ファイルが見つからない、あるいは第二ステップでコードページ変換に失敗すると検索文字列をそのまま出力する。問題は第三ステップで、LC_CTYPEロケールが"C"以外の場合にシフトJISの出力を失敗する。通常の日本語環境なら出力とコマンドプロンプトのコードページが一致するがそれでも失敗する。LC_CTYPEロケールをコードページに対して正しく設定した(例えば"Japanese_Japan.932")としても事情は変わらない。この失敗はコマンドプロンプトだけでファイルへのリダイレクトは問題ない。

各ステップ毎に実装を調査する。最初の2ステップはlibintlライブラリgettext関数が行う。ソースコードは入手可能で調査に利用する。最後の1ステップはC++/C言語標準ライブラリの範疇でコマンドプロンプトへの出力はmsvcrtランタイムが行う。ソースコードは入手できずテスト動作から推測するため正確性は劣る。

libintlライブラリ

ダウンロードリンク

libintlライブラリのソースコードはGNU gettextの一部としてgettext-runtime\intlディレクトリに展開される。以降、ファイル相対パスは断らない限りgettext-runtime\intlからとする。mingw-w64をターゲットとする部分のみを参照する。調査はバージョン0.21.1で行った。

libintl.hインクルード

libintlライブラリのクライアントはlibintl.hをインクルードするが、libint.hはパッケージの一部としてMSYS2インクルード(例えばC:\msys64\mingw64\include)に存在する。一方でダウンロードしたソースコードは雛形としてlibgnuintl.in.hを含みautotoolsツールでlibintl.hとlibgnuintl.hを生成する。libgnuintl.hはlibintl.hとほぼ同一でライブラリ内部で用いる。本項目はMSYS2インクルードのlibintl.hを参照するがダウンロードのそれと同一と見て問題ない。

libintlライブラリの公開関数(例えばgettext)は全てlibintl_を前置して定義(libintl_gettext)してlibintl.hがリダイレクトする。さらに重要な役目としてsetlocale関数を置換する。setlocaleは標準ライブラリの一部でC言語ロケールを設定/取得する(JTC1/SC22/WG14 N1570 7.11.1.1)。POSIXはこれをメッセージ言語の指定にも拡張してLC_MESSAGESカテゴリを追加し(IEEE 1003.1-2017 setlocale)、gettextもその利用を前提とする。mingw-w64のsetlocale実装はmsvcrtランタイムでPOSIXをカバーせず、定義を置換して機能拡張する。libintl_setlocale関数がそれで、libintl.hはこれをsetlocaleに#defineする。つまりlibintl.hインクルードの有無でsetlocaleの定義が異なる。

[MSYS2インクルード]/libintl.h

...
#define LC_MESSAGES 1729
...
extern char *libintl_gettext (const char *__msgid);
static inline char *gettext (const char *__msgid)
{
return libintl_gettext (__msgid);
}
extern char *libintl_dgettext (const char *__domainname, const char *__msgid);
static inline char *dgettext (const char *__domainname, const char *__msgid)
{
return libintl_dgettext (__domainname, __msgid);
}
extern char *libintl_dcgettext ...
...
#define setlocale libintl_setlocale
extern char *setlocale (int, const char *);
...
覚え書き
LC_MESSAGESの値はLC_MAX(5)より大きければ良く、何もラマヌジャン数である必要はないのだが。

setlocale関数の置換

libintl.hはsetlocaleをlibintl_setlocale(setlocale.c:1389)に#defineして機能拡張する。libintl.hをインクルードするクライアントがsetlocaleをコールするとlibintl_setlocaleをコールする。gettextソースコードからsetlocaleをコールする場合もlibintl.h/libgnuintl.hをインクルードしていればlibintl_setlocale、そうでなければmsvcrtランタイムのsetlocaleとなる。

libintl_setlocaleはsetlocale_unixlike関数(setlocale.c:647)あるいはsetlocale_single関数(setlocale.c:847)をコールしてロケールを設定/取得する。setlocale_unixlikeはロケール名にPOSIX準拠とウィンドウズ準拠の両方に対応してPOSIX準拠はウィンドウズ準拠に変換するが、LC_MESSAGESは扱えない。setlocale_singleはLC_MESSAGESの場合は特別な処理を行い、そうでなければsetlocale_unixlikeをコールする。libintl_setlocaleの処理は一貫性に欠け、LC_ALLがLC_MESSAGESも含めて変更するのは""に限られる。LANGなどの環境変数が設定されていないとして、libintl.hをインクルードした場合のsetlocale(すなわちlibintl_setlocale)の日本語環境での挙動をまとめる。ただし後述するがsetlocaleは翻訳ファイルの選択に全く寄与しない。

LC_MESSAGES以外 LC_MESSAGES
初期状態 C C
setlocale(LC_ALL,"ja_JP") Japanese_Japan.932 変更しない
setlocale(LC_ALL,"Japanese_Japan.932") Japanese_Japan.932 変更しない
setlocale(LC_ALL,"") Japanese_Japan.932 ja_JP
setlocale(LC_MESSAGES,"ja_JP") 変更しない ja_JP
setlocale(LC_MESSAGES,"Japanese_Japan.932") 変更しない Japanese_Japan.932
setlocale(LC_MESSAGES,"") 変更しない ja_JP

LC_MESSAGES以外のロケールは常にウィンドウズ準拠名になる。特に重要なものとして、LC_CTYPEロケールが使用する文字コードは文字コード名でなくコードページ(整数値)で指定される。

libintl_setlocale関数

ファイル(setlocale.c)はlibgnuintl.hをインクルードするのでsetlocaleを#undefしてmsvcrtランタイムに戻す。これは当たり前の話でさもなくば無限回帰になる。ロケール取得やエラー処理などは省いた。

...
#include gettextP.h // libgnuintl.hをインクルード
...
#undef setlocale
...
char *
libintl_setlocale (int category, const char *locale)
{
if (locale != NULL && locale[0] == '\0')
{
if (category == LC_ALL) // setlocale(LC_ALL,"")の場合
{
static int const categories[] = // カテゴリ配列
{
LC_CTYPE,
...
LC_MESSAGES
};
...
const char *base_name; // LC_CTYPEロケールで他カテゴリのデフォルト
unsigned int i; // カテゴリ配列インデックス
...
base_name = ...
// LC_CTYPEロケールを環境またはデフォルトから取得
if (setlocale_unixlike (LC_ALL, base_name) != NULL)
{ // LC_MESSAGESを除く全カテゴリにbase_nameを設定
i = 1;
}
else
{ // base_nameで設定できない場合は"C"ロケールに変更
base_name = "C";
if (setlocale_unixlike (LC_ALL, base_name) == NULL)
...
i = 0;
}
...
for (; i < sizeof (categories) / sizeof (categories[0]); i++)
{
int cat = categories[i]; // カテゴリ配列から選択
const char *name;
name = ...
// catのロケールを環境またはデフォルトから取得
if (strcmp (name, base_name) != 0 || cat == LC_MESSAGES)
// nameがbase_nameと異なればcatのロケールをnameに変更
// LC_MESSAGESはロケール未設定なので必ず設定
if (setlocale_single (cat, name) == NULL)
...
}
...
return setlocale (LC_ALL, NULL);
...
}
else // setlocale(単一ロケール,"")の場合
{
char *result;
const char *name = ...
// categoryのロケールを環境またはデフォルトから取得
result = setlocale_single (category, name);
// categoryのロケールを設定
...
return result;
}
}
else
{
if (category == LC_ALL && locale != NULL && strchr (locale, '.') != NULL)
// setlocale(LC_ALL,"ロケール.文字コード")の場合
{
...
if (setlocale_unixlike (LC_ALL, locale) == NULL)
// LC_MESSAGESを除く全カテゴリにlocaleを設定
{
...
}
...
return setlocale (LC_ALL, NULL);
}
else // setlocale(単一カテゴリ,...)
// またはsetlocale(LC_ALL,NULLか"ロケール")の場合
{
char *result = setlocale_single (category, locale);
// LC_MESSAGESを設定するのはcategory==LC_MESSAGESの場合だけ
...
return result;
}
}
}

setlocale_unix関数

setlocaleはmsvcrtランタイムをコールする。POSIX準拠名からウィンドウズ準拠名への変換は省略した。

static char *
setlocale_unixlike (int category, const char *locale)
{
char *result; // 返値
char llCC_buf[64]; // POSIX準拠名から変換したウィンドウズ準拠名
...
if (locale != NULL && strcmp (locale, "POSIX") == 0)
locale = "C"; // "POSIX"は"C"に置き換え
result = setlocale (category, locale);
if (result != NULL)
return result; // ウィンドウズ準拠名("C"含む)ならそのまま設定
if (strlen (locale) < sizeof (llCC_buf))
{ // ウィンドウズ準拠名でない場合
... // POSIX準拠名ならウィンドウズ準拠名に変換
if (strcmp (llCC_buf, locale) != 0)
{
result = setlocale (category, llCC_buf);
if (result != NULL)
return result;
} // POSIX準拠名から得たウィンドウズ準拠名で設定
...
}
return NULL;
}

setlocale_single関数

LC_MESSAGESはmsvcrtラインタイムのsetlocaleを使えないので自ら処理してグローバルに記憶する。

static char lc_messages_name[64] = "C";
static char *
setlocale_single (int category, const char *locale)
{
if (category == LC_MESSAGES)
{ // LC_MESSAGESの特別な処理
if (locale != NULL)
{
lc_messages_name[sizeof (lc_messages_name) - 1] = '\0';
strncpy (lc_messages_name, locale, sizeof (lc_messages_name) - 1);
}
return lc_messages_name;
}
else // LC_MESSAGES以外の処理
// LC_ALLでもLC_MESSAGESを除く全カテゴリ
return setlocale_unixlike (category, locale);
}

翻訳文字列の取得

libintlライブラリは公開関数(例えばgettext)を全てlibintl_を前置した関数(libintl_gettext)にリダイレクトする。リダイレクト関数を定義するソースコードファイル(gettext.c)は対応するマクロ(GETTEXT)を定義し、ファイル内の定義/参照はマクロ名で行う。gettext(またはGETTEXT)(gettext.c:49)はdcgettext(またはDCGETTEXT)(dcgettext.c:42)をコールし、dcgettextはdcigettext(またはDCIGETTEXT)(dcigettext.c:454)をコールする。まとめればgettext(msgid)はdcigettext(NULL,msgid,NULL,0,0,LC_MESSAGES)をコールする。dcigettextは内部関数だが公開関数に準じた命名(libintl_dcigettext)とマクロ定義(DCIGETETXT)を行っている。

dcigettext関数

dcigettextは内部関数で単数形gettext/dcgettextと複数形ngettext/dcngettextが共にコールする。翻訳文字列をテーブル保存して再利用する部分、翻訳ファイルの探索とバインド、複数形への対応などは省略した。カテゴリはcategory仮引数に指定するがLC_MESSAGESのみを想定して良く、ドキュメントもそれ以外の使い道は想像できないとする(GNU gettext utilities 11.2.2 Solving Ambiguities)。

...
# define DCIGETTEXT libintl_dcigettext
...
char *
DCIGETTEXT (const char *domainname, const char *msgid1, const char *msgid2,
int plural, unsigned long int n, int category)
{
struct loaded_l10nfile *domain; // 翻訳ファイル構造体
struct binding *binding; // 翻訳ファイルのバインド構造体
const char *categoryname; // "LC_MESSAGES"などのカテゴリ名
const char *categoryvalue; // デリミタ":"で連結したロケール名配列
const char *dirname; // 翻訳ファイルの[ドメインパス]
char *xdomainname; // 翻訳ファイルの[カテゴリ]\[ドメイン].mo
char *single_locale; // ロケール名配列から順次取得する一つのロケール名
char *retval; // 翻訳文字列
...
categoryvalue = guess_category_value (category, categoryname);
// ロケール名配列はguess_category_value関数で取得
...
single_locale = (char *) alloca (strlen (categoryvalue) + 1);
// single_localeメモリ領域確保
while (1) // single_localeで翻訳文字列を得るまで繰り返す
{
while (categoryvalue[0] != '\0' && categoryvalue[0] == ':') ++categoryvalue;
// デリミタをスキップ
if (categoryvalue[0] == '\0')
{ // 終端に達したら"C"ロケールとする
single_locale[0] = 'C';
single_locale[1] = '\0';
}
else
{ // single_localeへロケール一要素をコピーしてターミネータ付加
char *cp = single_locale;
while (categoryvalue[0] != '\0' && categoryvalue[0] != ':') *cp++ = *categoryvalue++;
*cp = '\0';
...
}
if (strcmp (single_locale, "C") == 0 || strcmp (single_locale, "POSIX") == 0)
break; // "C"ロケールはループブレークして翻訳前の検索文字列を返す
domain = _nl_find_domain (dirname, single_locale, xdomainname, binding);
// 翻訳ファイルは_nl_find_domain関数で選択する
if (domain != NULL)
{
retval = _nl_find_msg (domain, binding, msgid1, 1, &retlen);
// 翻訳文字列は_nl_find_msg関数で取得する
// 文字列は既に出力文字コードに変換されている
...
if (retval != NULL)
{ // 翻訳文字列を取得したら返す
return retval;
}
}
}
...
return (char *) msgid1;
}

翻訳ファイルの選択

翻訳ファイルはguess_category_value関数(dcigettext.c:1125)で得るロケール名配列から_nl_find_domain関数(finddomain.c:57)で選択する。guess_category_valueが配列を返すのはデフォルトロケールを複数持つシステムへの対応で、ウィンドウズ実装で要素数は1に限定される。guess_category_valueの処理を説明する。なお_nl_locale_name_XXXはgettextP.hでgl_locale_name_XXXマクロに定義され、ソースコードファイル中はマクロ名で定義/参照する。ここのロケール名は全てウィンドウズ準拠(例えばJapanese_Japan)でなくPOSIX準拠(ja_JP)で取得する。

  1. _nl_locale_name_posix(またはgl_locale_name_posix)(localename.c:3244)で環境変数からロケールの取得を試みる
  2. 環境変数からロケールが取得できなければ_nl_locale_name_default(またはgl_locale_name_default)(localename.c:3342)でデフォルトロケールを取得する
  3. "C"ロケールを取得していたらそのまま返す
  4. LANGUAGE環境変数が設定されていたらそれを返す
  5. 先に取得した環境変数ロケールまたはデフォルトロケールを返す

_nl_locale_name_posix(またはgl_locale_name_posix)が示すように、翻訳ファイルの選択にsetlocale(LC_MESSAGES,...)は全く寄与しない。例えば日本語(日本)ロケールならクライアントがsetlocale(LC_MESSAGES,"en_US")をコールしたとしても日本語に翻訳される。

guess_category_value関数

static const char *
internal_function
guess_category_value (int category, const char *categoryname)
{
const char *language;
const char *locale;
const char *language_default;
int locale_defaulted;
locale = _nl_locale_name_posix (category, categoryname);
locale_defaulted = 0;
if (locale == NULL)
{
locale = _nl_locale_name_default ();
locale_defaulted = 1;
}
if (strcmp (locale, "C") == 0)
return locale;
language = getenv ("LANGUAGE");
if (language != NULL && language[0] != '\0')
return language;
...
return locale;
}

gl_locale_name_posix関数

ファイル(localename.c)はlibintl.h/gnuliblintl.hをインクルードせず、setlocaleはmsvcrtランタイムとなる。従ってLC_MESSAGESカテゴリ以外だけsetlocale(category,NULL)をコールしウィンドウズ準拠名を返せばPOSIX準拠名に変換して返す。LC_MESSAGESの場合またはsetlocaleがウィンドウ準拠名を返さなかった場合、LANGあるいはLC_XXX環境変数から取得する。これがウィンドウズ準拠名ならPOSIX準拠名に変換して返し、そうでなければそのままで返す。取得できなければ取得失敗としてNULLを返す。

LC_MESSAGESをsetlocale(LC_MESSAGES,NULL)を使わず常に環境変数から取得する理由はソースコードコメントに示されていて、クライアントがlibintl.hをインクルードしない場合の備えだそうだ。setlocaleにLC_MESSAGESを加える苦労は結局何の意味も無かったのか。

const char *
gl_locale_name_posix (int category, const char *categoryname)
{
if (LC_MIN <= category && category <= LC_MAX)
{ // LC_MESSAGES以外はsetlocale(category,NULL)を試みる
const char *locname = setlocale (category, NULL);
LCID lcid = get_lcid (locname);
if (lcid > 0)
// ウィンドウズ準拠名ならPOSIX準拠名へ変換して返す
return gl_locale_name_from_win32_LCID (lcid);
}
{ // LC_MESSAGESは常に環境変数から取得を試みる
const char *locname;
locname = gl_locale_name_environ (category, categoryname);
// 環境変数の優先順位はLC_ALL、LC_XXX、LANG
if (locname != NULL)
{
LCID lcid = get_lcid (locname);
if (lcid > 0)
// ウィンドウズ準拠名ならPOSIX準拠名へ変換して返す
return gl_locale_name_from_win32_LCID (lcid);
}
return locname;
}
}

gl_locale_name_default関数

デフォルトロケールはウィンドウズAPI関数GetThreadLocaleで得られるロケールをPOSIX準拠名に変換したものとする。

const char *
gl_locale_name_default (void)
{
{
LCID lcid;
lcid = GetThreadLocale ();
return gl_locale_name_from_win32_LCID (lcid);
}
}

出力コードページへの変換

_nl_find_msg関数(dcigettext.c:945)は検索文字列に対応する翻訳文字列を出力コードページ(整数値)に変換して返す。_nl_find_msgはget_output_charset関数(dcigettextc:1669)をコールする。get_output_charsetは翻訳文字列を取得して出力コードページに変換する。デフォルトの出力コードページはlocale_charset関数(localecharset.c:827)で取得する。

_nl_find_msg関数

検索文字列から翻訳文字列を取得する部分は全て割愛し、翻訳文字列を出力コードページに変換する部分だけを説明する。変換後の翻訳文字列をテーブル保存して再利用する部分や複数形処理も省略した。iconvライブラリを利用するため出力コードページからiconv準拠の文字コードを取得し、iconv関数で翻訳ファイルの文字コードから変換する。出力文字コードの取得はget_output_charset関数が行う。

char *
_nl_find_msg (struct loaded_l10nfile *domain_file,
struct binding *domainbinding,
const char *msgid, int convert,
size_t *lengthp)
{
struct loaded_domain *domain; // ロードした翻訳文字列データ
char *result; // 文字コード変換前の翻訳文字列
...
domain = (struct loaded_domain *) domain_file->data;
result = ... // msgidからresultを取得
...
if (convert)
{
...
const char *encoding = get_output_charset (domainbinding);
// 出力文字コード
struct converted_domain *convd; // 文字コード変換後の翻訳文字列データで
// iconv変換ディスクリプタと変換後翻訳文字列のテーブルを持つ
convd = ... // domainからconvdの取得を試みる
...
if (convd == NULL) // convdが未取得の場合
{
...
convd = ... // convdを作成
convd->conv = (iconv_t) -1; // 変換ディスクリプタを不正値に初期設定
...
if (...) // 翻訳ファイルにヘッダエントリが存在する場合
{
...
if (...) // ヘッダエントリが文字コードを含む場合
{
...
char *charset; // 翻訳ファイル文字コード
...
charset = ... // ヘッダエントリから文字コード取得
convd->conv = iconv_open (encoding, charset);
// charsetからencodingへのiconv変換ディスクリプタ
...
}
}
}
if (convd->conv != (iconv_t) -1) // convd->convが不正でない場合
{
...
if (...) // 変換文字列がconvdのテーブルに見つからない場合
{
...
const unsigned char *inbuf; // iconv関数入力バッファへのポインタ
unsigned char *outbuf; // iconv関数出力バッファへのポインタ
...
inbuf = (const unsigned char *) result;
outbuf = ... // convdのテーブルに追加した新エントリへのポインタ
while (1) // 全文字列変換までメモリ操作しながら繰り返す
{
...
const char *inptr = (const char *) inbuf;
size_t inleft = ...
char *outptr = (char *) outbuf;
size_t outleft;
...
if (iconv (convd->conv, &inptr, &inleft, &outptr, &outleft)
!= (size_t) (-1)) // iconv関数コールして全文字列変換ならループブレーク
{
outbuf = (unsigned char *) outptr;
break;
}
...
}
}
result = ... // 変換後翻訳文字列を指すconvdテーブルのポインタ
...
}
}
...
return result;
}

get_output_charset関数

get_output_charsetは_nl_find_msgからコールされて以下の優先順位で出力文字コードを返す。

  1. bind_textdomain_codesetで設定された文字コード
  2. OUTPUT_CHARSET環境変数から得られる文字コード
  3. locale_charset関数から得られるロケール文字コード

OUTPUT_CHARSET環境変数から得る文字コードは1度だけ取得してローカルスタティックに保持する。なおbind_textdomain_codesetやOUTPUT_CHARSETから得るとLC_CTYPEロケールとの一致が保証されずコマンドプロンプト出力が文字化けする可能性がある。

static const char *
get_output_charset (struct binding *domainbinding)
{
if (domainbinding != NULL && domainbinding->codeset != NULL)
return domainbinding->codeset;
else
{
static char *output_charset_cache;
static int output_charset_cached;
if (!output_charset_cached)
{
const char *value = getenv ("OUTPUT_CHARSET");
if (value != NULL && value[0] != '\0')
{
size_t len = strlen (value) + 1;
char *value_copy = (char *) malloc (len);
if (value_copy != NULL)
memcpy (value_copy, value, len);
output_charset_cache = value_copy;
}
output_charset_cached = 1;
}
if (output_charset_cache != NULL)
return output_charset_cache;
else
{
return locale_charset ();
}
}
}

locale_charset関数

ファイル(localecharset.c)はlibintl.h/gnuliblintl.hをインクルードせず、setlocaleはmsvcrtランタイムとなる。LC_CTYPEロケールが"C"でないウィンドウズ準拠名であればコードページ文字列を取得してCP前置した出力文字コードとする。これが取得できない場合はウィンドウズAPI関数のGetACPでシステムロケールのコードページを取得してCP前置した出力文字コードとする。これがiconv準拠でない場合はさらに準拠名へ変換するが、その部分は省いた。

const char *
locale_charset (void)
{
const char *codeset;
char buf[2 + 10 + 1];
static char resultbuf[2 + 10 + 1];
char *current_locale = setlocale (LC_CTYPE, NULL);
char *pdot = strrchr (current_locale, '.');
if (pdot && 2 + strlen (pdot + 1) + 1 <= sizeof (buf))
sprintf (buf, "CP%s", pdot + 1);
else
{
sprintf (buf, "CP%u", GetACP ());
}
if (strcmp (buf + 2, "65001") == 0 || strcmp (buf + 2, "utf8") == 0)
codeset = "UTF-8";
else
{
strcpy (resultbuf, buf);
codeset = resultbuf;
}
...
return codeset;
}

ウィンドウズAPI関数

libintlのmingw-w64実装は二箇所でウィンドウズAPI関数をコールする。一つは_nl_locale_name_default(またはgl_locale_name_default)のGetThreadLocale関数で、もう一つはlocale_charsetのGetACP関数である。それぞれオペレーティングシステムから設定を取得する。

GetThreadLocaleはスレッドのウィンドウズ設定ロケールを返し、libintlはLANGなどの環境変数が設定されていない時にこれをロケールとして取得する。この値はSetThreadLocale関数で設定できる。SetThreadLocaleドキュメントは初期値をユーザーデフォルトロケールとするが、そうでないケースが存在する。例えばシステムロケール英語(米国)であれば初期値はユーザーデフォルトロケールだが、日本語(日本)であればシステムロケールとなる。GetThreadLocale/SetThreadLocaleは言語コード識別子(LCID)値でロケールを扱うもののLCIDは非推奨となってリリース間で安定しない。GetThreadLocaleが意図通りの値を間違いなく返すには事前にSetThreadLocaleをコールする。SetThreadLocale(LOCALE_USER_DEFAULT)をコールすればGetThreadLocaleは間違いなくユーザーデフォルトロケールを返し、SetThreadLocale(LOCALE_SYSTEM_DEFAULT)をコールすればシステムロケールを返す。

GetACPはシステムロケールのコードページ(整数値)を返し、これはコードページ(手法)を採るデスクトップアプリケーションのコードページ(整数値)(いわゆる"ANSI"コードページ)であるが、libintlはこれを翻訳文字列の出力文字コードとして取得する。ところでコマンドプロンプトのデフォルトコードページ(いわゆる"OEM"コードページ)を返すのはGetOEMCP関数で、コンソールアプリケーションは主にコマンドプロンプトへ出力する。なおコマンドプロンプトのコードページはCHCPコマンドで任意に変更できる。日本語(日本)で"ANSI"コードページと"OEM"コードページは等しい(932)が、例えば英語(米国)で異なり(1252と437)ASCII外で同じ文字に対応しない。

コマンドプロンプトへの出力

mingw-w64でのlibintlによる翻訳にsetlocale(LC_MESSAGES,...)は寄与せずコールの必要は無い事を述べてきた。ここからはsetlocale(LC_CTYPE,...)がシフトJIS文字列のコマンドプロンプト出力を阻害する問題を議論する。この問題はlibintlと直接関係しないがGNU gettextドキュメントを盲信してsetlocale(LC_ALL,"")すると日本語の翻訳文字列がコマンドプロンプトに出力されない。つまりドキュメントに反しsetlocale(LC_ALL,"")はコールすべきでないが、別の理由でsetlocale(LC_CTYPE,...)を必要とするかもしれない。登場人物はC++標準ライブラリ(GCC libstdc++)、libintlライブラリ(GNU gettext)、C言語標準ライブラリ(msvcrtランタイム)、ウィンドウズAPI関数で、全員が2バイト文字コードにまともな対応をしてこなかった結果のようだ。勇気を出してこの沼に飛び込もう。

以下、断らない限り日本語環境でのコマンドプロンプト出力を前提とする。問題はlibintlの翻訳と無関係なのでサンプルコードに日本語文字列を直接書き込む。コンパイルオプションに-finput-charset=ソースコード物理ファイル文字コード(CP932あるいはUTF-8)と-fexec-charset=CP932を加える。libintl.hをインクルードする場合は適切なライブラリファイル(例えばlibintl.a)を追加する。

問題の再現

これ以上ありえないシンプルなソースコードにsetlocale(LC_CTYPE,"")しただけで出力は失敗する。

#include <iostream>
#include <locale.h>
int main()
{
setlocale(LC_CTYPE,"");
std::cout<<"こんにちは世界!"<<std::endl;
return 0;
}

ASCII文字列、C言語標準ライブラリprintf関数、libintl.hインクルード、sync_with_stdio(false)コールをを検討水準に加える。coutが出力エラーとなる場合に備え都度clearメンバ関数をコールしている。

#include <iostream>
#include <locale.h>
char str1[]="Hello";
char str2[]="こんにちは";
char str3[]="xこんにちは";
#define MY_TEST_1() \
{ \
printf("\n*** %s ***\n",__func__); \
printf("\nprintf(str1):");printf("%s",str1); \
printf("\nprintf(str2):");printf("%s",str2); \
printf("\nptintf(str3):");printf("%s",str3); \
std::cout<<"\ncout<<str1 :"<<str1;std::cout.clear(); \
std::cout<<"\ncout<<str2 :"<<str2;std::cout.clear(); \
std::cout<<"\ncout<<str3 :"<<str3;std::cout.clear(); \
std::cout<<std::endl; \
}
void without_libintl_h() {MY_TEST_1();}
#include <libintl.h>
void with_libintl_h() {MY_TEST_1();}
void after_sync_with_stdio_false()
{
std::ios_base::sync_with_stdio(false);
MY_TEST_1();
}
int main()
{
setlocale(LC_CTYPE,"");
without_libintl_h();
with_libintl_h();
after_sync_with_stdio_false();
return 0;
}

結果をまとめる。全てにおいてASCII(あるいはANK)文字列は問題ないが、シフトJIS文字列の結果は混乱する。シフトJIS文字列出力に失敗しても先頭にASCII(ANK)を追加すれば成功する場合がある。printfはlibintl.hインクルード有無で失敗パターンが異なる。この段階でシフトJIS文字列が常に成功するのはsync_with_stdio(false)コール後のC++標準出力coutだけのようだ。

関数 条件 文字列 出力 結果 実装
printf libintl.hインクルードしない Hello Hello 成功 fputcループ(推測)
こんにちは アノソヘ 失敗、文字化け
xこんにちは xアノソヘ 失敗、文字化け
libintl.hインクルードする Hello Hello 成功 fwrite
こんにちは (エラー) 失敗、出力エラー
xこんにちは xこんにちは 成功
cout sync_with_stdio(false)コールしない Hello Hello 成功
こんにちは (エラー) 失敗、出力エラー
xこんにちは xこんにちは 成功
sync_with_stdio(false)コールする Hello Hello 成功 write
こんにちは こんにちは 成功
xこんにちは xこんにちは 成功

libintl.hはprintfをlibintl_printf(printf.c:153)に#defineする。これはlibintl_vprintfを経由してlibintl_vfprintf(printf.c:104)をコールしてC言語標準ライブラリfwrite関数(N1570 7.21.8.2)で出力する。sync_with_stdio(false)はcoutのストリームバッファを切り替える。コールしなければstdio_sync_filebufクラス([MSYS2インクルード]/c++/[mingw-w64バージョン番号]/ext/stdio_sync_filebuf.h)、コールすればstdio_filebufクラス(.../ext/stdio_filebuf.h)を用いる。stdio_sync_filebufはC言語標準出力とバッファを共有し、バッファ書き込み(xsputnメンバ関数)はfwriteで出力し、バッファフラッシュ(overflowメンバ関数)はC言語標準出力をフラッシュする。stdio_filebufはbasic_filebufクラス(.../bits/fstream.tcc)を継承しxsputnは自ら所有するバッファへ書き込み、overflowは_M_convert_to_externalメンバ関数を経由して__basic_fileクラス(.../bits/basic_file.h)のxsputnメンバ関数がPOSIXのwrite関数(IEEE 1003.1-2017 pwrite)で直接出力する。writeはユニックスライクではシステムコールだがウィンドウズはmsvcrtランタイムが供給する。なお__basic_file::xsputnはソースコードファイル([libstdc++ソースコード]/basic_file.cc)が実装するため調査はリンク先で行った。

C言語標準ライブラリfwrite関数とPOSIXのwrite関数の違いの一つは、前者がC言語標準出力バッファへ書き込むのに対し後者は出力先へ直接書き込む。stdio_filebufクラスは自前のバッファを所有してC言語バッファをバイパスする必要があり、これを理由としてfwriteでなくwriteを使用する。

libintl.hをインクルードしないprintfはmsvcrtランタイムでソースコードの調査手段が無くテストコードで推測するが、C言語標準ライブラリで1符号ずつ出力すれば同様の文字化けが発生する。本サイトはfputc関数(N1570 7.21.7.3)ループを仮定するもののfwrite関数ループなど他の可能性も考えられる。

int fputc_loop(const char* str,size_t len,FILE *stream)
{
for (const char* ch=str;ch<str+len;++ch)
{
fputc(*ch,stream); // No error check
//fwrite(ch,1,1,stream); // Another possible implementation
}
return 0;
}

コマンドプロンプトコードページへの変換

msvcrtランタイムのコマンドプロンプト出力はLC_CTYPEロケールが"C"でないと文字列をLC_CTYPEロケールに対応するコードページ(整数値)からコマンドプロンプトのコードページへ変換して出力する。

サンプルコードは英語(米国)"OEM"コードページ437と"ANSI"コードページ1252それぞれの文字列"Ää"の出力を試す。TestWithCP関数は指定したコードページ(例えば437)からウィンドウズ準拠のロケール名(".437")を生成してsetlocale(LC_CTYPE,...)してprintfをコールする。printfはこれに従うコードページで出力し、同じコードページの"Ää"は正しく表示して違う場合は文字化けする。CHCPでコマンドプロンプトのコードページを変更しても結果に影響しない。ただし日本語932など字形を持たないコードページは"Aa"で代替表示する。

msvcrtランタイムなのでコードページ変換の実装をソースコードで確認できない。考えられるのはユニコード(UTF-16)文字列経由でウィンドウズAPIのMultiByteToWideChar/WideCharToMultiByte関数を使う。WriteConsoleWithCP関数はそれを試しロケール設定したprintfと等しい出力を得る。

#include <stdio.h>
#include <locale.h>
#include <windows.h>
const char str1[]="<A-umrat><a-umrat> in CP437 :" "\x8E\x84";
const char str2[]="<A-umrat><a-umrat> in CP1252:" "\xC4\xE4";
void WriteConsoleWithCP(UINT cp,const char* str)
{
wchar_t buf[256];
char out[256]="WriteConsoleWithCP failed.\n";
if (::MultiByteToWideChar(cp,0,str,-1,buf,sizeof(buf)/sizeof(wchar_t)))
{
if (int len=::WideCharToMultiByte
(::GetConsoleOutputCP(),0,buf,-1,out,sizeof(out)-1,NULL,NULL))
{
out[len-1]='\n';out[len]='\0';
}
}
::WriteConsoleA(::GetStdHandle(STD_OUTPUT_HANDLE),out,strlen(out),NULL,NULL);
}
void TestWithCP(UINT cp)
{
printf("\nConsole/Data:CP%d/CP%d\n",::GetConsoleOutputCP(),cp);
char locname[256];
sprintf(locname,".%d",cp);
setlocale(LC_CTYPE,locname);
printf("%s\n",str1);
printf("%s\n",str2);
WriteConsoleWithCP(cp,str1);
WriteConsoleWithCP(cp,str2);
}
int main()
{
TestWithCP(437);
TestWithCP(1252);
return 0;
}

もちろん我々の問題は出力文字列もコマンドプロンプトもコードページ932で変換不要だが、それでもLC_CTYPEロケールが"C"でなければ変換処理が行われる。

バッファリング

プログラム起動時、対話装置を指す標準出力へのC言語標準ライブラリによる出力(C言語標準出力)はバッファリングしない(JTC1/SC22/WG14 N1570 7.21.3/p7)。標準出力へのC++標準ライブラリによる出力(C++標準出力)はC言語標準出力との同期をデフォルトとする(JTC1/SC22/WG21 N4659 30.4.2/p5)。つまり規格はコマンドプロンプトへのC言語/C++標準出力はバッファリングしない事をデフォルトとする。サンプルコードでこれを確認する。putc_by_posix_write関数はPOSIXのwrite関数、putc_by_winapi_writeconsoleはウィンドウズAPIのWriteConsoleA関数で出力する。writeとWriteConsoleAはC言語標準ライブラリに属さないのでバッファリングせず直接出力する。コマンドプロンプト出力はコード順を維持し、C言語/C++標準出力への8ビット符号/16ビット符号文字列出力もバッファリングせず規格通りである事を確認する。

#include <iostream>
#include <io.h> // write
#include <windows.h> // WriteConsoleA
void putc_by_posix_write(int c)
{
const char str[]={(char)c,0};
write(STDOUT_FILENO,str,strlen(str));
}
void putc_by_winapi_writeconsole(int c)
{
const char str[]={(char)c,0};
::WriteConsoleA(::GetStdHandle(STD_OUTPUT_HANDLE),str,strlen(str),NULL,NULL);
}
int main()
{
//setvbuf(stdout,NULL,_IOLBF,256);// In win32 _IOLBF is the same as _IOFBF
//std::ios_base::sync_with_stdio(false);
for (auto i=0;i<5;++i)
{
printf("%c",'1');
std::cout<<'2';
wprintf(L"%c",L'3');
std::wcout<<L'4';
putc_by_posix_write('5');
putc_by_winapi_writeconsole('6');
}
fflush(stdout);
std::cout<<std::flush;
std::wcout<<std::flush;
return 0;
}

setvbuf(stdout,...)(N1570 7.21.5.6)をアンコメントするとC言語/C++標準出力はバッファリングして、ループ内putc_by_posix_write/putc_by_winapi_writeconsole出力の後にfflush(stdout)で残りをバッファからフラッシュする。さらにsync_with_stdio(false)をアンコメントするとC++標準出力バッファはC言語標準出力バッファから独立し、putc_by_posix_write/putc_by_winapi_writeconsole出力、C言語標準バッファフラッシュの後にフラッシュする。C言語標準出力バッファは8ビット符号文字/16ビット符号文字で共通(8ビット符号文字で保持)だが、C++標準出力バッファはそれぞれ別のバッファを持つ。

問題の考察

LANGなどの環境変数が設定されていない日本語環境でsetlocale(LC_ALL,"")すればLC_CTYPEロケールは"Japanese_Japan.932"となる。出力文字列コードページ(整数値)はこれで932を選択するがそもそも日本語環境デフォルトなので値が変更されるわけではない。重要なのはLC_CTYPEロケールが"C"から"Japanese_Japan.932"に変更される事で、"C"以外となってコードページ変換を行う。日本語環境コマンドプロンプトコードページも932なので二つのコードページは等しく変換無用だがmsvcrtランタイムは冗長無害と見なして変換処理する。これは1バイト文字コードなら正しいがシフトJISなどの2バイト文字コードで問題となる。

コードページ変換はコマンドプロンプトへ書き込む時に実行する。C言語/C++標準出力がバッファリングせず1バイト出力すれば、その1バイトでコードページ変換する。それがシフトJIS第一符号なら変換失敗し、第二符号でたまたま1符号文字(ANK)に一致すれば変換成功する。fputc関数ループはエラーチェック無しでこれを繰り返しANKへの文字化けとなる。fwrite関数は文字列先頭バイトが第一符号で変換失敗するとエラーと見なし全文字列の出力を中止する。だたし文字列先頭に1符号文字を置けば2符号文字が続いても失敗とならず、まるで先頭バイト(または第二仮引数単位要素)と残りバイト(要素)列を別々に出力するかの振る舞いで理由が全く解らない。POSIXのwrite関数はこの問題を持たずバッファリングも無関係でシフトJIS出力に成功する。

setvbuf(stdout,...)でC言語/C++標準出力をバッファリングすれば1バイト出力も後続バイト列と共に書き込まれて、fputc関数ループとfwrite関数もシフトJIS出力に成功する。問題の再現コードにこれを追加すれば全ての出力に成功する。ただしsync_with_stdio(false)した後はC++標準出力バッファがC言語標準バッファから独立して出力順がコード順と一致しない。

...
int main()
{
setvbuf(stdout,NULL,_IOLBF,256);// In win32 _IOLBF is the same as _IOFBF
setlocale(LC_CTYPE,"");
without_libintl_h();
with_libintl_h();
after_sync_with_stdio_false();
return 0;
}

libintlとコマンドプロンプト出力をいかに両立するか

libintlとコマンドプロンプト出力を両立させる方法をまとめる。既に述べたが問題はLC_CTYPEロケールとコマンドプロンプトとの関係で、これはlibintlに限定する話ではない。翻訳のロケールはSetThreadLocale関数で設定するものとして必要なら環境変数でオーバーライドするが、ここでの議論に関係しない。setlocaleをコールしないのが最も単純で本サイトはこれを強く勧めるものの、他の理由でコールする場合がありその対処も示す。

  • setlocaleしない。
  • setlocaleして標準出力バッファリングする。
  • setlocaleしてC++標準入出力を非同期とする。

setlocaleしない

GNU gettextドキュメントが推奨するsetlocale(LC_ALL,"")をコールしない。より正確にはLC_CTYPEロケールを"C"に限定する。

  • 最も単純でリアルタイム出力を維持する。
  • LC_CTYPEロケールを"C"以外とする場合に使えない。

setlocaleして標準出力バッファリングする

setlocale(LC_ALL,"")をコールする場合、より正確にはLC_CTYPEロケールを"C"以外とする場合の第一の方法を示す。setvbuf(stdio,...)で標準出力をバッファリングする。

  • C言語標準出力とC++標準出力の両方に対応する。
  • リアルタイム出力とならず必要ならフラッシュする。

setlocaleしてC++標準入出力を非同期とする

setlocale(LC_ALL,"")をコールする場合、より正確にはLC_CTYPEロケールを"C"以外とする場合の第二の方法を示す。sync_with_stdio(false)でC++標準入出力をC言語標準入出力と非同期で行う。

  • C++標準出力のみに対応する。
  • リアルタイム出力とならず必要ならフラッシュする。

ところでC++標準出力でUTF-16からシステムロケールに対応する文字コードへ変換するにはsetlocale(LC_CTYPE,"")するが、その際もsync_with_stdio(false)が必要になる。つまり日本語環境のC++標準出力で8ビット文字列出力にシフトJIS、16ビット文字列出力にUTF-16を用いるには、setlocale(LC_CTYPE,"")とsync_with_stdio(false)をコールすれば良い。これは偶然の結果だろうか。

#include <iostream>
#include <locale.h>
int main()
{
setlocale(LC_CTYPE,"");
std::ios_base::sync_with_stdio(false);
std::cout<<"こんにちは世界!(シフトJIS)"<<std::endl;
std::wcout<<L"こんにちは世界!(ユニコードUTF-16)"<<std::endl;
return 0;
}