怎麼樣獲取PHP變量的變量名之擴展實現

很長時間沒有更新博客了. 一來最近工做比較忙,沒有時間好好研究問題, 二是以爲沒有很好的材料能夠寫. 也有一些沒有完全研究透的問題,寫着寫着沒有了頭緒,都扔在了草稿箱裏了. 此次順帶也要更新一下博客的模版了, 如今的這個模版主體有點窄,不適合閱讀. 我這個博客如今,之後主要仍是寫一些技術的東西.仍是換一個眼睛友好的主題吧.php

本文要解決的是從去年就一直在考慮的一個PHP的問題: 怎麼樣獲取PHP變量的變量名. 一直以來都沒有好好的研究.最近斷斷續續的開始看PHP源代碼.並嘗試解決. 直到兩星期前把問題都解決了纔開始把這些東西都記下來.node

若是有興趣先看看這個功能是怎麼實現的. 能夠先 點擊這裏下載代碼 .linux

1.問題:能在PHP中獲取php變量自己的名字麼?

一年多前作一個模版引擎的何時有了這樣一個需求: 獲取變量的變量名. 好比:json

1vim

2數組

3函數

4工具

5學習

$some_variable_name = "blahblah";ui

//...

echo get_var_name($some_variable_name); //  這裏指望輸出"some_variable_name";

?>

若是你也有這樣的需求. 你對需求的理解絕對有問題. 不事後來想一想這需求雖然不合理. 可是若是我偏有這樣不合理的需求, 我有辦法真的能知足麼?

2.有哪些解決方法

