Abstract
理由
Windows用にビルドされたGNU Emacs(NTEmacs)では、他のOS用のGNU Emacsと同様に、menu-tree.elによるメニューの日本語化を行なえる。ところが、NTEmacsでは、メニューの一部(あるいは全部)が文字化けするという問題がある。具体的には、以下の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に設定しておかないと、メニューバーの表示([ファイル]や[編集]など)が後で文字化けするので注意されたい。- GNU Emacsでload-pathのどこかにあるであろう
menu-tree.el
を開き、1行目のutf-8
をcp932
に書き換える - ファイルを別の名前で保存する(C-x C-w
menu-tree-cp932.el
)。その際、1行目の内容によりファイルのエンコードがcp932に変更される。 - 保存したファイルをバイトコンパイルする(M-x
byte-compile-file
RETmenu-tree-cp932.el
) - 初期化ファイル(
~/.emacs.d/init.el
や~/.emacs
など)から、menu-tree.elをロードしている部分を探し、次の2行を追加して保存する。以前の内容はコメントにしておくとよい。 (setq menu-tree-coding-system 'cp932)
の直後でC-x C-e、次に(require 'menu-tree-cp932)
の直後でC-x C-eして動作を確認→最初はメニューが文字化けする- フレーム端をドラッグしてサイズを変更し、文字化けが直ったら作業完了。以後、GNU Emacsを起動するたびにこうしたドラッグによるサイズ変更を行なえば文字化けが直るようになる
- GNU Emacsでload-pathのどこかにあるであろう
-
文字化けしないように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 件のコメント:
コメントを投稿