PLT Scheme (DrScheme) の日本語フォント修正パッチ


[戻る]

1. 概要

PLT Scheme (DrScheme) で日本語フォントの表示が見にくいのを修正するパッチを提供します。

PLT Scheme が開発している無償の Scheme 統合開発環境である DrScheme は、日本語インターフェイスが提供されていますが、日本語の文字間が詰まりすぎていて非常に見にくく、 また、日本語がところどころ切れてしまっています。このような日本語表示を使うくらいなら、英語表示で使うほうがずっとましですし、 実際そうしている方が多いかと思います (下図参照)。

どうやらこれは、DrScheme が利用している GUI ライブラリである MrEd が、 日本語テキストに対して適切に横幅を計算してくれないことに原因があるようです。

実はこのことを、5年ほど前に PLT 開発チームに報告したのですが、未だに修正されていないんですよねぇ。

「まったくぅ~、お兄ちゃんはアタシがいないとナンにもできないんだからぁ~」(^^)

というわけで、ソースコードをハックして日本語表示を修正してみました。

動作環境は以下を想定しています。

2. 何が問題なのか

結論から言いますと、plt-4.2.3/src/wxwindow/src/msw/wx_dc.cxx の wxTextSize 関数が原因でした (下図参照)。 この関数で、1文字の幅を計算しているのですが、Windows NT 系の場合、 GetCharABCWidthsFloat 関数を使用して文字幅の計算をしています。 この関数は、デバイスコンテキストに日本語フォントが選択されているなら、 正常に文字幅を計算してくれますが、日本語が含まれていないフォントが選択されていると、 正しく文字幅を計算してくれません。

Windows API のドキュメントに「既定文字の ABC 幅は現在選択されているフォントの範囲外の文字に対して使われます。」 と書かれていますね。MSDN の翻訳が下手なので意味が分かりづらいですが、つまり、フォントに含まれていない文字の幅は計算できないから、 何らかの既定の値を返しますよ、という意味です。

static void wxTextSize(HDC dc, Scheme_Hash_Table *ht, wchar_t *ustring, int d, int alen, double *ow, double *oh)
{
  /* Gets the text size, caching the result in font when alen == 1 */

  if ((alen == 1) && is_nt()) {
    double *sz;

    if (ht) {
      sz = (double *)scheme_hash_get(ht, scheme_make_integer(ustring[d]));
    } else {
      sz = NULL;
    }

    if (sz) {
      *ow = sz[0];
      *oh = sz[1];
    } else {
      ABCFLOAT cw;
      SIZE sizeRect;
      GetCharABCWidthsFloatW(dc, ustring[d], ustring[d], &cw);
      *ow = (cw.abcfA + cw.abcfB + cw.abcfC);
      GetTextExtentPointW(dc, ustring XFORM_OK_PLUS d, alen, &sizeRect);
      *oh = (double)sizeRect.cy;

      if (ht) {
        sz = (double *)scheme_malloc_atomic(sizeof(double) * 2);
        sz[0] = *ow;
        sz[1] = *oh;
        scheme_hash_set(ht, scheme_make_integer(ustring[d]), (Scheme_Object *)sz);
      }
    }
  } else {
    SIZE sizeRect;
    GetTextExtentPointW(dc, ustring XFORM_OK_PLUS d, alen, &sizeRect);
    *ow = (double)sizeRect.cx;
    *oh = (double)sizeRect.cy;
  }
}

で、実際にこの関数に渡されてくるデバイスコンテキストに、 どんなフォントが設定されているか調べてみました。

    wchar_t fontName[100];
    GetTextFaceW(dc, 100, fontName);

そうしたら、デバイスコンテキストに選択されていたのは "Arial" フォントでした。 これでは日本語の文字幅を正しく計算できませんね。

