home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

C言語系/memos/VC++/03, UNICODE対応とtchar.h

C言語系/memos/VC++/03, UNICODE対応とtchar.h

C言語系 / memos / VC++ / 03, UNICODE対応とtchar.h
id: 586 所有者: msakamoto-sf    作成日: 2010-02-12 13:46:27
カテゴリ: C言語 Windows 

Visual C++ 2008 Expressn Edition でのC言語のUNICODEサポートについて、Win32コンソールアプリを題材に簡単にまとめる。

※詳細はMSDNを参照して下さい。この記事で題材にしている非UNICODEアプリをUNICODE対応に変更する手順なども、あくまでもサンプルです。MSDNを一次情報として参照して下さい。この記事はあくまでもUNICODE対応を確認する為のメモや実験のまとめで、MSDNの一次情報を代替しません。また、C++/CLR/.NETについてはこの記事では扱いません。

参考MSDN(Express Edition):

  • 「Visual C++」→「一般的なプログラミング手順」→「文字セット」
    • →「Unicodeのサポート」→「wmainの使用」
    • →「Tchar.h における汎用テキストのマッピング」
  • 「Visual C++」→「リファレンス」→「ライブラリリファレンス」→「カテゴリ別ランタイムルーチン」
    • →「入出力」→「テキスト モードとバイナリ モードの Unicode ストリーム入出力」
    • →「国際化」→「Unicode: ワイド文字セット」
    • →「国際化」→「汎用テキスト マップの使用」
  • 「Visual C++」→「リファレンス」→「ライブラリリファレンス」→「汎用テキスト マップ」


tchar.h と "/D UNICODE", "/D _UNICODE"

Visual C++ 2008 Express Edition のプロジェクトの新規作成で、ウィザードを使用してWin32プロジェクトを作成すると、デフォルトで次の"/D"オプションがコンパイラオプションに含まれる。

/D "_UNICODE" /D "UNICODE"

これはWin32 アプリケーション ウィザードの [アプリケーションの設定] ページ、[アプリケーションの種類] で何を選んだかに依らない。コンソール/Windows/DLL/スタティックライブラリのいずれの場合も、上記コンパイラオプションがデフォルトで有効になっている。

この定義により、UNICODE対応をサポートする為に Tchar.h が提供しているマクロが使用可能になる。
また、コンソールとWindowsアプリケーションのテンプレートを見ると、"tchar.h"がインクルードされ、エントリポイントも次のように変化している。

コンソール:

int _tmain(int argc, _TCHAR* argv[])

Windows:

int APIENTRY _tWinMain(HINSTANCE hInstance,
                    HINSTANCE hPrevInstance,
                    LPTSTR    lpCmdLine,
                    int       nCmdShow)

"_tmain", "_TCHAR", "_tWinMain", "LPTSTR"は Tchar.h により、UNICODE用の適切なエントリポイントや型名に置換される。これらのマクロを活用することで、UNICODE/MBCS/ASCIIを"#define"だけで切り替えることができるようになる。

逆にこの状態で

printf()

のようにASCII用の関数を直接呼ぶと、他はUNICODE/MBCSで処理しているのにそこだけASCIIとして処理する為、UNICODEではNULL文字(\00)が含まれる場合もあるため、おかしな結果になってしまう。

非UNICODE版Win32コンソールアプリケーションをUNICODE対応させてみるサンプル

次のWin32コンソールアプリケーションをUNICODE対応させてみる。
chartest01.c : ソースコード自体はCP932(Windows-31J)で保存する

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {
	int i = 0;
	for (i = 0; i < argc; i++) {

		printf("argv[%d] = %s\n", i, argv[i]);

		if (!strcmp("foo", argv[i])) {
			printf("foo!\n");
		}

		if (!strcmp("あああ", argv[i])) {
			printf("あいうえお!\n");
		}
	}
	return 0;
}

とりあえずコンパイル&実行してみる:

> cl /nologo chartest01.c
chartest01.c

> chartest01 a foo bar foobar あああ
argv[0] = chartest01
argv[1] = a
argv[2] = foo
foo!
argv[3] = bar
argv[4] = foobar
argv[5] = あああ
あいうえお!