在遇到這個問題以前,沒有太系統的去看過PHP的C實現. 從問題提出到目前爲止,我想到了以下幾種方法:

  • 直接寫一個PHP函數來獲取.好比:

    1

    2

    3

    4

    function get_var_name($var) {

    // 可是... 我怎麼的到變量的名字呢...

    // echo ?  How To?

    }

    用過$GLOBALS變量的人應該知道能夠經過 $GLOBALS[\'var\']的方式來獲取變量$var的值. 這樣的話,我應該就能這樣實現了

    1

    2

    3

    4

    5

    6

    7

    function get_var_name($var) {

    foreach($GLOBALS as $var_name  => $var_value) {

    if($var === $var_value) {

    return $var_name;

    }

    }

    }

    這個是不可行的. 首先, 這個方法只能返回全局做用域內的變量. 若是在函數體內調用這個函數會有問題. 而且經過值比較也徹底不可靠.

  • 隨後我開始看PHP的內部實現.知道了在PHP執行過程當中全部的變量都是存放在符號表(symbol_table)中, 和$GLOBALS變量相似, 以變量名 =>值的方式存儲.. 而且在不一樣的做用域內有不一樣的active_symbol_table, 這樣的話就不存在做用域的問題了, 那咱們是否是能夠從當前的符號表中來根據傳遞進來的變量值來進行比較呢. 在符號表的值是存放在一個指向zval結構的指針. 那咱們是否可能經過比較指針地址的方式來查找保存該值的變量名呢? 其實這也是行不通的. 由於在PHP內部可能有多個變量指向同一個內部值.也就是引用計數. 看來經過符號表仍是解決不了問題.
  • 經過對PHP內部實現的進一步學習發如今腳本運行的時候仍是有不少其餘豐富的內部信息能夠利用.好比以下的腳本運行時全局變量. 這也是解決這個問題的突破口所在, 本文將根據這些運行時信息來編寫一個實現該功能的擴展.

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    struct _zend_executor_globals {

    zval **return_value_ptr_ptr;

    zval uninitialized_zval;

    zval *uninitialized_zval_ptr;

    zval error_zval;

    zval *error_zval_ptr;

    zend_ptr_stack arg_types_stack;

    /* symbol table cache */

    HashTable *symtable_cache[SYMTABLE_CACHE_SIZE];

    HashTable **symtable_cache_limit;

    HashTable **symtable_cache_ptr;

    zend_op **opline_ptr;

    HashTable *active_symbol_table;   // 當前做用域的變量符號表

    HashTable symbol_table;     /* main symbol table */   // 全局符號表

    HashTable included_files;   /* files already included */

    //..

    };

  • - 最後確定能實現的一種方式是爲PHP增長一個相似echo的語法結構. 這種方式的侵入性最大, 在這篇日誌中將不討論這種實現方式, 我將在下一篇日誌中介紹經過修改PHP語法的方式來支持開篇所提出的問題.
  • 3.擴展實現

    好比模塊提供一個叫作get_var_name()的函數來獲取變量名字. 若是你們有寫過PHP擴展的經驗的話,應該看過相似以下的函數實現(取自php json擴展$PHP_SRC/ext/json/json.c):

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    /* {{{ proto string json_encode(mixed data [, int options])

    Returns the JSON representation of a value */

    static PHP_FUNCTION(json_encode)

    {

    zval *parameter;

    smart_str buf = {0};

    long options = 0;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z|l", ¶meter, &options) == FAILURE) {   // 這裏將傳入的參數取出來. 參考文檔 http://www.php.net/manual/en/internals2.funcs.php

    return;

    }

    php_json_encode(&buf, parameter, options TSRMLS_CC);

    ZVAL_STRINGL(return_value, buf.c, buf.len, 1);

    smart_str_free(&buf);

    }

    /* }}} */

    這中實如今函數體內能夠經過zend_parse_parameter的方式來獲取傳遞進來的變量, 但這樣只能獲取到變量的值. 卻沒法獲得其餘更多的信息.,咱們往低層看看在PHP中函數是怎麼調用,參數是怎麼傳遞的.

    3.1 PHP中函數的調用

    在研究函數怎麼調用以前, 咱們須要看看PHP代碼是怎麼執行的.

    大體能夠分爲2個步驟:

    - 詞法分析,語法分析而後編譯成opcode

    - 執行opcode

    PHP函數的執行也只能在opcode執行階段執行.

    這裏以前要介紹一個查看OPCODE的絕佳工具 vld(http://pecl.php.net/package/vld)

    裝好這擴展。能夠在命令行下查看php腳本編譯後的opcode

    咱們看看下面這個php腳本被編譯後opcode是什麼樣的.

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    $str = "http://reeze.cn";

    $len = strlen ($str);

    $len2 = strlen ($str2=10);

    echo $len;

    echo $len2;

    ?>

    $ php -dvld.active=1  func_call.php

    \

    也能夠再增長一個參數 -dvld.verbosity=3, 這樣將會顯示更多的信息.

    \

    它被編譯爲上面的10條opcode命令.

    op的名稱一看也能看出什麼意思. .. 其中以 「!」開頭的數字表示編譯後的變量,, 以」~」開頭的變量表示零時變量.

    上面可能夠看出若是函數調用存在參數的話,在DO_FCALL以前會執行SEND_VAR 或者 SEND_VAR_NO_REF指令。而且這些指令後面操做的是編譯過變量或者一個臨時變量.

    在PHP中調用時咱們是能夠訪問到DO_FCALL這個操做的opcode信息 。能夠經過 EG(active_opline_ptr) 獲取到當前指令

    PHP中存在一系列*G宏, EG 則爲在執行opcode時的全局變量。

    見文件: $PHP_SRC/Zend/zend_globals.h

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    26

    27

    28

    29

    30

    31

    32

    33

    34

    35

    36

    37

    38

    39

    40

    struct _zend_executor_globals {

    // ...

    zend_op **opline_ptr;     // 指向當前正在執行的zend_op對象

    HashTable *active_symbol_table;

    HashTable symbol_table;     /* main symbol table */

    HashTable included_files;   /* files already included */

    jmp_buf *bailout;

    int error_reporting;

    int orig_error_reporting;

    int exit_status;

    zend_op_array *active_op_array;

    // ...

    };

    struct _zend_op_array {

    /* Common elements */

    zend_uchar type;

    char *function_name;

    zend_class_entry *scope;

    zend_uint fn_flags;

    union _zend_function *prototype;

    zend_uint num_args;

    zend_uint required_num_args;

    zend_arg_info *arg_info;

    zend_bool pass_rest_by_reference;

    unsigned char return_reference;

    /* END of common elements */

    zend_bool done_pass_two;

    zend_uint *refcount;

    zend_op *opcodes;              // zend_op數組.

    zend_uint last, size;

    zend_compiled_variable *vars;  // 全部編譯後的變量信息Since PHP5.1   這是一個數組

    int last_var, size_var;        // last_var 最後一個編譯變量的索引

    // ...

    };

    當前執行的op_array中保存全部編譯變量的信息, 再看看zend_compiled_variable的結構吧。

    1

    2

    3

    4

    5

    typedef struct _zend_compiled_variable {

    char *name;

    int name_len;

    ulong hash_value;

    } zend_compiled_variable;

    這正是我想獲取的變量名稱.

    咱們能夠經過全局變量EG(opline_ptr)指針獲取到當前執行的zend_op, zend_op的結構以下:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    struct _zend_op {

    opcode_handler_t handler;   // 處理該OPCODE的處理函數

    znode result;                  // 該opcode執行的結果

    znode op1;                                    // 有的opcode須要1個,有的須要兩個操做數。

    znode op2;

    ulong extended_value;

    uint lineno;

    zend_uchar opcode; // 該opcode的值 見$PHP_SRC/Zend/zend_vm_opcodes.h

    };

    這也就是咱們函數調用時執行的opcode.咱們如今能夠獲取到DO_FCALL時的opcode, 經過VLD察看opcode工具很容易就知道函數調用以前,若是函數有參數的話,在DO_FCALL以前必定有SEND_VAR或者 SEND_VAR_NO_REF指令, 指針後退一個則必定是指向SEND_VAR或SEND_VAR_NO_REF指令的。 這樣的話咱們根據DO_FCALL獲取到的zend_op指令後退不久能夠獲取SEND_VAR指令了麼. SEND_VAR指令會操做compiled_var,這樣咱們就能獲得變量的信息了..

    看看znode都有哪些信息:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    typedef struct _znode {

    int op_type;

    union {

    zval constant ;

    zend_uint var;  // 這個var就是當前變量在zend_op_array.vars 中的compiled_variable數組中的索引.不過這個索要並非字面上的. 詳情請看最後的代碼實現.

    zend_uint opline_num; /*  Needs to be signed */

    zend_op_array *op_array;

    zend_op *jmp_addr;

    struct {

    zend_uint var;  /* dummy */

    zend_uint type;

    } EA;

    } u;

    } znode;

    如在在上面的註釋. 經過獲取znode.u.var的值就能夠獲取到變量的信息了.

    這樣的話.程序的實現也就簡單了.

    下面是實現:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    26

    27

    28

    29

    30

    31

    32

    33

    34

    35

    36

    37

    38

    39

    40

    41

    42

    43

    44

    45

    46

    47

    48

    49

    50

    51

    52

    53

    54

    55

    56

    57

    58

    /* {{{ get_var_name

    *

    * 這個擴展要求PHP >= 5.1

    * 由於依賴PHP 5.1引入的compiled variable

    *

    * 在PHP空間導出一個get_var_name函數.

    * echo get_var_name($var_name);   // expect: var_name

    * echo get_var_name($lineno=100); // expect: lineno

    */

    PHP_FUNCTION(get_var_name)

    {

    int len;

    char *strg = "";

    if(ZEND_NUM_ARGS() < 1) {

    return;

    }

    /* 顯示全部的編譯變量

    int i;

    zend_compiled_variable *vars = EG(active_op_array)->vars;

    for(i=0; i < EG(active_op_array)->last_var; ++i) { // last_var 最後一個編譯變量的索引

    spprintf(&strg, 0, "%s\\nVar:%s\\n", strg, EG(active_op_array)->vars[i].name);

    ++vars;

    }

    */

    zend_op *pre_opline_ptr = *EG(opline_ptr);

    pre_opline_ptr--;

    // 支持這類的調用:  get_var_name($a="VALUE"); // expect: a

    // 這裏增長在賦值的狀況下也能正確返回變量的名字的處理方法, 若是方法參數是賦值的的話, 編譯的OPCODE 中SEND_VAR以前將會

    // 有一個ZEND_ASSIGN 操做, 而且ZEND_ASSIGN操做的返回值被使用.好比: $c = $d + 1;  $d + 1的返回值就被使用了. 就能夠確認

    // 是前面的調用方式

    zend_op *pre_pre_online_ptr = pre_opline_ptr - 1;

    if(pre_pre_online_ptr && pre_pre_online_ptr->opcode == ZEND_ASSIGN && !(pre_pre_online_ptr->result.u.EA.type & EXT_TYPE_UNUSED)) {

    // 經過賦值以前的zend_op來獲取變量信息

    pre_opline_ptr = pre_pre_online_ptr;

    }

    int index;

    // 好比get_var_name($name); 這時SEND_VAR OPCODE的op1操做數類型就是IS_CV 也就是IS Compiled Variable

    // 只有compiled variable纔是直接存儲索引的. PHP >= 5.1

    if(pre_opline_ptr->op1.op_type == IS_CV) {

    index = pre_opline_ptr->op1.u.var;

    }

    else {

    // 請參考VLD的源代碼  $VLD_SRC/srm_oparray.c LINE:320 vld_dump_znode函數

    index = pre_opline_ptr->op1.u.var / sizeof(temp_variable);

    }

    zend_compiled_variable var = EG(active_op_array)->vars[index];

    len = spprintf(&strg, 0, "%s", strg, var.name);

    RETURN_STRINGL(strg, len, 0);

    }

    /* }}} */

    點擊這裏下載代碼

相關文章
相關標籤/搜索