PHP5.4に拡張メソッドの文法追加してみた話

PHP5.4のパーサとコンパイラをいじって拡張メソッドの文法を追加してみたという話。PHP内部のコンパイラとパーサの勉強がてらつくってみた。

拡張メソッドって何?

クラスの外部からそのクラスのメソッドを追加できる機能で、「あーこのクラスにこういうメソッドがあったらな〜」という欲望を満たす。C#とかについてる。

例えば、こんな感じのクラスがあったとする。

<?php

class Hoge
{
    function __construct()
    {
        $this->hoge = "hogehoge";
    }
}

んで、このHogeクラスに対して、こんな感じでメソッドを追加できる。

<?php

public function Hoge::fuga()
{
    echo $this->hoge;
}

(new Hoge)->fuga(); // "hogehoge"が出力される

staticなメソッドも当然追加できる。

<?php

static public function Hoge::piyo()
{
    echo "piyopiyo";
}

Hoge::piyo(); // "piyopiyo"が出力される

以下、diffと解説

以下、たまたまチェックアウトした時点でのPHP5.4のコードに対するdiffと解説。

Index: Zend/zend_language_parser.y
===================================================================
--- Zend/zend_language_parser.y	(revision 323545)
+++ Zend/zend_language_parser.y	(working copy)
@@ -45,12 +45,10 @@
 # define YYPARSE_PARAM tsrm_ls
 # define YYLEX_PARAM tsrm_ls
 #endif
-
-
 %}
 
 %pure_parser
-%expect 3
+%expect 5
 
 %token END 0 "end of file"
 %left T_INCLUDE T_INCLUDE_ONCE T_EVAL T_REQUIRE T_REQUIRE_ONCE
@@ -226,7 +224,19 @@
 
 top_statement:
 		statement						{ zend_verify_namespace(TSRMLS_C); }
+
+	|	method_modifiers function T_STRING T_PAAMAYIM_NEKUDOTAYIM T_STRING { 
+			zend_do_begin_class_method_declaration(&$3 TSRMLS_DC);
+			zend_do_begin_function_declaration(&$2, &$5, 1, 0, &$1 TSRMLS_CC);
+		} '(' parameter_list ')' '{' inner_statement_list '}' { 
+			zend_do_abstract_method(&$5, &$1, &$11 TSRMLS_CC);
+			zend_do_end_function_declaration(&$2 TSRMLS_CC);
+			zend_do_end_class_method_declaration(TSRMLS_DC);
+			zend_verify_namespace(TSRMLS_C);
+		}
+
 	|	function_declaration_statement	{ zend_verify_namespace(TSRMLS_C); zend_do_early_binding(TSRMLS_C); }
+
 	|	class_declaration_statement		{ zend_verify_namespace(TSRMLS_C); zend_do_early_binding(TSRMLS_C); }
 	|	T_HALT_COMPILER '(' ')' ';'		{ zend_do_halt_compiler_register(TSRMLS_C); YYACCEPT; }
 	|	T_NAMESPACE namespace_name ';'	{ zend_do_begin_namespace(&$2, 0 TSRMLS_CC); }

zend_language_parser.yに拡張メソッド用の文法定義を追加。通常のクラスメソッドの定義とは一部違うので、zend_do_begin_class_method_declaration関数とzend_do_end_class_method_declaration関数という新しく書いた関数をセマンティックアクションの中で呼び出す。zend_do_begin_function_declarationとかは、普通にクラスのメソッドを追加する際に利用する既存の関数。CG(active_class_entry)に対してメソッドの中身を登録する。zend_do_end_*関数は後始末。

何やってんのかさっぱりわからない人はbisonのマニュアル読みましょう。

Index: Zend/zend_compile.c
===================================================================
--- Zend/zend_compile.c	(revision 323545)
+++ Zend/zend_compile.c	(working copy)
@@ -4859,6 +4859,68 @@
 }
 /* }}} */
 
