【PHP7內核剖析】面向對象-類的實現及編譯

更多《PHP7內核剖析》系列文章:https://github.com/pangudashu/php7-internal

目錄:

3.4.1.1 類的結構及存儲

3.4.1.2 類常量

3.4.1.3 成員屬性

3.4.1.4 成員方法

3.4.1.5 類的編譯

3.4.1 類

類是現實世界或思維世界中的實體在計算機中的反映,它將某些具有關聯關係的數據以及這些數據上的操作封裝在一起。在面向對象中類是對象的抽象,對象是類的具體實例。

在PHP中類編譯階段的產物,而對象是運行時產生的,它們歸屬於不同階段。

PHP中我們這樣定義一個類:

class 類名 {
    常量;
    成員屬性;
    成員方法;
}

一個類可以包含有屬於自己的常量、變量(稱爲“屬性”)以及函數(稱爲“方法”),本節將圍繞這三部分具體弄清楚以下幾個問題:

  • a.類的存儲及索引
  • b.成員屬性的存儲結構
  • c.成員方法的存儲結構
  • d.成員方法的調用過程及與普通function調用的差別

3.4.1.1 類的結構及存儲

首先我們看下類的數據結構:

struct _zend_class_entry {
    char type;          //類的類型:內部類ZEND_INTERNAL_CLASS(1)、用戶自定義類ZEND_USER_CLASS(2)
    zend_string *name;  //類名,PHP類不區分大小寫,統一爲小寫
    struct _zend_class_entry *parent; //父類
    int refcount;
    uint32_t ce_flags;  //類掩碼,如普通類、抽象類、接口,除了這還有別的含義,暫未弄清

    int default_properties_count;        //普通屬性數,包括public、private
    int default_static_members_count;    //靜態屬性數,static
    zval *default_properties_table;      //普通屬性值數組
    zval *default_static_members_table;  //靜態屬性值數組
    zval *static_members_table;
    HashTable function_table;  //成員方法哈希表
    HashTable properties_info; //成員屬性基本信息哈希表,key爲成員名,value爲zend_property_info
    HashTable constants_table; //常量哈希表,通過constant定義的

    //以下是構造函授、析構函數、魔法函數的指針
    union _zend_function *constructor;
    union _zend_function *destructor;
    union _zend_function *clone;
    union _zend_function *__get;
    union _zend_function *__set;
    union _zend_function *__unset;
    union _zend_function *__isset;
    union _zend_function *__call;
    union _zend_function *__callstatic;
    union _zend_function *__tostring;
    union _zend_function *__debugInfo;
    union _zend_function *serialize_func;
    union _zend_function *unserialize_func;

    zend_class_iterator_funcs iterator_funcs;

    //下面這幾個暫時忽略,後面碰到的時候再分析其作用
    /* handlers */
    zend_object* (*create_object)(zend_class_entry *class_type);
    zend_object_iterator *(*get_iterator)(zend_class_entry *ce, zval *object, int by_ref);
    int (*interface_gets_implemented)(zend_class_entry *iface, zend_class_entry *class_type); /* a class implements this interface */
    union _zend_function *(*get_static_method)(zend_class_entry *ce, zend_string* method);

    /* serializer callbacks */
    int (*serialize)(zval *object, unsigned char **buffer, size_t *buf_len, zend_serialize_data *data);
    int (*unserialize)(zval *object, zend_class_entry *ce, const unsigned char *buf, size_t buf_len, zend_unserialize_data *data);

    uint32_t num_interfaces; //實現的接口數
    uint32_t num_traits;
    zend_class_entry **interfaces; //實現的接口

    zend_class_entry **traits;
    zend_trait_alias **trait_aliases;
    zend_trait_precedence **trait_precedences;

    union {
        struct {
            zend_string *filename;
            uint32_t line_start;
            uint32_t line_end;
            zend_string *doc_comment;
        } user;
        struct {
            const struct _zend_function_entry *builtin_functions;
            struct _zend_module_entry *module; //所屬擴展
        } internal;
    } info;
}

舉個例子具體看下,定義一個User類,它繼承了Human類,User類中有一個常量、一個靜態屬性、兩個普通屬性:

//父類
class Human {}

class User extends Human
{
    const type = 110;

    static $name = "uuu";
    public $uid = 900;
    public $sex = 'w';

    public function __construct(){
    }

    public function getName(){
        return $this->name;
    }
}

