定制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结构是怎么实现的吧:
1
2 3
|
$str = \' var_dump(token_get_all($str)); |
PHP被切分为一个一个的token 比如 print 就被分析为一个个的token
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
|
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文件了.
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
|
return T_EXIT; }
return T_EXIT; }
return T_FUNCTION; }
return T_CONST; }
return T_RETURN; }
return T_TRY; }
return T_CATCH; }
return T_THROW; } |
… 下面是词法分析中变量的词法分析代码.
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
|
/* Make sure a label character follows "->", otherwise there is no property
* and "->" will be taken literally */ 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 */ 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; }
zend_copy_value(zendlval, (yytext+1), (yyleng-1)); zendlval->type = IS_STRING; return T_VARIABLE; } |
这里把这见关键字token化为特定的token,变量的词法分析有一些不一样. 变量在token化的时候会把变量的名字(yytext)保存起来.这对于我们后续将变量的名字返回出来比较有用.
在Zend/zend_language_parser.y中
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
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我希望能这样使用:
1
2 3 4
|
$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了.
1
2 3
|
return T_VARIABLE_NAME; } |
匹配好了以后需要在Zend/zend_language_parser.y
1
2 3 4 5 6 7 8 9 10 11
|
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的方法了.
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
|
// 这里就是编译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中我增加了:
1
2
|
157 /* Added by reeze */
158 #define ZEND_VARIABLE_NAME 154 |
至于为什么是154就没什么好说的了. 只是依照顺序opcode 的顺序依次增加.
好了.到现在opcode已经编译好了. 大家都知道opcode是一条一条执行的.那么现在我们就需要进入执行阶段了.我们看看opcode是怎么执行的吧
入口在Zend/zend_vm_execute.h
zend_op的结构为:
1
2 3 4 5 6 7 8 9
|
struct _zend_op {
opcode_handler_t handler; znode result; znode op1; znode op2; ulong extended_value; uint lineno; zend_uchar opcode; }; |
其中的handler就是在opcode在执行的时候需要执行的处理函数.
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
|
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脚本.
1
2 3 4 5 6
|
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吧:
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
|
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 & |
建议继续学习:
- 怎样获取PHP变量的变量名之PHP实现 (阅读:2716)
- 怎么样获取PHP变量的变量名之扩展实现 (阅读:2682)
- PHP的可变变量名 (阅读:2253)
- 让 Lua 支持中文变量名 (阅读:1546)
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:reeze 来源: Zen Space [睿]
- 标签: 变量名
- 发布时间:2011-03-02 23:04:44
- [54] IOS安全–浅谈关于IOS加固的几种方法
- [52] android 开发入门
- [52] 如何拿下简短的域名
- [51] 图书馆的世界纪录
- [49] Go Reflect 性能
- [49] Oracle MTS模式下 进程地址与会话信
- [47] 【社会化设计】自我(self)部分――欢迎区
- [46] 读书笔记-壹百度:百度十年千倍的29条法则
- [35] 程序员技术练级攻略
- [27] 视觉调整-设计师 vs. 逻辑