ICU charset detector

ICU(International Components for Unicode)というライブラリのなかにcharset detectorという文字コード/文字種判定器がはいってます。

使い方は下のような感じ

#include <iostream>
#include <fstream>
#include <unicode/ucsdet.h>
#include <string>

int main(int argc, char** argv)
{
	if (argc > 2)
		return -1;

	std::string filename(argv[argc-1]);
	std::ifstream ifs(filename.c_str(), std::ios_base::in);

	if (!ifs.is_open())
		return -1;

	UErrorCode error = U_ZERO_ERROR;
	UCharsetDetector* detector = ucsdet_open(&error);
	if (U_FAILURE(error))
		return -1;

	std::string buffer;
	buffer.resize(65536);
	ifs.read(&*buffer.begin(), buffer.length());
	buffer.resize(ifs.gcount());

	ucsdet_setText(detector, buffer.c_str(), buffer.length(), &error);
	if (U_FAILURE(error))
		return -1;

	int32_t founded = 0;
	const UCharsetMatch** matchers =
		ucsdet_detectAll(detector, &founded, &error);
	if (U_FAILURE(error))
		return -1;

	if (founded > 0)
		std::cout << "filename: " << filename
				  << " name: " << ucsdet_getName(matchers[0], &error)
				  << " language:" << ucsdet_getLanguage(matchers[0], &error)
				  << std::endl;

	ucsdet_close(detector);
}

まぁこんな感じでさして使うのも難しくないライブラリなんですが、クラスでラッピングしていったら何を判定してもShift JISとしか判定しない@Windows Visual C++ 2008 Express と言う現象が・・・

最初はwindows版のbuild済みライブラリを使ってたのでそっちが変なのか?と思ってソース落としてきてbuildしてみて動かす・・・駄目。

しょうがないので上の様に素直なコードをクラス版と併記して動かしてみると素直バージョンの方ではちゃんと判定しやがってます・・・。

クラス内部ではメソッドごとにUErrorCode変数を作ってたのでもしかしてUErrorCode変数のアドレスにコンテキスト束縛してるかも?(ないない)と思ってメンバ変数にしてみるも駄目。

あとは怪しそうなのはucsdet_setText()に対しての文字列データ渡してる部分くらいしか・・・ということでucsdet_setText()をを呼び出してるメソッドを見直し。
最初の時点では以下の様になってました。

template <typename iteratorT>
void operator()(iteratorT head, iteratorT tail)
{
	std::vector<char> buffer(head, tail);

	UErrorCode error = U_ZERO_ERROR;
	ucsdet_setText(detector,
				   &*buffer.begin(), buffer.size(), &error);

	...
}

こんな感じでイテレータを受け取って内部でバッファに詰め込んでから改めてucsdet_setText()にながしこんでます。

で、以下の様に変更

void operator()(const char* head, size_t length)
{
	UErrorCode error = U_ZERO_ERROR;
	ucsdet_setText(detector,
				   head, length, &error);

	...
}

そうすると・・・動きやがったよ・・・

まぁ、大体その時点で想像はついてたんですが裏を取るためにICUのソースを読みにいきました。
まずはucsdet_setText()・・・

U_CAPI void U_EXPORT2
ucsdet_setText(UCharsetDetector *ucsd, const char *textIn,
    int32_t len, UErrorCode *status)
{
    if(U_FAILURE(*status)) {
        return;
    }

    ((CharsetDetector *) ucsd)->setText(textIn, len);
}

これだけです。
一応ucsdの具象クラスをucsdet_open()みて確認します。

    CharsetDetector* csd = new CharsetDetector(*status);

とやってるのでポリモルフィックではないようです。
安心してCharsetDetector::setText()を見にいきます。

void CharsetDetector::setText(const char *in, int32_t len)
{
    textIn->setText(in, len);
    fFreshTextSet = TRUE;
}

さらにメンバ変数に渡してました。
型を調べるとtextInはInputText型だそうです。
さらに追跡。

void InputText::setText(const char *in, int32_t len)
{
    fInputLen  = 0;
    fC1Bytes   = FALSE;
    fRawInput  = (const uint8_t *) in;
    fRawLength = len == -1? uprv_strlen(in) : len;
}

旅が終わりました・・・中でコピーしてucsdet_detect/ucsdet_detectAllに備えてるはず!!と最初に思ってたところは見事に参照してるだけでした・・・
要するに自分のクラスのコードでは・・・

  • std::vectorにデータをコピー
  • ucsdet_setText()に渡す
    • 中ではstd::vectorのメモリ位置を保存して帰る
  • メソッド抜けてstd::vectorが破棄される
  • detect/detectAll()するメソッドを呼び出す
    • detect/detectAll()のなかで既に無効なstd::vectorのメモリを参照して文字コード判定。

と言う愉快なことになっていた様です・・・
linuxwindowsで試してみてたんですがlinuxだとちゃんと動いてたんですが削除時に0xddで埋める、というwindowsデバッグモード用アロケータの効果が働いていたおかげだった模様。

まぁ、勝手に安全側に振ってあるだろう(内部でコピー持ってるはず)と思ってた自分が悪いんですが、ライブラリなんだから安全側に振っといてほしいってもんです・・・