GNU Emacs に内蔵されている Lisp インタプリタを C インタフェースから操作する実験

はじめに

今から約 1 ヶ月前、GNU Emacs のコードリーディングをはじめました。祝日などの時間を使って少しずつ GNU Emacs の解析を進めました。今日、ようやくコードリーディングを開始した時に立てた目標が達成できたので、このエントリーを書くことにしました。

GNU Emacs のコードリーディングをはじめた当初の目標は、GNU Emacs に内蔵された 「Lisp インタプリタ」をどうにかショートカットして「C インタフェース」から直接操作することでした。

では、なぜそのような目標を設定したのか少し説明します。

GNU Emacs は、Lisp システムと密に連携してエディタの機能を統合しています。GNU Emacs の世界は Lisp システムによって支配されていると言えます。したがって、GNU Emacs の仕組みを理解するには GNU Emacs に内蔵された Lisp システムの理解が必要不可欠です。確かに Lisp の言語的な側面を学びたいならば、ELisp を書けば、Lisp 自身の勉強にはなるでしょうが、私はそこにはあまり興味がありません。それよりも、むしろどのようにして Lisp システムが GNU Emacsとうまく統合されているのかに興味があります。このような興味があったため、Lisp システムが用意している様々な C インタフェースを通して、Lisp インタプリタを直接操作することを目標にしました。

手がかり

Lisp インタプリタを自由に操作するには、以下を知る必要がありました。

  1. GNU Emacs のコードに散在する DEFUN マクロの役割
  2. Read: 式を Lisp インタプリタが解釈可能な構文木データに変換する方法
  3. Eval: Lisp インタプリタの核の動作方法
  4. Print: Lisp インタプリタで評価した結果を印字可能な文字列に変換する方法
DEFUN マクロ: Lisp と C インタフェースを同時に定義し、Lisp の組み込み関数を作る

GNU Emacs が記述されている C のコードのファイルをいくつもざっと見ていくと、Lisp の世界と C の世界をうまくつなぎ合わせるためのマクロに気がつきました。DEFUN マクロです。例えば、eval 関数は、DEFUNマクロを使って以下のように定義されています。重要なのは、DEFUN マクロの第一引数(eval 関数だと"eval")と第二引数(eval 関数だと、Feval)、そして定義しようとしている関数への引数(eval 関数だと、Lisp_Object form)です。DEFUN マクロを使うことで、Lisp と C インタフェースを同時に定義しつつ、ELisp からも利用できる組み込み関数を作ることができます。

emacs-22.1/src/eval.c