其對應的zend_class_entry存儲結構如下圖。

zend_class

開始的時候已經提到,類是編譯階段的產物,編譯完成後我們定義的每個類都會生成一個zend_class_entry,它保存着類的全部信息,在執行階段所有類相關的操作都是用的這個結構。

所有PHP腳本中定義的類以及內核、擴展中定義的內部類通過一個以”類名”作爲索引的哈希表存儲,這個哈希表保存在Zend引擎global變量中:zend_executor_globals.class_table(即:EG(class_table)),與function的存儲相同,關於這個global變量前面《3.3.1.3 zend_executor_globals》已經講過。

zend_eg_class

在接下來的小節中我們將對類的常量、成員屬性、成員方法的實現具體分析。

3.4.1.2 類常量

PHP中可以把在類中始終保持不變的值定義爲常量,在定義和使用常量的時候不需要使用 $ 符號,常量的值必須是一個定值(如布爾型、整形、字符串、數組,php5.*不支持數組),不能是變量、數學運算的結果或函數調用,也就是說它是隻讀的,無法進行賦值。

常量通過 const 定義:

class my_class {
    const 常量名 = 常量值;
}

常量通過 class_name::常量名 訪問,或在class內部通過 self::常量名 訪問。

常量是類維度的數據(而不是對象的),它們通過zend_class_entry.constants_table進行存儲,這是一個哈希結構,通過 常量名 索引,value就是具體定義的常量值。

常量的讀取:

根據前面我們對PHP opcode已有的瞭解,我們可以猜測常量訪問的opcode的組成:常量名保存在literals中(其op_type = IS_CONST),執行時先取出常量名,然後去zend_class_entry.constants_table哈希表中索引到具體的常量值即可。

事實上我們的這個猜測並不是完全正確的,因爲有的情況確實是我們猜想的那樣,但是還有另外一種情況,比較下兩個例子的不同:

//示例1
echo my_class::A1;

class my_class {
    const A1 = "hi";
}
//示例2

class my_class {
    const A1 = "hi";
}

echo my_class::A1;

唯一的不同就是常量的使用時機:示例1是在定義前使用的,示例2是在定義後使用的。我們都知道PHP變量無需提前聲明,這倆會有什麼不同呢?

事實上這兩種情況內核會有兩種不同的處理方式,示例1這種情況的處理與我們上面的猜測相同,而示例2則有另外一種處理方式:PHP代碼的編譯是順序的,示例2的情況編譯到echo my_class::A1這行時首先會嘗試檢索下是否已經編譯了my_class,如果能在CG(class_table)中找到,則進一步從類的contants_table查找對應的常量,找到的話則會複製其value替換常量,簡單的講就是類似C語言中的宏,編譯時替換爲實際的值了,而不是在運行時再去檢索。

具體debug下上面兩個例子會發現示例2的主要的opcode只有一個ZEND_ECHO,也就是直接輸出值了,並沒有設計類常量的查找,這就是因爲編譯的時候已經將 my_class::A1 替換爲 hi 了,echo my_class::A1;等同於:echo "hi";;而示例1首先的操作則是ZEND_FETCH_CONSTANT,查找常量,接着纔是ZEND_ECHO。

3.4.1.3 成員屬性

類的變量成員叫做“屬性”。屬性聲明是由關鍵字 publicprotected 或者 private 開頭,然後跟一個普通的變量聲明來組成,關於這三個關鍵字這裏不作討論,後面分析可見性的章節再作說明。

【修飾符(public/private/protected/static)】【成員屬性名】= 【屬性默認值】;

屬性中的變量可以初始化,但是初始化的值必須是常數,這裏的常數是指 PHP 腳本在編譯階段時就可以得到其值,而不依賴於運行時的信息才能求值,比如public $time = time();這樣定義一個屬性就會觸發語法錯誤。

成員屬性又分爲兩類:普通屬性靜態屬性。靜態屬性通過 static 聲明,通過 self::property__ 或 __類名:: property 訪問;普通屬性通過 this->property__ 或 __ object->property 訪問。

class my_class {
    //普通屬性
    public $property = 初始化值;

    //靜態屬性
    public static $property_2 = 初始化值;
}

與常量的存儲方式不同,成員屬性的 初始化值 並不是 直接 用以”屬性名”作爲索引的哈希表存儲的,而是通過數組保存的,普通屬性、靜態屬性各有一個數組分別存儲。

zend_class_property

