函数的编译

3.2.1.3 函数的编译

我们在上一篇文章介绍过PHP代码的编译过程,主要是PHP->AST->Opcodes的转化,上面也说了函数其实就是将一组PHP代码编译为单独的opcodes,函数的调用就是不同opcodes间的切换,所以函数的编译过程与普通PHP代码基本一致,只是会有一些特殊操作,我们以3.2.1.2开始那个例子简单看下编译过程。

普通函数的语法解析规则:

function_declaration_statement:
    function returns_ref T_STRING backup_doc_comment '(' parameter_list ')' return_type
    '{' inner_statement_list '}'
        { $$ = zend_ast_create_decl(ZEND_AST_FUNC_DECL, $2, $1, $4,
            zend_ast_get_str($3), $6, NULL, $10, $8); }
;

规则主要由五部分组成:

  • __returns_ref:__ 是否返回引用,在函数名前加&,比如function &test(){...}
  • __T_STRING:__ 函数名
  • __parameter_list:__ 参数列表
  • __return_type:__ 返回值类型
  • inner_statement_list: 函数内部代码

函数生成的抽象语法树根节点类型是zend_ast_decl,所有函数相关的信息都记录在这个节点中(除了函数外类也是用的这个):

typedef struct _zend_ast_decl {
    zend_ast_kind kind; //函数就是ZEND_AST_FUNC_DECL,类则是ZEND_AST_CLASS
    zend_ast_attr attr; /* Unused - for structure compatibility */
    uint32_t start_lineno; //函数起始行
    uint32_t end_lineno;  //函数结束行
    uint32_t flags;   //其中一个标识位用来标识返回值是否为引用,是则为ZEND_ACC_RETURN_REFERENCE
    unsigned char *lex_pos;
    zend_string *doc_comment;
    zend_string *name;  //函数名
    zend_ast *child[4]; //child有4个子节点,分别是:参数列表节点、use列表节点、函数内部表达式节点、返回值类型节点
} zend_ast_decl;

上面的例子最终生成的语法树:

具体编译为opcodes的过程在zend_compile_func_decl()中:

void zend_compile_func_decl(znode *result, zend_ast *ast)
{
    zend_ast_decl *decl = (zend_ast_decl *) ast;
    zend_ast *params_ast = decl->child[0]; //参数列表
    zend_ast *uses_ast = decl->child[1]; //use列表
    zend_ast *stmt_ast = decl->child[2]; //函数内部
    zend_ast *return_type_ast = decl->child[3]; //返回值类型
    zend_bool is_method = decl->kind == ZEND_AST_METHOD; //是否为成员函数

    //这里保存当前正在编译的zend_op_array:CG(active_op_array),然后重新为函数生成一个新的zend_op_array,
    //函数编译完再将旧的还原
    zend_op_array *orig_op_array = CG(active_op_array);
    zend_op_array *op_array = zend_arena_alloc(&CG(arena), sizeof(zend_op_array)); //新分配zend_op_array
    ...

    if (is_method) {
        zend_bool has_body = stmt_ast != NULL;
        zend_begin_method_decl(op_array, decl->name, has_body);
    } else {
        zend_begin_func_decl(result, op_array, decl); //注意这里会在当前zend_op_array(不是新生成的函数那个)生成一条ZEND_DECLARE_FUNCTION的opcode
    }
    CG(active_op_array) = op_array;
    ...

    zend_compile_params(params_ast, return_type_ast); //编译参数
    if (uses_ast) {
        zend_compile_closure_uses(uses_ast); 
    }
    zend_compile_stmt(stmt_ast); //编译函数内部语法
    ...
    pass_two(CG(active_op_array));
    ...
    CG(active_op_array) = orig_op_array; //还原之前的
}

> 编译过程主要有这么几个处理:

