[PHP-Internal]定制PHP语法获取PHP变量的变量名

上一篇”获取PHP变量名扩展的“文章中通过一个PHP扩展的方式,实现了获取变量的变量名实现. 这次将通过为PHP语言增加语法结构的方式来实现这样一个功能。
PHP的语法实现是通过lex以及yacc 实现的。 lex负责词法分析,yacc负责语法分析。
语法实现的文件有两个:

$PHP_SRC/Zend/zend_language_scanner.l 词法定义 一般的语法结构错误在这里检查。比如
$PHP_SRC/Zend/zend_language_parser.y 语法定义 而类似 函数定义缺少function关键字的错误。在这个环节报出来。运行时的错误(函数不存在,类找不到之类的)或异常在opcode执行期间处理

为了给PHP增加语法结构就得从语法分析开始。 如果没有接触过lex&yacc,可以先去看看后面提到的lex/yacc学习链接。

我们要实现的语法结构和print有点像, 这两个结构都不是PHP函数调用,都有返回值.那我们先看看print怎么实现的吧(为什么是print而不是echo, 因为我们的需求是需要返回变量的变量名,需要有返回值, print有返回值, 而echo没有,不是NULL, 如果你尝试给将将echo的返回值赋给一个变了会出现语法错误,注意是语法错误.). 首先我们看看print结构是怎么实现的吧:

$str = ' var_dump(token_get_all($str));

PHP被切分为一个一个的token 比如 print 就被分析为一个个的token

array(6) {
[0]=>
array(3) {
[0]=>
int(368) // PHP脚本开始标记 T_OPEN_TAG
[1]=>
string(6) " [2]=>
int(1)
}
[1]=>
array(3) {
[0]=>
int(266) // 对应于 token的 Zend/zend_language_parser.h T_PRINT
[1]=>
string(5) "print"
[2]=>
int(1)
}
[2]=>
string(1) "("
[3]=>
array(3) {
[0]=>
int(315) // T_CONSTANT_ENCAPSED_STRING 常量字符串
[1]=>
string(8) ""result""
[2]=>
int(1)
}
[4]=>
string(1) ")"
[5]=>
string(1) ";"
}

token化了以后就交由 Zend/zend_language_parse.y处理了,根据y文件里的规则进行opcode编译. 将这些token编译为opcode. 编译完了以后再执行.
如果你熟悉编译原理的话,这些东西看起来就没有什么问题了,如果还不熟悉,可以看看 Yacc 与Lex 快速入门 或者下载《Lex与Yacc》中文第二版(带源码) 花不了你多少时间就可以看懂Zend/language_scanner.l以及 language_parser.y文件了.


"exit" {
return T_EXIT;
}

"die" {
return T_EXIT;
}

"function" {
return T_FUNCTION;
}

"const" {
return T_CONST;
}

"return" {
return T_RETURN;
}

"try" {
return T_TRY;
}

"catch" {
return T_CATCH;
}

"throw" {
return T_THROW;
}

... 下面是词法分析中变量的词法分析代码.


/* Make sure a label character follows "->", otherwise there is no property
* and "->" will be taken literally
*/
"$"{LABEL}"->"[a-zA-Z_x7f-xff] {
yyless(yyleng - 3);
yy_push_state(ST_LOOKING_FOR_PROPERTY TSRMLS_CC);
zend_copy_value(zendlval, (yytext+1), (yyleng-1));
zendlval->type = IS_STRING;
return T_VARIABLE;
}

/* A [ always designates a variable offset, regardless of what follows
*/
"$"{LABEL}"[" {
yyless(yyleng - 1);
yy_push_state(ST_VAR_OFFSET TSRMLS_CC);
zend_copy_value(zendlval, (yytext+1), (yyleng-1));
zendlval->type = IS_STRING;
return T_VARIABLE;
}

"$"{LABEL} {
zend_copy_value(zendlval, (yytext+1), (yyleng-1));
zendlval->type = IS_STRING;
return T_VARIABLE;
}

这里把这见关键字token化为特定的token,变量的词法分析有一些不一样. 变量在token化的时候会把变量的名字(yytext)保存起来.这对于我们后续将变量的名字返回出来比较有用.

在Zend/zend_language_parser.y中

