D言語研究室



文字コードにまつわるエトセトラ

unicodeとマルチバイト文字列の変換 - Win9x系でstd.fileが動かない

unicodeとマルチバイト文字列の変換

D言語のソースには、マルチバイト文字(shift jis), UTF8 , UTF16(unicode)が使えます。D言語のソースを何で書くかというのは、意見の分かれるところでしょうけれど、ソース中のコメントなどに日本語を使うなら、shift jisではまずいことが多いです。例えば、


printf("表示");


このプログラムは、正しく表示されません。それというのも「表示」の2バイト目には'\'が使われていて、それをエスケープシーケンスとみなされてしまうからです。wysiwyg文字列を用いて、


printf(r"表示");


のように書けば正しく動作しますが、毎回'r'を指定するのは、どうも気持ち悪いです。「文字列はマジックナンバーの一種であって、コメント以外はソース中に埋め込むな」という意見もあります。私も、それには基本的には賛成ですが、ちょっとしたファイル名ぐらいは直接ソース上に書きたいと思うことがあります。

そう考えると、ソースは、UTF-8かUTF-16。どちらも似たようなものなのですが、DIDEのことを考えると、utf-8を用いたほうが良いです。utf-16では日本語コメントが化けて何やらわかりません。秀丸では、「その他→動作環境→編集→文字コードを自動認識する」にチェックを入れておけば、BOMのあるなしにかかわらずutf-8をチェックしてくれるので、utf-8を用いることをお勧めします。

テキストエディタで、保存するときにunicode形式で保存さえしてやれば、okです。

ちなみにUTF-8とUTF-16とは情報欠損なしに相互変換できます。簡単な変換フィルタで、UTF-8からUTF16、あるいはその逆に戻すことが出来ます。

std.utfにそれらの関数があります。

D言語では、char[]型は、UTF-8を仮定してあり、wchar型はUTF-16を仮定してあります。そこで、""によって囲まれた文字列は自動的にこの種の変換を受けることになります。

例)ソースコードをUTF-8やUTF-16で書いた場合。
char[] s = "漢字"; // s にはUTF-8に変換された文字列が入る
wchar[] s = "漢字"; // s にはUTF-16に変換された文字列が入る
実装までは追いかけてませんが、""で囲まれた文字列は、char[]とwchar[]にcastできる型で、castした瞬間に内容が確定するのでしょうか。文字列を関数の引数として渡すときに、
void foo(char[] s);
void foo(wchar[] s);
の二つの関数があった場合、
foo("漢字"); // 曖昧エラー
foo(cast(char[])"漢字"); // UTF-8に変換された文字列でfoo(char[])が呼び出される
foo(cast(wchar[])"漢字"); // UTF-16に変換された文字列でfoo(wchar[])が呼び出される

となります。この文字列のコード自動変換は、なかなか便利な機能だとは思います。

ところが、unicodeでソースを書くと、printf等はmbs(マルチバイト文字列)を渡されると仮定してあるので、正しく表示されなくなります。これは、本来は、言語側が変換をやってくれないといけない処理だとは思うのですが、呼び出されないものは仕方ありません。

必要に応じてunicodeからmbsに変換して使うことにします。つまりは、以下のようにphobosを呼び出すときは、


	wchar[] s ="こんにちは";
	printf(toMBSz(s));


と書くか、自分でphobosをwrapして、上のようにunicodeを用いているときは、toMBSzを経由して呼び出すようにするか、です。

まあ、いずれコンパイラ側で対応されるでしょうから、ここでは、お手軽にtoMBSzを呼び出すことにします。

..と言ってもtoMBSという関数をこれから作らないといけないのですが。Windows系ならば、WideCharToMultiByte、linux系ならばiconvを用いれば出来るそうですが、Cの標準関数(mbstowcs,wcstombs)を用いて簡単に出来ます。

以下は、その実装例です。(Y.Tさんのホームページを参考にさせていただきました)

※ Windowsとそれ以外を切り分けていますが、分けなくても大丈夫です。また、setlocaleはスレッドセーフでないため、そのための保護が必要になります。(? このへんよくわかってません。)


module y4d_aux.widestring;

private import std.string;
private import std.utf;

version(Win32){
extern(Windows) export int WideCharToMultiByte(
	 uint	  CodePage,
	 uint	 dwFlags,
	 char*	lpWideCharStr,
	 int	  cchWideChar,
	 char*	 lpMultiByteStr,
	 int	  cbMultiByte,
	 char*	 lpDefaultChar,
	 bool*	lpUsedDefaultChar);

extern(Windows) export int MultiByteToWideChar(
	 uint	  CodePage,
	 uint	 dwFlags,
	 char*	 lpMultiByteStr,
	 int	 cchMultiByte,
	 wchar*	 lpWideCharStr,
	 int		cchWideChar);

} else {
	extern(C){
		enum { LC_ALL = 0 }
		char* setlocale(int, char*);
		int mbstowcs(wchar*, char*, int);
		int wcstombs(char*, wchar*, int);
	}
}

