深入理解PHP原理之实现自己的PHP语法

来源:互联网 发布:淘宝网店经营范围 编辑:程序博客网 时间:2024/06/05 09:25

  前面的文章中已经讲过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上,上面的两条规则分别对于类似:

<?phpecho 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/
  

0 0
原创粉丝点击