じゃあ、どうしたらいいのかというと、 GetTextExtentPoint 関数で取得すればいいんでないの? と思うのですが。上記のコードでは、何故か横幅だけわざわざ GetCharABCWidthsFloat 関数で取得しているのが、 なんだか変なんですよねー。Windows NT 系でなければ、GetTextExtentPoint 関数を呼び出すだけのコードになっているのに。

まぁいいや。とにかく、GetTextExtentPoint 関数で文字の横幅を計算するようにしたら、 日本語がきれいに表示されるようになりました (下図)。

3. PLT Scheme のビルド

原因がわかったので、ソースコードを修正して再ビルドすれば、日本語がきれいに表示されます。

plt-4.2.3\src\worksp\mred\mred.sln を開いて、 wxwin プロジェクトの Source Files / WX_DC.cxx を開いて修正すればいいです。 後は、PLT Scheme の指示通りにビルドすれば、修正されたバイナリができあがります。

4. バイナリパッチの作成

でもさー、こんな問題のためだけに PLT Scheme をいちいち再ビルドしたくないよねー。 まぁ、この修正を本家にフィードバックしておきましたけど、 いつ反映されるか分かりませんし。 そこでここでは、 PLT Scheme のサイトからダウンロードできるバイナリパッケージに対するバイナリパッチを作成してみましょう。

バイナリパッチって、どうやって作るんでしょうね! MrEd ライブラリは DLL として分離されていますから、この DLL だけ入れ替えれば、 一応はパッチを当てたことになりますね。 でも、そんなことをしたら、マイナーバージョン アップのたびに DLL を配布しないといけなくなり、 あんまりイケテないね!

じゃあ、wxTextSize 関数の部分だけを差し替えるパッチを作ってみようか。 でも、これをやるのは、大変なんだよね。 なんでかっていうと、wxTextSize 関数の逆アセンブルコードを見てみれば分かる。