> (1) 保存当前正在编译的zend_op_array,新分配一个结构,因为每个函数、include的文件都对应独立的一个zend_op_array,通过CG(active_op_array)记录当前编译所属zend_op_array,所以开始编译函数时就需要将这个值保存下来,等到函数编译完成再还原回去;另外还有一个关键操作:zend_begin_func_decl,这里会在当前zend_op_array(不是新生成的函数那个)生成一条 ZEND_DECLARE_FUNCTION 的opcode,也就是函数声明操作。

$a = 123;  //当前为CG(active_op_array) = zend_op_array_1,编译到这时此opcode加到zend_op_array_1

//新分配一个zend_op_array_2,并将当前CG(active_op_array)保存到origin_op_array,
//然后将CG(active_op_array)=zend_op_array_2
function test(){
    $b = 234; //编译到zend_op_array_2
}//函数编译结束,将CG(active_op_array) = origin_op_array,切回zend_op_array_1
$c = 345; //编译到zend_op_array_1

> (2) 编译参数列表,函数的参数我们在上一小节已经介绍,完整的参数会有三个组成:参数类型(可选)、参数名、默认值(可选),这三部分分别保存在参数节点的三个child节点中,编译参数的过程有两个关键操作:

> 操作1: 为每个参数编号

> 操作2: 每个参数生成一条opcode,如果是可变参数其opcode=ZEND_RECV_VARIADIC,如果有默认值则为ZEND_RECV_INIT,否则为ZEND_RECV

> 上面的例子中$a编号为96,$b为112,同时生成了两条opcode:ZEND_RECV、ZEND_RECV_INIT,调用的时候会根据具体传参数量跳过部分opcode,比如这个函数我们这么调用my_function($a)则ZEND_RECV这条opcode就直接跳过了,然后执行ZEND_RECV_INIT将默认值写到112位置,具体的编译过程在zend_compile_params()中,上面已经介绍过。 > > 参数默认值的保存与普通变量赋值相同:$a = array()array()保存在literals,参数的默认值也是如此。 > > (3) 编译函数内部语法,这个跟普通PHP代码编译过程无异。

> (4) pass_two(),上一篇介绍过,不再赘述。

最终生成两个zend_op_array:

总体来看,PHP在逐行编译时发现一个function则生成一条ZEND_DECLARE_FUNCTION的opcode,然后调到函数中编译函数,编译完再跳回去继续下面的编译,这里多次提到ZEND_DECLARE_FUNCTION这个opcode是因为在函数编译结束后还有一个重要操作:zend_do_early_binding(),前面我们说过总的编译入口在zend_compile_top_stmt(),这里会对每条语法逐条编译,而函数、类在编译完成后还有后续的操作:

void zend_compile_top_stmt(zend_ast *ast)
{
    ...
    if (ast->kind == ZEND_AST_STMT_LIST) {
        for (i = 0; i < list->children; ++i) {
            zend_compile_top_stmt(list->child[i]);
        }
    }

    zend_compile_stmt(ast); //编译各条语法,函数也是在这里编译完成

    //函数编译完成后
    if (ast->kind == ZEND_AST_FUNC_DECL || ast->kind == ZEND_AST_CLASS) {
        CG(zend_lineno) = ((zend_ast_decl *) ast)->end_lineno;
        zend_do_early_binding();
    }
}

zend_do_early_binding()核心工作就是 将function、class加到CG(function_table)、CG(class_table)中 ,加入成功了就直接把 ZEND_DECLARE_FUNCTION 这条opcode干掉了,加入失败的话则保留,这个相当于 有一部分opcode在『编译时』提前执行了 ,这也是为什么PHP中可以先调用函数再声明函数的原因,比如:

$a = 1234;
echo my_function($a);
function my_function($a){
    ...
}

实际原始的opcode以及执行顺序:

类的情况也是如此,后面我们再作说明。

联系我们

邮箱 626512443@qq.com
电话 18611320371(微信)
QQ群 235681453

Copyright © 2015-2024

备案号:京ICP备15003423号-3