漢字がきちんと表示されない件の解析メモ

現象

mutttをUTF-8環境(LANGが ja_JP.UTF-8)の所で使うと、キー入力した時、Unicodeで、幅が不定な文字、例えば「★」の表示が乱れる。幅が定まっている文字、例えば「〒」は乱れない。現象としては下記の通り。

  • FreeBSD8.1の場合
    • クライアントがOpenSUSEのmltermの場合は、★が*になって表示される。
    • クライアントがOpenSUSEのgnome-terminalの場合は、1文字入力すると「★」は正しく表示されるが、2文字続けて入力すると、前の★に被さって次の★が表示される。
    • クライアントがOpenSUSEのgnome-terminal で、VTE_CJK_WIDTH=1を設定した場合、連続して★を入力しても、正しく表示されるが、バックスペースを入力すると、1バイト分しか戻らない。
    • クライアントがWindowsのteraterm(4.68)の場合、★を入力した後、カーソルが3バイト分進む。

ソースはどうなっている?

入力された文字列をハンドリングしているのは、 enter.c内の _mutt_enter_string()である。ここの、

 287       clrtoeol ();
 288       move (y, x + my_wcswidth (state->wbuf + state->begin, state->curpos - state->begin));
 289     }
 290     mutt_refresh ();
 291 
 292     if ((ch = km_dokey (MENU_EDITOR)) == -1)
 293     {
 294       rv = -1; 
 295       goto bye;
 296     }

の、mutt_refresh()を何回か通過した時に、入力された文字列が表示される。

実際にキーが入力されるのを待ち合わせるのは、keymap.cの中にある km_dokey()の中。更にその中から、

  94 #ifdef KEY_RESIZE
  95   /* ncurses 4.2 sends this when the screen is resized */
  96   ch = KEY_RESIZE;
  97   while (ch == KEY_RESIZE)
  98 #endif /* KEY_RESIZE */
  99     ch = getch ();
 100   mutt_allow_interrupt (0);
 101 

で、mutt_getch()が呼ばれる。ここで★(UTF8では、E2 98 85)を入力すると、まず「E2」が帰る。ちなみに、Ctrl-Gを押すと、エラーが帰るように、mutt_getch()内でハードコードされている。入力した文字は、大域変数LastKeyに格納される。その後、mapテーブルを検索した後、retry_generic()を呼び出し、結果を返す。

 663         k = mbrtowc (&wc, &c, 1, &mbstate);
 664         if (k == (size_t)(-2))
 665           continue;
 666         else if (k && k != 1)
 667         {
 668           memset (&mbstate, 0, sizeof (mbstate));
 669           continue;
 670         }
 671       }

次に、マルチバイト文字(UTF-8)からワイド文字(UCS-2)への変換を行う。これは、mbrtowc()関数を呼び出すことで行う。1バイトのみの解析なので、戻り値は-2(解析できなかった)が返り、ループの先頭に戻る。もう一度mutt_getch()を呼び出し、UTF-8の2バイト目を得る。これもmbrtowc()で解析失敗するので、-2が返る。3回目のループで、UTF-8の3バイト目を得る。ここで、結果が1となり、mbrtowcの引数wcにはUCS-2の文字が返る。kが1なので、mbstateの初期化は行わない。

 699       else if (wc && (wc < ' ' || IsWPrint (wc))) /* why? */
 700       {
 701         if (state->lastchar >= state->wbuflen)
 702         {
 703           state->wbuflen = state->lastchar + 20;
 704           safe_realloc (&state->wbuf, state->wbuflen * sizeof (wchar_t));
 705         }
 706         memmove (state->wbuf + state->curpos + 1, state->wbuf + state->curpos, (state->lastchar - state->curpos) * sizeof (wchar_t));
 707         state->wbuf[state->curpos++] = wc;
 708         state->lastchar++;
 709       }
701行目のif文が真なので、703行目に移動する。すると、lastcharに20を加算したものをwbuflenとし、そのサイズをwchar_t単位(実際はint)で割り当て、wbufのcurposからwbufのcurpos+1に、lastcharからcurposの、wchar_t単位でのバイト数を移動する。言い換えれば、wbufの先頭1要素を空ける。その後、先頭にwcをコピーし、ポインタを1つずらす。またループの先頭に戻る。

ループに戻るとmy_wcwidth()が呼ばれるところがある。ここで、bufの先頭文字の大きさが返る。UCS-2文字なので2が返る。この文字(buf内にある文字)をmy_addwch()で描画する。その後mutt_addwch()で今度はマルチバイトに変換する。bufにUTF-8文字が設定され、3が返る。

その後、my_wcswidth()が呼ばれ、2が返る。

wcwidth()は呼ばれる?

文字の大きさを決めるのはwcwidth()だが、それは呼ばれていない様子。ncurses内にも呼び出している所はあるが、ブレークポイントを仕掛けても引っかからない。--without-wc-funcsを指定しているからか。

→嘘。ちゃんと呼んでいるところがあった。ただし、wcwidthではなくて__wcwidth。で、★だと戻りが1,〒だと2になることを確認。そこで一旦止めて、★の場合、強引に戻り値を2にしたら表示が正しくなった。これが原因か。

wcwidthの仕組みはFreeBSDのwcwidthにまとめる。

どうやって描画される?

シングルバイトの場合は、1バイトずつ、マルチバイトの場合は、_mutt_enter_string()でUTF-8文字の組み立てを終え、3バイトになってから、mutt_addwch()を呼び出す。この中で、addstr()を呼び出している。addstr()を呼び出す前に引数bufの中身がどうなっているかを見ると、

★の場合

[2011-02-13 20:54:35] before_addwch w=0002 wbuf=2605
[2011-02-13 20:54:35] addstr buf=ffffffe2 ffffff98 ffffff85        0
[2011-02-13 20:54:35] before move2 y,x=35,11
[2011-02-13 20:54:36]    a
[2011-02-13 20:54:36] ★

〒の場合

[2011-02-13 20:59:43] before move1 y,x=35,9
[2011-02-13 20:59:43] before_addwch w=0002 wbuf=3012
[2011-02-13 20:59:43] addstr buf=ffffffe3 ffffff80 ffffff92        0
[2011-02-13 20:59:43] before move2 y,x=35,11
[2011-02-13 20:59:43]    a
[2011-02-13 20:59:43] 〒

で、正しく3バイトがcursesの方に渡されている。

その他