类的实现

前面介绍了 PHP 7 的类的种类和常用特性。从本节开始,会依次介绍类在 PHP 7 中的存储数据结构和类的静态属性、常量、方法、接口和特性的源码实现。最后,以继承的源码实现的分析结束本节。

类的结构

面向对象的核心是类,先来看看 PHP 7 中存储类的数据结构 zend_class_entry

struct _zend_class_entry {
    char type;
    zend_string *name;
    struct _zend_class_entry *parent;
    int refcount;
    uint32_t ce_flags;

    int default_properties_count;
    int default_static_members_count;
    zval *default_properties_table;
    zval *default_static_members_table;
    zval *static_members_table;
    HashTable function_table;
    HashTable properties_info;
    HashTable constants_table;

    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;
};

此结构体的主要字段有以下几个。

  • type:类的类型,共有两种——1 代表内置的类,2 代表用户自定义的类。

    #define ZEND_INTERNAL_CLASS             //内置类
    #define ZEND_USER_CLASS                 //用户自定义的类
  • name:类名。

  • parent:继承的父类指针。

  • refcount:引用计数。

  • ce_flags:位组合标记。其中 0x10 表示此类有抽象方法,0x20 表示此类为抽象类,0x40 表示接口,0x80 表示特性,0x100 表示匿名类,0x400 表示其为某个类的子类。定义如下:

    #define ZEND_ACC_IMPLICIT_ABSTRACT_CLASS    0x10
    #define ZEND_ACC_EXPLICIT_ABSTRACT_CLASS    0x20
    #define ZEND_ACC_INTERFACE                   0x40
    #define ZEND_ACC_TRAIT                       0x80
    #define ZEND_ACC_ANON_CLASS                  0x100
    #define ZEND_ACC_ANON_BOUND                  0x200
    #define ZEND_ACC_INHERITED                   0x400
  • default_properties_count:默认普通属性个数。

  • default_static_members_count:默认静态属性个数。

  • default_properties_table:默认普通属性值数组。

  • default_static_members_table:默认静态属性值数组。

  • static_members_table:静态属性成员。

  • constructor:构造方法。

  • destructor:析构方法。

  • clone:克隆方法。

  • __get:魔术方法 __get

  • __set:魔术方法 __set

  • __unset:魔术方法 __unset

  • __isset:魔术方法 __isset

  • __call:魔术方法 __call

  • __callstatic:魔术方法 __callstatic

  • __tostring:魔术方法 __tostring

  • __debugInfo:魔术方法 __debugInfo

  • serialize_func:对象序列化方法。

  • unserialize_func:对象反序列化方法。

  • iterator_funcs:PHP 5 开始,支持接口并且内置了 Iterator 接口,其为此接口的相关操作方法。

  • create_object:实例化对象时调用的方法,默认为函数 zend_objects_new,可以通过扩展或修改源码来改变此值。

  • serialize:序列化方法回调指针。

  • unserialize:反序列化方法回调指针。

  • num_interfaces:类 implements 的接口个数。

  • num_traits:类 use 的特性个数。

  • interfaces:类 implements 的接口指针。

  • traits:类 usetraits 指针。

  • trait_aliases:类 use 的特性方法的别名。

  • trait_precedences:类 use 的特性方法的优先级(用于多个特性有相同名称的方法时,解决冲突,6.1.4节中有代码示例)。

  • info:记录类的其他信息,比如类所在的文件、注释之类。结合 PHP 代码说明特性相关的字段,示例为如下代码:

trait Win
{
    public function exec(){
        echo "I am Win! \n";
    }
}
trait Mac {
    public function exec(){
        echo "I am Mac! \n";
    }
}
trait Config {
    public function filename() {
        echo "php.ini\n";
    }
}

class Php {
    use Config {
        Config::filename as configName;
    }
    use Win, Mac {
        Win::exec insteadof Mac;
    }
}

上面代码定义了 PHP 类。

  1. 它用了 3 个特性(分别为 WinMacConfig),所以 ce->num_traits 为 3,3 个特性的结构体分别为 ce->traits[0]ce->traits[1]ce->traits[2]

  2. 代码 “Config::filename as configName” 添加了一条别名信息,存储在 ce->trait_aliases[0]

  3. 代码 “Win::exec insteadof Mac” 添加了一条优先级信息,存储在 ce->trait_precedences[0]

