Pages

自己紹介

某IT技術情報誌に連載記事を持っていたテクニカルライター。またもや休刊してしまったよ。
Copyright © 2011-2016 Daregada. All rights reserved. Powered by Blogger.

Windows用のGNU Emacsでmenu-tree.elによる日本語メニューの文字化けを解決する方法

2012-01-21

Windows用にビルドされたGNU Emacs(NTEmacs) 23で、メニューを日本語化するmenu-tree.elを導入すると文字化けする理由を調べ、文字化けしないようにGNU Emacsのソースを修正してみたよ、という話。(追記:2012-01-21 16:00頃まで公開していたパッチには1箇所誤りがある。現在公開しているパッチを当て直すか、両者を比較して1文字修正されたい)

Abstract

理由
Windows用のメニューを構成するw32menu.cにおいて、メニュー関連のWindows APIのマルチバイト対応版とUnicode(UTF-16-LE)対応版の切り替えに問題があるため
対策
以下のいずれかを行なう

理由

Windows用にビルドされたGNU Emacs(NTEmacs)では、他のOS用のGNU Emacsと同様に、menu-tree.elによるメニューの日本語化を行なえる。ところが、NTEmacsでは、メニューの一部(あるいは全部)が文字化けするという問題がある。具体的には、以下の2点だ。

  1. ラジオボタン風の選択項目が最初から文字化けする...たとえば、[オプション]-[行の折り返し]のサブメニューや、[オプション]-[表示/非表示]-[スクロールバー]のサブメニューなど、「候補の中からどれかひとつを選択する」タイプの項目が文字化けしている。
  2. メニュー全体がフレームサイズの変更をきっかけに文字化けする...たとえば、フレームの右端をドラッグし、メニューが2段になるまで横幅を縮めると、それまで正常に表示されていた日本語のメニューが文字化けしてしまい、以後はずっとそのまま直らない。

後述するように、これらの文字化けはmenu-tree.elのせいではなく、GNU Emacsそのものが原因で発生している。このため、menu-tree.elのWebページで解説されているmenu-tree-coding-systemを設定しても直らない。

他のOS用のGNU Emacsではこうした問題は起きておらず、NTEmacsを(-nwオプションを付けて)コマンドプロンプトで実行すると文字化けしないことから、Windows GUIのメニュー関連の処理に問題があると当たりを付けた。GNU Emacs 23.3のソースのsrc/w32menu.cを読んでいたところ、add_menu_iten()の中に、いかにも怪しい部分を見つけた。以下、説明が長くなるため、解決方法だけ知りたい方は「対策」まで読み飛ばして構わない。

まずは、問題Bに該当しそうな箇所src/w32menu.cの1503行あたりから存在する。

これは、メニュー末尾に項目を追加するコードだ。out_stringには、メニューの項目名と(もしあれば)ショートカットキーを連結した文字列(UTF-8エンコーディング)が格納されている。Unicode対応版Windows API(末尾にWがついたもの)はUTF-16-LEを要求するので、utf8to16()でUTF-16-LEエンコーディングに変換した文字列をutf16_stringに格納し、それを関数ポインタunicode_append_menuによる関数呼び出しの引数に指定している。この関数ポインタには、Unicode対応版Windows APIのAppendMenuWのアドレスが設定済みだ。

問題は次のif文だ。コメントによると「Windows 95/98/MEで不完全なAppendMenuWが存在する場合」に対応するためのものらしい。

