怎麼樣擷取PHP變數的變數名之擴充實現

來源:互聯網
上載者:User

標籤:

很長時間沒有更新部落格了. 一來最近工作比較忙,沒有時間好好研究問題, 二是覺得沒有很好的材料可以寫. 也有一些沒有徹底研究透的問題,寫著寫著沒有了頭緒,都扔在了草稿箱裡了. 這次順帶也要更新一下部落格的模版了, 現在的這個模版主體有點窄,不適合閱讀. 我這個部落格現在,以後主要還是寫一些技術的東西.還是換一個眼睛友好的主題吧.

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

如果有興趣先看看這個功能是怎麼實現的. 可以先 點擊這裡下載代碼 .

1.問題:能在PHP中擷取php變數本身的名字麼?

一年多前做一個模版引擎的什麼時候有了這樣一個需求: 擷取變數的變數名. 比如:

1

2

3

4

5

$some_variable_name = "blahblah";

//...

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);

    }

    /* }}} */

    點擊這裡下載代碼

  • 本文來自:Linux學習教程網

怎麼樣擷取PHP變數的變數名之擴充實現

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.