104 %token T_CASE
105 %token T_DEFAULT
106 %token T_BREAK
107 %token T_CONTINUE
108 %token T_GOTO
109 %token T_FUNCTION
110 %token T_CONST
111 %token T_RETURN
...
220 unticked_statement:
....

238 | T_CONTINUE ';' { zend_do_brk_cont(ZEND_CONT, NULL TSRMLS_CC); }
239 | T_CONTINUE expr ';' { zend_do_brk_cont(ZEND_CONT, &$2 TSRMLS_CC); }
240 | T_RETURN ';' { zend_do_return(NULL, 0 TSRMLS_CC); }
241 | T_RETURN expr_without_variable ';' { zend_do_return(&$2, 0 TSRMLS_CC); }
242 | T_RETURN variable ';' { zend_do_return(&$2, 1 TSRMLS_CC); }
...
651 | T_PRINT expr { zend_do_print(&$$, &$2 TSRMLS_CC); }

最后一行的规则匹配一个T_PRINT标记,然后是一个表达式的标记. 匹配到后则执行zend_do_print方法.其中的参数$$表示返回值, $2表示第二个标记也就是表达式expr


回到我们的需求,我希望给php增加一个语法结构.叫做var_name我希望能这样使用:


$a_named_variable = "http://reeze.cn/tags/php";
echo var_name($a_named_variable); // "a_named_variable";
?>

按照上面的这写语法编译的方式. 那么我们需要增加一个token用来标示var_name 现在我就叫他T_VARIABLE_NAME吧. 后面的变量在php中已经有了响应的处理方式,比如那个变量的token T_VARIABLE, 那么我需要在Zend/zend_language_scanner.l中处理这个token, 我在里面增加了如下内容, 这样就可以匹配脚本中的var_name了.


"var_name" {
return T_VARIABLE_NAME;
}

匹配好了以后需要在Zend/zend_language_parser.y

internal_functions_in_yacc:
T_ISSET '(' isset_variables ')' { $$ = $3; }
| T_EMPTY '(' variable ')' { zend_do_isset_or_isempty(ZEND_ISEMPTY, &$$, &$3 TSRMLS_CC); }
| T_INCLUDE expr { zend_do_include_or_eval(ZEND_INCLUDE, &$$, &$2 TSRMLS_CC); }
| T_INCLUDE_ONCE expr { zend_do_include_or_eval(ZEND_INCLUDE_ONCE, &$$, &$2 TSRMLS_CC); }
| T_EVAL '(' expr ')' { zend_do_include_or_eval(ZEND_EVAL, &$$, &$3 TSRMLS_CC); }
| T_REQUIRE expr { zend_do_include_or_eval(ZEND_REQUIRE, &$$, &$2 TSRMLS_CC); }
| T_REQUIRE_ONCE expr { zend_do_include_or_eval(ZEND_REQUIRE_ONCE, &$$, &$2 TSRMLS_CC); }
| T_VARIABLE_NAME '(' T_VARIABLE ')' { zend_do_variable_name(&$$, &$3 TSRMLS_CC); }
| T_VARIABLE_NAME T_VARIABLE { zend_do_variable_name(&$$, &$2 TSRMLS_CC); }
;

增加了两行 最后需要在Zend/zend_compile.c中增加匹配到这个规则所做的动作. 最后两个规则表示可以通过var_name($variable); 或者var_name $variable的方式使用. 类似于echo.
好了.下面这个方法就是用来编译opcode的方法了.

// 这里就是编译opcode
void zend_do_variable_name(znode *result, znode *variable TSRMLS_DC) /* {{{ */
{
// 生成一条zend_op
zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);

// 因为我们需要有返回值, 并且返回值只作为中间值.所以就是一个临时变量
opline->result.op_type = IS_TMP_VAR;
opline->result.u.var = get_temporary_variable(CG(active_op_array));

