PHP-7.1 源代碼學習:代碼生成 之 函數定義

前言

本文從函數定義的語法規則開始,簡要介紹 PHP 解釋器如何 "編譯" 函數定義php

函數對應的 AST 節點

爲了看起來清楚一些,咱們將 語法規則定義 與 語法動做分開:node

// zend_language_parser.y

top_statement: function_declaration_statement
function_declaration_statement:
    function returns_ref T_STRING backup_doc_comment '(' parameter_list ')' return_type
    backup_fn_flags '{' inner_statement_list '}' backup_fn_flags

        { $$ = zend_ast_create_decl(ZEND_AST_FUNC_DECL, $2 | $13, $1, $4,
              zend_ast_get_str($3), $6, NULL, $11, $8); CG(extra_fn_flags) = $9; }

根據語法動做,這條函數定義規則會建立一個 ZEND_AST_FUNC_DECL 類型的 AST 結點,咱們來看看 zend_ast_create_create_decl 方法:數組

// zend_ast.c

ZEND_API zend_ast *zend_ast_create_decl(
    zend_ast_kind kind,
    uint32_t flags,
    uint32_t start_lineno,
    zend_string *doc_comment,
    zend_string *name,
    zend_ast *child0,
    zend_ast *child1,
    zend_ast *child2,
    zend_ast *child3) {
    zend_ast_decl *ast;

    ast = zend_ast_alloc(sizeof(zend_ast_decl));
    ast->kind = kind;
    ast->attr = 0;
    ast->start_lineno = start_lineno;
    ast->end_lineno = CG(zend_lineno);
    ast->flags = flags;
    ast->lex_pos = LANG_SCNG(yy_text);
    ast->doc_comment = doc_comment;
    ast->name = name;
    ast->child[0] = child0;
    ast->child[1] = child1;
    ast->child[2] = child2;
    ast->child[3] = child3;

    return (zend_ast *) ast;
}
  • zend_ast_create_decl 是一個通用的方法,經過 kind 參數區分不一樣類型的定義函數

  • 同理,參數 child0, child1 .etc 的命名也是很 "範化" 的oop

編譯 AST

zend_compile_func_decl 用於編譯函數定義 AST,因爲函數代碼相對比較長,咱們分塊開分析佈局

// zend_compile.c

void zend_compile_func_decl(znode *result, zend_ast *ast) {
    // 獲取 AST 子節點
    zend_ast_decl *decl = (zend_ast_decl *) ast;
    zend_ast *params_ast = decl->child[0];
    zend_ast *uses_ast = decl->child[1];
    zend_ast *stmt_ast = decl->child[2];
    zend_ast *return_type_ast = decl->child[3];
    zend_bool is_method = decl->kind == ZEND_AST_METHOD;

    // 將 CG 的 active_op_array(字節碼數組) 保存在 orig_op_array 中,由於每一個函數會有本身的 op_array
    zend_op_array *orig_op_array = CG(active_op_array);
    // 新建 op_array
    zend_op_array *op_array = zend_arena_alloc(&CG(arena), sizeof(zend_op_array));
    zend_oparray_context orig_oparray_context;
    // 初始化新建的 op_array
    init_op_array(op_array, ZEND_USER_FUNCTION, INITIAL_OP_ARRAY_SIZE);
    op_array->fn_flags |= (orig_op_array->fn_flags & ZEND_ACC_STRICT_TYPES);
    op_array->fn_flags |= decl->flags;
    op_array->line_start = decl->start_lineno;
    op_array->line_end = decl->end_lineno;
    if (decl->doc_comment) {
        op_array->doc_comment = zend_string_copy(decl->doc_comment);
    }
    if (decl->kind == ZEND_AST_CLOSURE) {
        op_array->fn_flags |= ZEND_ACC_CLOSURE;
    }
}

