怎么样获取PHP变量的变量名之扩展实现

很长时间没有更新博客了. 一来最近工作比较忙,没有时间好好研究问题, 二是觉得没有很好的材料可以写. 也有一些没有彻底研究透的问题,写着写着没有了头绪,都扔在了草稿箱里了. 这次顺带也要更新一下博客的模版了, 现在的这个模版主体有点窄,不适合阅读. 我这个博客现在,以后主要还是写一些技术的东西.还是换一个眼睛友好的主题吧.

本文要解决的是从去年就一直在考虑的一个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);

    }

    /* }}} */

    点击这里下载代码

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。