看到這裏可能有個疑問:使用時成員屬性是如果找到的呢?

實際只是成員屬性的 VALUE 通過數組存儲的,訪問時仍然是根據以”屬性名”爲索引的散列表查找具體VALUE的,這個散列表並沒有按照普通屬性、靜態屬性分爲兩個,而是隻用了一個:HashTable properties_info 。此哈希表存儲元素的value類型爲 zend_property_info

typedef struct _zend_property_info {
    uint32_t offset; //普通成員變量的內存偏移值
                     //靜態成員變量的數組索引
    uint32_t flags;  //屬性掩碼,如public、private、protected及是否爲靜態屬性
    zend_string *name; //屬性名
    zend_string *doc_comment;
    zend_class_entry *ce; //所屬類
} zend_property_info;

//flags標識位
#define ZEND_ACC_PUBLIC     0x100
#define ZEND_ACC_PROTECTED  0x200
#define ZEND_ACC_PRIVATE    0x400

#define ZEND_ACC_STATIC         0x01
  • offset:這個值記錄的就是上面說的通過數組保存的屬性值的索引,也就是說屬性值保存在一個數組中,然後將其在數組中的位置保存在offset中,另外需要說明的一點的是普通屬性、靜態屬性這個值用法是不一樣的,靜態屬性是類的範疇,與對象無關,所以其offset爲default_static_members_table數組的下標:0,、1、2……,而普通屬性歸屬於對象,每個對象有其各自的屬性,所以這個offset記錄的實際是 各屬性在object中偏移值 (在後面《3.4.2 對象》一節我們再具體說明普通屬性的存儲方式),其值是:40、56、72……是按照zval的內存大小偏移的
  • flags:bit位,標識的是屬性的信息,如public、private、protected及是否爲靜態屬性

所以訪問成員屬性時首先是根據屬性名查找到此屬性的存儲位置,然後再進一步獲取屬性值。

舉個例子:

class my_class {
    public $property_1 = "aa";
    public $property_2 = array();

    public static $property_3 = 110;
}

default_properties_tabledefault_static_properties_tableproperties_info 關係圖:

zend_property_info

下面我們再看下普通成員屬性與靜態成員屬性的不同:靜態成員變量保存在類中,各對象共享同一份數據,而普通屬性屬於對象,各對象獨享。

成員屬性在類編譯階段就已經分配了zval,靜態與普通的區別在於普通屬性在創建一個對象時還會重新分配zval(這個過程類似zend引擎執行前分配在zend_execute_data後面的動態變量空間),對象對普通屬性的操作都是在其自己的空間進行的,各對象隔離,而靜態屬性的操作始終是在類的空間內,各對象共享。

3.4.1.4 成員方法

每個類可以定義若干屬於本類的函數(稱之爲成員方法),這種函數與普通的function相同,只是以類的維度進行管理,不是全局性的,所以成員方法保存在類中而不是EG(function_table)。

zend_class_function

成員方法的定義:

【修飾符(public/private/protected/static/abstruct/final)】function 【&】【成員方法名】(【參數列表】)【返回值類型】{【成員方法】};

成員方法也有靜態、非靜態之分,靜態方法中不能使用this this訪問屬於本對象的成員屬性。

靜態方法也是通過static關鍵詞定義:

class my_class {
    static public function test() {
        $a = "hi~";
        echo $a;
    }
}
//靜態方法可以這麼調用:
my_class::test();

//也可以這樣:
$method = 'test';
my_class::$method();

靜態方法中調用其它靜態方法或靜態變量可以通過 self 訪問。

成員方法的調用與普通function過程基本相同,根據對象所屬類或直接根據類取到method的zend_function,然後執行,具體的過程《3.3 Zend引擎執行過程》已經詳細說過,這裏不再重複。

3.4.1.5 類的編譯

前面我們先介紹了類的相關組成部分,接下來我們從一個例子簡單看下類的編譯過程,這個過程最終的產物就是zend_class_entry。

//示例
class Human {
    public $aa = array(1,2,3);
}

class User extends Human
{
    const type = 110;

    static $name = "uuu";
    public $uid = 900;
    public $sex = 'w';

    public function __construct(){
    }

    public function getName(){
        return $this->name;
    }
}

類的定義組成部分:

【修飾符(abstract/final)】 class 【類名】 【extends 父類】 【implements 接口1,接口2】 {}

語法規則爲:

