• 自己动手为PHP7添加新的语法特性


    好文章!

    nikic介绍了如何向PHP添加新的语法特性,原文写的非常精彩,具体是添加in语法功能,使最终实现:

    <?php
    $words = ['hello', 'world', 'foo', 'bar'];
    var_dump('hello' in $words); // true
    var_dump('foo' in $words);   // true
    var_dump('blub' in $words);  // false
    
    $string = 'PHP is fun!';
    var_dump('PHP' in $string);    // true
    var_dump('Python' in $string); // false
    

    我进行了一下实践,根据PHP7进行了些修改,具体记录下自己的实践过程和心得。

    环境准备

    Github上下载PHP7的源码,准备好PHP的开发编译环境,我是基于ubuntu,所以运行如下:

    $ apt-get -y install build-essential autoconf bison re2c
    

    下一步,编译PHP7

    $ cd php-src
    // create new branch for in operator
    $ git checkout -b addInOperator
    // build ./configure script
    $ ./buildconf
    // configure PHP in debug mode and with thread safety
    $ ./configure --disable-all --enable-debug --enable-maintainer-zts
    // compile (4 is the number of cores you have)
    $ make -j4
    

    PHP的二进制就是sapi/cli/php,可以运行以下命令测试是否编译成功:

    $ ./sapi/cli/php -v
    $ ./sapi/cli/php -r 'echo "Hallo World!";'
    

    PHP脚本的生命历程

    PHP脚本运行经过3个主要阶段:

    1. 词法分析
    2. 语法分析&编译
    3. 执行

    词法分析

    这个阶段是将源码根据规则分解成称为token更小的单元,为后面的语法分析提供材料。
    修改位于Zend/目录的zend_language_scanner.l,这个文件定义了词法记号规则,为了让PHP能识别in作为关键词。

    <ST_IN_SCRIPTING>"in" {
        RETURN_TOKEN(T_IN);
    }
    

    为了让Zend引擎能识别T_IN这个记号,在zend_language_parser.y中加入:

    %token T_IN "in (T_IN)"
    

    重新生成tokenizer系列源文件

    $ cd ext/tokenizer
    $ ./tokenizer_data_gen.sh
    

    语法分析&编译

    向语法分析增加in所应用的表达式规则,在zend_language_parser.y加入:

    expr_without_variable:
    ...
    |   expr T_IN expr  { $$ = zend_ast_create_binary_op(ZEND_IN, $1, $3); }
    ...
    

    再设置in的操作符优先级,在语法分析文件中找到以下行,并在行尾加入T_IN

    %nonassoc '<' T_IS_SMALLER_OR_EQUAL '>' T_IS_GREATER_OR_EQUAL
    

    执行

    最后在zend_vm_def.h中添加具体的执行逻辑:

    ZEND_VM_HANDLER(173, ZEND_IN, CONST|TMPVAR|CV, CONST|TMPVAR|CV)
    {
        USE_OPLINE
        zend_free_op free_op1, free_op2;
        zval *op1, *op2;
    
        SAVE_OPLINE();
        op1 = GET_OP1_ZVAL_PTR_UNDEF(BP_VAR_R);
        op2 = GET_OP2_ZVAL_PTR_UNDEF(BP_VAR_R);
    
        // TODO
    
        FREE_OP1();
        FREE_OP2();
        CHECK_EXCEPTION();
        ZEND_VM_NEXT_OPCODE();
    }
    

    在PHP源码根目录运行:

    $ ./sapi/cli/php ./Zend/zend_vm_gen.php
    $ make -j4
    

    zend_vm_gen.php是根据zend_vm_def.h中的定义生成zend_vm_*.*系列的文件。

    之后测试编译后的PHP

    $ ./sapi/cli/php -r 'var_dump("s" in "str");'
    

    会出现Segmentation fault,原因是我之前在zend_language_parser.y中加入的操作是zend_ast_create_binary_op,所以在编译时会调用位于zend_opcode.cget_binary_op方法,这个方法返回一个函数指针,用于处理expr in expr这个操作两个参数的语句。

    binary_op_type op = get_binary_op(ast->attr);
    ret = op(result, &op1, &op2);
    

    所以之后我在3处添加代码:

    zend_opcode.c中添加查询ZEND_IN操作函数的代码

    ZEND_API binary_op_type get_binary_op(int opcode)
    {
        switch (opcode) {
            ... 
            case ZEND_IN:
                return (binary_op_type) in_function;
            ...
    }
    

    zend_operators.c中添加处理函数

    ZEND_API int ZEND_FASTCALL in_function(zval *result, zval *op1, zval *op2)
    {
        zval op1_copy, op2_copy;
        int use_copy1 = 0, use_copy2 = 0;
    
        switch (Z_TYPE_P(op2)) {
            case IS_STRING:
                use_copy1 = zend_make_printable_zval(op1, &op1_copy);
                if (use_copy1) {
                    op1 = &op1_copy;
                }
    
                if (Z_STRLEN_P(op1) == 0) {
                    ZVAL_TRUE(result);
                } else {
                    const char *found = zend_memnstr_ex(
                        Z_STRVAL_P(op2),
                        Z_STRVAL_P(op1),
                        Z_STRLEN_P(op1),
                        Z_STRVAL_P(op2) + Z_STRLEN_P(op2)
                    );
    
                    ZVAL_BOOL(result, found != NULL);
                }
                break;
            case IS_ARRAY:
                use_copy1 = zend_make_printable_zval(op1, &op1_copy);
                if (use_copy1) {
                    op1 = &op1_copy;
                }
    
                ZVAL_BOOL(result, zend_hash_exists(Z_ARR_P(op2), Z_STR_P(op1)));
                break;
            default:
                zend_error(E_EXCEPTION | E_ERROR, "Unsupported operand types");
                ZVAL_FALSE(result);
                return FAILURE;
        }
    
        if (use_copy1) {
            zval_dtor(&op1_copy);
        }
        return SUCCESS;
    }
    

    zend_vm_def.h中补上TODO该做的处理逻辑

    // 我在系统里的最新操作号是173,根据实际为准
    ZEND_VM_HANDLER(173, ZEND_IN, CONST|TMPVAR|CV, CONST|TMPVAR|CV)
    {
        USE_OPLINE
        zend_free_op free_op1, free_op2;
        zval *op1, *op2;
    
        SAVE_OPLINE();
        op1 = GET_OP1_ZVAL_PTR_UNDEF(BP_VAR_R);
        op2 = GET_OP2_ZVAL_PTR_UNDEF(BP_VAR_R);
    
        in_function(EX_VAR(opline->result.var), op1, op2);
        ZVAL_TRUE(EX_VAR(opline->result.var));
    
        FREE_OP1();
        FREE_OP2();
        CHECK_EXCEPTION();
        ZEND_VM_NEXT_OPCODE();
    }
    

    依旧是在PHP源码根目录下运行:

    $ ./sapi/cli/php ./Zend/zend_vm_gen.php
    $ make -j4
    

    现在再测试生成的php,应该会满足:

    $ ./sapi/cli/php -r 'var_dump("s" in "str");'
    bool(true)
    $ ./sapi/cli/php -r 'var_dump("s" in "xxx");'
    bool(false)
    $ ./sapi/cli/php -r 'var_dump("a" in ["a"=>1]);'
    bool(true)
    $ ./sapi/cli/php -r 'var_dump("a" in ["b"=>1]);'
    bool(false)
    $ ./sapi/cli/php -r 'var_dump("s" in 123);'
    Fatal error: Unsupported operand types in Command line code on line 1
    

    小结

    实际进行了一番操作之后,对于PHP的运行机制会落实到更小的单位,从解释阶段到文件级别。
    PHP7版本引入了ast这个中间结构,比起nikic那个版本来说,需要修改更多文件,下一步要剖析一下astPHP7中的作用和在执行过程中的角色。

  • 相关阅读:
    同台电脑 多Git账号同时使用
    netty对http协议解析原理解析(转载)
    Netty 线程模型与Reactor 模式
    增量/存量数据按时间维度分组
    网易技术分享:Nginx缓存引发的跨域惨案
    全面剖析Redis Cluster原理和应用
    聊聊阿里社招面试,谈谈“野生”Java程序员学习的道路
    美团点评基于 Flink 的实时数仓建设实践
    美团技术分享:大众点评App的短视频耗电量优化实战
    美团技术分享:美团深度学习系统的工程实践
  • 原文地址:https://www.cnblogs.com/pier2/p/add-new-feature-for-php7.html
Copyright © 2020-2023  润新知