// 很多人说echo和strlen这类函数的区别就像C中的宏和函数的区别一样
// 其实在PHP中并不是如此,这里的区别只是opcode的值不一样,如果是函数的话
// opcode将会是ZEND_DO_FCALL.
// 真是因为需要执行,下面的ZEND_VARIABLE_NAME是我增加的一个opcode类型.见Zend/zend_vm_opcodes.h
opline->opcode = ZEND_VARIABLE_NAME;
// 我们把var_name($var)中作为操作数传递进来
// 这里的variable是哪里来的呢? 见我在Zend/zend_language_parser.y中增加的
// T_VARIABLE_NAME T_VARIABLE { zend_do_variable_name(&$$, &$2 TSRMLS_CC); } 对应于 T_VARIABLE
opline->op1 = *variable;

// 我们只需要一个操作数就好了
SET_UNUSED(opline->op2);

*result = opline->result;
}

在Zend/zend_vm_opcodes.h中我增加了:

157 /* Added by reeze */
158 #define ZEND_VARIABLE_NAME 154

至于为什么是154就没什么好说的了. 只是依照顺序opcode 的顺序依次增加.

好了.到现在opcode已经编译好了. 大家都知道opcode是一条一条执行的.那么现在我们就需要进入执行阶段了.我们看看opcode是怎么执行的吧
入口在Zend/zend_vm_execute.h
zend_op的结构为:


struct _zend_op {
opcode_handler_t handler;
znode result;
znode op1;
znode op2;
ulong extended_value;
uint lineno;
zend_uchar opcode;
};

其中的handler就是在opcode在执行的时候需要执行的处理函数.

static opcode_handler_t zend_vm_get_opcode_handler(zend_uchar opcode, zend_op* op)
{
static const int zend_vm_decode[] = {
_UNUSED_CODE, /* 0 */
_CONST_CODE, /* 1 = IS_CONST */
_TMP_CODE, /* 2 = IS_TMP_VAR */
_UNUSED_CODE, /* 3 */
_VAR_CODE, /* 4 = IS_VAR */
_UNUSED_CODE, /* 5 */
_UNUSED_CODE, /* 6 */
_UNUSED_CODE, /* 7 */
_UNUSED_CODE, /* 8 = IS_UNUSED */
_UNUSED_CODE, /* 9 */
_UNUSED_CODE, /* 10 */
_UNUSED_CODE, /* 11 */
_UNUSED_CODE, /* 12 */
_UNUSED_CODE, /* 13 */
_UNUSED_CODE, /* 14 */
_UNUSED_CODE, /* 15 */
_CV_CODE /* 16 = IS_CV */
};
// 映射关系.
return zend_opcode_handlers[opcode * 25 + zend_vm_decode[op->op1.op_type] * 5 + zend_vm_decode[op->op2.op_type]];
}
// 为 opcode设定处理函数.
ZEND_API void zend_vm_set_opcode_handler(zend_op* op)
{
op->handler = zend_vm_get_opcode_handler(zend_user_opcodes[op->opcode], op);
}

// 为此增加执行的处理函数
static int ZEND_FASTCALL ZEND_VARIABLE_NAME_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
zend_op *opline = EX(opline);

// PHP中所有的变量在内部都是存储在zval结构中的.
zval *result = &EX_T(opline->result.u.var).tmp_var;

// 把变量的名字赋给临时返回值
Z_STRVAL(*result) = estrndup(opline->op1.u.constant.value.str.val, opline->op1.u.constant.value.str.len);
Z_STRLEN(*result) = opline->op1.u.constant.value.str.len;
Z_TYPE(EX_T(opline->result.u.var).tmp_var) = IS_STRING;

ZEND_VM_NEXT_OPCODE();
}

为了能让opcode映射到这个处理函数.需要在void zend_init_opcodes_handlers(void)函数中根据映射关系添加函数.
根据static opcode_handler_t zend_vm_get_opcode_handler(zend_uchar opcode, zend_op* op)中的映射关系.在末尾增加了ZEND_VARIABLE_NAME_HANDLER函数指针.

现在好了.把Zend/zend_language*.c文件删除.然后重新make一下. 执行一下重新编译好的php ($PHP_SRC/sapi/cli/php). 因为这修改了php本身所以必须重新编译php.

PS: 重新编译需要lex&yacc或者类似的变体(flex&bison). 还需要安装re2c. 否则也是无法正常编译的. 下面是测试的php脚本.


echo var_name($name); // "name"
echo var_name($variable); // "variable"
echo var_name($afdsafs=10); // Syntax Error: 这次的实现和php扩展的方式不一样. 如果像这样的调用会出现语法错误.当然这个也是可以解决的.不过这就不是这次的重点了
?>



