call_user_func(_array)を読み解く
前回の宣言どおり、callback疑似型の挙動を追いかけるため、call_user_funcのソースコードを読んでいきたいと思います。
PHP_FUNCTION(call_user_func)
/ PHP_FUNCTION(call_user_func_array)
call_user_func及び、call_user_func_arrayの関数は ext/standard/basic_functions.c
にあります。
軽くコメント入れてます。ここ自体はPHPスクリプトからZend APIへの橋渡しなので、重要ではないです。やってることが特殊なので、Extensionの実装の参考にはあんまりならないですね。
/* {{{ proto mixed call_user_func(string function_name [, mixed parmeter] [, mixed ...]) Call a user function which is the first parameter */ PHP_FUNCTION(call_user_func) { zval ***params; zval *retval_ptr; char *name; int argc = ZEND_NUM_ARGS(); // 引数は1個以上必要 if (argc < 1) { WRONG_PARAM_COUNT; } // メモリアロケート。なるほど、safe_emallocってこう使うのね。。 params = safe_emalloc(sizeof(zval **), argc, 0); // 1個目の引数(callback)を取る // zval*** paramsに、1個分引数を取ってくる。***なんて憂鬱。 if (zend_get_parameters_array_ex(1, params) == FAILURE) { efree(params); RETURN_FALSE; } // 文字列でなく、配列でもない場合は、文字列に変換する // 引数をconvertするときは、SEPARATE_ZVALを忘れずに if (Z_TYPE_PP(params[0]) != IS_STRING && Z_TYPE_PP(params[0]) != IS_ARRAY) { SEPARATE_ZVAL(params[0]); convert_to_string_ex(params[0]); } // zend_is_callable関数で呼び出し可能な関数なのかチェック if (!zend_is_callable(*params[0], 0, &name)) { php_error_docref1(NULL TSRMLS_CC, name, E_WARNING, "First argument is expected to be a valid callback"); efree(name); efree(params); RETURN_NULL(); } // 残りのパラメータもとってくる if (zend_get_parameters_array_ex(argc, params) == FAILURE) { efree(params); RETURN_FALSE; } // 関数呼び出す if (call_user_function_ex(EG(function_table), NULL, *params[0], &retval_ptr, argc-1, params+1, 0, NULL TSRMLS_CC) == SUCCESS) { // 成功。関数の戻り値があるときは返却用に複製 if (retval_ptr) { COPY_PZVAL_TO_ZVAL(*return_value, retval_ptr); } } else { // 取れなかった。引数があるときは、エラー用に表示させるために文字列に変換 if (argc > 1) { SEPARATE_ZVAL(params[1]); convert_to_string_ex(params[1]); if (argc > 2) { SEPARATE_ZVAL(params[2]); convert_to_string_ex(params[2]); php_error_docref1(NULL TSRMLS_CC, name, E_WARNING, "Unable to call %s(%s,%s)", name, Z_STRVAL_PP(params[1]), Z_STRVAL_PP(params[2])); } else { php_error_docref1(NULL TSRMLS_CC, name, E_WARNING, "Unable to call %s(%s)", name, Z_STRVAL_PP(params[1])); } } else { php_error_docref1(NULL TSRMLS_CC, name, E_WARNING, "Unable to call %s()", name); } } // 解放。おつかれさまでした。 efree(name); efree(params); } /* }}} */ /* {{{ proto mixed call_user_func_array(string function_name, array parameters) Call a user function which is the first parameter with the arguments contained in array */ PHP_FUNCTION(call_user_func_array) { zval ***func_params, **func, **params; zval *retval_ptr; HashTable *func_params_ht; char *name; int count; int current = 0; // こっちは引数は必ず2個。引数は2個で、zval** func と zval** params に詰める if (ZEND_NUM_ARGS() != 2 || zend_get_parameters_ex(2, &func, ¶ms) == FAILURE) { WRONG_PARAM_COUNT; } // paramsを配列に変換 SEPARATE_ZVAL(params); convert_to_array_ex(params); // ここからはcall_user_funcとおなじ if (Z_TYPE_PP(func) != IS_STRING && Z_TYPE_PP(func) != IS_ARRAY) { SEPARATE_ZVAL(func); convert_to_string_ex(func); } if (!zend_is_callable(*func, 0, &name)) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "First argument is expected to be a valid callback, '%s' was given", name); efree(name); RETURN_NULL(); } // パラメータ配列のzvalからHashTableを取り出す func_params_ht = Z_ARRVAL_PP(params); // 配列の要素数を数える count = zend_hash_num_elements(func_params_ht); if (count) { // zval**の領域を配列の要素数分malloc func_params = safe_emalloc(sizeof(zval **), count, 0); // 配列の内部ポインタをリセット (http://php.net/reset と同様) // 最後までなめてfunc_paramsに詰める for (zend_hash_internal_pointer_reset(func_params_ht); zend_hash_get_current_data(func_params_ht, (void **) &func_params[current]) == SUCCESS; zend_hash_move_forward(func_params_ht) ) { current++; } } else { func_params = NULL; } // 関数呼び出す if (call_user_function_ex(EG(function_table), NULL, *func, &retval_ptr, count, func_params, 0, NULL TSRMLS_CC) == SUCCESS) { if (retval_ptr) { COPY_PZVAL_TO_ZVAL(*return_value, retval_ptr); } } else { php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to call %s()", name); } efree(name); if (func_params) { efree(func_params); } } /* }}} */
最終的なコールバックの呼び出しは、call_user_function_ex()
が行っていることが分かります。やってることはいたってシンプルだけど、いかんせん手順が多いので、自分で書けと言われると、解放漏れとか変数破壊とかやっちゃうな。こりゃ。
call_user_function()
/ call_user_function_ex()
PHPスクリプト中から、関数を呼び出すには、call_user_func()
/ call_user_func_array()
を使うワケですが、"user_func"っていいつつも、ユーザ定義関数以外も呼ぶことに使うことがあるので、違和感があります。
実はCレベルのZEND APIもcall_user_function()
と似たような名前。こちらは、ユーザ定義関数を呼ぶことが中心なので(Cレベルの関数ならそのまま呼べばいいからね)、まぁ、納得いく名前です。PHPの関数名の違和感は、ZEND API関数のwrapperの名残なんでしょうかね。。
ユーザ関数を呼び出す関数は、call_user_function()
とcall_user_function_ex()
があります。前者の簡単な使い方は、PHP: ユーザ関数のコール - Manualに説明されています。
ZEND_API int call_user_function(HashTable *function_table, zval **object_pp, zval *function_name, zval *retval_ptr, zend_uint param_count, zval *params[] TSRMLS_DC); ZEND_API 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 APIには、ノーマルと _ex のついたものと2つ関数があるものがいろいろあります。_exって漠然な命名なので、いまいち違いがつかみにくいのですが、_exの方がより低レベルで拡張性があって、hoge_ex + 何か = hoge と、いう感じです。call_user_function()の場合も、内部でcall_user_function_ex()を呼び出しています。
引数
- HashTable *function_table
- 関数テーブル。だいたい、EG(function_table)。
- zval **object_pp
- オブジェクトを格納しているzval。指定しない場合はNULLで。古い関数となっているcall_user_method()が利用しています。
- zval *function_name
- いわゆるcallback疑似型。配列か、関数名の文字列。
- zval *retval_ptr
- zval **retval_ptr_ptr
- 呼び出す関数の戻り値が格納される
- zend_uint param_count
- params配列の要素数
- zval **params[]
- パラメータzval
- int no_separation
- よくわかんなかった。call_user_funcは 1 を指定。
- HashTable *symbol_table
- たぶん、関数を呼び出すときのシンボルテーブル(スコープ変数が格納される)。call_user_funcは NULL を指定。
最後の方すごく曖昧ですね。まぁ、必要になったときに調べてみればいいかと思います。あんまり深くほじると、マニュアルも無いので、たいへん。
時間切れなので、サンプルコードとか書いてないや。