Xdebug にパッチを送ってみた。


PHPデバッグのお供には Xdebug が定番です。

Xdebug を導入するだけで、 var_dump はHTMLでハイライトされ、例外やエラーが発生したときは見やすく彩られたバックトレースが表示されます。また、適切な設定と eclipse (vimでも可能らしい)があれば、ソースレベルのリモートデバッグが可能になります。さらには、プロファイリングもできるので、ボトルネックの検出にも一役買います。

一度使うと、これ無しに開発するなど考えられないライブラリです。

そんな Xdebug を使っている時に、バグらしき現象に遭遇しました。


xdebug の設定に、 xdebug.show_local_vars という設定があります。
デフォルトでは 0 になっているのですが、これを 0 以外の値にしてやると、バックトレースが表示されるときに、ローカル変数の値を出力してくれるようになります。

これが便利で、基本的に有効にしているのですが、PHP5.2系では問題なく出力されていたのですが、PHP5.3 にしてから 変数名は列挙されるものの、すべての値が Undefined と表示されてしまっていました。

そのとき利用していたバージョンは、Xdebug 2.1.0 beta1 。まぁ、まだベータだし、そのうち直るだろう。なんて思っていたのですが、RC1 がリリースされても直る気配がない。仕方ないので、ソースコードを追ってみることにしました。

まずは、 Xdebugソースコードをダウンロード。このときは RC1のソースコードで追うことにしました。

最初に探したのが、 Variables in local scope の文字列。検索してみると、 xdebug_stack.c にありました。 static char* html_formats[12] という変数の一部です。なので今度は、それを利用している箇所を html_formats で検索。利用しているのは、以下の関数。

  • xdebug_append_error_head
  • xdebug_append_error_description
  • xdebug_append_printable_stack
  • dump_used_var_with_contents
  • xdebug_append_error_footer


名前から dump_used_var_with_contents が怪しいので、処理を追ってみると、どうやら、xdebug_get_php_symbol で取得した値を xdebug_get_zval_value_fancy (あるいは xdebug_get_zval_value)で整形して出力しているらしいということが分かりました。

xdebug_get_zval_value_fancy は、渡された値を整形して出力するだけなので、値を取得できないのは xdebug_get_php_symbol で適切に値が取得できていないからということになります。というわけで、xdebug_get_php_symbol 関数を精査することに。

この関数は xdebug_var.c に定義されていました。

この関数は、

  • XG(active_symbol_table)
  • EG(active_op_array)->static_variables
  • &EG(symbol_table)

に対して、zend_hash_find 関数を使い、該当する変数値を検索して、なければ NULL を返す(= Undefined として表示される)ようになっています。

いろいろ調べてみたところ、上から順に ローカル変数、(ローカルで定義された)static 変数、 グローバル変数が格納されたオブジェクトのようです。このうち、ローカル変数を保持するオブジェクトである XG(active_symbol_table) は、呼び出し前に

XG(active_symbol_table) = EG(active_symbol_table);

とされていることから、EG(active_symbol_table) と等価であることが分かります。
EG(active_symbol_table) は確かに、ローカル変数を保持するシンボルテーブルらしい(php 本体のソースコードを参照)のですが、どうやら、ここから変数値が取得できていないのが原因のようでした。
ですが、何が原因なのかさっぱり分かりません。

ここで、ふと思いつきました。

(PHPの) compact 関数は、ローカル変数の値を元に配列を構築する関数だよな……。

ということは、PHP 本体の compact 関数に相当する部分のソースコードが参考になるんじゃないかと。
PHP の関数は、PHP_FUNCTION マクロで構築されるので、検索対象文字列は PHP_FUNCTION(compact) です。
ext/standard.array.c にありました。

ここで妙なものを発見。

if (!EG(active_symbol_table)) {
        zend_rebuild_symbol_table(TSRMLS_C);
}

Xdebugソースコードでは、 EG(active_symbol_table) すなわち XG(active_symbol_table) が必ず存在するかのように扱っていたのですが、compact 関数のソースコードを見ると、 EG(active_symbol_table) が存在しない場合があるかのようなコードが記述されている。

これは妙だと思い、この部分が付け足されたコミットログを追ってみると、ビンゴ。
php5.3 のリポジトリ rev.258703 において、EG(active_symbol_table) の初期を必要になるまで遅延する修正がコミットされていました。

これにより、EG(active_symbol_table) は、常に存在はせず、必要に応じて zend_rebuild_symbol_table を呼ぶ必要が出来たらしいです。

そんな訳で、EG(active_symbol_table) を利用する前に、zend_rebuild_symbol_table を呼ぶようにしてみたら、見事ローカル変数が出力されるようになりました。
ざっと調べてみたところ、 xdebug_debug_zval と xdebug_debug_zval_stdout もこれに依存して正常に動いていなかったようなので、以下のようなパッチを作り、本家に投げました。

--------------------------------------------------------------------
--- xdebug.c.original	2010-04-15 15:39:56.000000000 +0900
+++ xdebug.c	2010-04-15 16:40:06.000000000 +0900
@@ -1457,7 +1457,13 @@
         efree(args);
         WRONG_PARAM_COUNT;
     }
-	
+
+#if PHP_VERSION_ID >= 50300
+	if (!EG(active_symbol_table)) {
+	 zend_rebuild_symbol_table(TSRMLS_C);
+	}
+#endif
+
     for (i = 0; i < argc; i++) {
         if (Z_TYPE_PP(args[i]) == IS_STRING) {
             XG(active_symbol_table) = EG(active_symbol_table);
@@ -1498,7 +1504,13 @@
         efree(args);
         WRONG_PARAM_COUNT;
     }
-	
+
+#if PHP_VERSION_ID >= 50300
+	if (!EG(active_symbol_table)) {
+	 zend_rebuild_symbol_table(TSRMLS_C);
+	}
+#endif
+
     for (i = 0; i < argc; i++) {
         if (Z_TYPE_PP(args[i]) == IS_STRING) {
             XG(active_symbol_table) = EG(active_symbol_table);



--- xdebug_stack.c.original	2010-04-15 15:45:52.000000000 +0900
+++ xdebug_stack.c	2010-04-15 16:39:21.000000000 +0900
@@ -90,6 +90,12 @@
         return;
     }
 
+#if PHP_VERSION_ID >= 50300
+	if (!EG(active_symbol_table)) {
+	 zend_rebuild_symbol_table(TSRMLS_C);
+	}
+#endif
+
     tmp_ht = XG(active_symbol_table);
     XG(active_symbol_table) = EG(active_symbol_table);
     zvar = xdebug_get_php_symbol(name, strlen(name) + 1);
--------------------------------------------------------------------

英語は苦手なので、 excite 翻訳と、友人の力を借りつつ、なんとかBTSに投稿。

待つこと数日。

拙い英語ながらも通じたらしく、


投稿したパッチを取り込んでもらえました。

まだ、これが反映されたバージョンはリリースされていませんが、 2.1.0 の正式版では反映されてるのではないかと期待してます。

OSS の世界に自分の名前が残るって嬉しいですね!