おぎろぐはてブロ

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

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, &params) == 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 APIcall_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 を指定。

最後の方すごく曖昧ですね。まぁ、必要になったときに調べてみればいいかと思います。あんまり深くほじると、マニュアルも無いので、たいへん。
時間切れなので、サンプルコードとか書いてないや。