PHPのfunc_get_argsの謎

うまく表現できないけどFlexyにはテンプレート内での関数呼び出しの引数に定数と変数を混ぜて評価できない、という制約がある。

{setTitle(#{username}のプロフィール#)}

っていうのは無理なので、関数側をいじってなんとかするしかない。そこでテンプレートには

{setTitle(username,#のプロフィール#)}

と、別々の引数として書くことにして、関数側で繋げることにした。で、

function setTitle() {
    $title = join('', func_get_args() );
}

と書いたらなんかfunc_get_args

PHP Fatal error:  func_get_args(): Can't be used as a function parameter

なんて言い出すの。意味分かんない。

っていうのを、先週ogiちゃんに投げてたら おぎろぐはてな – func_get_args系の関数の変な動きから、EG(argument_stack)を中途半端に眺める になって帰ってきた。

引数が評価される仕組みは分かったけれど、やっぱりなんでそんな制約が導入されているのか理解できない。さらに分からないことに引数の順番を変えて

function setTitle() {
    $title = join(func_get_args(), '' );
}

にすると問題なく動くようになる。(PHP5.1.4の場合)

PHP: func_get_args – Manual には

注意: この関数は、カレントスコープに依存してパラメータの詳細を決定しますので、関数パラメータとして使用することはできません。もし、この値を渡さなければならない場合、戻り値を変数に割り当て、その変数を渡してください。

と書いてあったり、この一貫性のないかんじの動作は納得いかない。

いろいろ調べてみたけど分かんないのでもうメモリをダンプしてみた。
まず

$title = join(func_get_args(), '' );

の場合。

コードを読むと、php-5.1.4/Zend/zend_execute_API.cinit_executorのはじめで

zend_ptr_stack_push(&EG(argument_stack), (void *) NULL);

していたりzend_call_functionの、関数呼び出しの最後の部分で

    zend_ptr_stack_2_push(&EG(argument_stack), (void *) (long) fci->param_count, NULL);

していたりするところからして、スタックフレームの始めにはNULLを積む、というルールがあるらしいので、そのフレームを示しているNULLだと思われるところに色を付けてあります。

element_top -  0 0855976c: 00000000
element_top -  1 08559768: 00000000
element_top -  2 08559764: 00000000
element_top -  3 08559760: 00000000
element_top -  4 0855975c: 00000003
element_top -  5 08559758: 08563304
element_top -  6 08559754: 085630fc
element_top -  7 08559750: 0856314c
element_top -  8 0855974c: 00000000
element_top -  9 08559748: 00000100
element_top - 10 08559744: 08559858
element_top - 11 08559740: 08559628
element_top - 12 0855973c: 00000000
element_top - 13 08559738: 00000111
element_top - 14 08559734: 00000000
element_top - 15 08559730: 00000000

element_topphp-5.1.4/Zend/zend_builtin_functions.cの中のEG(argument_stack).top_elementのアドレスです。

ZEND_FUNCTION(func_get_args)
{
    void **p;
    int arg_count;
    int i;

    p = EG(argument_stack).top_element-1-1;
    arg_count = (int)(zend_uintptr_t) *p;       /* this is the amount of arguments passed to func_get_args(); */
    p -= 1+arg_count;
    if (*p) {
        zend_error(E_ERROR, "func_get_args(): Can't be used as a function parameter");
    }

スタックの内容がこうなっているので、一番目の引数として結果として if (*p)element_top - 3 の0の値を参照していてエラーにならない。ちなみに

$a =func_get_args();

でも、スタックの中身はおなじになりました。

それに対して

$title = join('', func_get_args());

の場合は

element_top -  0 08559770: 00000000
element_top -  1 0855976c: 00000000
element_top -  2 08559768: 00000000
element_top -  3 08559764: 085632dc
element_top -  4 08559760: 00000000
element_top -  5 0855975c: 00000003
element_top -  6 08559758: 08563304
element_top -  7 08559754: 085630fc
element_top -  8 08559750: 0856314c
element_top -  9 0855974c: 00000000
element_top - 10 08559748: 00000100
element_top - 11 08559744: 08559858
element_top - 12 08559740: 08559628
element_top - 13 0855973c: 00000000
element_top - 14 08559738: 00000111
element_top - 15 08559734: 00000000

になってて
(element_top - 3)には085632dcが入っているのでエラーが出る。ここに入っている値がなんなのかを調べるために

    printf("Z_TYPE = %d\n", Z_TYPE_P(z) );
    printf("Z_STRVAL_P = %s\n", Z_STRVAL_P(z) );

を入れてみると、結果はZ_TYPE = IS_STRINGで空文字列が入っている。joinの一番目の引数がはいっているようだ。

なんでこういう動作になるのかがわかるには道のりが険しそうですが、スタックには

element_top – 0 次のスタックフレームの先頭(まだ未使用)
element_top – 1 セパレータ(NULL)
element_top – 2 関数の引数の数N
(element_top – 2 – 1) ~ (element_top – 2 – N) 引数の値へのポインタ
(element_top – 2 – N) ~ 何らかのM(M>=0)個の値
(element_top – 2 – N – M) 前の関数のスタックフレームのセパレータ(NULL)

という形式で値が積まれるようです。

php-5.1.4/Zend/zend_builtin_functions.czend_fetch_debug_backtraceには

    while (--args > EG(argument_stack).elements) {
        if (*args--) {
            break;
        }
        args -= *(ulong*)args;
        frames_on_stack++;

        /* skip args from incomplete frames */
        while (((args-1) > EG(argument_stack).elements) && *(args-1)) {
            args--;
        }

        if ((args-1) == EG(argument_stack).elements) {
            arg_stack_consistent = 1;
            break;
        }
    }

明らかに”NULLが出てくるまでスタック上を下にトレース”している部分があるので、コメントでいうincompleteな状態のときには、引数とひとつ前のスタックフレームの先頭のNULLの間に何かが詰まっている模様。

この、スタックフレームのアタマに誰が値を詰めているか、コードの中でスタックを表示させてしらべつつ探すと

        zend_execute(EG(active_op_array) TSRMLS_CC);

の中で詰められているのが分かりました。zend_executeはオペコード列を実行するものなので、今度は おぎろぐはてな – facebookでのAPCの設定 のfacebookのPDFで知った Projects: Vulcan Logic Disassembler – Derick Rethans でオペコードをダンプして調べます。

ダンプしたのは、結果の分かりやすさの都合で

function bar ($a, $b, $c, $d) {}
function foo() {
        bar( "first", "second", "third", func_get_args() );
}
call_user_func( "foo", 1,2,3);

というコード。このVLDは、PHP5.2.2でビルドすると

error: `ZEND_JMP_NO_CTOR' undeclared (first use in this function)

なんていわれますが、とりあえずこの部分はさくっとコメントアウトすればビルドが通るようになります。

できあがったVLDに先ほどのコードをかけると

filename:       /home/kuma/vld-0.8.0/t.php
function name:  (null)
number of ops:  9
line     #  op                           fetch          ext  operands
-------------------------------------------------------------------------------
   3     0  NOP
   7     1  NOP
  14     2  SEND_VAL                                             'foo'
         3  SEND_VAL                                             1
         4  SEND_VAL                                             2
         5  SEND_VAL                                             3
         6  DO_FCALL                                      4      'call_user_func', 0
  16     7  RETURN                                               1
         8  ZEND_HANDLE_EXCEPTION

Function bar:
filename:       /home/kuma/vld-0.8.0/t.php
function name:  bar
number of ops:  6
line     #  op                           fetch          ext  operands
-------------------------------------------------------------------------------
   3     0  RECV                                                 1
         1  RECV                                                 2
         2  RECV                                                 3
         3  RECV                                                 4
   5     4  RETURN                                               null
         5  ZEND_HANDLE_EXCEPTION

End of function bar.

Function foo:
filename:       /home/kuma/vld-0.8.0/t.php
function name:  foo
number of ops:  8
line     #  op                           fetch          ext  operands
-------------------------------------------------------------------------------
  10     0  SEND_VAL                                             'first'
         1  SEND_VAL                                             'second'
         2  SEND_VAL                                             'third'
         3  DO_FCALL                                      0  $0, 'func_get_args', 0
         4  SEND_VAR_NO_REF                                  $0
         5  DO_FCALL                                      4      'bar', 0
  12     6  RETURN                                               null
         7  ZEND_HANDLE_EXCEPTION

End of function foo.

こういうオペコードが出てきます。

コードからしてRECV がスタックからのpopで、SEND_VALがスタックへのpushなのがわかります。
上のほうの表の (element_top - 2 - N - M) の部分にある値は、関数の引数として func_get_args を渡したときに、それよりも左側にある引数だというのがわかります。

ということは、やっぱりスタックを次のスタックフレームの印であるNULLまで走査してあげれば func_get_args が返すべき引数の値は分かりそうなかんじがします。ほんとにただの手抜きなのか….

func_get_args の注意書き(正確さを期すため英語版)

Note: Because this function depends on the current scope to determine parameter details, it cannot be used as a function parameter. If you must pass this value, assign the results to a variable, and pass the variable.

関数のスコープ(と書かれると、関数の変数スコープのことだと思う)はぜんぜん関係なくて、関数引数の実装上取得するのが困難になる、といったほうがより正確。

わりとたのしかった。
そのうちクロージャのある言語で関数引数やローカル変数がメモリ上のどこに確保されてどう解決されるのかしりたい。

おまけでいちおうSEND_VALの中でスタックに値が積まれているかどうかを確認しておくと、ファイルは
Zend/zend_vm_def.h で、その中に

ZEND_VM_HANDLER(65, ZEND_SEND_VAL, CONST|TMP|VAR|CV, ANY)
{
    zend_op *opline = EX(opline);
    if (opline->extended_value==ZEND_DO_FCALL_BY_NAME
        && ARG_MUST_BE_SENT_BY_REF(EX(fbc), opline->op2.u.opline_num)) {
            zend_error_noreturn(E_ERROR, "Cannot pass parameter %d by reference", opline->op2.u.opline_num);
    }
    {
        zval *valptr;
        zval *value;
        zend_free_op free_op1;

        value = GET_OP1_ZVAL_PTR(BP_VAR_R);

        ALLOC_ZVAL(valptr);
        INIT_PZVAL_COPY(valptr, value);
        if (!IS_OP1_TMP_FREE()) {
            zval_copy_ctor(valptr);
        }
        zend_ptr_stack_push(&EG(argument_stack), valptr);
        FREE_OP1_IF_VAR();
    }
    ZEND_VM_NEXT_OPCODE();
}

という部分があります。


About this entry