DEFUN ("eval", Feval, Seval, 1, 1, 0,
       doc: /* Evaluate FORM and return its value.  */)
     (form)
     Lisp_Object form;
{
  Lisp_Object fun, val, original_fun, original_args;
  Lisp_Object funcar;
  struct backtrace backtrace;
  struct gcpro gcpro1, gcpro2, gcpro3;

  if (handling_signal)
    abort ();

  if (SYMBOLP (form))
    return Fsymbol_value (form);
  if (!CONSP (form))
    return form;

  QUIT;
[...]

DEFUN で定義された他の関数と比較すればわかるのですが、C インタフェースからアクセスできる関数は、prefix として、必ず"F"がつきます。また、C からアクセスできる関数の名前は、"_"を使って単語を区切っていくのに対し、Lisp からアクセスできる関数は、"-"を使って区切られます。これらのルールは GNU Emacs のコード全体でのルールになっているようです。

emacs-22.1/src/buffer.c

DEFUN ("set-buffer-multibyte", Fset_buffer_multibyte, Sset_buffer_multibyte,
       1, 1, 0,
[...]

こうして、DEFUN マクロの仕組みを理解することで、GDB から Lisp システムをつなぐ重要なインタフェースでブレークポイントを自由に置けるようになりました。例えば、こんな感じでブレークポイントを置いて、バックトレースを取れるようになりました。

% cd ~/src/emacs-22.1/src
% gdb ./emacs
[...]
(gdb) b Feval
Breakpoint 3 at 0xe6ebf: file eval.c, line 2205.
(gdb) r
Starting program: /Users/ysano/src/emacs-22.1/src/emacs -geometry 80x40+0+0
Reading symbols for shared libraries .....................................................................+ done
Reading symbols for shared libraries . done

Breakpoint 3, Feval (form=1762389) at eval.c:2205
2205      if (handling_signal)
gdb) bt
#0  Feval (form=1762389) at eval.c:2205
#1  0x000e97f1 in internal_lisp_condition_case (var=41944073, bodyform=1762389, handlers=1762509) at eval.c:1426
#2  0x00117470 in Fbyte_code (bytestr=1762011, vector=1762028, maxdepth=9) at bytecode.c:869
#3  0x000e76d3 in funcall_lambda (fun=1761972, nargs=1, arg_vector=0xbfffeb44) at eval.c:3184
#4  0x000e7b93 in Ffuncall (nargs=2, args=0xbfffeb40) at eval.c:3054
#5  0x0011853a in Fbyte_code (bytestr=1762923, vector=1762940, maxdepth=2) at bytecode.c:679
#6  0x000e76d3 in funcall_lambda (fun=1762900, nargs=0, arg_vector=0xbfffecd4) at eval.c:3184
#7  0x000e7b93 in Ffuncall (nargs=1, args=0xbfffecd0) at eval.c:3054
#8  0x000e8f60 in call0 (fn=42299985) at eval.c:2761
#9  0x0000bfbd in init_display () at dispnew.c:6937
#10 0x00079e2c in main (argc=3, argv=0xbfffee44) at emacs.c:1658

Lisp Backtrace:
"face-set-after-frame-default" (0x1500fdc)
"tty-set-up-initial-frame-faces" (0xb217)
Lisp 対話モード

Read-Eval-Print については、EmacsLisp 対話モードがヒントになりました。Lisp 対話モード (M-x lisp-interaction-mode) では、カーソルの直前にある S 式を C-j で評価してくれます。この Lisp 対話モードでは、まさに Read => Eval => Print を行っています。したがって、Lisp 対話モードの仕組みを調べれば、解決の糸口が見つかるだろうと予想しました。

Lisp 対話モードになった状態で、M-x describe-mode を実行し、C-j が何の関数にバインドされているのか調べました。そうすると、C-j は eval-print-last-sexp 関数にバインドされていることがわかりました。

Commands:
Delete converts tabs to spaces as it moves back.
Paragraphs are separated only by blank lines.
Semicolons start comments.
key             binding
---             -------

TAB		lisp-indent-line
C-j		eval-print-last-sexp
ESC		Prefix Command
DEL		backward-delete-char-untabify

M-TAB		lisp-complete-symbol
C-M-q		indent-pp-sexp
C-M-x		eval-defun

C-M-q		indent-sexp  (shadowed)

grep で eval-print-last-sexp 関数の定義を調べました。
(注意: このエントリーを書いていて気がついたのですが、C-j にバインドされているのは、本当は edebug-eval-print-last-sexp ではなく eval-print-last-sexp です。しかし、結果として、edebug-eval-print-last-sexp の方が役に立ったので、こちらを使います。)

emacs-22.1/lisp/emacs-lisp/edebug.el

(defun edebug-eval-print-last-sexp ()
  "Evaluate sexp before point in outside environment; insert value.
This prints the value into current buffer."
  (interactive)
  (let* ((edebug-form (edebug-last-sexp))
     (edebug-result-string
      (edebug-outside-excursion
       (edebug-safe-prin1-to-string (edebug-safe-eval edebug-form))))
     (standard-output (current-buffer)))
    (princ "\n")
    ;; princ the string to get rid of quotes.
    (princ edebug-result-string)
    (princ "\n")
    ))   

edebug-last-sexp 関数が怪しそうだったので、edebug-last-sexp 関数の定義を見ました。emacs-22.1/lisp ディレクトリ以下の中には、read-from-string 関数の定義は見つかりませんでした。

emacs-22.1/lisp/emacs-lisp/edebug.el

(defun edebug-last-sexp ()
  ;; Return the last sexp before point in current buffer.
  ;; Assumes Emacs Lisp syntax is active.
  (car 
   (read-from-string
    (buffer-substring
     (save-excursion
       (forward-sexp -1)
       (point))
     (point)))))

そこで、emacs-22.1/src 以下で grep してみたところ、C 言語で定義された関数が見つかりました。先ほど説明した DEFUN マクロで定義されています。どうやら、この関数を使えば、文字列として表現された S 式を Lisp インタプリタ (eval) が解釈できる内部形式(構文木)に変換できそうです。

emacs-22.1/src/lread.c

DEFUN ("read-from-string", Fread_from_string, Sread_from_string, 1, 3, 0,
       doc: /* Read one Lisp expression which is represented as text by STRING.
Returns a cons: (OBJECT-READ . FINAL-STRING-INDEX).
START and END optionally delimit a substring of STRING from which to read;
 they default to 0 and (length STRING) respectively.  */)
     (string, start, end)
     Lisp_Object string, start, end;
{
  Lisp_Object ret;
  CHECK_STRING (string);
  /* read_internal_start sets read_from_string_index. */
  ret = read_internal_start (string, start, end);
  return Fcons (ret, make_number (read_from_string_index));
}
評価した結果を印字可能な文字列に

Read-Eval-Print の処理が書かれた Elisp (emacs-22.1/lisp/emacs-lisp/edebug.el) を読んでも、評価した結果を印字可能な文字列に変換する方法はわかりませんでした。

そこで、違うソースコードを読みあさりました。そうすると、eval-buffer 関数の中で、readevalloop と言ういかにも怪しい関数を見つけました。

emacs-22.1/src/lread.c

DEFUN ("eval-buffer", Feval_buffer, Seval_buffer, 0, 5, "",
       doc: /* Execute the current buffer as Lisp code.
Programs can pass two arguments, BUFFER and PRINTFLAG.
BUFFER is the buffer to evaluate (nil means use current buffer).
PRINTFLAG controls printing of output:
A value of nil means discard it; anything else is stream for print.

If the optional third argument FILENAME is non-nil,
it specifies the file name to use for `load-history'.
The optional fourth argument UNIBYTE specifies `load-convert-to-unibyte'
for this invocation.

The optional fifth argument DO-ALLOW-PRINT, if non-nil, specifies that
`print' and related functions should work normally even if PRINTFLAG is nil.

This function preserves the position of point.  */)
     (buffer, printflag, filename, unibyte, do_allow_print)
     Lisp_Object buffer, printflag, filename, unibyte, do_allow_print;
{
[...]
  readevalloop (buf, 0, filename, Feval,
		!NILP (printflag), unibyte, Qnil, Qnil, Qnil);
[...]
}

コードを追ってみると、Fprin1 関数を使えば、Feval で評価した結果を印字可能な文字列に変換できることがわかりました。

emacs-22.1/src/lread.c

/* UNIBYTE specifies how to set load_convert_to_unibyte
   for this invocation.
   READFUN, if non-nil, is used instead of `read'.

   START, END specify region to read in current buffer (from eval-region).
   If the input is not from a buffer, they must be nil.  *

static void
readevalloop (readcharfun, stream, sourcename, evalfun,
	      printflag, unibyte, readfun, start, end)
     Lisp_Object readcharfun;
     FILE *stream;
     Lisp_Object sourcename;
     Lisp_Object (*evalfun) ();
     int printflag;
     Lisp_Object unibyte, readfun;
     Lisp_Object start, end;
{
[...]
      /* Now eval what we just read.  */
      val = (*evalfun) (val);

      if (printflag)
	{
	  Vvalues = Fcons (val, Vvalues);
	  if (EQ (Vstandard_output, Qt))
	    Fprin1 (val, Qnil);
	  else
	    Fprint (val, Qnil);
	}
[...]

Fprin1 関数でも、機能的には十分でしたが、標準出力などの stream に対して結果を出力するのではなく、文字列データとして出力する関数があれば良いなあと思いました。それで、探してみると、Fprin1_to_string 関数でそれができることがわかりました。

emacs-22.1/src/print.c

DEFUN ("prin1-to-string", Fprin1_to_string, Sprin1_to_string, 1, 2, 0,
       doc: /* Return a string containing the printed representation of OBJECT.
OBJECT can be any Lisp object.  This function outputs quoting characters
when necessary to make output that `read' can handle, whenever possible,
unless the optional second argument NOESCAPE is non-nil.  For complex objects,
the behavior is controlled by `print-level' and `print-length', which see.

OBJECT is any of the Lisp data types: a number, a string, a symbol,
a list, a buffer, a window, a frame, etc.

A printed representation of an object is text which describes that object.  */)
     (object, noescape)
     Lisp_Object object, noescape;
{
[...]

統合

ここまで調査してきた結果を統合して、GNU Emacs を改造しました。簡単のため、emacs の引数に S 式を与え、それを評価した結果を標準出力に出力するようにしました。

こんなコードを作りました。

emacs.c の main 関数の最後にある Frecursive_edit 関数を実行しないようにして、その直前で S 式を評価するコードを組み込みました。Frecursive_edit 関数を実行しないと、Emacs は編集のためのユーザインタフェースを立ち上げません。Frecursive_edit 関数の直前は、Feval 関数で S 式を評価するための準備が十分にできているため、この場所にコードを書くことにしました。

build_string 関数は、C の文字列を Lisp_Object 形式に変換します。Fread_from_string 関数で、S 式を構文木に変換します。その結果は、cons セルになっていて、ほしいのは car の方なので、Fcdr 関数を使っています。構文木を Feval 関数に与え、その結果を Fprin1_to_string 関数で、印字可能な文字列に変換しています。XSTRING マクロは、Lisp_Object から C の文字列にアクセスするために使いました。

--- emacs.c.orig	2008-02-11 09:28:08.000000000 +0900
+++ emacs.c	2008-02-11 12:04:49.000000000 +0900
@@ -1758,8 +1758,32 @@
   tzset ();
 #endif /* defined (LOCALTIME_CACHE) */
 
+#define HACK_REP
+#ifdef HACK_REP
+  {
+    Lisp_Object str;
+    Lisp_Object expr_and_pos;
+    Lisp_Object expr;
+    Lisp_Object result;
+
+    if (argc < 2) {
+      fprintf(stderr, "Usage: %s sexp\n", argv[0]);
+      fprintf(stderr, "Example: %s \"(+ 1 2)\"\n", argv[0]);
+      exit(1);
+    }
+
+    str = build_string(argv[1]);
+    expr_and_pos = Fread_from_string(str, Qnil, Qnil);
+    expr = Fcar(expr_and_pos);
+    result = Feval(expr);
+  
+    str = Fprin1_to_string(result, Qt);
+    printf("=> %s\n", XSTRING(str)->data);
+  }
+#else /* HACK_REP */
   /* Enter editor command loop.  This never returns.  */
   Frecursive_edit ();
+#endif /* HACK_REP */
   /* NOTREACHED */
   return 0;
 }

ビルドして実行するとこんな感じで S 式を評価してくれます。

% cd ~/src/emacs-22.1/src
% make temacs
% ./temacs "(+ 1 2)"
=> 3
% ./temacs "(cons 'hello 'world)"
=> (hello . world)
% ./temacs "(car(cons 'hello 'world))"
=> hello

実行ファイルが emacs ではなく temacs となっている所に注意して下さい。実行ファイル emacs は、実行ファイル temacs によって生成されます。しかし、今回 Emacs を改造して、S 式の評価以外何もできないようにしたので、temacs ファイルから emacs ファイルを生成できなくなりました。そこで、temacs を使うようにしました。

まとめ

GUN Emacs を改造して、C インタフェースから S 式を評価できるようになりました。今回の実験によって、GNU EmacsLisp システムについてより一層理解が深まりました。

実は、GNU EmacsGDBデバッグしやすくするための工夫が入っています。例えば、GDB セッション中に Lisp_Object の内容をインスペクトする方法なんかが提供されています。また今度の機会に紹介できたらなと思います。