自定义类的编译

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 { $$ = CG(zend_lineno); }
        T_STRING extends_from implements_list backup_doc_comment '{' class_statement_list '}'
            { $$ = zend_ast_create_decl(ZEND_AST_CLASS, $1, $3, $7, zend_ast_get_str($4), $5, $6, $9, NULL); }
    |   T_CLASS { $$ = CG(zend_lineno); }
        T_STRING extends_from implements_list backup_doc_comment '{' class_statement_list '}'
            { $$ = zend_ast_create_decl(ZEND_AST_CLASS, 0, $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) {
            ...
            //将opcode重置为ZEND_DECLARE_INHERITED_CLASS_DELAYED
            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,重置为0,执行时直接跳过
    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,报错
联系我们

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

Copyright © 2015-2024

备案号:京ICP备15003423号-3