标签:
很长时间没有更新博客了. 一来最近工作比较忙,没有时间好好研究问题, 二是觉得没有很好的材料可以写. 也有一些没有彻底研究透的问题,写着写着没有了头绪,都扔在了草稿箱里了. 这次顺带也要更新一下博客的模版了, 现在的这个模版主体有点窄,不适合阅读. 我这个博客现在,以后主要还是写一些技术的东西.还是换一个眼睛友好的主题吧.
本文要解决的是从去年就一直在考虑的一个PHP的问题: 怎么样获取PHP变量的变量名. 一直以来都没有好好的研究.最近断断续续的开始看PHP源代码.并尝试解决. 直到两星期前把问题都解决了才开始把这些东西都记下来.
如果有兴趣先看看这个功能是怎么实现的. 可以先 点击这里下载代码 .
一年多前做一个模版引擎的什么时候有了这样一个需求: 获取变量的变量名. 比如:
1 2 3 4 5 |
$some_variable_name = "blahblah"; //... echo get_var_name($some_variable_name); // 这里期望输出"some_variable_name"; ?> |
如果你也有这样的需求. 你对需求的理解绝对有问题. 不过后来想想这需求虽然不合理. 但是如果我偏有这样不合理的需求, 我有办法真的能满足么?
在遇到这个问题之前,没有太系统的去看过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; } } } |
这个是不可行的. 首先, 这个方法只能返回全局作用域内的变量. 如果在函数体内调用这个函数会有问题. 并且通过值比较也完全不可靠.
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 */ //.. }; |
比如模块提供一个叫做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中函数是怎么调用,参数是怎么传递的.
在研究函数怎么调用之前, 我们需要看看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 |
也可以再增加一个参数 -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); } /* }}} */ |
标签:
原文地址:http://www.cnblogs.com/zgh2015/p/4247995.html