往期精选(欢迎转发~~)
- 如何看待程序员35岁职业危机?
- Java全套学习资料(14W字),耗时半年整理
- 我肝了三个月,为你写出了GO核心手册
- 消息队列:从选型到原理,一文带你全部掌握
- 肝了一个月的ETCD,从Raft原理到实践
- 更多…
前面的文章中已经讲过PHP的词法分析、语法分析、opcodes编译,有了上面的基础,我们可以通过修改PHP源码,实现自己的PHP语法,示例如下:
<?php
$demo = 'tipi';
echo var_name($demo); //执行结果,输出:demo
?>
其执行过程如下:
该过程为词法分析–>语法分析–>opcodes编译–>执行,下面我们看看每一步对源码有哪些修改。
1.词法分析和语法分析
我们知道词法分析和语法分析的文件分别为zend_language_scanner.l和zend_language_parser.y。首先我们需要加入新的Token,即在文件zend_language_scanner.l中加入以下内容:
"var_name" {
return T_VARIABLE_NAME;
}
也就是在词分析阶段遇到var_name这个字符串的时候会被标记为我们定义的T_VARIABLE_NAME token。同样,在 zend_language_parser.y 也需要加入对这个token进行响应的逻辑处理。我们要实现的语法和PHP内置的echo print结构类似,所以我们把这个处理放到 internal_functions_in_yacc规则里面:
| T_VARIABLE_NAME '(' T_VARIABLE ')' { zend_do_variable_name(&$$, &$3 TSRMLS_CC); }
| T_VARIABLE_NAME T_VARIABLE { zend_do_variable_name(&$$, &$2 TSRMLS_CC); }
第一个参数是当前表达式的返回值(编辑器不能连续打两个美元符号),&$3表是第三个表达式的值,也就是T_VARIABLE上,上面的两条规则分别对于类似:
<?php
echo var_name($varname);
echo var_name $varname;
2.opcodes编译
opcode在PHP中通常是一个数字唯一标识,首先,我们在Zend/zend_vm_opcodes.h 为我们的新opcode 加入一个宏定义,这个数字要求在0-255之间,并且不能与现有opcode重复:
#define ZEND_VARIABLE_NAME 154
第二步,在Zend/zend_compile.c中加入我们对opcode的处理,也就是将代码操作转化为op_array放入到opline中:
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));
opline->opcode = ZEND_VARIABLE_NAME;
opline->op1 = *variable;
// 我们只需要一个操作数就好了
SET_UNUSED(opline->op2);
*result = opline->result;
}
这样,我们就完成了对opcode的编译。
3.内部处理逻辑的编写
前面只是基本语法处理与编译,这部分才是核心,包括如何处理自定义的opcode,以及编写具体的代码逻辑。前面我们提到 Zend/zend_vm_execute.h中的zend_vm_get_opcode_handler()函数,这个函数是用来获取opcode的执行函数,其对应关系通过公式计算,公式如下:
return zend_opcode_handlers[opcode * 25 + zend_vm_decode[op->op1.op_type] * 5 + zend_vm_decode[op->op2.op_type]];
从这个公式我们可以看出,最终的处理函数与参数类型有关,根据计算,我们要满足所有类型的映射,尽管我们可以可以使用同一函数进行处理, 于是我们在zend_opcode_handlers这个数组的结尾,加上25个相同的函数定义:
void zend_init_opcodes_handlers(void)
{
static const opcode_handler_t labels[] = {
....
ZEND_VARIABLE_NAME_HANDLER,
....
ZEND_VARIABLE_NAME_HANDLER
}
如果我们不想支持某类型的数据,只需要将类型代入公式计算出的数字做为索引,使
opcode_handler_t中相应的项为:ZEND_NULL_HANDLER。最后,我们在Zend/zend_vm_def.h 中增加相应的处理函数,增加代码如下:
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();
}
进行完上面的修改之后,我们要删除r2ec&flex已经编译好的原文件,即删除Zend/zend_language*.c文件以使新的语法规则生效。 这样我们再次对PHP源码进行make时,会自动生成新的编译好的语法规则处理程序,不过编译环境要安装有lex&yacc和re2c。
参考: http://www.php-internals.com/