class_declaration_statement:
        class_modifiers T_CLASS { $<num>$ = CG(zend_lineno); }
        T_STRING extends_from implements_list backup_doc_comment '{' class_statement_list '}'
            { $$ = zend_ast_create_decl(ZEND_AST_CLASS, $1, $<num>3, $7, zend_ast_get_str($4), $5, $6, $9, NULL); }
    |   T_CLASS { $<num>$ = CG(zend_lineno); }
        T_STRING extends_from implements_list backup_doc_comment '{' class_statement_list '}'
            { $$ = zend_ast_create_decl(ZEND_AST_CLASS, 0, $<num>2, $6, zend_ast_get_str($3), $4, $5, $8, NULL); }
;

//整個類內爲list,每個成員屬性、成員方法都是一個子節點
class_statement_list:
        class_statement_list class_statement
            { $$ = zend_ast_list_add($1, $2); }
    |   /* empty */
            { $$ = zend_ast_create_list(0, ZEND_AST_STMT_LIST); }
;

//類內語法規則:成員屬性、成員方法
class_statement:
        variable_modifiers property_list ';'
            { $$ = $2; $$->attr = $1; }
    |   T_CONST class_const_list ';'
            { $$ = $2; RESET_DOC_COMMENT(); }
    |   T_USE name_list trait_adaptations
            { $$ = zend_ast_create(ZEND_AST_USE_TRAIT, $2, $3); }
    |   method_modifiers function returns_ref identifier backup_doc_comment '(' parameter_list ')'
        return_type method_body
            { $$ = zend_ast_create_decl(ZEND_AST_METHOD, $3 | $1, $2, $5,
                  zend_ast_get_str($4), $7, NULL, $10, $9); }
;

生成的抽象語法樹:

類的語法樹根節點爲ZEND_AST_CLASS,此節點有3個子節點:繼承子節點、實現接口子節點、類中聲明表達式節點,其中child2爲zend_ast_list,每個常量定義、成員屬性、成員方法對應一個節點,比如上面的例子中user類有6個子節點,這些子節點類型有3類:常量聲明(ZEND_AST_CLASS_CONST_DECL)、屬性聲明(ZEND_AST_PROP_DECL)、方法聲明(ZEND_AST_METHOD)。

編譯爲opcodes操作爲:zend_compile_class_decl(),它的輸入就是ZEND_AST_CLASS節點,這個函數中再針對常量、屬性、方法、繼承、接口等分別處理。

void zend_compile_class_decl(zend_ast *ast)
{
    zend_ast_decl *decl = (zend_ast_decl *) ast;
    zend_ast *extends_ast = decl->child[0]; //繼承類節點,zen_ast_zval節點,存的是父類名
    zend_ast *implements_ast = decl->child[1]; //實現接口節點
    zend_ast *stmt_ast = decl->child[2]; //類中聲明的常量、屬性、方法
    zend_string *name, *lcname;
    zend_class_entry *ce = zend_arena_alloc(&CG(arena), sizeof(zend_class_entry));
    zend_op *opline;
    ...

    lcname = zend_new_interned_string(lcname);

    ce->type = ZEND_USER_CLASS; //類型爲用戶自定義類
    ce->name = name; //類名
    zend_initialize_class_data(ce, 1);
    ...
    if (extends_ast) {
        ...
        //有繼承的父類則首先生成一條ZEND_FETCH_CLASS的opcode
        zend_compile_class_ref(&extends_node, extends_ast, 0);
    }

    //在當前父空間生成一條opcode
    opline = get_next_op(CG(active_op_array));
    zend_make_var_result(&declare_node, opline);
    ...
    opline->op2_type = IS_CONST;
    LITERAL_STR(opline->op2, lcname);

    if (decl->flags & ZEND_ACC_ANON_CLASS) {
        //暫不清楚這種情況
    }else{
        zend_string *key;

        if (extends_ast) {
            opline->opcode = ZEND_DECLARE_INHERITED_CLASS; //有繼承的類爲這個opcode
            opline->extended_value = extends_node.u.op.var;
        } else {
            opline->opcode = ZEND_DECLARE_CLASS; //無繼承的類爲這個opcode
        }

        key = zend_build_runtime_definition_key(lcname, decl->lex_pos); //這個key並不是類名,而是:類名+file+lex_pos

        opline->op1_type = IS_CONST;
        LITERAL_STR(opline->op1, key);//將這個臨時key保存到操作數1中

        zend_hash_update_ptr(CG(class_table), key, ce); //將半成品的zend_class_entry插入CG(class_table),注意這裏並不是執行時用於索引類的,它的key不是類名!!!
    }
    CG(active_class_entry) = ce;
    zend_compile_stmt(stmt_ast); //將常量、成員屬性、方法編譯到CG(active_class_entry)中

    ...

    CG(active_class_entry) = original_ce;
}