這裏有幾個地方比較有意思:ui

  • 函數參數 ast 的類型是 zend_ast,可是被強制轉換成了 zend_ast_decl,這個 zend_ast_decl 結構體和 zend_ast 結構體在內存佈局上有者相同的 "頭部",C 語言常常使用這種技巧類實現相似 面向對象裏面 繼承 的概念code

  • 這裏又遇到了 CG,參考以前的系列文章,CG 是解釋器在 編譯代碼 過程當中用於保存編譯上下文的一個 "對象",當遇到函數定義時,解釋器會把當前已經生成的 active_op_array 保存起來,爲函數定義新建一個 op_array,至於這個新建的 op_array 保存在哪?請見下文分解對象

咱們接着看源代碼:繼承

// zend_compile_func_decl @ zend_compile.c

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);
    if (uses_ast) {
        zend_compile_closure_binding(result, uses_ast);
    }
}

CG(active_op_array) = op_array;

zend_oparray_context_begin(&orig_oparray_context);
  • 函數定義有兩種,全局函數以及類裏面的"方法",is_method 標誌區分這兩種狀況,若是是方法定義就調用 zend_begin_method_decl,這裏先略過不表

  • zend_begin_func_decl 函數用於在編譯以前作一些準備工做,注意到這裏傳入了新建的 op_array

下面是 zend_begin_func_decl 函數的實現,咱們只保留和函數 op_array 相關的代碼

static void zend_begin_func_decl(...) {
    ...

    key = zend_build_runtime_definition_key(lcname, decl->lex_pos);
    // 將 函數 key,op_array 存儲在 CG 的 function_table 中 !!!
    zend_hash_update_ptr(CG(function_table), key, op_array);

    if (op_array->fn_flags & ZEND_ACC_CLOSURE) {
        ...
    } else {
        // 在當前 active_op_array 中生成一條函數定義指令 !!!
        opline = get_next_op(CG(active_op_array));
        opline->opcode = ZEND_DECLARE_FUNCTION;
        opline->op1_type = IS_CONST;
        ...
    }
}

如今明白了,原來函數的 op_array 是保存在 CG 的 function_table 中,這裏還有一個有意思的地方,php 生成了一條函數定義指令,這一點正是 動態腳本 語言和 靜態類型語言(Java)很是不一樣的地方!靜態類型的語言不須要執行代碼來添加函數 or 方法,由於它們在代碼編譯階段就已經肯定了,固然也就缺乏了一點靈活性

咱們迴歸主線,接着看 zend_compile_func_decl 代碼

// 上面已經將 CG(active_op_array)暫存起來了,因此這裏將 CG(active_op_array) 設置成 函數的 op_array
// 函數內部的語句的字節碼都會保存在 CG(active_op_array) 中 !!!
CG(active_op_array) = op_array;

zend_oparray_context_begin(&orig_oparray_context);

if (CG(compiler_options) & ZEND_COMPILE_EXTENDED_INFO) {
    zend_op *opline_ext = zend_emit_op(NULL, ZEND_EXT_NOP, NULL, NULL);
    opline_ext->lineno = decl->start_lineno;
}

{
    /* Push a separator to the loop variable stack */
    zend_loop_var dummy_var;
    dummy_var.opcode = ZEND_RETURN;

    zend_stack_push(&CG(loop_var_stack), (void *) &dummy_var);
}
// 編譯參數
zend_compile_params(params_ast, return_type_ast);
if (CG(active_op_array)->fn_flags & ZEND_ACC_GENERATOR) {
    zend_mark_function_as_generator();
    zend_emit_op(NULL, ZEND_GENERATOR_CREATE, NULL, NULL);
}
if (uses_ast) {
    zend_compile_closure_uses(uses_ast);
}
// 編譯函數內部語句
zend_compile_stmt(stmt_ast);

if (is_method) {
    zend_check_magic_method_implementation(
        CG(active_class_entry), (zend_function *) op_array, E_COMPILE_ERROR);
}

/* put the implicit return on the really last line */
CG(zend_lineno) = decl->end_lineno;

zend_do_extended_info();
zend_emit_final_return(0);

pass_two(CG(active_op_array));
zend_oparray_context_end(&orig_oparray_context);

/* Pop the loop variable stack separator */
zend_stack_del_top(&CG(loop_var_stack));

CG(active_op_array) = orig_op_array;

總結

相關文章
相關標籤/搜索