PHP 的类在编译(通过函数 zend_compile_class_decl())时生成。每个类对应着一个结构体 struct _zend_class_entry,存储在一个以类名字(全部转为小写)为 keyHashTable 中,也就是全局变量 EG(class_table) 中。

介绍完了类的存储结构,再来介绍存储属性和方法的相关数据结构 zend_property_info

typedef struct _zend_property_info {
    uint32_t offset; /* property offset for object properties or
        property index for static properties */
    uint32_t flags;
    zend_string *name;
    zend_string *doc_comment;
    zend_class_entry *ce;
} zend_property_info;

各字段含义如下。

  • offset:当查找普通属性时,此值为地址偏移量;查找静态属性时,此值为索引。可能大家很难理解,为什么如此相近的属性,却一个用地址偏移量,另一个用索引?因为普通属性存储在对象结构体 struct_zend_object 的柔性数组 properties_table 中(详见6.4.1),而静态属性存储在类结构体 struct_zend_class_entry 的静态属性指针 default_static_members_table 指向的内存块中。

    一个结构体中只能有一个柔性数组,而对象结构中,只有这一个字段 properties_table 是变长的数组,所以把此字段放在结构体最后面,用柔性数组即可。而类结构中,有三个字段(default_properties_tabledefault_static_members_tablestatic_members_table)是变长数组,所以只能通过指针的方式实现。

  • flags:属性的访问权限以及是否是静态属性。

    #define ZEND_ACC_PUBLIC     0x100
    #define ZEND_ACC_PROTECTED  0x200
    #define ZEND_ACC_PRIVATE    0x400
    #define ZEND_ACC_STATIC     0x01
  • name:属性名称。

  • doc_comment:注释。

  • ce:所属的类指针。

仍然以示例说明:

class Php{
    const VERSION_5 = 5;  //
    const VERSION_7 = 7; //
    protected $_version; //
    public function version(){
        return $this->_version;
    }
}
class Php7 extends Php{
    protected $_ast; //
    public function ast(){
        return $this->_ast;
    }
}

PHP 通过 zend_compile_class_decl() 函数将 AST 树转成 struct _zend_class_entry 结构体,通过 gdb 打印这段代码生成的两个 struct _zend_class_entry 结构。

查看 PHP 类的名称:

(gdb) p *compiler_globals.class_table.arData[157].val.value.ce.name.val@3
$2 = "Php"

示例代码中 PHP 类在源码中的数据结构存在的地址如下:

(gdb) p compiler_globals.class_table.arData[157].val.value.ce
$19 = (zend_class_entry *) 0x7ffff7c03018

类的数据结构示意图如图6-1所示。

image 2024 06 09 15 39 29 517
Figure 1. 图6-1 类的数据结构示意图

大家知道,一个类的静态属性和静态方法是和类相关的,而普通属性是和对象相关的,所以下节先详细介绍静态属性、常量和方法。

静态属性、常量和方法

  1. 静态属性:如前文所述,静态属性存储在 properties_infodefault_static_members_table 中。properties_info 是一个 HashTable,当访问一个静态属性时,以变量名为 key,在 properties_info 中找到对应的 value,再取结构体 struct _zend_property_info 中的字段 offset。而 default_static_members_table 是一个数组,所以 default_static_members_table[offset] 即为目标属性值。

    类的静态属性查找示意图如图 6-2 所示。

    image 2024 06 09 15 41 36 749
    Figure 2. 图6-2 类的静态属性查找示意图
  2. 常量:类常量存储在 HashTable 类型的 constants_table 字段中。

  3. 方法:类的方法(包括类的静态方法和类的普通方法)和普通方法(非成员方法)编译后生成的 zend_op_array 基本没有区别。唯一区别就是类的方法编译后生成的 zend_op_array 是存在于类结构体的 function_table 中,不像普通方法,编译后存储在全局变量 CG(function_table) 中。

类成员方法的访问权限(privateprotectedpublic)以及是否是静态方法等信息,存储在 zend_op_array 中的 fn_flags 字段里。

类的普通方法调用与静态方法调用基本无异。区别在于普通方法可以使用 $this 变量获取到当前所在对象。因此,在 ZEND_INIT_METHOD_CALL 操作中,最后初始化调用栈的时候会将当前对象当成参数 $this 传入。

因此,如果在一个类的普通方法的实现中,没有用到 $this 变量,那么把普通方法当成静态方法调用是没有问题的,否则会报语法错误。这和 C++ 语言的实现基本上一样。

class Php
{
    protected $_version;

    public function version1()
    {
        echo "7.1.1\n";
    }