上面這個過程主要操作是新分配一個zend_class_entry,如果有繼承的話首先生成一條ZEND_FETCH_CLASS的opcode,然後生成一條類聲明的opcode(這個地方與之前3.2.1.3節介紹函數的編譯時相同),接着就是編譯常量、屬性、成員方法到新分配的zend_class_entry中,這個過程還有一個容易誤解的地方:將生成的zend_class_entry插入到CG(class_table)哈希表中,這個操作這是中間步驟,它的key並不是類名,而是類名後面帶來一長串其它的字符,也就是這個時候通過類名在class_table是索引不到對應類的,後面我們會說明這樣處理的作用。

Human類情況比較簡單,不再展開,我們看下User類在zend_compile_class_decl()中執行到zend_compile_stmt(stmt_ast)這步時關鍵數據結構:

接下來我們分別看下常量、成員屬性、方法的編譯過程。

(1)常量編譯

常量的節點類型爲:ZEND_AST_CLASS_CONST_DECL,每個常量對應一個這樣的節點,處理函數爲:zend_compile_class_const_decl()

void zend_compile_class_const_decl(zend_ast *ast)
{
    zend_ast_list *list = zend_ast_get_list(ast);
    zend_class_entry *ce = CG(active_class_entry);
    uint32_t i;

    for (i = 0; i < list->children; ++i) { //不清楚這個地方爲什麼要用list,試了幾個例子這個節點都只有一個child,即for只循環一次
        zend_ast *const_ast = list->child[i];
        zend_ast *name_ast = const_ast->child[0]; //常量名節點
        zend_ast *value_ast = const_ast->child[1];//常量值節點
        zend_string *name = zend_ast_get_str(name_ast); //常量名
        zval value_zv;

        //取出常量值
        zend_const_expr_to_zval(&value_zv, value_ast);

        name = zend_new_interned_string_safe(name);
        //將常量添加到zend_class_entry.constants_table哈希表中
        if (zend_hash_add(&ce->constants_table, name, &value_zv) == NULL) {
            ...
        }
        ...
    }
}

(2)屬性編譯

屬性節點類型爲:ZEND_AST_PROP_DECL,對應的處理函數:zend_compile_prop_decl():

void zend_compile_prop_decl(zend_ast *ast)
{
    zend_ast_list *list = zend_ast_get_list(ast);
    uint32_t flags = list->attr; //屬性修飾符:static、public、private、protected
    zend_class_entry *ce = CG(active_class_entry);
    uint32_t i, children = list->children;

    //也不清楚這裏爲啥用循環,測試的情況child只有一個
    for (i = 0; i < children; ++i) {
        zend_ast *prop_ast = list->child[i]; //這個節點類型爲:ZEND_AST_PROP_ELEM
        zend_ast *name_ast = prop_ast->child[0]; //屬性名節點
        zend_ast *value_ast = prop_ast->child[1]; //屬性值節點
        zend_ast *doc_comment_ast = prop_ast->child[2];
        zend_string *name = zend_ast_get_str(name_ast); //屬性名
        zend_string *doc_comment = NULL;
        zval value_zv;
        ...
        //檢查該屬性是否在當前類中已經定義
        if (zend_hash_exists(&ce->properties_info, name)) {
            zend_error_noreturn(...);
        }
        if (value_ast) {
            //取出默認值
            zend_const_expr_to_zval(&value_zv, value_ast);
        } else {
            //默認值爲null
            ZVAL_NULL(&value_zv);
        }

        name = zend_new_interned_string_safe(name);
        //保存屬性
        zend_declare_property_ex(ce, name, &value_zv, flags, doc_comment);
    }
}

開始的時候我們已經介紹:屬性值是通過 數組 保存的,然後其存儲位置通過以 屬性名 爲key的哈希表保存,使用的時候先從這個哈希表中找到屬性信息同時得到屬性值的保存位置,然後再進一步取出屬性值。