///	日本語のコード変換を提供する。
/**
日本語が出ないと嫌なので、簡単な変換関数を用意しました。
Unicode文字列を、マルチバイト(shift_jis等)へ変換します。

Y.Tさんのコード
http://hp.vector.co.jp/authors/VA028375/contents/D_try2.html
を利用させていただいています。

<その他、参考ページ。

USC-2とUTF-8:
http://homepage1.nifty.com/nomenclator/unicode/ucs_utf.htm

ShiftJIS,Unicode(UTF-8):
http://kamoland.com/comp/unicode.html

漢字コードについて:
http://tohoho.wakusei.ne.jp/wwwkanji.htm
<*/
char[] toMBS(wchar[] s)
{
	char[] result;
version(Win32){
	result.length = WideCharToMultiByte(0, 0, (char*)s, s.length,
		null, 0, null, null);
	WideCharToMultiByte(0, 0, (char*)s, s.length, result,
		result.length, null, null);
}else{
	synchronized (locale_sync_object) {
		char* o = setlocale(LC_ALL, null);
		setlocale(LC_ALL, "");
		result.length = wcstombs(null, s, 0);
		wcstombs(result, s, result.length);
		setlocale(LC_ALL, o);
	}
}
	return result;
}

///	マルチバイト→unicode変換
wchar[] toWCS(char[] s)
{
	wchar[] result;
version(Win32){
	result.length = MultiByteToWideChar(0, 0, (char*)s, s.length,
		null, 0);
	MultiByteToWideChar(0, 0, (char*)s, s.length,(wchar*)result,
		result.length);
}else{
	synchronized (locale_sync_object) {
		char* o = setlocale(LC_ALL, null);
		setlocale(LC_ALL, "");
		result.length = mbstowcs(null, s, 0);
		mbstowcs(result, s, result.length);
		setlocale(LC_ALL, o);
	}
}
	return result;
}


///	ゼロ終端なWCSを返す
char* toMBSz(wchar[] s) { return std.string.toStringz(toMBS(s)); }

///	ゼロ終端なMBSを返す
char* toMBSz(wchar* s) { return std.string.toStringz(toMBS(s[0..std.string.wcslen(s)])); }

/// UTF8からWCSを返す
char[] toMBS(char[] s) { return toMBS(std.utf.toUTF16(s)); }

/// UTF8からゼロ終端なWCSを返す
char* toMBSz(char[] s) { return std.string.toStringz(toMBS(s)); }

/*
/// ゼロ終端なWCSを返す
wchar[] toWCSz(char[] s) { return std.string.toStringz(toWCS(s)); }
/// ゼロ終端なWCSを返す
wchar[] toWCSz(char* s) { return std.string.toStringz(toWCS(s[0..std.string.strlen(s)])); }
*/

version(Win32){
} else {
/// set localeでスレッド排他するための同期オブジェクト
private static Object locale_sync_object;
}


→ DMD ver 0.77で、std.utfというutf8,16,32の相互変換ライブラリが追加されましたが、mbsには変換できないようなので、上のコードはしばらく必要です。

Win9x系でstd.fileが動かない


・DMD0.77での話。

これも文字列にまつわる問題に含まれると思うのですが、std.fileのなかで、*W 系のWindowsAPIを呼び出してあります。その結果、std.file.read等は、Win9xでは動きません。*W系のAPIを呼び出してあるのは、たいていstd.fileなので、これの代替手段を用意することにしました。

std.file.read等は引数としてchar[]をとります。これは、前述の通りUTF-8だと仮定されています。そこで、std.utf.toUTF16にて、UTF-16に変換して、*W系のWindowsAPIの呼び出しが行なわれています。これに失敗したならば、これをtoMBSzでMBSに変換して、*A系のAPIを呼び出せば良いのです。

具体的には以下のようになります。


enum { ERROR_CALL_NOT_IMPLEMENTED = 120 }

void[] read(char[] name)
{
	DWORD size;
	DWORD numread;
	HANDLE h;
	byte[] buf;
	wchar* namez = std.utf.toUTF16z(name);

	h = CreateFileW(namez,GENERIC_READ,FILE_SHARE_READ,null,OPEN_EXISTING,
	FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,(HANDLE)null);
	if (GetLastError()== ERROR_CALL_NOT_IMPLEMENTED){
	h = CreateFileA(toMBSz(namez),GENERIC_READ,FILE_SHARE_READ,null,OPEN_EXISTING,
	FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,(HANDLE)null);
	}

	if (h == INVALID_HANDLE_VALUE)
	goto err1;

	size = GetFileSize(h, null);
	buf = new byte[size];

	if (ReadFile(h,buf,size,&numread,null) != 1)
	goto err2;

	if (numread != size)
	goto err2;

	if (!CloseHandle(h))
	goto err;

	return buf;

err2:
	CloseHandle(h);
err:
	delete buf;
err1:
	throw new FileException(name, GetLastError());
}


このように、std.fileを書き換えたものをyaneSDK4Dのy4d_aux/file.dに用意しておきましたので、こちらを使うようにすれば、Win9xでも正しく動作します。

これについては、news groupのほうに投稿して、伺ってます。「global flagを持たせて1度W系APIに失敗すればAのAPIを呼び出すようにすれば速度的に問題ないだろうし」と、別の内容のスレでWalter氏が答えてますので、この問題については認識しているんだとは思います。

http://www.digitalmars.com/drn-bin/wwwnews?D/21170

・DMD 0.78での話。

そのあと、何度かメールでWalter氏とやりとりして、問題を理解してもらえました。DMD 0.78でその修正が来ました。ところが、Win9x系の判定の論理値が逆になっていてWin98なのに*W系のAPIを呼び出していて動かないのです。これについてはレポートしておきましたので、0.79で修正されるでしょう。

・DMD 0.79での話。

上記の問題は修正されました!



Last Updated : 2004-4-19

written by yaneurao
http://bm98.yaneu.com/dlang/