所有的修改见文章最后的diff,我是基于PHP5.3.3修改的, 所以你的PHP代码版本最好是5.3分支的. 你可以从官方下载, 也可以从github的镜像下载. 如果你只想试试看是什么效果. 可以下载这个文件. 至于怎么给代码打补丁.网上搜一下patch命令怎么用把.也可以直接man patch:).

当然我并不推荐自己在生产环境修改一个自己的php分支. 如果真的觉得你对语法的修改对大家都有用.可以写一个RFC给php相关的邮件组.或者你觉得其他语言有非常有用的特性能为php所用,推荐订阅PHP的邮件列表http://www.php.net/mailing-lists.php 的Internal list http://marc.info/?l=php-internals,在这里能看到PHP语言的演变和他们对PHP的一些讨论以及最新的PHP的会具有的特性.

还是直接附上diff吧:

diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index ddae339..707192c 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -966,6 +966,34 @@ void zend_check_writable_variable(const znode *variable) /* {{{ */
}
/* }}} */

+
+// 这里就是编译opcode
+void zend_do_variable_name(znode *result, znode *variable TSRMLS_DC) /* {{{ */
+{
+ // 生成一条zend_op
+ zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);
+
+ // 因为我们需要有返回值, 并且返回值只作为中间值.所以就是一个临时变量
+ opline->result.op_type = IS_TMP_VAR;
+ opline->result.u.var = get_temporary_variable(CG(active_op_array));
+
+ // 很多人说echo和strlen这类函数的区别就像C中的宏和函数的区别一样
+ // 其实在PHP中并不是如此,这里的区别只是opcode的值不一样,如果是函数的话
+ // opcode将会是ZEND_DO_FCALL.
+ // 真是因为需要执行,下面的ZEND_VARIABLE_NAME是我增加的一个opcode类型.见Zend/zend_vm_opcodes.h
+ opline->opcode = ZEND_VARIABLE_NAME;
+ // 我们把var_name($var)中作为操作数传递进来
+ // 这里的variable是哪里来的呢? 见我在Zend/zend_language_parser.y中增加的
+ // T_VARIABLE_NAME T_VARIABLE { zend_do_variable_name(&$$, &$2 TSRMLS_CC); } 对应于 T_VARIABLE
+ opline->op1 = *variable;
+
+ // 我们只需要一个操作数就好了
+ SET_UNUSED(opline->op2);
+
+ *result = opline->result;
+}
+/* }}} */
+
void zend_do_begin_variable_parse(TSRMLS_D) /* {{{ */
{
zend_llist fetch_list;
diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y
index 682b594..239c742 100644
--- a/Zend/zend_language_parser.y
+++ b/Zend/zend_language_parser.y
@@ -149,6 +149,8 @@
%token T_DIR
%token T_NS_SEPARATOR

+%token T_VARIABLE_NAME
+
%% /* Rules */

start:
@@ -987,6 +989,8 @@ internal_functions_in_yacc:
| T_EVAL '(' expr ')' { zend_do_include_or_eval(ZEND_EVAL, &$$, &$3 TSRMLS_CC); }
| T_REQUIRE expr { zend_do_include_or_eval(ZEND_REQUIRE, &$$, &$2 TSRMLS_CC); }
| T_REQUIRE_ONCE expr { zend_do_include_or_eval(ZEND_REQUIRE_ONCE, &$$, &$2 TSRMLS_CC); }
+ | T_VARIABLE_NAME '(' T_VARIABLE ')' { zend_do_variable_name(&$$, &$3 TSRMLS_CC); }
+ | T_VARIABLE_NAME T_VARIABLE { zend_do_variable_name(&$$, &$2 TSRMLS_CC); }
;

isset_variables:
diff --git a/Zend/zend_language_scanner.l b/Zend/zend_language_scanner.l
index a4faff3..157101c 100644
--- a/Zend/zend_language_scanner.l
+++ b/Zend/zend_language_scanner.l
@@ -1110,6 +1110,10 @@ NEWLINE ("r"|"n"|"rn")
return T_ISSET;
}

