zend_call_functionを読み解いて挫折
前回のcall_user_func(_array)を読み解く - おぎろぐはてなでは、目的のcallback疑似型の挙動を追いかけるが終わってなかったので、さらに続きます。
ここまでのおさらい
- PHPのcall_user_func や call_user_func_arrayなど、callbackを引数に指定する関数は、callback疑似型というフォーマットでコールバック関数・メソッドを指定している
- callback疑似型というのは、疑似という通り、実際の型ではなく、以下のような指定方法
- 普通に文字列で関数名を指定する
- array(クラス名, メソッド名)の文字列配列で、staticなクラスメソッドを指定する
- array(object $obj, メソッド名)の配列で、オブジェクトのメソッドをコールする
- PHP Extensionレベルで呼び出す際には、Zend/zend_execute_API.c の call_user_function() や call_user_function_ex() を使う
- call_user_function()は、call_user_function_ex()のwrapper
call_user_function_ex()
さらに、このcall_user_func_ex()は、zend_call_function()のwrapperです。中身はいたってシンプルで、引数を構造体に詰めてるだけ。
call_user_function()はまだしも、これはなんだかなぁ。ext/下のソースコードでも、call_user_function(), call_user_function_ex(), zend_call_function()は同じくらいの回数使われている。
int call_user_function_ex(HashTable *function_table, zval **object_pp, zval *function_name, zval **retval_ptr_ptr, zend_uint param_count, zval **params[], int no_separation, HashTable *symbol_table TSRMLS_DC) { zend_fcall_info fci; fci.size = sizeof(fci); fci.function_table = function_table; fci.object_pp = object_pp; fci.function_name = function_name; fci.retval_ptr_ptr = retval_ptr_ptr; fci.param_count = param_count; fci.params = params; fci.no_separation = (zend_bool) no_separation; fci.symbol_table = symbol_table; return zend_call_function(&fci, NULL TSRMLS_CC); }
zend_call_function()
ここが本丸。
int zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_cache TSRMLS_DC)
で、第2引数は置いておいて、call_user_function_ex()が詰めているような構造体を受けて、実際に関数を呼び出します。
ここから、array($obj, $name)
のハンドリング。fci->function_name
に$name
、fci->object_pp
に$obj
がセットされる。
658 if (Z_TYPE_P(fci->function_name)==IS_ARRAY) { /* assume array($obj, $name) couple */ 659 zval **tmp_object_ptr, **tmp_real_function_name; 660 661 if (zend_hash_index_find(Z_ARRVAL_P(fci->function_name), 0, (void **) &tmp_object_ptr)==FAILURE) { 662 return FAILURE; 663 } 664 if (zend_hash_index_find(Z_ARRVAL_P(fci->function_name), 1, (void **) &tmp_real_function_name)==FAILURE) { 665 return FAILURE; 666 } 667 fci->function_name = *tmp_real_function_name; 668 SEPARATE_ZVAL_IF_NOT_REF(tmp_object_ptr); 669 fci->object_pp = tmp_object_ptr; 670 (*fci->object_pp)->is_ref = 1; 671 }
ここで、fci->object_pp
がセットされてるときの処理。
説明がうまくできないのですが、そこはコードを読んで確認してくださいという感じで
- array($this, $name)と、第1要素にオブジェクトが指定された場合
- オブジェクト内のスコープの関数テーブルに入れ替えます。これで、クラス内で、$thisで参照するのと同じ状態になります。
- array("self", $name)と、第1要素に"self"が指定されて且つ、クラス内である場合
- object_ppに、カレントのオブジェクトをセットする
- array("parent", $name)と、第1要素に"parent"が指定されて且つ、クラス内である場合
- object_ppに、親のオブジェクトをセットする
- array("Hoge", $name)と、第1要素に文字列(クラス名)が指定された場合
という感じで、呼び出しスコープ(変数参照のスコープ)、関数スコープが正規化されます。
677 if (fci->object_pp) { 678 /* TBI!! new object handlers */ 679 if (Z_TYPE_PP(fci->object_pp) == IS_OBJECT) { ... 684 /* ここで関数テーブル(function_table)が差し替えられる */ 685 calling_scope = Z_OBJCE_PP(fci->object_pp); 686 fci->function_table = &calling_scope->function_table; 687 EX(object) = *fci->object_pp; 688 } else if (Z_TYPE_PP(fci->object_pp) == IS_STRING) { 689 zend_class_entry **ce; 690 int found = FAILURE; 691 /* クラスの中で、array("self", $name), array("parent", $name)の呼び出しを可能に */ 692 if (EG(active_op_array) && strcmp(Z_STRVAL_PP(fci->object_pp), "self") == 0) { 693 if (!EG(active_op_array)->scope) { 694 zend_error(E_ERROR, "Cannot access self:: when no class scope is active"); 695 } 696 ce = &(EG(active_op_array)->scope); 697 found = (*ce != NULL?SUCCESS:FAILURE); 698 fci->object_pp = EG(This)?&EG(This):NULL; 699 EX(object) = EG(This); 700 } else if (strcmp(Z_STRVAL_PP(fci->object_pp), "parent") == 0 && EG(active_op_array)) { 701 702 if (!EG(active_op_array)->scope) { 703 zend_error(E_ERROR, "Cannot access parent:: when no class scope is active"); 704 } 705 if (!EG(active_op_array)->scope->parent) { 706 zend_error(E_ERROR, "Cannot access parent:: when current class scope has no parent"); 707 } 708 ce = &(EG(active_op_array)->scope->parent); 709 found = (*ce != NULL?SUCCESS:FAILURE); 710 fci->object_pp = EG(This)?&EG(This):NULL; 711 EX(object) = EG(This); 712 } else { /* array(クラス名, $name)のパターン */ 713 zend_class_entry *scope; 714 scope = EG(active_op_array) ? EG(active_op_array)->scope : NULL; 715 /* クラスを探す */ 716 found = zend_lookup_class(Z_STRVAL_PP(fci->object_pp), Z_STRLEN_PP(fci->object_pp), &ce TSRMLS_CC); 717 if (found == FAILURE) { 718 zend_error(E_ERROR, "Class '%s' not found", Z_STRVAL_PP(fci->object_pp)); 719 } 720 if (scope && EG(This) && 721 instanceof_function(Z_OBJCE_P(EG(This)), scope TSRMLS_CC) && 722 instanceof_function(scope, *ce TSRMLS_CC)) { 723 fci->object_pp = &EG(This); 724 EX(object) = EG(This); 725 } else { 726 fci->object_pp = NULL; 727 } 728 } ... 731 /* 関数テーブルと呼び出しスコープをクラスの物に。 */ 732 fci->function_table = &(*ce)->function_table; 733 calling_scope = *ce; 734 } else { ... 742 }
次に関数名側のケアです。
ここでは、"::" が付いていたときに、また、クラスもしくはparent, selfを探して、再度、上と同じように呼び出しスコープ(変数参照のスコープ)、関数スコープが正規化されます。
748 fname = Z_STRVAL_P(fci->function_name); 749 fname_len = Z_STRLEN_P(fci->function_name); /* Class::Method の :: が付いた文字列のケア */ 750 if ((colon = strstr(fname, "::")) != NULL) { 751 int clen = colon - fname; 752 int mlen = fname_len - clen - 2; 753 zend_class_entry **pce, *ce_child = NULL; /* クラスを探して、pce, ce_childに詰める */ 754 if (zend_lookup_class(fname, clen, &pce TSRMLS_CC) == SUCCESS) { 755 ce_child = *pce; 756 } else { /* parent, self のケア */ 757 char *lcname = zend_str_tolower_dup(fname, clen); 758 /* caution: lcname is not '\0' terminated */ 759 if (calling_scope) { 760 if (clen == sizeof("self") - 1 && memcmp(lcname, "self", sizeof("self") - 1) == 0) { 761 ce_child = EG(active_op_array) ? EG(active_op_array)->scope : NULL; 762 } else if (clen == sizeof("parent") - 1 && memcmp(lcname, "parent", sizeof("parent") - 1) == 0 && EG(active_op_array)->scope) { 763 ce_child = EG(active_op_array) && EG(active_op_array)->scope ? EG(scope)->parent : NULL; 764 } 765 } 766 efree(lcname); 767 } ... 772 check_scope_or_static = calling_scope; 773 fci->function_table = &ce_child->function_table; 774 calling_scope = ce_child; 775 fname = fname + clen + 2; 776 fname_len = mlen; 777 }
この後は、ここで正規化された情報を元に、関数やメソッドを呼び出したり、メソッドが見つからなかったら__call()
マジックメソッドを探したりの処理に入るので割愛
肝心なところが
ソースを読んでみようと思った、
callbackを、array($this, "parent::hogeMethod") もしくは array($this, "Base::hogeMethod")とすれば呼び出せる。call_user_func('MyClass::myCallbackMethod')が使えないのに、なんだか気持ち悪いけど。
http://d.hatena.ne.jp/i_ogi/20070512/1178961961
の疑問点が、上のコードだと、きちんといけそうな気がする。。750行目からの処理で、array()形式での呼び出しだからということに関係なしにfunction_nameだけを見て処理が行われているから。むー。
実際、
call_user_func("AA:BB");
のコードを実行すると、
Warning: call_user_func(AA:BB): First argument is expected to be a valid callback in tmp.php on line 11
のエラーが出るのですが、この "a valid callback" かどうかの判定は、call_user_func_ex()を呼び出す前に、zend_is_callable()
で判定しています。
だからといって、本当は叩けるということもなくて、このチェックを外してcall_user_func_ex()を叩いても失敗するんだよなぁ。しかも、エラーメッセージ出さずに。
まとめ
gdbで追わないとダメ。
もしかしたらやるかも。