January 5, 2012

Cocoa Emacs + inline patch で "Args out of range: x, y" エラーが頻発する問題

Google の検索をしてもほとんど引っかからないので、他の人はこの問題にはハマってないのであろうか、というのが一番の疑問だったりするのだけれども、自分の環境では Cocoa Emacs + inline patch がイマイチ不安定で、どうにも困った。

現時点(2012年1月初頭)では、
  • Emacs23 の最新版 emacs-23.3b.tar.gz (http://ftp.gnu.org/gnu/emacs/emacs-23.3b.tar.gz)
  • MacEmacs JP の inline patch の最新版 trunk (http://sourceforge.jp/projects/macemacsjp/) - 以下を含む:

    • emacs-inline.patch: patch for input method
    • font.patch: patch to fix trembling font when user inputs japanese by input method
    • xcode4.patch: patch to compile by xcode4 (gcc-4.2 64bit)
    • lion.patch: patch to compile on MacOSX 10.7. It contains xcode4.patch.

  • KAYAC さんの OS X Lion 用のフルスクリーン用パッチ (http://bm11.kayac.com/2009/project/opensource/kayac-emacs)
  • popup 時にクラッシュする問題に対するパッチ (http://moimoitei.blogspot.com/2010/05/fix-cocoa-emacs-23.html)
  • MacEmacsJP の ML で報告された SIGSEGV 問題(http://sourceforge.jp/projects/macemacsjp/lists/archive/users/2011-August/001699.html) に対するパッチ (https://github.com/n-miyo/homebrew/commit/f45111c99bf287b197f21a6faf28417e9ffa72ef)
でビルドして、Lion についてくるOS標準のことえりで日本語入力をしている。

以上の先達の成果物によって Emacs 自体がクラッシュしてしまうことはなくなったのだが、漢字変換中にしばしば "Args out of range: x, y" エラーが頻発し、これが出ると変換された文字がバッファ内に表示されないという問題が出現する。

こういうときには Google に頼るのだが、数件の問題報告は見るものの、パッチが出てきた様子はない。
というわけで、Email も Emacs + Wanderlust で読み書きしている自分には死活問題なので、Lisp は知らないけれど、2011〜2012の年末年始にこの問題を追うことにした。

基本はいわゆる「printf デバッグ」作戦を Emacs Lisp に対してやってみたのだが、src/nsterm.m の "#define NS_KEYLOG 0" を 1 にして入力キーの様子を stdout に出しつつ、lisp/term/ns-win.el に message 出力を仕込んでみたところ、問題っぽいところを発見した。

こんなふうに message 出力を仕込んで
--- lisp/term/ns-win.el.patched 2012-01-01 00:56:06.000000000 +0900
+++ lisp/term/ns-win.el 2012-01-05 18:28:27.000000000 +0900
@@ -649,10 +649,20 @@
 `ns-working-overlay' and `ns-marked-overlay'.  Any previously existing
 working text is cleared first. The overlay is assigned the faces 
 `ns-working-text-face' and `ns-marked-text-face'."
+  (message "DEBUG: pos=[\%d]  len=[\%d]  ns-working-text=[\%s]  text-len=[\%d]" pos len ns-working-text (length ns-working-text))
   (ns-delete-working-text)
+  (message "DEBUG: ns-delete-working-text done")
   (let ((start (point)))
-    (put-text-property pos len 'face 'ns-working-text-face ns-working-text)
+    ; check pos and ns-working-text length
+    (if (<= pos (length ns-working-text))
+      (progn
+      (put-text-property pos len 'face 'ns-working-text-face ns-working-text)
+      (message "DEBUG: put-text-property pos len 'face 'ns-working-text-face ns-working-text done")
+    )
+      (message "DEBUG: SKIPPED put-text-property pos len 'face 'ns-working-text-face ns-working-text")
+    )
     (insert ns-working-text)
+    (message "DEBUG: insert-ns-working-text done")
     (if (= len 0)
         (overlay-put (setq ns-working-overlay
                            (make-overlay start (point) (current-buffer) nil t))

この状態でことえり経由で文字をピコピコ入力する経過を *Message* バッファで見張ってみた。そうしたところ、得られた結果がこれ:
DEBUG: pos=[1]  len=[0]  ns-working-text=[k]  text-len=[1]
DEBUG: ns-delete-working-text done
DEBUG: put-text-property pos len 'face 'ns-working-text-face ns-working-text done
DEBUG: insert-ns-working-text done
DEBUG: pos=[1]  len=[0]  ns-working-text=[か]  text-len=[1]
DEBUG: ns-delete-working-text done
DEBUG: put-text-property pos len 'face 'ns-working-text-face ns-working-text done
DEBUG: insert-ns-working-text done
DEBUG: pos=[2]  len=[0]  ns-working-text=[かn]  text-len=[2]
DEBUG: ns-delete-working-text done
DEBUG: put-text-property pos len 'face 'ns-working-text-face ns-working-text done
DEBUG: insert-ns-working-text done
DEBUG: pos=[3]  len=[0]  ns-working-text=[かんg]  text-len=[3]
DEBUG: ns-delete-working-text done
DEBUG: put-text-property pos len 'face 'ns-working-text-face ns-working-text done
DEBUG: insert-ns-working-text done
DEBUG: pos=[3]  len=[0]  ns-working-text=[かんが]  text-len=[3]
DEBUG: ns-delete-working-text done
DEBUG: put-text-property pos len 'face 'ns-working-text-face ns-working-text done
DEBUG: insert-ns-working-text done
DEBUG: pos=[4]  len=[0]  ns-working-text=[かんがえ]  text-len=[4]
DEBUG: ns-delete-working-text done
DEBUG: put-text-property pos len 'face 'ns-working-text-face ns-working-text done
DEBUG: insert-ns-working-text done
DEBUG: pos=[5]  len=[0]  ns-working-text=[かんがえt]  text-len=[5]
DEBUG: ns-delete-working-text done
DEBUG: put-text-property pos len 'face 'ns-working-text-face ns-working-text done
DEBUG: insert-ns-working-text done
DEBUG: pos=[5]  len=[0]  ns-working-text=[かんがえた]  text-len=[5]
DEBUG: ns-delete-working-text done
DEBUG: put-text-property pos len 'face 'ns-working-text-face ns-working-text done
DEBUG: insert-ns-working-text done
DEBUG: pos=[6]  len=[0]  ns-working-text=[かんがえたほ]  text-len=[6]
DEBUG: ns-delete-working-text done
DEBUG: put-text-property pos len 'face 'ns-working-text-face ns-working-text done
DEBUG: insert-ns-working-text done
DEBUG: pos=[6]  len=[0]  ns-working-text=[かんがえたほ]  text-len=[6]
DEBUG: ns-delete-working-text done
DEBUG: put-text-property pos len 'face 'ns-working-text-face ns-working-text done
DEBUG: insert-ns-working-text done
DEBUG: pos=[7]  len=[0]  ns-working-text=[かんがえたほう]  text-len=[7]
DEBUG: ns-delete-working-text done
DEBUG: put-text-property pos len 'face 'ns-working-text-face ns-working-text done
DEBUG: insert-ns-working-text done
DEBUG: pos=[8]  len=[0]  ns-working-text=[かんがえたほうh]  text-len=[8]
DEBUG: ns-delete-working-text done
DEBUG: put-text-property pos len 'face 'ns-working-text-face ns-working-text done
DEBUG: insert-ns-working-text done
DEBUG: pos=[8]  len=[0]  ns-working-text=[かんがえたほうほ]  text-len=[8]
DEBUG: ns-delete-working-text done
DEBUG: put-text-property pos len 'face 'ns-working-text-face ns-working-text done
DEBUG: insert-ns-working-text done
DEBUG: pos=[9]  len=[0]  ns-working-text=[かんがえたほうほう]  text-len=[9]
DEBUG: ns-delete-working-text done
DEBUG: put-text-property pos len 'face 'ns-working-text-face ns-working-text done
DEBUG: insert-ns-working-text done
DEBUG: pos=[10]  len=[0]  ns-working-text=[かんがえたほうほうh]  text-len=[10]
DEBUG: ns-delete-working-text done
DEBUG: put-text-property pos len 'face 'ns-working-text-face ns-working-text done
DEBUG: insert-ns-working-text done
DEBUG: pos=[10]  len=[0]  ns-working-text=[考えた方法は]  text-len=[6]                <----- !!!
DEBUG: ns-delete-working-text done                                                    <----- !!!
DEBUG: SKIPPED put-text-property pos len 'face 'ns-working-text-face ns-working-text  <----- !!!
DEBUG: insert-ns-working-text done
DEBUG: pos=[0]  len=[3]  ns-working-text=[考えた方法は]  text-len=[6]
DEBUG: ns-delete-working-text done
DEBUG: put-text-property pos len 'face 'ns-working-text-face ns-working-text done
DEBUG: insert-ns-working-text done

あれ、ns-working-text がおかしい。「かんがえたほうほうh」のあと "a" をタイプしたので「かんがえたほうほうは」になるはずなのに、それがなくて、いきなり感じ変換されちゃった後の「考えた方法は」になってるね。なのに、そのとき pos の数値はまだ「かんがえたほうほうは」をベースにした 10 になっている。結果、ns-working-text の方の文字列はすでに漢字変換後のデータになっていて 6 文字しかないのに、pos の数値は 10 になってる。ふむむ。

ソースの中で関連しそうなのは src/keyboard.c の
   4204       struct input_event *event;
   4205 
   4206       event = ((kbd_fetch_ptr < kbd_buffer + KBD_BUFFER_SIZE)
   4207                ? kbd_fetch_ptr
   4208                : kbd_buffer);
   
       ...... (snip) ......
   
   4241 #if defined (HAVE_NS)
   4242       else if (event->kind == NS_TEXT_EVENT)
   4243         {
   4244           if (event->code == KEY_NS_PUT_WORKING_TEXT)
   4245             obj = Fcons (intern ("ns-put-working-text"), Qnil);
   4246           else if (event->code == KEY_NS_UNPUT_WORKING_TEXT)
   4247             obj = Fcons (intern ("ns-unput-working-text"), Qnil);
   4248           else if (event->code == KEY_NS_PUT_MARKED_TEXT)
   4249             obj = Fcons (intern ("ns-put-marked-text"), event->arg);
   4250           kbd_fetch_ptr = event + 1;
   4251           if (used_mouse_menu)
   4252             *used_mouse_menu = 1;
   4253         }
   4254 #endif

と、さっき手を入れた lisp/term/ns-win.el の
    638 (defun ns-put-marked-text (event)
    639   (interactive "e")
    640 
    641   (let ((pos (nth 1 event))
    642         (len (nth 2 event)))
    643     (if (ns-in-echo-area)
    644         (ns-echo-marked-text pos len)
    645       (ns-insert-marked-text pos len))))
    646 
    647 (defun ns-insert-marked-text (pos len)
    648   "Insert contents of `ns-working-text' as UTF-8 string and mark with
    649 `ns-working-overlay' and `ns-marked-overlay'.  Any previously existing
    650 working text is cleared first. The overlay is assigned the faces 
    651 `ns-working-text-face' and `ns-marked-text-face'."
    652   (message "DEBUG: pos=[\%d]  len=[\%d]  ns-working-text=[\%s]  text-len=[\%d]" pos len ns-working-text (length ns-working-text))
    653   (ns-delete-working-text)
    654   (message "DEBUG: ns-delete-working-text done")
    655   (let ((start (point)))
    656     ; check pos and ns-working-text length
    657     (if (<= pos (length ns-working-text))
    658       (progn
    659       (put-text-property pos len 'face 'ns-working-text-face ns-working-text)
    660       (message "DEBUG: put-text-property pos len 'face 'ns-working-text-face ns-working-text done")
    661     )
    662       (message "DEBUG: SKIPPED put-text-property pos len 'face 'ns-working-text-face ns-working-text")
    663     )
    664     (insert ns-working-text)
    665     (message "DEBUG: insert-ns-working-text done")
    666     (if (= len 0)
    667         (overlay-put (setq ns-working-overlay
    668                            (make-overlay start (point) (current-buffer) nil t))
    669                      'face 'ns-working-text-face)
    670       (overlay-put (setq ns-working-overlay
    671                          (make-overlay start (point) (current-buffer) nil t))
    672                    'face 'ns-unmarked-text-face)
    673       (overlay-put (setq ns-marked-overlay 
    674                          (make-overlay (+ start pos) (+ start pos len)
    675                                        (current-buffer) nil t))
    676                    'face 'ns-marked-text-face))
    677     (goto-char (+ start pos))))

のあたり。

keyboard.c で (input_event)event にセットしたイベントを引数にして Lisp 関数の ns-put-marked-text を呼んでいて、ns-win.el 側で定義されている Lisp 関数 ns-put-marked-text 側で渡された引数から変数 pos / len に何文字目をいじるのか、を入れているようなのけれど、稀に pos/len の数値は漢字に変換される前のデータなのに対し、ns-working-text の文字列は漢字に変換された後のデータとなっていて、ここに情報に不一致があるために ns-win.el 側で put-text-property するところで pos/len のデータが ns-working-text の文字数をはみ出してしまうので out of range エラーとなるみたい。
タイミングの問題かなにかで、event が発生した後、put-text-property するまでの間に ns-working-text が更新されてしまっているのか?

というわけで、本当なら pos/len のデータ ns-working-text を一致させるべきなのだろうけど、どうやって同期を取ったらいいのかわからないので、とりあえず pos の数値が ns-working-text の文字数を超えていたら put-text-property はしないでやり過ごす、ということにした。

こんな感じ:

--- lisp/term/ns-win.el.patched 2012-01-05 20:07:58.000000000 +0900
+++ lisp/term/ns-win.el 2012-01-05 20:10:16.000000000 +0900
@@ -651,7 +651,8 @@
 `ns-working-text-face' and `ns-marked-text-face'."
   (ns-delete-working-text)
   (let ((start (point)))
-    (put-text-property pos len 'face 'ns-working-text-face ns-working-text)
+    (if (<= pos (length ns-working-text))
+        (put-text-property pos len 'face 'ns-working-text-face ns-working-text))
     (insert ns-working-text)
     (if (= len 0)
         (overlay-put (setq ns-working-overlay

副作用として、漢字変換で入力している文字がたま〜に位置がずれて挿入されちゃうのが確認されているのですが、これで out of range エラーなく動いているので、とりあえず ok とします。



(2012/01/07 追記): 上の副作用があったので、こうしました。
--- lisp/term/ns-win.el.patched 2012-01-05 20:07:58.000000000 +0900
+++ lisp/term/ns-win.el 2012-01-05 20:10:16.000000000 +0900
@@ -651,6 +651,8 @@
 `ns-working-text-face' and `ns-marked-text-face'."
   (ns-delete-working-text)
   (let ((start (point)))
+   (if (<= pos (length ns-working-text))
+    (progn
     (put-text-property pos len 'face 'ns-working-text-face ns-working-text)
     (insert ns-working-text)
     (if (= len 0)
@@ -664,7 +666,7 @@
                          (make-overlay (+ start pos) (+ start pos len)
                                        (current-buffer) nil t))
                    'face 'ns-marked-text-face))
-    (goto-char (+ start pos))))
+    (goto-char (+ start pos))))))
     
 (defun ns-echo-marked-text (pos len)
   "Echo contents of `ns-working-text' in message display area.
これで、漢字変換している文字がずれて挿入される問題も回避できている模様。
ただ、これは根本的な解決策ではない。本当は、event の中身と ns-working-text の同期をとれるようにしないといけないはず。まあ、でも、こうしてブログに晒しておけば、だれかもっと造詣の深い心優しい人がもっとまともなパッチを作ってくれるかもしれないし。

(2012/01/14 追記): ソースの引っ張り方とそれぞれのパッチの当て方を教えて、というリクエストがありました。長くなるので、コメント (Comment) のほうに一通りの手順を載せておきます。たしかに trunk とか、パッチのパッチみたいなのもあって、わかりにくかったですね。ごめんなさい。