このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

シフトJIS文字列とユニコード文字列の変換のときの終端ヌルとかバッファ長とか

Windows APIのなかにユニコード文字列を扱うものがいくつかあります。例えば、エンコーディングスキームがShift_JISである文字列(以下「シフトJIS文字列」)を、エンコーディングスキームがUTF-16LEである文字列(以下「ユニコード文字列」)に変換するための関数MultiByteToWideCharがあります。

https://msdn.microsoft.com/ja-jp/library/cc448053.aspx によると:

// 戻り値は、書き込まれたワイド文字の数
int MultiByteToWideChar(
  UINT CodePage,         // コードページ
  DWORD dwFlags,         // 文字の種類を指定するフラグ
  LPCSTR lpMultiByteStr, // マップ元文字列のアドレス
  int cchMultiByte,      // マップ元文字列のバイト数
  LPWSTR lpWideCharStr,  // マップ先ワイド文字列を入れるバッファのアドレス
  int cchWideChar        // バッファのサイズ
);

説明を読んでも、なんだか挙動がハッキリとは分かりません。実験したほうが速いですね。

知りたかったのは:

  1. マルチバイト文字が中途半端に断ち切られてしまったときはどうなるのか?
  2. 文字列の中間にヌル文字があったときはどうなるのか?
  3. 文字列終端のヌル文字はどのように扱われるのか?
  4. 出力バッファ長が足りないとき何が起きるのか?

あたりです。

実験には次のデータを使います。プログラミング言語C++言語(使ってる機能はC言語相当)です。

// 実験用のデータ
char srcStr[] = "あ\0a"; // 終端のnullを入れて5バイトのデータ

ソースコードエンコーディングShift_JISにしておけば、文字列リテラル"あ\0a"は5バイトのデータとなります。この5バイトデータの先頭から1バイトを指定すると、ひらがな「あ」の“半分”を指定したことになり、「マルチバイト文字が中途半端に断ち切られた」状況になります。"あ\0a"の2文字目(3バイト目)にヌル文字があり、終端にももちろんヌル文字があります。

入力文字列"あ\0a"に対するバイト数や、出力バッファのサイズの指定を色々と変えてみれば、MultiByteToWideChar関数の実際の挙動を知ることができるでしょう。入力文字列のバイト数は、1, 2, 3, 4, 5, そして -1 と変えます。出力バッファ長は 2, 3, 4, 5 と変化させます。

この記事の最後にあるテストプログラムにより実験を行います。このプログラムは、はてな記法のテキストを出力するので、結果をそのまんま以下に貼ります。


  • srcStr = "あ\0a"(82A0006100)
  • あ : Unicode Character 'HIRAGANA LETTER A' (U+3042)
  • a : Unicode Character 'LATIN SMALL LETTER A' (U+0061)
srcLen destLen retVal dump
1 2 1 FB30FFFFFFFFFFFFFFFF
1 3 1 FB30FFFFFFFFFFFFFFFF
1 4 1 FB30FFFFFFFFFFFFFFFF
1 5 1 FB30FFFFFFFFFFFFFFFF
2 2 1 4230FFFFFFFFFFFFFFFF
2 3 1 4230FFFFFFFFFFFFFFFF
2 4 1 4230FFFFFFFFFFFFFFFF
2 5 1 4230FFFFFFFFFFFFFFFF
3 2 2 42300000FFFFFFFFFFFF
3 3 2 42300000FFFFFFFFFFFF
3 4 2 42300000FFFFFFFFFFFF
3 5 2 42300000FFFFFFFFFFFF
4 2 0 42300000FFFFFFFFFFFF
4 3 3 423000006100FFFFFFFF
4 4 3 423000006100FFFFFFFF
4 5 3 423000006100FFFFFFFF
5 2 0 42300000FFFFFFFFFFFF
5 3 0 423000006100FFFFFFFF
5 4 4 4230000061000000FFFF
5 5 4 4230000061000000FFFF
-1 2 2 42300000FFFFFFFFFFFF
-1 3 2 42300000FFFFFFFFFFFF
-1 4 2 42300000FFFFFFFFFFFF
-1 5 2 42300000FFFFFFFFFFFF


驚いたのは、「あ」の“半分”だけをユニコードに変換してもエラーにならないことです。戻り値は1なので、1文字分変換したと報告しています。変換後の文字はFB30です。これはリトルエンディアンでのバイト配置なので、ユニコード文字番号は30FBです。

  • Unicode Character 'KATAKANA MIDDLE DOT' (U+30FB)