zend_declare_property_ex()這步操作就是來確定屬性的存儲位置的,它將屬性值按靜態、非靜態分別保存在default_static_members_table、default_properties_table兩個數組中,同時將其存儲位置保存到屬性結構的offset中。

//zend_API.c
ZEND_API int zend_declare_property_ex(zend_class_entry *ce, zend_string *name, zval *property, int access_type,...)
{
    zend_property_info *property_info, *property_info_ptr;

    if (ce->type == ZEND_INTERNAL_CLASS) {//內部類
        ...
    }else{
        property_info = zend_arena_alloc(&CG(arena), sizeof(zend_property_info));
    }

    if (access_type & ZEND_ACC_STATIC) {
        //靜態屬性
        ...
        property_info->offset = ce->default_static_members_count++; //分配屬性編號,同變量一樣,靜態屬性的就是數組索引
        ce->default_static_members_table = perealloc(ce->default_static_members_table, sizeof(zval) * ce->default_static_members_count, ce->type == ZEND_INTERNAL_CLASS);

        ZVAL_COPY_VALUE(&ce->default_static_members_table[property_info->offset], property);
        if (ce->type == ZEND_USER_CLASS) {
            ce->static_members_table = ce->default_static_members_table;
        }
    }else{
        //非靜態屬性
        ...
        //非靜態屬性值存儲在對象中,所以與靜態屬性不同,它的offset並不是default_properties_table數組索引
        //而是相對於zend_object大小的(因爲普通屬性值數組保存在zend_object結構之後,這個與局部變量、zend_execute_data關係一樣)
        property_info->offset = OBJ_PROP_TO_OFFSET(ce->default_properties_count); 
        ce->default_properties_count++;
        ce->default_properties_table = perealloc(ce->default_properties_table, sizeof(zval) * ce->default_properties_count, ce->type == ZEND_INTERNAL_CLASS);

        ZVAL_COPY_VALUE(&ce->default_properties_table[OBJ_PROP_TO_NUM(property_info->offset)], property);
    }

    //設置property_info其它的一些值
    ...
}

這個操作中重點是offset的計算方式,靜態屬性這個比較好理解,就是default_static_members_table數組索引;非靜態屬性zend_class_entry.default_properties_table保存的只是默認屬性值,我們在下一篇介紹對象時再具體說明object、class之間屬性的存儲關係。

(3)成員方法編譯
3.4.1.4一節已經介紹過成員方法與普通函數的關係,兩者沒有很大的區別,實現上是相同,不同的地方在於成員方法保存在各zend_class_entry中,調用時會有一些可見性方面的限制,如private、public、protected,還有一些專有用法,比如this、self等,但在編譯、執行、存儲結構等方面兩者基本是一致的。

成員方法的語法樹根節點爲ZEND_AST_METHOD

void zend_compile_stmt(zend_ast *ast)
{
    ...
    switch (ast->kind) {
        ...
        case ZEND_AST_FUNC_DECL: //函數
        case ZEND_AST_METHOD:  //成員方法
            zend_compile_func_decl(NULL, ast);
            break;
        ...
    }
}

如果你還記得3.2.1.3函數處理的過程就會發現函數、成員方法的編譯是同一個函數:zend_compile_func_decl()

void zend_compile_func_decl(znode *result, zend_ast *ast)
{
    //參數、函數內語法編譯等不看了,與函數的相同,不清楚請看3.2.1.3節
    ...

    if (is_method) {
        zend_bool has_body = stmt_ast != NULL;
        zend_begin_method_decl(op_array, decl->name, has_body);
    } else {
        //函數是在當前空間生成了一條ZEND_DECLARE_FUNCTION的opcode
        //然後在zend_do_early_binding()中"執行"了這條opcode,即將函數添加到CG(function_table)
        zend_begin_func_decl(result, op_array, decl);
    }
    ...
}

這個過程之前已經說過,這裏不再重複,我們只看下與普通函數處理不同的地方:zend_begin_method_decl(),它的工作也比較簡單,最重要的一個地方就是將成員方法的zend_op_array插入 zend_class_entry.function_table

void zend_begin_method_decl(zend_op_array *op_array, zend_string *name, zend_bool has_body)
{
    zend_class_entry *ce = CG(active_class_entry);
    ...

    op_array->scope = ce;
    op_array->function_name = zend_string_copy(name);

    lcname = zend_string_tolower(name);
    lcname = zend_new_interned_string(lcname);

    //插入類的function_table中
    if (zend_hash_add_ptr(&ce->function_table, lcname, op_array) == NULL) {
        zend_error_noreturn(..);
    }

    //後面主要是設置一些構造函數、析構函數、魔法函數指針,以及其它一些可見性、靜態非靜態的檢查
    ...
}

