おぎろぐはてブロ

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

func_get_args系の関数の変な動きから、EG(argument_stack)を中途半端に眺める

※この記事ではちと勘違いがあったので、次のエントリも一緒に読んでください。

くまさんからのリクエスト。

func_get_argsなどの関数の引数を受け取る関数は、任意の引数を取る関数などを書く際によく使います。これで、ありがちなのが、

<?php
function hoge()
{
  $content = join('', func_get_args());
}
hoge();
?>

をすると、

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

と警告でちゃうこと。
その理由として、PHPマニュアルには、

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

PHP: func_get_args - Manual

と書いてある。
いやいや、カレントスコープに依存って、仮にも変数のスコープと同じなら

$a = 3;
hoge( $a );

って、やれば、$aの値 (参照渡しであればその参照)がhogeに渡るわけで、これができないことになる。hoge( moge() ); とやるとスコープが違うとはどういうことだろう。

先に結論

変数スコープと、この関数が見に行くパラメータ引数のスコープは違うからさ、ってこと。どちらかというと、そういう仕様ですってことだな。

func_get_argsのコード

まずは、関数がどうなっているかを眺めてみます。

zend_builtin_functions.c (PHP5.2.3)

/* {{{ proto array func_get_args()
   Get an array of the arguments that were passed to the function */
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");
    }    
    --p; 

    if (p<EG(argument_stack).elements) {
        zend_error(E_WARNING, "func_get_args():  Called from the global scope - no function context");
        RETURN_FALSE;
    }    
    arg_count = (int)(zend_uintptr_t) *p;

    array_init(return_value);
    for (i=0; i<arg_count; i++) {
        zval *element;

        ALLOC_ZVAL(element);
        *element = **((zval **) (p-(arg_count-i)));
        zval_copy_ctor(element);
        INIT_PZVAL(element);
        zend_hash_next_index_insert(return_value->value.ht, &element, sizeof(zval *), NULL);
    }    
}
/* }}} */

意外にすっきりしていますが、普通のExtensionと違うのは、EG(argument_stack)とか変なモノを触っているということでしょう。
ダブルポインタ操作のお勉強的なコードですね。。

EG(argument_stack)とは

EG(argument_stack) が指すのは、executor globals構造体内の zend_ptr_stack構造体という、汎用的なスタックです。スクリプト実行時に、関数やメソッドを呼び出すたびに、引数が積まれていくイメージです。

typedef struct _zend_ptr_stack {
    int top, max;
    void **elements;
    void **top_element;
} zend_ptr_stack;

関数を呼び出したときに、このスタックに引数が積まれるイメージです。

関数パラメータ内で呼び出したときのハンドリング

void **p; 
int arg_count;

// argument_stackの先頭要素-2
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(); */
// 引数個+1デクリメント
p -= 1+arg_count;
if (*p) {
    zend_error(E_ERROR, "func_get_args(): Can't be used as a function parameter");
}    

となっています。
ここでやってるのは、func_get_args()自体を呼び出す際にも引数を付けることができるワケで、その数の分だけデクリメントをしているという形。そして、その次のところに、何かフラグが入っている模様。
結局のところ、引数として呼び出そうとしたときは、何かしらスタックに積まれているので、一筋縄ではデータが取れないということでしょう。

結局、スタックの積み方とかがよく分からないのですが、ポインタの動きを理解するために参考までに書いて見た絵を。PCで書くのうざいから手書き。
どっかにargument_stackの詰め方の説明ないのかしらん。


手抜きではないかという気もしなくもない。

いくら探しているところには無いとはいえ、スタックなのでさらに前に積んであるのです。
例えば、debug_backtraceは同じように関数の引数も表示してくれるのですが、

<?php
function hoge()
{
  $bt = debug_backtrace();
  var_dump($bt);

  moge(debug_backtrace());
}
function moge($param)
{
  var_dump($param);
}
hoge("A");
?>

として、関数内に普通に書いたパターンと、関数の引数に書いたパターンがある場合、

array(1) {
  [0]=>
  array(4) {
    ["file"]=>
    string(23) "/home/iogi/tmp/test.php"
    ["line"]=>
    int(13)
    ["function"]=>
    string(4) "hoge"
    ["args"]=>
    array(1) {
      [0]=>
      &string(1) "A"
    }
  }
}
array(1) {
  [0]=>
  array(4) {
    ["file"]=>
    string(23) "/home/iogi/tmp/test.php"
    ["line"]=>
    int(13)
    ["function"]=>
    string(4) "hoge"
    ["args"]=>
    array(1) {
      [0]=>
      &string(1) "A"
    }
  }
}

と同じ結果が得られ、きちんと関数の引数のレベルを無視しているように見えます。

まとめ

  • わかったこと
    • 関数の引数から呼び出したら、エラーが出る理由
    • どちらにしろ、イケてないこと
  • わからないこと
    • EG(argument_stack)へのデータ格納の仕様

こんなの調べても何にも役にたたないよ!


※この記事ではちと勘違いがあったので、次のエントリも一緒に読んでください。