+void zend_do_begin_class_method_declaration(znode *class_name TSRMLS_DC) /* {{{ */
+{
+	char *lcname;
+	int error = 0;
+	zval **ns_name;
+
+	if (CG(active_class_entry)) {
+		zend_error(E_COMPILE_ERROR, "Class declarations may not be nested");
+		return;
+	}
+
+	lcname = zend_str_tolower_dup(class_name->u.constant.value.str.val, class_name->u.constant.value.str.len);
+
+	if (!(strcmp(lcname, "self") && strcmp(lcname, "parent"))) {
+		efree(lcname);
+		zend_error(E_COMPILE_ERROR, "Cannot use '%s' as class name as it is reserved", Z_STRVAL(class_name->u.constant));
+	}
+
+	/* Class name must conflict with import names */
+	if (CG(current_import) &&
+	    zend_hash_find(CG(current_import), lcname, Z_STRLEN(class_name->u.constant)+1, (void**)&ns_name) == SUCCESS) {
+		efree(lcname);
+		zend_error(E_COMPILE_ERROR, "Cannot use '%s' as class name as it is reserved", Z_STRVAL(class_name->u.constant));
+	}
+
+	if (CG(current_namespace)) {
+		/* Prefix class name with name of current namespace */
+		znode tmp;
+
+		tmp.u.constant = *CG(current_namespace);
+		zval_copy_ctor(&tmp.u.constant);
+		zend_do_build_namespace_name(&tmp, &tmp, class_name TSRMLS_CC);
+		class_name = &tmp;
+		efree(lcname);
+		lcname = zend_str_tolower_dup(Z_STRVAL(class_name->u.constant), Z_STRLEN(class_name->u.constant));
+	} 
+
+	if (zend_hash_find(CG(class_table), lcname, Z_STRLEN(class_name->u.constant)+1, (void**)&ns_name) == FAILURE) {
+		efree(lcname);
+		zend_error(E_COMPILE_ERROR, "'%s' class is undefined", Z_STRVAL(class_name->u.constant));
+	}
+
+	efree(class_name->u.constant.value.str.val);
+
+	zend_class_entry **class_entry;
+	zend_hash_find(CG(class_table), lcname, strlen(lcname) + 1, (void**)&class_entry);
+	CG(active_class_entry) = *class_entry;
+
+	efree(lcname);
+}
+/* }}} */
+
 void zend_do_begin_class_declaration(const znode *class_token, znode *class_name, const znode *parent_class_name TSRMLS_DC) /* {{{ */
 {
 	zend_op *opline;
@@ -4984,6 +5046,14 @@
 }
 /* }}} */
 
+void zend_do_end_class_method_declaration(TSRMLS_DC) /* {{{ */
+{
+	CG(active_class_entry) = NULL;
+}
+/* }}} */
+
 void zend_do_end_class_declaration(const znode *class_token, const znode *parent_token TSRMLS_DC) /* {{{ */
 {
 	zend_class_entry *ce = CG(active_class_entry);

zend_do_begin_class_method_declaration関数の中では、指定されたクラスをCG(class_table)から探してきて、CG(active_class_entry)に代入している。PHPで定義したクラス(zend_class_entry構造体)は、CG(class_table)というHashTableの中に格納される。CG()はコンパイラグローバル変数にアクセスするためのマクロ。CG(active_class_entry)は、今現在どのクラスの中にいるかを表すための変数で、メソッドのコードをコンパイルする際にはCG(active_class_entry)に対してそのメソッドの中身のコンパイル結果(zend_op_array構造体)を登録する。zend_do_begin_class_method_declaration関数のコード自体は、zend_do_begin_class_declaration関数から切り貼りして取ってきた。

zend_do_end_class_method_declaration関数は見たとおりCG(active_class_entry)を元に戻すための後片付け関数。

Index: Zend/zend_compile.h
===================================================================
--- Zend/zend_compile.h	(revision 323545)
+++ Zend/zend_compile.h	(working copy)
@@ -542,7 +542,10 @@
 void zend_do_case_after_statement(znode *result, const znode *case_token TSRMLS_DC);
 void zend_do_default_before_statement(const znode *case_list, znode *default_token TSRMLS_DC);
 
+
+void zend_do_begin_class_method_declaration(znode *class_name TSRMLS_DC);
 void zend_do_begin_class_declaration(const znode *class_token, znode *class_name, const znode *parent_class_name TSRMLS_DC);
+void zend_do_end_class_method_declaration(TSRMLS_DC);
 void zend_do_end_class_declaration(const znode *class_token, const znode *parent_token TSRMLS_DC);
 void zend_do_declare_property(const znode *var_name, const znode *value, zend_uint access_type TSRMLS_DC);
 void zend_do_declare_class_constant(znode *var_name, const znode *value TSRMLS_DC);

最後に、追加した関数の宣言をヘッダファイルに足しておく。

ハマったところ

  • 最初思いついて一時間ぐらいでできるんではとか思ってたらハマって一晩中かかった。C言語普段全く書かないのでこういうとき苦労する。
  • zend_do_begin_class_declaration関数を参考にして切り貼りしてたら、余計なopcodeも生成する部分が混じっていたがそれに気づかずzend_do_early_binding周りでエラーが出て悩んでた。
  • T_STRINGトークンが格納されるznode構造体には、トークンの中身が入った文字列がemallocされて格納されていて、これをコンパイル中に開放してあげないとダメなのだが、それに気づかずmemory leak警告が出て悩んだ。
  • とにかくちょっといじっただけでどこかでmemory leak警告がでて怠かった

終わり

PHP5.4に拡張メソッドを追加してみた。コード自体は短いが案外ハマったのでブログに書いておく。