株式会社エス・スリー・フォー

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() で得られます。

UConverter の生成と解放

文字コード変換はUConverterucnv_から始まる関数の組み合わせによって行います。

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 への相互変換は:

  1. X から Unicodeへ(ucnv_toUnicode)
  2. 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を設定します。flushFALSEであるとき、入力末尾の変換状態が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;
}

実行結果

日本