上面我們分別介紹了常量、成員屬性、方法的編譯過程,最後再用一張圖總結下整個類的編譯過程:

圖中還有一步我們沒有說到:zend_do_early_binding() ,這是非常關鍵的一步,如果你看過3.2.1.3一節那麼對這個函數應該不陌生,沒錯,在函數編譯的最後一步也會調用這個函數,它的作用是將編譯的function以函數名爲key添加到CG(function_table)中,同樣地上面整個過程中你可能發現所有的操作都是針對zend_class_entry,並沒有發現最後把它存到什麼位置了,這最後的一步就是把zend_class_entry以類名爲key添加到CG(class_table)。

void zend_do_early_binding(void)
{
    ...
    switch (opline->opcode) {
        ...
        case ZEND_DECLARE_CLASS:
            if (do_bind_class(CG(active_op_array), opline, CG(class_table), 1) == NULL) {
                return;
            }
            table = CG(class_table);
            break;
        case ZEND_DECLARE_INHERITED_CLASS:
            //比較長,後面單獨摘出來
            break;
    }

    //將那個以(類名+file+lex_pos)爲key的值從CG(class_table)中刪除
    //同時刪除兩個相關的literals:key、類名
    zend_hash_del(table, Z_STR_P(CT_CONSTANT(opline->op1)));
    zend_del_literal(CG(active_op_array), opline->op1.constant);
    zend_del_literal(CG(active_op_array), opline->op2.constant);
    MAKE_NOP(opline); //將ZEND_DECLARE_CLASS或ZEND_DECLARE_INHERITED_CLASS的opcode置爲空,表示已執行
}

這個地方會有兩種情況,上面我們說過,如果是普通的沒有繼承的類定義會生成一條ZEND_DECLARE_CLASS的opcode,而有繼承的類則會生成ZEND_FETCH_CLASSZEND_DECLARE_INHERITED_CLASS兩條opcode,這兩種有很大的不同,接下來我們具體看下:

(1)無繼承類: 這種情況直接調用do_bind_class()處理了。

ZEND_API zend_class_entry *do_bind_class(
    const zend_op_array* op_array, 
    const zend_op *opline, 
    HashTable *class_table, 
    zend_bool compile_time)
{
    if (compile_time) { //編譯時
        //還記得zend_compile_class_decl()中有一個把zend_class_entry以(類名+file+lex_pos)
        //爲key存入CG(class_table)的操作嗎?那個key的存儲位置保存在op1中了
        //這裏就是從op_array.literals中取出那個key
        op1 = CT_CONSTANT_EX(op_array, opline->op1.constant);
        //op2爲類名
        op2 = CT_CONSTANT_EX(op_array, opline->op2.constant);
    } else { //運行時,如果當前類在編譯階段沒有編譯完成則也有可能在zend_execute執行階段完成
        op1 = RT_CONSTANT(op_array, opline->op1);
        op2 = RT_CONSTANT(op_array, opline->op2);
    }
    //從CG(class_table)中取出zend_class_entry
    if ((ce = zend_hash_find_ptr(class_table, Z_STR_P(op1))) == NULL) {
        zend_error_noreturn(E_COMPILE_ERROR, ...);
        return NULL;
    }
    ce->refcount++; //這裏加1是因爲CG(class_table)中多了一個bucket指向這個ce了

    //以標準類名爲key將zend_class_entry插入CG(class_table)
    //這纔是後面要用到的類
    if (zend_hash_add_ptr(class_table, Z_STR_P(op2), ce) == NULL) {
        //插入失敗
        return NULL;
    }else{
        //插入成功
        return ce;
    } 
}

這個函數就是將類以 正確的類名 爲key插入到CG(class_table),這一步完成後zend_do_early_binding()後面就將ZEND_DECLARE_CLASS這條opcode置爲0了,這樣在運行時就直接跳過此opcode了,現在清楚爲什麼執行時會有很多爲0的opcode了吧?

(2)有繼承類: 這種類是有繼承的父類,它的定義有兩條opcode:ZEND_FETCH_CLASSZEND_DECLARE_INHERITED_CLASS,上面我們一張圖畫過示例中user類編譯的情況,我們先看下它的opcode再作說明。