中黒(ナカグロ)ですね。「あ」の“半分”が中黒になるのは道理が合わないので、変換できないことを示すゲタ文字(下駄記号)として中黒を使っているのでしょう。実際、MultiByteToWideChar関数の第2引数であるフラグにMB_ERR_INVALID_CHARS(無効な入力文字に対して関数を失敗させる)を指定すると、エラーになり戻り値0が返ります。GetLastError() でエラー原因を調べると、ERROR_NO_UNICODE_TRANSLATION です。

MultiByteToWideChar関数にとって、ヌル文字は特別な意味を持たないようです。文字列の途中でも終端でも、1バイトの0x00を2バイトの0x0000にして出力側に置くだけです。これはヌル文字に限らず、他の制御文字(例えばバックスペース文字)のときも同じで、1バイトを同じ値の2バイトにするだけ。ただし、フラグにMB_USEGLYPHCHARSを指定すれば、制御文字の代わりに図形文字を使うようです。

入力側文字列の指定されたバイト長の終わりがヌル文字でなくても補ったりはしません。僕はここを勘違いしていて、変換結果は2バイトのヌル文字(0x0000)で終端していると思ってオーバーランさせちゃいましたよ。strncpy()でしくじるのと状況が似てます。

入力側文字列バイト長として-1を指定すると、MultiByteToWideChar関数が長さを測ってくれます。このときの挙動は、バイト長を明示的に3とした場合と同じです。ということは、内部で strlen(srcStr) + 1 してるのでしょう。

出力側のバッファ長が足りないときは戻り値0でエラーを報告します(エラーの理由はGetLastError())が、出力側バッファのダンプを見ると、途中まで作業した結果は残っています。

これで、MultiByteToWideChar関数の挙動は分かった気がしました。

以下にテストプログラムのソースコード
// -*- coding: sjis -*-
// a.cpp (quick-and-dirty run-only-once)
#include <Windows.h>
#include <stdio.h>

// 実験用のデータ
char srcStr[] = "あ\0a"; // 終端のnullを入れて5バイトのデータ
wchar_t destBuff[5];     // 5ワイド文字=10バイトまでのバッファ

// 16進ダンプの文字列を作る。
// 返す結果は内部に持つ単一バッファだから、
// 以前の結果はどんどん上書きされる。
char *hex_dump(unsigned char *byteBuff, unsigned int size)
{
#define MAX_BYTES 100
    static const char *hexDigit = "0123456789ABCDEF";
    static char result[2*MAX_BYTES + 1];
    int i;
    for(i = 0; i < size && i < MAX_BYTES; i++) {
        unsigned char b = byteBuff[i];
        result[2*i]     = hexDigit[b>>4];   // hi
        result[2*i + 1] = hexDigit[b&0x0F]; // lo
    }
    result[2*i] = '\0';
    return result;
#undef MAX_BYTES
}

void print_head()
{
    printf("- srcStr = \"\\0a\"(%s)\n"
           "- あ : Unicode Character 'HIRAGANA LETTER A' (U+3042)\n"
           "- a  : Unicode Character 'LATIN SMALL LETTER A' (U+0061)\n"
           "|* srcLen |* destLen |* retVal |* dump |\n",
           hex_dump(reinterpret_cast<unsigned char *>(srcStr), sizeof(srcStr)));
}

void print_data(int srcLen, int destLen)
{
    memset(reinterpret_cast<void *>(destBuff), 0xFF, sizeof(destBuff));
    int retVal = MultiByteToWideChar(
                                     // コードページ
                                     // CP_ACPはANSIコードページ
                                     CP_ACP,
                                     // フラグ、今回は使わない
                                     0,
                                     // 入力元文字列
                                     srcStr,
                                     // 入力元文字列のサイズ、
                                     // バイト数で指定
                                     // -1を指定すると自動計算
                                     srcLen,
                                     // 出力先バッファ
                                     destBuff,
                                     // 出力先バッファのサイズ
                                     // こっちはワイド文字数で指定
                                     destLen
                                     );
    char *dump = hex_dump(reinterpret_cast<unsigned char *>(destBuff), sizeof(destBuff));
    printf("| %d | %d | %d | %s |\n",
           srcLen, destLen, retVal, dump);
}

int main()
{
#define D(n, m)  print_data(n, m)
    print_head();
    D(1, 2);D(1, 3);D(1, 4);D(1, 5);
    D(2, 2);D(2, 3);D(2, 4);D(2, 5);
    D(3, 2);D(3, 3);D(3, 4);D(3, 5);
    D(4, 2);D(4, 3);D(4, 4);D(4, 5);
    D(5, 2);D(5, 3);D(5, 4);D(5, 5);
    D(-1,2);D(-1,3);D(-1,4);D(-1,5);
    return 0;
}