    public function version2()
    {
        $this->_version = "7.1.0";
        echo $this->_version;
    }
}
Php::version1();
Php::version2();

因为 PHP 没有指针的概念,为了更加深刻地理解类的普通方法的 this 变量,可通过一段神奇的 C++ 代码来横向对比一下。

#include<iostream>
using namespace std;
class Php{
    protected:
        std::string _version;
        public:
        void version()
        {
            std::cout << "7.1.0" << endl;
            this->_version = "7.1.0"; //会报错
        }
};
int main(int argc, char* argv[])
{
        Php* php = (Php*)0;
        php->version();
        return 0;
}

上面的代码并没有实例化一个 PHP 对象,而是直接使用了 0 这个指针,但通过这个空指针调用这个类的普通方法,程序仍然可以正常运行,和 PHP 的原理一样,因为我们并没有使用 this 指针,也没有修改非法内存。

接口和特性

(1)接口

PHP 只支持单一继承,也就是一个子类只能有一个父类。而为了实现类似于 C++ 的多重继承的功能,PHP 引入了接口的概念。接下来介绍接口的实现。

在一个类初始化时,已经确定了此类的接口个数,而关联一个类与其所实现的接口,是通过函数 zend_do_implement_interface() 来实现的。

关联接口和类时,根据 PHP 关于接口的语法,可猜测进行了哪些操作。

  1. ce->num_interfaces 加 1,将此接口的结构体指针赋值给 ce->interfaces[ce->num_interfaces-1]

  2. 遍历接口中的 constants_table,并依次插入到 ce->constants_table。如果类和接口有相同名字的常量,则报错。

  3. 遍历接口中的 function_table,根据继承的逻辑,判断是否可以插入到类的 function_table 中。如果可以,则继承此方法。否则不进行任何操作。

  4. 将接口中的 interfaces 按顺序拷贝到类的 interfaces 后。

(2)特性

前文已介绍了特性的具体定义和代码示例,现在来介绍特性的具体实现。

特性与类进行关联通过方法 zend_do_bind_traits() 来实现:

ZEND_API void zend_do_bind_traits(zend_class_entry *ce) /* {{{ */
{
    if (ce->num_traits <= 0) {
        return;
    }
    /* complete initialization of trait strutures in ce */
    zend_traits_init_trait_structures(ce);
    /* first care about all methods to be flattened into the class */
    zend_do_traits_method_binding(ce);
    /* Aliases which have not been applied indicate typos/bugs. */
    zend_do_check_for_inconsistent_traits_aliasing(ce);
    /* then flatten the properties into it, to, mostly to notfiy developer about problems */
    zend_do_traits_property_binding(ce);
    /* verify that all abstract methods from traits have been implemented */
    zend_verify_abstract_class(ce);
    /* Emit E_DEPRECATED for PHP 4 constructors */
    zend_check_deprecated_constructor(ce);
    /* now everything should be fine and an added ZEND_ACC_IMPLICIT_ABSTRACT_CLASS
        should be removed */
    if (ce->ce_flags & ZEND_ACC_IMPLICIT_ABSTRACT_CLASS) {
        ce->ce_flags -= ZEND_ACC_IMPLICIT_ABSTRACT_CLASS;
    }
}

主要完成了如下操作。

  1. 完成类结构体指针 ce 的特性初始化。遍历 ce 的特性优先级变量 ce->trait_precedences(假如遍历中的当前变量叫作 cur_pre)。校验优先级最高的类和方法的语法,如果优先级中类 cur_pre->exclude_from_classes 还未编译,则进行编译(类似 include 的编译);然后保证优先级中用到的特性类 cur_pre->trait_method->ce 存在于变量 ce->traits 中,且优先级特性中的方法 cur_pre->trait_method->method_name 在特性类的方法表 cur_pre->trait_method->ce->function_table 中,再遍历被排除的类和方法的变量 cur_pre->exclude_from_classes,校验其相关语法。遍历类结构体中的特性别名变量 ce->trait_aliases,和上面的逻辑一样,编译未编译的类,校验相关语法。

  2. 将特性的方法拷贝到类中。遍历类结构体中的变量 ce->traits,将其方法拷贝到 ce->function_table。在拷贝之前执行判断:先将 ce->trait_precedences 中因为优先级被排除的类的方法排除掉,如果有方法存在别名,则将以别名为新名的方法拷贝到 ce->function_table

  3. 校验别名的相关语法。遍历类结构体中的特性别名变量 ce->trait_aliases,如发现仍然有别名所属的类未找到,则抛出语法错误。

  4. 拷贝特性的属性到类中。遍历 ce->num_traits,即遍历每个特性的属性 properties_info,如果此属性在类中存在,且是继承于父类,则将此属性删除,然后将特性中的属性拷贝到类中;若不是继承自父类,则继续遍历。如果此属性在类中不存在,则将特性中的属性拷贝到类中。

  5. 校验类是否已实现所有抽象方法,没有则报错。

  6. 校验类是否存在与类同名的方法来做构造方法,有则提示。