AppendMenuWの返り値が0(エラーを示す値)の場合にif文の条件が成立し、UTF-8エンコーディングのout_stringを引数とするAppendMenuを呼び出す。AppendMenuは、(GNU Emacsのソースでは#define UNICODEしていないので)マルチバイト対応版(日本語の場合はcp932)のAppendMenuAに置換される。また、関数ポインタunicode_append_menuをNULLに設定することで、これ以降はAppendMenuWの代わりにAppendMenuAを使う(コードが別の部分にある)。

プリプロセッサ識別子UNICODEによるUnicode対応版/マルチバイト対応版APIの切り替えは、2種類のバイナリを簡単に作成する手法として使われる。詳細は、Windows API - Unicode対応を参照のこと。GNU Emacsはこの手法を使わず、2種類のAPIを明示して使い分けている。

さて、メニューの初期値(英語)のように、UTF-8とcp932でコードポイントがほぼ同一な部分しか使っていないなら、APIの切り替えが起こっても文字化けは起こらず、ユーザーが気がつくことはないだろう。しかし、menu-tree.elでメニューを日本語化した状態では、cp932を要求するAppendMenuAにUTF-8エンコーディングの日本語文字列を渡すことになるので、この部分が実行されれば文字化けが起きるのは当然だ。

このコードの欠陥は、正しく動作するAppendMenuWをWindows 2000/XP/Vista/7などで使っている場合でも、何らかの理由で0が返ってくる可能性を考慮していない点にある。実際に、フレーム端をドラッグしてサイズを変更すると、メニューバーの段数が変わるタイミングでしばしば0が返ってくることを確認している(原因を究明して0が返らないようにすればいいのだが、面倒なので放置→2012-01-29追記: ちょっと調べてみた。対策のソース修正の項を参照)。すると、それ以後はAppendMenuWの代わりにAppendMenuAが使われるため、文字化けがずっと続くことになる。

続いて、問題Aに該当しそうな箇所src/w32menu.cの1550行あたりから存在する。

これは、(必要であれば)メニュー項目をラジオボタン風の「どれかひとつしか選択できない」形式に設定するコードだ。MENUITEMINFO構造体の変数infoのメンバーdwTypeDataに、ラジオボタン風の項目の文字列としてout_stringを設定し、関数ポインタset_menu_item_infoによる関数呼び出しの引数にinfo(のアドレス)を指定している。この関数ポインタには、マルチバイト対応版Windows APIのSetMenuItemInfoAのアドレスが設定済みだ。

このコードの欠陥は、out_stringにはUTF-8エンコーディングの文字列が格納されているのに、MENUITEMINFO構造体やSetMenuItemInfoAが、いずれもマルチバイト対応版であることだ。MENUITEMINFO構造体は、前出のAppendMenuと同じ理由で、マルチバイト対応版のMENUITEMINFOA構造体に置換される。つまり、cp932を要求するSetMenuItemInfoAに(MENUITEMINFOA構造体の変数を経由して)UTF-8エンコーディングの日本語文字列を渡すことになる。英語メニューだと気がつかないものの、文字化けが起きるのは当然だったのだ。

対策

対策としては、「menu-tree.elをcp932で保存し直し、わざと文字化けを起こす」か「文字化けしないようにsrc/w32menu.cを修正し、Emacsをビルドする」かのいずれかだ。前者は場当たり的だが現状のバイナリのままで動作する。後者は正統的だが、ソースを修正してビルドする必要がある。なお、gnupackで、この修正を取り入れたGNU Emacsのバイナリが公開されたので、ビルドする環境がない人は、そちらをダウンロードするのが手っ取り早いだろう。

  • menu-tree.elをcp932で保存し直し、わざと文字化けを起こす方法

    menu-tree.elはUTF-8エンコーディングで保存されており、エンコーディングを変更したいならmenu-tree-coding-systemを設定する(menu-tree.el内部で変換する)のが本来の使い方だ。しかし、「理由」で述べたように、文字化けが起きている状態では、cp932を要求するマルチバイト対応版のWindows API(AppendMenuAやSetMenuItemInfoA)が呼び出されている。

    そこで、あらかじめcp932エンコーディングに変更したmenu-tree.elをロードしておき、わざと文字化けを起こす操作をすれば、文字列のエンコーディングとWindows APIが要求するエンコーディングと合致して文字化けが解消される。

    問題が修正されたバイナリに入れ替えた後のことを考慮し、エンコーディングを変更したmenu-tree.elは別名(menu-tree-cp932.elなど)で保存するとよいだろう。また、この方法では、menu-tree-coding-systemもcp932に設定しておかないと、メニューバーの表示([ファイル]や[編集]など)が後で文字化けするので注意されたい。

    1. GNU Emacsでload-pathのどこかにあるであろうmenu-tree.elを開き、1行目のutf-8cp932に書き換える
    2. ファイルを別の名前で保存する(C-x C-w menu-tree-cp932.el)。その際、1行目の内容によりファイルのエンコードがcp932に変更される。
    3. 保存したファイルをバイトコンパイルする(M-x byte-compile-file RET menu-tree-cp932.el)
    4. 初期化ファイル(~/.emacs.d/init.el~/.emacsなど)から、menu-tree.elをロードしている部分を探し、次の2行を追加して保存する。以前の内容はコメントにしておくとよい。
    5. (setq menu-tree-coding-system 'cp932)の直後でC-x C-e、次に(require 'menu-tree-cp932)の直後でC-x C-eして動作を確認→最初はメニューが文字化けする
    6. フレーム端をドラッグしてサイズを変更し、文字化けが直ったら作業完了。以後、GNU Emacsを起動するたびにこうしたドラッグによるサイズ変更を行なえば文字化けが直るようになる
  • 文字化けしないようにsrc/w32menu.cを修正し、Emacsをビルドする方法

    理由」で述べた2つの問題を解決するパッチemacs-23.3-fix-unicode-menu-greeking.patchを作成した。GNU Emacs 23.3用だが、(ソースを見たところ該当箇所が修正されていないので)Emacs 24系列にもそのまま適用できそうだ。23.3より古いバージョンについては確認していない。

    なお、パッチの当て方やビルドの方法については、ソースからGNU Emacsをビルドしようという人には自明だと思われるのでここでは説明しない。gnupack Users Guideのビルド記録が参考になるだろう。

    変更点の詳細はパッチ自体を参照してもらうとして、問題Aに対しては、UTF-16-LEエンコーディングに変換された文字列を、Unicode対応版のMENUITEMINFOW構造体を経由してSetMenuItenInfoWに渡すようにした。また、問題Bに対しては、問題となるif文を#if 0#endifで囲んで除外しただけだ。

    なお、素のWindows 95/98/MEでは、AppendMenuWが実装されていないので、問題Bのif文にはそもそも到達しない(最初からAppendMenuAが使われる)。Windows 95/98/MEにUnicode対応版APIを提供するMicrosoft Layer for Unicodeや代替品を使って、そのAppendMenuWの実装が不完全な場合にのみ問題が起きる。それ以前に、Windows 95/98/MEでGNU Emacs 23.3が正常に動作するのか? ってところからして疑問なんだけどね。

    (2012-01-29追記) 問題Bにおいて、関数ポインタunicode_append_menu(実体はAppendMenuのUnicode対応版であるAppendMenuW)が0(エラーを示す値)を返す場合をもう少し詳しく調べてみた。

    AppendMenuのリファレンス(MSDN)によると、成功時の返り値は非0、失敗時の返り値は0で、拡張エラー情報を取得したければGetLastErrorを呼び出せ、とある。

    問題Bのコードに、AppendMenuWが0を返してきた直後にGetLastErrorを呼び出す処理を追加すると、エラーコードとしてERROR_SUCCESS(つまり0)が返ってきた。これは、本来ならエラーが起きなかったときの値のはずだ。今回は面倒なのでやっていないが、FormatMessageでエラーコードを文字列に変換すると、「この操作を正しく終了しました。」となる(日本語環境の場合)。

    拡張エラー情報を設定すると明記されたWindows APIに対して、失敗時にGetLastErrorを呼んでERROR_SUCCESSが返ってくるなんて、(わけがわからないよ|こんなの絶対おかしいよ)。もう少し調べてみよう。

0 件のコメント:

コメントを投稿