case ZEND_DECLARE_INHERITED_CLASS:
{
    zend_op *fetch_class_opline = opline-1;
    zval *parent_name;
    zend_class_entry *ce;

    parent_name = CT_CONSTANT(fetch_class_opline->op2); //父類名

    //在EG(class_table)中查找父類(注意:EG(class_table)與CG(class_table)指向同一個位置)
    if (((ce = zend_lookup_class_ex(Z_STR_P(parent_name), parent_name + 1, 0)) == NULL) || ...) {
        //沒找到父類,有可能父類沒有定義、有可能父類在子類之後定義的......
        if (CG(compiler_options) & ZEND_COMPILE_DELAYED_BINDING) {
            uint32_t *opline_num = &CG(active_op_array)->early_binding;

            while (*opline_num != (uint32_t)-1) {
                opline_num = &CG(active_op_array)->opcodes[*opline_num].result.opline_num;
            }
            *opline_num = opline - CG(active_op_array)->opcodes;
            opline->opcode = ZEND_DECLARE_INHERITED_CLASS_DELAYED;
            opline->result_type = IS_UNUSED;
            opline->result.opline_num = -1;
        }
        return;
    }
    if (do_bind_inherited_class(CG(active_op_array), opline, CG(class_table), ce, 1) == NULL) {
        return;
    }

    //清理無用的opcode:ZEND_FETCH_CLASS
    zend_del_literal(CG(active_op_array), fetch_class_opline->op2.constant);
    MAKE_NOP(fetch_class_opline);

    table = CG(class_table);
    break;
}

通過上面的處理我們可以看到,首先是查找父類:

1)如果父類沒有找到則將opcode置爲ZEND_DECLARE_INHERITED_CLASS_DELAYED,這種情況下當前類是沒有編譯到CG(class_table)中去的,也就是這個時候這個類是無法使用的,在執行的時候會再次嘗試這個過程,那個時候如果找到父類了則再加入EG(class_table);

2)如果找到父類了則與無繼承的類處理一樣,將zend_class_entry添加到CG(class_table)中,然後將對應的兩條opcode刪掉,除了這個外還有一個非常重要的操作:zend_do_inheritance(),這裏主要是進行屬性、常量、成員方法的合併、拷貝,這個過程這裏暫不展開,《3.4.3繼承》一節再作具體說明。

總結:

上面我們介紹了類的編譯過程,整個流程東西比較但並不複雜,主要圍繞zend_class_entry進行的操作,另外我們知道了類插入EG(class_table)的過程,這個相當於類的聲明在編譯階段提前”執行”了,也有可能因爲父類找不到等原因延至運行時執行,清楚了這個過程你應該能明白下面這些例子爲什麼有的可以運行而有的則報錯的原因了吧?

//情況1
new A();

class A extends B{}
class B{}

===================
完整opcodes:
1 ZEND_NEW                    => 執行到這報錯,因爲此時A因爲找不到B尚未編譯進EG(class_table)
2 ZEND_DO_FCALL
3 ZEND_FETCH_CLASS             
4 ZEND_DECLARE_INHERITED_CLASS 
5 ZEND_DECLARE_CLASS           => 註冊class B
6 ZEND_RETURN

實際執行順序:5->1->2->3->4->6
//情況2
class A extends B{}
class B{}

new A();
===================
完整opcodes:
1 ZEND_FETCH_CLASS             
2 ZEND_DECLARE_INHERITED_CLASS => 註冊class A,此時已經可以找到B
3 ZEND_DECLARE_CLASS           => 註冊class B
4 ZEND_NEW
5 ZEND_DO_FCALL
6 ZEND_RETURN

實際執行順序:3->1->2->4->5->6,執行到4時A都已經註冊,所以可以執行
//情況3
class A extends B{}
class B extends C{}
class C{}

new A();
===================
完整opcodes:
1 ZEND_FETCH_CLASS             => 找不到B,直接報錯
2 ZEND_DECLARE_INHERITED_CLASS
3 ZEND_FETCH_CLASS             
4 ZEND_DECLARE_INHERITED_CLASS => 註冊class B,此時可以找到C,所以註冊成功
5 ZEND_DECLARE_CLASS           => 註冊class C
6 ZEND_NEW
7 ZEND_DO_FCALL
8 ZEND_RETURN

實際執行順序:5->1->2->3->4->5->6->7->8,執行到1發現還是找不到父類B,報錯
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章