突然ですが
C言語のwchar.hで定義されているwchar_tって何ビットでしょう?
16ビットだと思いましたか?(私も思いました)
実は環境依存です。16ビットじゃない時もあるのです。
※話を簡単にするため、ここではサロゲートペアは考えません。
検証
# include <stdio.h>
# include <wchar.h>
int main() {
wchar_t *s = L"ABCD";
printf("%d %d\n", wcslen(s), sizeof(s[0]));
return 0;
}
これをgccでコンパイルしたとき、結果は
- Windows & gcc 6.4.0:
4 2 - Linux & gcc 4.8.5:
4 4
となりました。
ワイド文字を表すL""と、文字数を数えるwcslen()とは辻褄が合うように作られていますが、wchar_tの実態は前者が16ビット、後者が32ビットと異なる結果に。
何が困るか
外部から与えられたUnicode文字列データが、1文字16ビット(UTF-16)のヌル終端文字列として表現されていたとしましょう。
その文字列と文字数を出力しようとすると、例えば以下のようなプログラムになりそうです。
# include <stdio.h>
# include <wchar.h>
int main() {
char data[] = {0x40, 0x00, 0x41, 0x00, 0x42, 0x00, 0x00, 0x00}; // これが与えられたとする
wchar_t *s = (wchar_t *)data;
printf("%ls %d\n", s, wcslen(s));
return 0;
}
ただし、これはwchar_tが16ビットの環境では
@AB 3
と期待通りに表示されますが、32ビットの環境だと予期しない動きをします。
どうすればよいか
C++11で、UTF-16の文字1個(サロゲートペア除く)を表すデータ型としてchar16_tという型ができました。同様に、32ビットの文字はchar32_tとなります。
また、UTF-16/UTF-32で文字列リテラルを表す記法も追加されています。
冒頭の例は、以下のように書けば良さそうです(C++ですが)。
# include <stdio.h>
# include <string>
using namespace std;
int main() {
char16_t s[] = u"ABCD"; // UTF-16文字列リテラル
printf("%d %d\n", char_traits<char16_t>::length(s), sizeof(s[0]));
return 0;
}
コンパイルオプションでC++11を明示してください。
g++ -std=c++11 main.cpp
wchar_tのサイズに関係なく、4 2 が出力されます。
wchar_tと異なり、char16_tはprintf()関数では出力手段がありません。
代わりに、こんな感じでいけそうです。(ソースコードはUTF-8で保存してください)
# include <string>
# include <codecvt>
# include <locale>
# include <iostream>
using namespace std;
int main() {
char16_t s[] = u"あいうえお";
wstring_convert<codecvt_utf8<char16_t>, char16_t> cv;
cout << cv.to_bytes(s) << endl;
return 0;
}
ただしcodecvt_utf8の利用には以下の条件が付きます。→codecvt_utf8 - cpprefjp - C++日本語リファレンス
- C++17では非推奨
- GCC 5.1以上が必要
wchar_tトラップ:Java (JNI) の場合
Javaで1文字を表すchar型は16ビットです。
例えば、JNI(Java Native Interface)において、UTF-16(ヌル終端)で表された文字列データをJavaのString型(jstring)として返したいと思ったときに、文字数をwcslenで数えてしまうとハマります。
// jbyteArray (byte[]) 型の引数argが与えられたとする
jbyte *arg_ptr = env->GetByteArrayElements(arg, NULL);
// wcslenが予期しない結果になるかも
jstring ret_string = env->NewString((jchar *)arg_ptr, wcslen((wchar_t *)arg_ptr));
env->ReleaseByteArrayElements(arg, arg_ptr, 0);
対策としては、こんな感じでしょうか?(必要なヘッダの宣言とusing namespace std;を書いておいてください)
jstring ret_string = env->NewString((jchar *)arg_ptr, char_traits<char16_t>::length((char16_t *)arg_ptr));
ヌル終端文字列の長さを調べるだけなら、自分でforループ回してもできますが…
wchar_tトラップ:Python (ctypes) の場合
PythonからC/C++の共有ライブラリ (.dll, .so) を呼び出すためのctypesというライブラリがあります。
ここでも、バイト列からUnicode文字の配列を作って操作したり、他の関数に渡したりしようとしてハマる可能性があります。
import ctypes
wstr = ctypes.create_unicode_buffer(u"あいうえお")
print(ctypes.sizeof(wstr))
wchar_tが16ビットの環境だと12が、32ビットの環境だと24が出力されます。
ここでいう「環境」とは、Pythonのビルドに使われたコンパイラの環境のようです。
実は、create_unicode_buffer()自体があまり使い所のないものなのかもしれません。
WindowsのAPIを扱うときには、wchar_t *型の引数などを相手にするので良いのでしょうが。
まとめ
wchar_t怖い。wcslen怖い。
私のように、なめてかかってひどい目に遭う人が増えないように祈っています。