ICUの文字コード変換を使いたいのですが…
文字コードは日本に生を受けたプログラマの悩みのタネです。UNIX-serverとWindows-clientとの間でメッセージのやりとりを行うとき、そのメッセージがアルファベット(ASCII)だけなら何の問題もないのですが、漢字や仮名を含んでいる場合は片方が使っている文字コードを他方が使っている文字コードに変換してあげないと’文字化け’が生じます。また、SMTP/POP3などを介した電子メールなどではJISコード(iso-2022-jp)との相互変換が必要となります。さらにはXMLではUnicodeが使われますから…
IBMのOpenSourceプロジェクト’ICU’は世界中で使われている数多くの文字コードとUnicodeとの相互変換を提供するライブラリです。’ICU’による文字コード変換を、現在最も広く用いられている2つのencoding:euc-jpとshift_jisとの文字コードの変換を例に解説します。
※ICUの文字コード変換にはC/C++の双方に対応したAPIが用意されています。C++-APIはC-APIにかぶせた薄いwrapperですから、ここではC-APIについて解説します。
※ ここでは文字コード変換に必要な最小限のAPIの解説にとどめます。詳細はICUのドキュメントを参照してください。
前準備
ucnv.h: 文字コード変換APIのヘッダ-
文字コード変換に必要なAPIを使うのに
#includeしなければならないヘッダはucnv.hただひとつです。ソースコードの先頭付近に:#include <unicode/ucnv.h>を書きましょう。’ucnv_’で始まる文字コード変換APIのプロトタイプはすべてこの
ucnv.hで宣言されています。 UErrorCode: エラーコード-
文字コード変換APIはほとんどがその引数に
UErrorCodeへのポインタを与えます。処理中に発生したエラーはここに書き込まれます。// 例: UErrorCode error = U_ZERO_ERROR; // [1] UConverter* conv = ucnv_open("shift_jis", &error); if ( U_FAILURE(error) ) { // [2] cout >> u_errorName(error) >> endl; // [3] return; }- [1]各関数の呼び出しに先だって必ずエラーコードを
U_ZERO_ERRORに設定しておいてください。 - [2]エラーの有無はマクロ
U_SUCCESS/U_FAILUREで検出できます。 - [3]エラーコードの文字列表現は関数
u_errorName()で得られます。
- [1]各関数の呼び出しに先だって必ずエラーコードを
UConverter の生成と解放
文字コード変換はUConverterとucnv_から始まる関数の組み合わせによって行います。
UConverter の生成と使用後の解放には、それぞれ ucnv_open() / ucnv_close() を呼び出します。
ucnv_open:UConverterの生成-
UConverter* ucnv_open(const char* converterName, UErrorCode* err)converterName : 変換の対象となる文字コード名、たとえば”shift_jis”,”euc-jp”,”iso-2022-jp”を指定します。
大文字/小文字は区別されません。また’-',’_',’ ‘(ダッシュ,アンダースコア,スペース)も無視されます。
*err : エラーコードが設定されます。
戻り値 : Unicodeコンバータ(エラー時には0)
ucnv_close:UConverterの解放-
void ucnv_close(UConverter* converter)converter : 解放するUnicodeコンバータ
対Unicode変換
ICUの文字コード変換は任意の文字コードで表現されたマルチバイト文字列とUnicodeとの相互変換を提供しています。
マルチバイト文字列 X から Y への相互変換は:
- X から Unicodeへ(
ucnv_toUnicode) - Unicode から Yへ(
ucnv_fromUnicode)
の2つのステップを踏むことになります。
※ マルチバイト文字列相互の変換API ucnv_convertがありますが、パフォーマンスが若干落ちるのでお薦めしません。
ucnv_toUnicode: マルチバイト文字列をUnicode文字列へ変換-
void ucnv_toUnicode(UConverter* converter, UChar** target, const UChar* targetLimit, const char** source, const char* sourceLimit, int32_t* offsets, UBool flush, UErrorCode* err)converter : Unicodeコンバータ
*target : 出力バッファの先頭
targetLimit : 出力バッファの上限
*source : 入力バッファの先頭
sourceLimit : 入力バッファの上限
offsets : 常に0
flush : 入力シーケンスの終端であれば
TRUE*err : エラーコード
ucnv_fromUnicode: Unicode文字列をマルチバイト文字列へ変換-
void ucnv_fromUnicode(UConverter* converter, UChar** target, const UChar* targetLimit, const char** source, const char* sourceLimit, int32_t* offsets, UBool flush, UErrorCode* err)converter : Unicodeコンバータ
*target : 出力バッファの先頭
targetLimit : 出力バッファの上限
*source : 入力バッファの先頭
sourceLimit : 入力バッファの上限
offsets : 常に0
flush : 入力シーケンスの終端であれば
TRUE*err : エラーコード
ucnv_toUnicode/ucnv_fromUnicode いずれの場合も入力/出力バッファの先頭位置の与え方が特殊なので注意してください。
入力/出力バッファの先頭位置のポインタを与えます。ここで与えた引数は入力/出力バッファの位置を与えるだけでなく、変換後の位置すなわち入力バッファをどこまで読んだか/出力バッファにどこまで書き込んだかを返すのに用いられるからです。
たとえば:
UConverter* converter; char in[IN]; wchar_t out[OUT]; const char* source = in; const char* sourceLimit = in + IN; wchar_t * target = out; wchar_t * targetLimit = out + OUT; UErrorCode err = U_ZERO_ERROR; ucnv_toUnicode(converter, &target, targetLimit, &source, sourceLimit, 0, TRUE, &err);
上記のコードを実行すると、ucnv_toUnicode実行後、inからsource直前までのマルチバイト文字がoutからtarget直前までのUnicode文字に変換されたことになります。
引数flushは、一連の文字列を一気に変換できない場合にFALSEを設定します。flushがFALSEであるとき、入力末尾の変換状態がUConverter内に保持され、後に続く変換時に使われます。この機能によって、入力マルチバイト文字列が2バイト文字の途中で途切れていた場合でも正しく変換できます。
バッファの大きさ
ucnv_toUnicode /ucnv_fromUnicode でマルチバイト文字列/Unicode相互の変換を行う時、変換結果を格納するバッファの大きさは、以下のように求めることができます。
- マルチバイトからUnicode
-
関数
ucnv_getMinCharSize(UConverter* converter)が、converterに指定された文字コードにおいて、1文字を表現するのに必要な最小のバイト数を返します。したがって、入力されるマルチバイト文字列のバイト数を
ucnv_getMinCharSize(converter)で割れば、出力Unicodeバッファに必要な文字数が得られます。 - Unicodeからマルチバイト
-
関数
ucnv_getMaxCharSize(UConverter* converter)が、converterに指定された文字コードにおいて、1文字を表現するのに必要な最大のバイト数を返します。したがって、入力されるUnicode文字列の文字数に
ucnv_getMaxCharSize(converter)を掛ければ、出力マルチバイト文字列バッファに必要な文字数が得られます。
サンプル
では、ICUの文字コード変換APIでeuc-jpで書かれた”日本”をshift_jisに変換してみましょう。Windows環境下であれば、以下のコードをコンパイル/実行すれば “日本” と出力されます。
/*
* IBM ICU 1.8.1 による euc-jp から shift_jis への変換
*/
#include <unicode/ucnv.h>
#include <iostream>
using namespace std;
int main() {
UErrorCode error = U_ZERO_ERROR;
UConverter* sourceConverter = ucnv_open("euc-jp",&error); // UConverterの生成
if ( U_FAILURE(error) ) {
cerr << u_errorName(error);
return 1;
}
const char euc_jp[] = "¥xC6¥xFC¥xCB¥xDC"; // "日本"のeuc-jp表現
const char* source = euc_jp; // 入力の先頭位置
size_t sourceLength = 4; // 入力の長さ(バイト数)
size_t targetLength ;
// 出力(Unicode)に必要な文字数
targetLength = sourceLength / ucnv_getMinCharSize(sourceConverter);
wchar_t* wbuffer = new wchar_t[targetLength]; // 出力領域の確保
wchar_t* wtarget = wbuffer;
// マルチバイト文字列(euc-jp) から Unicode へ変換
ucnv_toUnicode(sourceConverter,
&wtarget, wtarget + targetLength,
&source, source + sourceLength,
0, TRUE, &error);
if ( U_FAILURE(error) ) {
delete[] wbuffer;
cerr << u_errorName(error);
return 1;
}
ucnv_close(sourceConverter); // UConverterの開放
char shift_jis[10]; // 変換結果をここに出力
UConverter* targetConverter = ucnv_open("shift_jis",&error); // UConverterの生成
if ( U_FAILURE(error) ) {
cerr << u_errorName(error);
return 1;
}
// 出力(shift-jis)に必要なバイト数
targetLength = (wtarget - wbuffer) * ucnv_getMaxCharSize(targetConverter);
if ( targetLength >= 10 ) {
cerr << "not enougth room to convert!" << endl;
return 1;
}
char* buffer = shift_jis;
const wchar_t* wsource = wbuffer; // 入力(Unicode)の先頭位置
char* target = buffer; // 出力(shift_jis)の先頭位置
// Unicode から マルチバイト文字列(shift_jis) へ変換
ucnv_fromUnicode(targetConverter,
&target, target + targetLength,
&wsource, wtarget,
0, TRUE, &error);
delete[] wbuffer; // 使用済みバッファの開放
if ( U_FAILURE(error) ) {
cerr << u_errorName(error);
return 1;
}
ucnv_close(targetConverter); // UConverterの開放
*target = '¥0';
cout << shift_jis << endl;
return 0;
}
もっと簡単に…
文字コード変換API ucnv_...()は極めてプリミティブなAPIであるためその挙動を細かく制御することができますが、それだけ複雑な呼び出しとなっています。文字コード変換をもっと簡単に行うために、出力バッファの確保や引数の設定を行う’薄いラッパ’の実装を試みました。これによって:
widen: マルチバイト -> Unicode- const char* -> std::wstring
- std::string -> std::wstring
narrow: Unicode -> マルチバイト- const wchar_t* -> std::string
- std::wstring -> std::string
convert: マルチバイト -> マルチバイト- const char_t* -> std::string
- std::string -> std::string
の各変換が非常にシンプルな呼び出しで利用できます。使い方(インタフェース)はヘッダ converter.h を参照してください。
converter.h
#ifndef __CONVERTER_H__
#define __CONVERTER_H__
#include <exception>
#include <string>
struct UConverter;
enum UErrorCode;
namespace s34 {
/*
* 'icu_error' exception
*/
class icu_error : public std::exception {
UErrorCode error_;
public:
explicit icu_error(UErrorCode error) : error_(error) {}
virtual const char* what() const;
UErrorCode code() const { return error_; }
};
/*
* 'open_converter'
*/
UConverter* open_converter(const char* name, UErrorCode* error =0);
/*
* 'close_converter'
*/
void close_converter(UConverter*);
/*
* 'widen' : multibyte -> doublebyte
*/
std::wstring
widen(const char* source,
size_t sourceLength,
UConverter* converter,
bool flush =true,
UErrorCode* error = 0
);
inline
std::wstring
widen(const char* source,
UConverter* converter,
bool flush =true,
UErrorCode* error =0
)
{ return widen(source, std::char_traits<char>::length(source),
converter, flush, error); }
inline
std::wstring
widen(const char* source,
const char* sourceLimit,
UConverter* converter,
bool flush =true,
UErrorCode* error =0
)
{ return widen(source, sourceLimit - source,
converter, flush, error); }
inline
std::wstring
widen(const std::string& source,
UConverter* converter,
bool flush =true,
UErrorCode* error =0
)
{ return widen(source.data(), source.length(),
converter, flush, error); }
/*
* 'narrow' : doublebyte -> multibyte
*/
std::string
narrow(const wchar_t* source,
size_t sourceLength,
UConverter* converter,
bool flush =true,
UErrorCode* error =0
);
inline
std::string
narrow(const wchar_t* source,
UConverter* converter,
bool flush =true,
UErrorCode* error =0
)
{ return narrow(source, std::char_traits<wchar_t>::length(source),
converter, flush, error); }
inline
std::string
narrow(const wchar_t* source,
const wchar_t* sourceLimit,
UConverter* converter,
bool flush =true,
UErrorCode* error =0
)
{ return narrow(source, sourceLimit - source,
converter, flush, error); }
inline
std::string
narrow(const std::wstring& source,
UConverter* converter,
bool flush =true,
UErrorCode* error =0
)
{ return narrow(source.data(), source.length(),
converter, flush, error); }
/*
* 'convert' : multibyte -> multibyte
*/
std::string
convert(const char* source,
size_t sourceLength,
UConverter* sourceConverter,
UConverter* targetConverter,
bool flush =true,
UErrorCode* error =0
);
inline
std::string
convert(const char* source,
UConverter* sourceConverter,
UConverter* targetConverter,
bool flush =true,
UErrorCode* error =0
)
{ return convert(source, std::char_traits<char>::length(source),
sourceConverter, targetConverter, flush, error); }
inline
std::string
convert(const char* source,
const char* sourceLimit,
UConverter* sourceConverter,
UConverter* targetConverter,
bool flush =true,
UErrorCode* error =0
)
{ return convert(source, sourceLimit - source,
sourceConverter, targetConverter, flush, error); }
inline
std::string
convert(std::string& source,
UConverter* sourceConverter,
UConverter* targetConverter,
bool flush =true,
UErrorCode* error =0
)
{ return convert(source.data(), source.length(),
sourceConverter, targetConverter, flush, error); }
}
#endif
converter.cpp
#include <unicode/ucnv.h>
#include "converter.h"
namespace s34 {
const char* icu_error::what() const { return u_errorName(error_); }
/*
* make_converter
*/
UConverter*
open_converter(const char* name, UErrorCode* error) {
UErrorCode innerError = U_ZERO_ERROR;
UErrorCode* perror = error ? error : &innerError;
UConverter* converter = ucnv_open(name, perror);
if ( U_FAILURE(*perror) && !error )
throw icu_error(innerError);
return converter;
}
void
close_converter(UConverter* converter) {
ucnv_close(converter);
}
/*
* widen : multibyte --> wide
*/
std::wstring
widen(const char* source,
size_t sourceLength,
UConverter* converter,
bool flush,
UErrorCode* error
) {
const size_t poolLength = 256;
wchar_t pool[poolLength];
size_t targetLength = sourceLength / ucnv_getMinCharSize(converter);
wchar_t* buffer = ( targetLength > poolLength ) ? new wchar_t[targetLength] : pool;
wchar_t* target = buffer;
UErrorCode innerError = U_ZERO_ERROR;
UErrorCode* perror = error ? error : &innerError;
ucnv_toUnicode(converter,
&target, target + targetLength,
&source, source + sourceLength,
0, flush, perror);
if ( U_FAILURE(*perror) && !error ) {
if ( buffer != pool ) delete[] buffer;
throw icu_error(innerError);
}
std::wstring result(buffer,target);
if ( buffer != pool ) delete[] buffer;
return result;
}
/*
* narrow : wide --> multibyte
*/
std::string
narrow(const wchar_t* source,
size_t sourceLength,
UConverter* converter,
bool flush,
UErrorCode* error
) {
const size_t poolLength = 256;
char pool[poolLength];
size_t targetLength = sourceLength * ucnv_getMaxCharSize(converter);
char* buffer = ( targetLength > poolLength ) ? new char[targetLength] : pool;
char* target = buffer;
UErrorCode innerError = U_ZERO_ERROR;
UErrorCode* perror = error ? error : &innerError;
ucnv_fromUnicode(converter,
&target, target + targetLength,
&source, source + sourceLength,
0, flush, perror);
if ( U_FAILURE(*perror) && !error ) {
if ( buffer != pool ) delete[] buffer;
throw icu_error(innerError);
}
std::string result(buffer, target);
if ( buffer != pool ) delete[] buffer;
return result;
}
/*
* convert : multibyte --> multibyte
*/
std::string
convert(const char* source,
size_t sourceLength,
UConverter* sourceConverter,
UConverter* targetConverter,
bool flush,
UErrorCode* error
) {
const size_t poolLength = 256;
size_t targetLength;
targetLength = sourceLength / ucnv_getMinCharSize(sourceConverter);
wchar_t wpool[poolLength];
wchar_t* wbuffer = ( targetLength > poolLength ) ? new wchar_t[targetLength] : wpool;
wchar_t* wtarget = wbuffer;
UErrorCode innerError = U_ZERO_ERROR;
UErrorCode* perror = error ? error : &innerError;
ucnv_toUnicode(sourceConverter,
&wtarget, wtarget + targetLength,
&source, source + sourceLength,
0, flush, perror);
if ( U_FAILURE(*perror) ) {
if ( wbuffer != wpool ) delete[] wbuffer;
if ( !error ) throw icu_error(innerError);
return std::string();
}
targetLength = (wtarget - wbuffer) * ucnv_getMaxCharSize(targetConverter);
char pool[poolLength];
char* buffer = ( targetLength > poolLength ) ? new char[targetLength] : pool;
const wchar_t* wsource = wbuffer;
char* target = buffer;
ucnv_fromUnicode(targetConverter,
&target, target + targetLength,
&wsource, wtarget,
0, flush, perror);
if ( U_FAILURE(*perror) && !error ) {
if ( wbuffer != wpool ) delete[] wbuffer;
if ( buffer != pool ) delete[] buffer;
throw icu_error(innerError);
}
std::string result(buffer, target);
if ( wbuffer != wpool ) delete[] wbuffer;
if ( buffer != pool ) delete[] buffer;
return result;
}
}
サンプル (tiral.cpp)
#include <iostream>
#include "converter.h"
int main() {
std::string euc_jp = "¥xC6¥xFC¥xCB¥xDC";
try {
UConverter* sourceConverter = s34::open_converter("euc-jp");
UConverter* targetConverter = s34::open_converter("shift_jis");
std::cout << s34::convert(euc_jp, sourceConverter, targetConverter) << std::endl;
s34::close_converter(sourceConverter);
s34::close_converter(targetConverter);
} catch ( s34::icu_error& ex ) {
std::cerr << ex.what() << std::endl;
}
return 0;
}
実行結果
日本