tchar.hの追加とエントリポイント、データ型の修正

  1. tchar.hをincludeする。
  2. エントリポイントをUNICODE用の"wmain()"関数に変更するが、ここではtchar.hによる汎用テキストマッピング機能を使い、"_tmain()"に修正する。
  3. char, intのデータ型を wchar_t, wint_t に変更するが、同様にtchar.hの汎用テキストマッピング機能を使い、"_TCHAR", "_TINT" に修正する。

修正後:chartest02.c

#include <stdio.h>
#include <string.h>
#include <tchar.h>

int _tmain(int argc, _TCHAR *argv[]) {
(以下同じ)

まず"UNICODE"定義無しでコンパイル&実行してみる:

> cl /nologo chartest02.c
chartest02.c

> chartest02 a foo bar foobar あああ
argv[0] = chartest02
argv[1] = a
argv[2] = foo
foo!
argv[3] = bar
argv[4] = foobar
argv[5] = あああ
あいうえお!

プリプロセス結果を見てみると、前と変わっていない事が分かる:

> cl /nologo /P chartest02.c
chartest02.c

> type chartest02.i
(...)
typedef char            _TCHAR;
(...)
int main(int argc, _TCHAR *argv[]) {
(...)

では、"UNICODE", "_UNICODE"定義付でコンパイル&実行してみる:

> cl /D "UNICODE" /D "_UNICODE" /nologo chartest02.c
chartest02.c
chartest02.c(9) : warning C4133: \
                  '関数' : '_TCHAR *' と 'const char *' の間で型に互換性がありません。
chartest02.c(12) : warning C4133: \
                   '関数' : '_TCHAR *' と 'const char *' の間で型に互換性がありません。

> chartest02 a foo bar foobar あああ
argv[0] = c
argv[1] = a
argv[2] = f
argv[3] = b
argv[4] = f
argv[5] = B0B0B0

9行/12行で発生しているのはstrcmp()にargvを渡している箇所で、argvがwchar_tになったためと思われる。
実行結果だが、argv[0]が"c"で終わっている。これはargv[0]がUNICODEになったため、"chartest02.exe"が実際には

c \00 h \00 a \00 ...

となり、cの次のNULLバイトが文字列終端と見なされたことが原因だろう。argv[5]も文字セットの処理がずれてしまっているため「文字化け」を興してしまっている。さらにstrcmp()もUNICODEになった文字列との比較で、「foo」や「あああ」が不一致になってしまっている。

"_T()"マクロの適用+汎用テキストのマッピング用の関数に修正する

  1. リテラル文字列を"_T()"マクロで囲む。
  2. printf(), strcmp()を汎用テキストのルーチン マップの関数に修正する。
    1. printf()は"_tprintf()"に対応するが、書式文字列などセキュリティ強化された"_tprintf_s()"に直す。
    2. strcmp()は"_tcscmp()"に修正する。

修正後:chartest03.c:

#include <stdio.h>
#include <string.h>
#include <tchar.h>

int _tmain(int argc, _TCHAR *argv[]) {
	int i = 0;
	for (i = 0; i < argc; i++) {

		_tprintf_s(_T("argv[%d] = %s\n"), i, argv[i]);

		if (!_tcscmp(_T("foo"), argv[i])) {
			_tprintf_s(_T("foo!\n"));
		}

		if (!_tcscmp(_T("あああ"), argv[i])) {
			_tprintf_s(_T("あいうえお!\n"));
		}
	}
	return 0;
}

コンパイル&実行:

> cl /D "UNICODE" /D "_UNICODE" /nologo chartest03.c
chartest03.c

> chartest03 a foo bar foobar あああ
argv[0] = chartest03
argv[1] = a
argv[2] = foo
foo!
argv[3] = bar
argv[4] = foobar
argv[5] = ???

日本語を除く箇所についてはうまく動き出した。
日本語の問題についてはひとまず置いておき、プリプロセス結果を確認してみる:

> cl /D "UNICODE" /D "_UNICODE" /P /nologo chartest03.c
chartest03.c

> chartest03.i
(...)
typedef wchar_t     _TCHAR;
(...)
int wmain(int argc, _TCHAR *argv[]) {
	int i = 0;
	for (i = 0; i < argc; i++) {

		wprintf_s(L"argv[%d] = %s\n", i, argv[i]);

		if (!wcscmp(L"foo", argv[i])) {
			wprintf_s(L"foo!\n");
		}

		if (!wcscmp(L"あああ", argv[i])) {
			wprintf_s(L"あいうえお!\n");
		}
	}
	return 0;
}

それぞれ適切なUNICODE用関数に置換されていることが確認できた。

ストリーム入出力関数とUNICODEとロケール対応

printf()はストリーム入出力関数に分類される。関連する関数の説明は、以下のMSDNから辿ることができる。

「Visual C++」→「リファレンス」→「ライブラリリファレンス」→
  「カテゴリ別ランタイムルーチン」→「入出力」

この中の「テキスト モードとバイナリ モードの Unicode ストリーム入出力」では、テキストモードで開かれたファイルに対してUNICODE対応のストリーム入出力関数を使うと、mbtowc()/wctomb()が呼び出されたかのようにしてUNICODEとマルチバイト文字セットへの変換が行われるという記述が見つかった。
今回はwprintf_s()を使っている、出力部分でおかしくなっている。そこで、UNICODE→マルチバイト文字セットへの変換について調べる為wctomb()(関数名から、UNICODE:ワイド文字列からマルチバイト文字セットへ変換する関数だろうと"アタリ"をつけた)のMSDNを確認してみる。
すると次のような記述が見つかった:

wctomb 関数は、すべてのロケールに依存する動作に現在のロケールを使用します。

ここから、適切なロケールが設定されていない為に wctomb() が "???" に変換しているのではないか、と推測される。

ロケール関連のMSDNを調べると、次のような記述が見つかった:

  • 「setlocale、_wsetlocale」:
プログラムの起動時に、次のステートメントと同等の処理が実行されます。

setlocale( LC_ALL, "C" );
  • 「カテゴリ別ランタイム ルーチン」→「国際化」→「コード ページ」:
既定では、Microsoft ランタイム ライブラリにあるロケール依存のルーチンはすべて、
"C" ロケールに対応するコード ページを使用します。

ここから、プログラム起動時はデフォルトで"C"ロケールになっており、これを日本語用のロケールに変更すればwctomb()が適切に動作すると予想される。

そこで、まずsetlocale()で「プログラム起動時のロケール」を確認するテストプログラムを作ってみる。

getlocale.c:

#include <stdio.h>
#include <locale.h>
#include <mbctype.h>

int main() {
	char *l;
	int cp;

	/* プログラム起動直後のロケールを取得 */
	l = setlocale(LC_ALL, NULL);
	if (NULL == l) {
		printf("error.\n");
		return 1;
	} else {
		printf("LC_ALL = %s\n", l);
	}
	/* プログラム起動直後のコードページを取得 */
	cp = _getmbcp();
	printf("CodePage = %d\n", cp);

	/* ロケールを変更
	 * - 言語:日本
	 * - 国:日本
	 * - コードページはシステムデフォルト
	 */
	l = setlocale(LC_ALL, "japanese_jpn");
	if (NULL == l) {
		printf("error.\n");
		return 1;
	} else {
		printf("LC_ALL = %s\n", l);
	}

	/* ロケール変更後のコードページを取得 */
	cp = _getmbcp();
	printf("CodePage = %d\n", cp);
	return 0;
}

コンパイル&実行:

> cl getlocale.c
Microsoft(R) 32-bit C/C++ Optimizing Compiler Version 15.00.30729.01 for 80x86
Copyright (C) Microsoft Corporation.  All rights reserved.

getlocale.c
Microsoft (R) Incremental Linker Version 9.00.30729.01
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:getlocale.exe
getlocale.obj

> getlocale.exe
LC_ALL = C
CodePage = 932
LC_ALL = Japanese_Japan.932
CodePage = 932

コードページは932になっているが、ロカールはデフォルトで"C"になっていることが確認出来た。またsetlocale()により日本語ロケールに変更出来ることも確認出来た。

以上の確認を踏まえ、setlocale(LC_ALL, "japanese_jpn")を最初に呼ぶようにしてみる。また、"setlocale()"も汎用テキストマッピングの"_tsetlocale()"に変更する。

chartest04.c:(ソースファイルはWindows-31Jで保存されている)

#include <stdio.h>
#include <string.h>
#include <tchar.h>
#include <locale.h>

int _tmain(int argc, _TCHAR *argv[]) {
	int i = 0;

	if (NULL == _tsetlocale(LC_ALL, _T("japanese_jpn"))) {
		_tprintf_s(_T("error.\n"));
		return 1;
	}

	for (i = 0; i < argc; i++) {

		_tprintf_s(_T("argv[%d] = %s\n"), i, argv[i]);

		if (!_tcscmp(_T("foo"), argv[i])) {
			_tprintf_s(_T("foo!\n"));
		}

		if (!_tcscmp(_T("あああ"), argv[i])) {
			_tprintf_s(_T("あいうえお!\n"));
		}
	}
	return 0;
}

コンパイル&実行:

> cl /D "UNICODE" /D "_UNICODE" /nologo chartest04.c
chartest04.c

> chartest04 a foo bar foobar あああ
argv[0] = chartest04
argv[1] = a
argv[2] = foo
foo!
argv[3] = bar
argv[4] = foobar
argv[5] = あああ
あいうえお!

予想通り、ロケールを日本語に設定することで、UNICODE対応のストリーム入出力関数が正常にマルチバイト文字セットに変換できるようになった事を確認出来た。">"を使えば、Windows-31J文字コードのファイルとして文字化けすることなく保存される。

OSのロケールに合わせる場合は、setlocale()の第二引数を空文字列にする。

setlocale(LC_ALL, "");

日本語のWindowsの場合は、こちらでも問題ない。

"UNICODE"と"_UNICODE", "TCHAR"と"_TCHAR"

Win32APIおよびVC++で提供されるUNICODE機能を調べていくと、"UNICODE"と"_UNICODE", "TCHAR"と"_TCHAR"というように紛らわしいキーワードにぶつかる。これについては以下のように区別できる。

UNICODE, TCHAR
Win32API関連のヘッダーファイル経由
_UNICODE, _TCHAR
C RunTime library(CRT) ヘッダーファイル経由

従って、純粋にWin32API「だけ」で構成するのであれば"UNICODE"をdefineし、文字列型としては"TCHAR"を使えばよい。
"strlen"などCRTで提供されている関数を利用するのであれば"_UNICODE"をdefineし、文字列型としては"_TCHAR"を使う。
一般的なプログラミングではWin32APIとCRT両方を用いることになるので、"UNICODE", "_UNICODE"の両方をdefineした上で、TCHAR/_TCHARについては好きな方を使えばよい。
2010年5月現在のVC++2008 Express Edition上では、TCHAR/_TCHARとも、UNICODE/_UNICODEのdefine切り替えに応じてchar/wchar_tにそれぞれ切り替わるため、プリプロセス結果としてはTCHAR/_TCHARのどちらを使っても構わない。

注意点や参考Webページなど

VC++2008のUNICODE対応については、こちらでも丁寧にまとめられています:

コンソール出力関数を使うとsetlocale()無しでも問題ない・・・というのは自分も確認しました。"_tcprintf, _tcprintf_s"関数を使えば、setlocale()無しでも文字化けしません。ただし、やはり「コンソールのロケールがOSのロケールと一緒」という記載が見つからないんですよね・・・。コンソール出力関数のデフォルトロケールに依存するコードは避けた方が良いのかな・・・。

VC++2005の時の話題ですが、日本語PATH名やC++のIOストリームが絡むとややこしくなるようです:

VC6の時代からマルチバイト, UNICODE対応についてCのランタイムライブラリが相当変化しています。
MSDNの関数リファレンスだけから推測を積み重ねて実験を繰り返して確認するよりは、遠回りでも体系的に解説されたMSDNのページ群を確認して一通りUNICODE/マルチバイト文字セットの対応の仕組みを把握してからの方が、トラブルになった場合も「なぜこうなるのか」「なぜこの対応方法で回避出来るのか」を理解しやすくなると思います。



プレーンテキスト形式でダウンロード
現在のバージョン : 2
更新者: msakamoto-sf
更新日: 2010-05-18 20:36:08
md5:072a0df209a3fe111799c5d8e404a272
sha1:c1787c66d54c9dafad5e58a1b450812cc19f6080
コメント
コメントを投稿するにはログインして下さい。