突然ですが
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
怖い。
私のように、なめてかかってひどい目に遭う人が増えないように祈っています。