上一篇”获取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