+"var_name" {
+ return T_VARIABLE_NAME;
+}
+
"empty" {
return T_EMPTY;
}
diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h
index 45f5c62..bffa443 100644
--- a/Zend/zend_vm_execute.h
+++ b/Zend/zend_vm_execute.h
@@ -3065,6 +3065,21 @@ static int ZEND_FASTCALL ZEND_CONCAT_SPEC_CONST_TMP_HANDLER(ZEND_OPCODE_HANDLER
ZEND_VM_NEXT_OPCODE();
}

+static int ZEND_FASTCALL ZEND_VARIABLE_NAME_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
+{
+ zend_op *opline = EX(opline);
+
+ // PHP中所有的变量在内部都是存储在zval结构中的.
+ zval *result = &EX_T(opline->result.u.var).tmp_var;
+
+ // 把变量的名字赋给临时返回值
+ Z_STRVAL(*result) = estrndup(opline->op1.u.constant.value.str.val, opline->op1.u.constant.value.str.len);
+ Z_STRLEN(*result) = opline->op1.u.constant.value.str.len;
+ Z_TYPE(EX_T(opline->result.u.var).tmp_var) = IS_STRING;
+
+ ZEND_VM_NEXT_OPCODE();
+}
+
static int ZEND_FASTCALL ZEND_IS_IDENTICAL_SPEC_CONST_TMP_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
zend_op *opline = EX(opline);
@@ -33867,7 +33882,33 @@ void zend_init_opcodes_handlers(void)
ZEND_NULL_HANDLER,
ZEND_NULL_HANDLER,
ZEND_NULL_HANDLER,
- ZEND_NULL_HANDLER
+ ZEND_NULL_HANDLER,
+ /* Added by reeze */
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER,
+ ZEND_VARIABLE_NAME_HANDLER
};
zend_opcode_handlers = (opcode_handler_t*)labels;
}
diff --git a/Zend/zend_vm_opcodes.h b/Zend/zend_vm_opcodes.h
index d048a85..14276a5 100644
--- a/Zend/zend_vm_opcodes.h
+++ b/Zend/zend_vm_opcodes.h
@@ -153,3 +153,6 @@
#define ZEND_USER_OPCODE 150
#define ZEND_JMP_SET 152
#define ZEND_DECLARE_LAMBDA_FUNCTION 153
+
+/* Added by reeze */
+#define ZEND_VARIABLE_NAME 154

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

上一篇文章里提到是用PHP扩展实现获取变量的变量名的方法. 今天发现有一个PHP实现的版本 . 实现方法来自:http://mach13.com/how-to-get-a-variable-name-as-a-string-in-php

刚开始以为这个方法好使, 仔细想想其实也是有问题的.

这个解决方法是用的PHP里的get_defined_vars()方法,该方法返回当前作用域内的所有变量信息.也是和$GLOBALS一样,以变量名 => 值的方式返回.
他的代码很简单:


function var_name (&$iVar, &$aDefinedVars)
{
foreach ($aDefinedVars as $k=>$v)
$aDefinedVars_0[$k] = $v;

$iVarSave = $iVar;
$iVar =!$iVar; // 将当前变量的值取反

$aDiffKeys = array_keys (array_diff_assoc ($aDefinedVars_0, $aDefinedVars)); // 对比取反前后的变量
$iVar = $iVarSave; // 恢复当前变量的值

return $aDiffKeys[0];
}

?>

它通过引用的方式改变当前变量的值, 然后通过对比前后两个数组的差异来获取值被改变了的变量.然后返回其名字.经过测试这的确是一个方法.相对我实现的方法. 它提供的方法移植性较好, 不需要赖以扩展. 而这个php版本的实现, 必须传递一个get_defined_vars()的参数, 我实现的那个扩展,则不需要. 对于类似 var_name($a=10,get_defined_vars()); 的调用,该方法无法正常获得变量名.

这个今天又仔细想了想,下面提供的方法是有问题的.. 他解决问题的方法是通过修改变量的值, 并对比前后所有的变量来找出值发生变化的变量. 而实际上.修改了其中一个变量另一个变量的值也会发生变化: 这就是引用, 如下

$a = 10;
$b = &$a;

echo var_name($b, get_defined_vars()); // 这回返回a, 而不是b. 和预期的并不一样.