static void wxTextSize(HDC dc, Scheme_Hash_Table *ht, wchar_t *ustring, int d, int alen, double *ow, double *oh)
{
11017FC0  sub         esp,14h
  /* Gets the text size, caching the result in font when alen == 1 */

  if ((alen == 1) && is_nt()) {
11017FC3  cmp         dword ptr [esp+1Ch],1
11017FC8  push        ebp
11017FC9  mov         ebp,dword ptr [esp+28h]
11017FCD  push        esi
11017FCE  mov         esi,eax
11017FD0  jne         wxTextSize+0CBh (1101808Bh)
11017FD6  call        is_nt (11016D10h)
11017FDB  test        eax,eax
11017FDD  je          wxTextSize+0CBh (1101808Bh)
    double *sz;

    if (ht) {
11017FE3  test        edi,edi
11017FE5  je          wxTextSize+57h (11018017h)
      sz = (double *)scheme_hash_get(ht, scheme_make_integer(ustring[d]));
11017FE7  mov         eax,dword ptr [esp+20h]
11017FEB  movzx       ecx,word ptr [eax+esi*2]
11017FEF  add         ecx,ecx
11017FF1  or          ecx,1
11017FF4  push        ecx
11017FF5  push        edi
11017FF6  call        dword ptr [__imp__scheme_hash_get (11167700h)]
11017FFC  add         esp,8
    } else {
      sz = NULL;
    }

    if (sz) {
11017FFF  test        eax,eax
11018001  je          wxTextSize+57h (11018017h)
      *ow = sz[0];
11018003  fld         qword ptr [eax]
11018005  mov         edx,dword ptr [esp+28h]
11018009  fstp        qword ptr [edx]
1101800B  pop         esi
      *oh = sz[1];
1101800C  fld         qword ptr [eax+8]
      GetCharABCWidthsFloatW(dc, ustring[d], ustring[d], &cw);
      *ow = (cw.abcfA + cw.abcfB + cw.abcfC);
      GetTextExtentPointW(dc, ustring XFORM_OK_PLUS d, alen, &sizeRect);
      *oh = (double)sizeRect.cy;

結局ね、scheme_make_integer とか GetTextExtentPoint とかは別の DLL に存在する関数なんだけど、 アセンブリコードはそれらの DLL 関数のサンクコード経由で呼び出されるわけね。 だけど、サンクコードのアドレスって、マイナーバージョン アップのたびに変わる可能性があるから、 決め打ちできない。とすると、それらのサンクコードのアドレスを解析して動的にコード生成するような パッチを作らないといけないことになる。そんな大変なことやりたくないよねー。

だから今回は、関数単位のパッチではなく、もっと簡単な方法でやることにした。 まず、wxTextSize 関数の次のコードに注目してみて。

  if ((alen == 1) && is_nt()) {

GetTextExtentPoint 関数で文字幅を取得するんだったら、結局この条件ブロックは不要なんだよね。 このブロックをごそっと削除してしまえばいいはず。 だいたい、こういう条件分岐は、アセンブリコードでは test とか cmp とかの後で jnz とか je とかやってることが多いから、そういう条件付きジャンプ命令を無条件ジャンプ命令 jmp に書き換えてやればいいよね。

というわけで、次は、MrEd DLL 内の実際のアセンブリコードを抽出してみよう。 単純に逆アセンブルしてもいいけど、命令の書き換えを行うので、そのアセンブリコードも知る必要があることと、 正常動作することも確認したいので、ここでは OllyDbg を使ってみました。

OllyDbg で MrEd.exe をデバッグ

MrEd ライブラリを使用している簡単なプログラムとして、PLT Scheme に付属の MrEd.exe を使います。 これを OllyDbg からデバッグ起動しましょう。OllyDbg のメニューから [File]-[Open] を選択して、 MrEd.exe を選択します。

実行前にブレークしますが、[Debug]-[Run] メニューで実行を継続しましょう。 PLT Scheme では MrEd DLL は実行時に動的にロードされるので、 実行前にブレークした段階では、MrEd DLL の中身がアドレス空間にマッピングされていません。 正常に起動すると MrEd REPL の次のような画面が表示されます。 この画面に日本語を入力しても、変な表示になります。

この段階で MrEd DLL がアドレス空間にマッピングされていますので、 中を覗いて wxTextSize 関数のアセンブリコードを抽出しましょう。 OllyDbg のメニューから [View]-[Executable modules] を選択して、 MrEd.exe が使用している DLL の一覧を表示します。

一覧から libmred3 をダブルクリックすると、DLL が逆アセンブルされます (実際のファイルは lib/libmred3m_6ncbi0.dll のような名前)。 次に、OllyDbg を右クリックして [Search for]-[All intermodular calls] メニューを選択します。

すると、モジュール間の呼び出しの一覧が現れます。次に、その一覧を右クリックして、 [Sort by]-[Destination] メニューを選択します。 そして、一覧の中から GetCharABCWidthsFlowW の行を見つけ出して、ダブルクリックしましょう。 この行は1つしかないと思います。つまり、libmred3 DLL からは、この関数を1か所でしか呼び出していないということです。 ということは、この呼び出しを行っているのが、wxTextSize 関数ということですね。

GetCharABCWidthsFlowW を呼び出しているアセンブリコードから、少し上に行くと、 wxTextSize 関数の先頭にたどり着きます。

予想したとおり、JNZ 命令による条件分岐がありますね。 これを無条件ジャンプに変えてみましょう。 JNZ の行を選択してスペースキーを押しますと、JNZ 命令の全文が出ますので、 JNZ の部分を JMP に変えて [Assemble] ボタンを押します。

そうすると、アセンブリコードが赤く変更されます。

この状態で、MrEd REPL に日本語を入力してみましょう。 きれいに表示されましたね!

5. ダウンロード

上記のバイナリパッチを次からダウンロードできます。

これを使えば、PLT Scheme を再ビルドすることなく、日本語がきれいに表示されます。

DrScheme 4.0~4.2.3 で動作検証しています。

mred-font-fix.zip [41KB]


[戻る]