おぎろぐはてブロ

なんだかんだエンジニアになって10年以上

zend_call_functionを読み解いて挫折

前回のcall_user_func(_array)を読み解く - おぎろぐはてなでは、目的のcallback疑似型の挙動を追いかけるが終わってなかったので、さらに続きます。

ここまでのおさらい

  • PHPcall_user_funccall_user_func_arrayなど、callbackを引数に指定する関数は、callback疑似型というフォーマットでコールバック関数・メソッドを指定している
  • callback疑似型というのは、疑似という通り、実際の型ではなく、以下のような指定方法
    1. 普通に文字列で関数名を指定する
    2. array(クラス名, メソッド名)の文字列配列で、staticなクラスメソッドを指定する
    3. 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$namefci->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要素に文字列(クラス名)が指定された場合
    • そのクラスのインスタンスである場合、object_ppにカレントのオブジェクトをセットする
    • そのクラスのインスタンスでない場合、関数テーブルのみクラス内に差し替えられる

という感じで、呼び出しスコープ(変数参照のスコープ)、関数スコープが正規化されます。

 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で追わないとダメ。
もしかしたらやるかも。