继承

继承划分了类的层次,父类代表的是更一般、更泛化的类,而子类则更为具体、细化。继承是实现代码重用、扩展软件功能的重要手段。子类中与父类完全相同的属性和方法不必重写,只需写出新增或改写的内容,不必一切从零开始。

PHP 只支持单一继承,实现相对简单。PHP 父类和子类是分别编译的。编译完成后,再对父类和子类进行继承。继承操作在函数 do_bind_inherited_class() 中完成。

继承属性

普通属性和静态属性的继承是先后完成的。在类结构中二者的存储十分相近,继承的操作也十分相近,这里只介绍普通属性继承的实现。

  1. 申请一个元素类型是 zval 的数组 table,大小为父类的普通属性个数(parent_ce->default_properties_count)和子类的普通属性个数(ce->default_properties_count)之和。

  2. 将父类的普通属性中 parent_ce->default_properties_table 的元素拷贝到数组 table

  3. 将子类的普通属性中的 ce->default_properties_tablece->default_properties_count 个元素拷贝到 table+parent_ce->default_properties_count

  4. 释放子类的普通属性指针 ce->default_properties_table,将 table 赋值给 ce->default_properties_table

这样就完成了普通属性的合并,请看如图6-3所示的示意图。

image 2024 06 09 15 58 48 349
Figure 3. 图6-3 普通属性的继承

类的静态属性也如此完成合并。

可以看出,子类的静态属性和普通属性在元素中的位置,相对于合并前都有偏移,所以要对其在 HashTable 中的偏移进行重置,重置的大致步骤如下。

  1. 遍历 properties_info:如果元素是静态属性,则对 offsetparent_ce->default_static_members_count

  2. 如果元素是普通属性,则对 offsetparent_ce->default_properties_count *sizeof(val)

  3. 接下来进行子类的 properties_info 和父类的 properties_info 的合并。

由于不同的属性可能拥有不同的权限,例如父类和子类有重复的属性,甚至重复属性的类型也不同(这里的类型指普通属性和静态属性),所以这两个 HashTable 的合并会有很复杂的逻辑,但是基于以上讲的数据结构,实现起来并不复杂,对 PHP 语法特别熟稔的同学,完全可以自己实现这段代码,这里就不啰嗦了。

继承常量

常量存储是用 HashTable 实现的,两个 HashTable 的合并比较简单,无非就是遍历父类的 constants_table

  • 如果子类中存在此常量,则不进行任何操作。

  • 如果子类中不存在此常量,则把此 key->val 键值对插入到子类的 constants_table 中。

if (zend_hash_num_elements(&parent_ce->constants_table)) {
    zend_class_constant *c;

    zend_hash_extend(&ce->constants_table,
        zend_hash_num_elements(&ce->constants_table) +
        zend_hash_num_elements(&parent_ce->constants_table), 0);

    ZEND_HASH_FOREACH_STR_KEY_PTR(&parent_ce->constants_table, key, c) {
        do_inherit_class_constant(key, c, ce);
    } ZEND_HASH_FOREACH_END();
}

继承方法

与常量继承的实现类似,方法的继承也是遍历父类的 function_table,然后将结果插入到子类的 function_table 中。不同的是,可能存在方法为 privateabstractfinal 特性,或者同一个方法在父类中为静态方法,而在子类中为普通方法等特殊情况。

if (zend_hash_num_elements(&parent_ce->function_table)) {
    zend_hash_extend(&ce->function_table,
        zend_hash_num_elements(&ce->function_table) +
        zend_hash_num_elements(&parent_ce->function_table), 0);
    ZEND_HASH_FOREACH_STR_KEY_PTR(&parent_ce->function_table, key, func) {
        zend_function *new_func = do_inherit_method(key, func, ce);

        if (new_func) {
            _zend_hash_append_ptr(&ce->function_table, key, new_func);
        }
    } ZEND_HASH_FOREACH_END();
}