PS: 如果你真的需要这种方法. 请重新思考一下你的需要真的需要这样的方法么?

[PHP-Internal]怎么样获取PHP变量的变量名之扩展实现

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

本文要解决的是从去年就一直在考虑的一个PHP的问题: 怎么样获取PHP变量的变量名. 一直以来都没有好好的研究.最近断断续续的开始看PHP源代码.并尝试解决. 直到两星期前把问题都解决了才开始把这些东西都记下来.

如果有兴趣先看看这个功能是怎么实现的. 可以先点击这里下载代码.

1.问题:能在PHP中获取php变量本身的名字么?

一年多前做一个模版引擎的什么时候有了这样一个需求: 获取变量的变量名. 比如:

$some_variable_name = "blahblah";
//...
echo get_var_name($some_variable_name); // 这里期望输出"some_variable_name";
?>

如果你也有这样的需求. 你对需求的理解绝对有问题.  不过后来想想这需求虽然不合理. 但是如果我偏有这样不合理的需求, 我有办法真的能满足么?

2.有哪些解决方法

在遇到这个问题之前,没有太系统的去看过PHP的C实现. 从问题提出到目前为止,我想到了如下几种方法:

  • 直接写一个PHP函数来获取.比如:

    function get_var_name($var) {
    // 但是... 我怎么的到变量的名字呢...
    // echo ? How To?
    }

    用过$GLOBALS变量的人应该知道可以通过 $GLOBALS['var']的方式来获取变量$var的值. 这样的话,我应该就能这样实现了

    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内部实现的进一步学习发现在脚本运行的时候还是有很多其他丰富的内部信息可以利用.比如如下的脚本运行时全局变量.  这也是解决这个问题的突破口所在, 本文将根据这些运行时信息来编写一个实现该功能的扩展.


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

      /* {{{ 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", &parameter, &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是什么样的.

      $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

      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的结构吧。

      typedef struct _zend_compiled_variable {
      char *name;
      int name_len;
      ulong hash_value;
      } zend_compiled_variable;

      这正是我想获取的变量名称.

      我们可以通过全局变量EG(opline_ptr)指针获取到当前执行的zend_op, zend_op的结构如下:

      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都有哪些信息:

      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的值就可以获取到变量的信息了.

      这样的话.程序的实现也就简单了.

      下面是实现:

      /* {{{ 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, "%snVar:%sn", 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);
      }
      /* }}} */

      点击这里下载代码

在PHP中检查PHP文件是否有语法错误

之前在当当的时候的一个项目中用到了一个简单的模板引擎,其实也是借鉴discuz来做的模板引擎,很简单,它所作的事情就是把一些自定义的标签编译成php代码。已经说了很简单了,所以编译的时候也名优进行模板语法的检查,那么在开发过程中就会出现编译出来的php文件有语法问题,有语法问题没有关系,我修改重新编译一下就好了。首先不能在每次请求的时候都把php模板重新编译一下,会严重影响性能,折中的处理时在每个编译好的php文件末尾检查一下该模板文件是否已经修改过,根据设定的更新频率,如果又需要则重新编译模板文件,现在的问题是编译出来的php文件自己有语法错误,根本执行不到模板检查那一步,所以即使修改了模板文件中的问题也不会重新编译。 所以我想寻找一种简单的方法来检查生成的php文件是否合法。不合法就重新编译,这样开发过程中就不用出现错误就得手动删除缓存文件了。

在网上找了一下。刚开始以为 token_get_all()函数能处理语法错误的问题,结果发现,它只是做简单的词法分析。没有办法。后来到论坛上去问了一下

http://groups.google.com/group/professional-php/browse_thread/thread/b8581f6b07b10ff0/2601a63c406bb1c1?lnk=gst&q=reeze#2601a63c406bb1c1

有人告诉我有这样一个函数 php_check_syntax() http://www.php.net/manual/en/function.php-check-syntax.php 我想问题就这么坚决了。。我真应该RTF(Read The Fuck Mannual). 仔细一看。这个函数已近被弃用了:
Note: For technical reasons, this function is deprecated and removed from PHP. Instead, use php -l somefile.php from the commandline.

这个technical reason 到底是什么呢? 先不管了,以后再慢慢研究,反正不能使用这个方法就对了。
他们的建议是使用命令行$php -l filename.php 来检查语法。
Gary Every给了我一个代码片段参考:

在命令行下检查问题也不大。如果我要放在在线应用呢? 这就涉及到可移植性的问题了。首先是操作系统,然后就是环境变量。这样的话就会依赖于服务器端的配置。在http://www.php.net/manual/en/function.php-check-syntax.php 上有人贴出了自己的php_check_syntax()函数实现。
有的采用的就是上面的命令行的方法。
后面有提到使用eval的方法来验证。eval方法会执行传入的代码, 如果代码有语法错误则会抛出parser error, 可以使用’@'错误抑制符去掉错误信息,eval和echo一样并不是函数,不能使用变量函数的方法调用比如:
$func = ‘eval’
$func()这样的调用就是无效的。它会提示没有eval函数,如果你自己定义这么一个函数也是有问题的。因为eval是一个关键字。
eval调用和include差不多,如果被包含文件中没有明确return就返回null。如果直接eval我们需要检查的文件会造成被检查的文件内代码被执行,这可不是我们想要的,我们只需要检查一下这个文件的语法是否正确。 我们可以在要检查的文件之前添加return 语句,让代码提前跳出,那么后面的代码就不会执行了。好的,就这么干。代码如下:
checker.php

if(!function_exists('php_check_syntax')) {
function php_check_syntax($file_name, &$error_message = null) {
$file_content = file_get_contents($file_name);

$check_code = "return true; ?>";
$file_content = $check_code . $file_content . "

if(!@eval($file_content)) {
$error_message = "file: " . realpath($file_name) . " have syntax error";
return false;
}

return true;
}
}

if(!php_check_syntax("file.php", $msg)) {
echo $msg;
}
else {
echo "Woohoo, OK!";
}

file.php

foreach:: a => b
?>

因为Parse error 是没法被 set_error_handler处理函数处理的。这个异常没办法catch到。所以才使用了@来抑制错误。这带来的问题就是我们无法得到详细的错误信息。 不过目前我需要的功能也只是检查语法是否正确。不正确的话重新编译模板文件,就这么简单,至于语法错误,在显示网页的时候自然会看得到。
最好的办法就是这个被遗弃的php_check_syntax这个方法回到php中。下次再研究下他们是出于什么原因把这个函数去掉的。

PHP5.2.6中无法在exception_handler函数中抛出异常

在PHP bugs列表中也找到这个bug,但是似乎没有被处理,bug提出的时间是2005年,不知道新版本的有没有解决。
PHP:5.2.6
OS: Mac OS Leopard 10.5.7
Server: Apache 2.2

这个代码就有问题:
function e_handler($e)
{
throw new Exception();
}
set_exception_handler(‘e_handler’);
throw new Exception();

这将会导致
Fatal error: Exception thrown without a stack frame in Unknown on line 0

Update: 这应该属于设计问题,如果在exception_handler()函数中可以抛出异常,而这个异常又会继续调用exception_handler(),这样下去就会出现死循环,这就是为什么程序会出错的原因吧。
建议在exception_handler函数体内的代码包在try catch中,避免意外抛出异常导致出这个问题

function e_handler($e)
{
try
{
throw new Exception();
}
catch (Exception $e)
{
echo “catched…”;
}
}
set_exception_handler(‘e_handler’);

throw new Exception();

PHP调试函数

在项目中经常要调试程序,但是我电脑上的ZendStudio总是没配置好,不能单步调试,不过有时候不一定需要让ZendStudio来帮我们调试,所以写了下面这个辅助函数来方便调试,因为有时候调试的位置加多了自己也不知道到底是加在什么地方了,下面的函数就是方便的dump对象信息,同时显示调试的问题和所在的行数。


//调试函数,方便显示调试函数的位置和文件
function p(){
$args = func_get_args();

// 调用栈,debug_backtrace()可以返回调用栈。这样 我们就可以方便的知道函数在哪里调用的。
$backtrace = debug_backtrace();

$file = $backtrace[0]['file'];
$line = $backtrace[0]['line'];
echo "

";
  echo "$file:$linen";
  foreach ($args as $arg)
  {
    var_dump($arg);
  }
  echo "

";
exit;
}