基本结构

zval(“Zend 值” 的缩写)表示一个任意的 PHP 值。因此,它可能是所有 PHP 结构中最重要的结构,你会经常用到它。本节将介绍 zval 及其使用的基本概念。

类型和值

除其他事项外,每个 zval 都存储了一些值和该值的类型。这是必要的,因为 PHP 是一种动态类型语言,因此变量类型只有在运行时才能知道,而不是在编译时。此外,类型会在 zval 的生命周期内发生变化,因此如果 zval 以前存储的是一个整数,那么在以后的时间点上可能会包含一个字符串。

类型以整数标记的形式存储,可以有多种值。有些值与 PHP 中可用的八种类型相对应,其他值仅用于内部引擎。这些值使用 IS_TYPE 形式的常量来表示。例如,IS_NULL 对应空类型,IS_STRING 对应字符串类型。

实际值存储在一个联合体中,其定义如下:

typedef union _zend_value {
    zend_long         lval;    // For IS_LONG
    double            dval;    // For IS_DOUBLE
    zend_refcounted  *counted;
    zend_string      *str;     // For IS_STRING
    zend_array       *arr;     // For IS_ARRAY
    zend_object      *obj;     // For IS_OBJECT
    zend_resource    *res;     // For IS_RESOURCE
    zend_reference   *ref;     // For IS_REFERENCE
    zend_ast_ref     *ast;     // For IS_CONSTANT_AST (special)
    zval             *zv;      // For IS_INDIRECT (special)
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww;
} zend_value;

对于那些不熟悉联合的概念的人来说:union 定义了多个不同类型的成员,但一次只能使用其中一个。例如,如果 value.lval 成员被设置,则还需要使用 value.lval 而不是其他成员来查找值(这样做会违反 “严格别名” 保证并导致未定义的行为)。原因在于,union 将其所有成员存储在同一内存位置,只是根据访问的成员不同,对该位置的值的解释也不同。联合体的大小就是其最大成员的大小。

在使用 zvals 时,可以使用类型标记来查找当前正在使用的联合成员。在了解用于此目的的 API 之前,我们先来了解一下 PHP 支持的不同类型及其存储方式:

最简单的类型是 IS_NULL:它不需要实际存储任何值,因为只有一个 null 值。

布尔使用 IS_TRUEIS_FALSE 类型,也不需要存储值。为了提高效率,PHP 在内部将 truefalse 表示为不同的类型,尽管从用户的角度来看,这两种类型都被认为是值。还有一种 _IS_BOOL 类型,但它从未作为 zval 类型使用。它在内部用于表示布尔类型的转换和类似用途。

为了存储数字,PHP 提供了 IS_LONGIS_DOUBLE,它们分别使用了 zend_long lvaldouble dval 成员。前者用于存储整数,后者用于存储浮点数。

关于 zend_long 类型,有几点需要注意: 首先,这是一个带符号的整数类型,即可以存储正整数和负整数,但通常不适合进行位操作。其次,zend_longlong 并不相同,因为它抽象了平台差异。在 32 位平台上,zend_long 总是 4 字节大,而在 64 位平台上则是 8 字节大,即使 long 类型的大小可能不同。

因此,使用专门为 zend_long 编写的宏很重要,例如 SIZEOF_ZEND_LONGZEND_LONG_MAX。你可以在 Zend/zend_long.h 中找到更多相关宏。

用于存储浮点数的 double 类型是一种 8 字节值,符合 IEEE-754 规范。这里不讨论这种格式的细节,但你至少应该知道,这种类型的精度有限,通常无法存储你想要的精确值。

其余四种类型在此只作简要介绍,详细讨论将在各自章节中进行:

字符串(IS_STRING)存储在一个 zend_string 结构中,该结构将字符串长度和字符串内容合并在一个分配中。关于 zend_string 结构及其专用 API 的更多信息,请参阅 字符串章节

数组使用 IS_ARRAY 类型标签,并存储在 zend_array *arr 成员中。HashTable 结构的工作原理将在 Hashtables 章节中讨论。

对象(IS_OBJECT)使用 zend_object *obj 成员。PHP 的类和对象系统将在 对象 一章中介绍。

资源(IS_RESOURCE)使用 zend_resource *res 成员。资源将在 资源 一章中介绍。

总之,下面的表格列出了所有可用的 “普通” 类型标记及其值的相应存储位置:

类型标记 存储位置

IS_NULL

none

IS_TRUE 或 IS_FALSE

none

IS_LONG

zend_long lval

IS_DOUBLE

double dval

IS_STRING

zend_string *str

IS_ARRAY

zend_array *arr

IS_OBJECT

zend_object *obj

IS_RESOURCE

zend_resource *res

特殊类型

还有一些类型没有直接对应的用户态类型,只在内部使用。在这些类型中,只有 IS_UNDEFIS_REFERENCE 是你会经常遇到的类型。

IS_UNDEF 类型用于表示未初始化的 zval。该类型标签的值为零,因此使用 memset 将一个 zval 清零将导致一个 UNDEF zvalIS_UNDEF 的确切含义取决于上下文,例如,它可以表示未初始化/未设置的对象属性,也可以表示未使用的 hashtable bucket。

IS_REFERENCE 类型与 zend_reference *ref 成员相结合,用于表示 PHP 引用。虽然从用户界面的角度来看,引用并不是一个独立的类型,但在内部,引用是作为另一个 zval 的包装器来表示的,可以被多个地方共享。

zend_refcounted *counted 成员访问所有引用计数类型的通用头,包括字符串、数组、对象、资源和引用。 内存管理 章节将讨论其工作原理。

IS_CONSTANT_AST 类型和 zend_ast_ref *ast 成员用于存储未评估的常量表达式抽象语法树(AST)。它只能出现在特定的地方,如属性默认值。AST 将在 编译器 章节中讨论。

IS_INDIRECT 类型和 zval *zv 成员用于存储指向另一个 zval 的直接指针。这主要用于符号类型和动态属性表,以便指向存储在其他地方的实际值。

IS_PTR 类型和 void *ptr 字段用于存储任意指针。在 C 语言中,任何指针类型都可以转换为 void *,反之亦然。这可用于在通常只接受 zvals 的地方存储指针,如 hashtable 值。

zend_class_entry *cezend_function *func 成员只是指定了一个更精确的类型,但在其他方面的作用与 ptr 相同。

zval结构

现在让我们看看 zval 结构的实际外观:

struct _zval_struct {
    zend_value value;
    union {
        uint32_t type_info;
        struct {
            ZEND_ENDIAN_LOHI_3(
                zend_uchar    type,
                zend_uchar    type_flags,
                union {
                    uint16_t  extra;
                } u)
        } v;
    } u1;
    union {
        uint32_t next;                 /* hash collision chain */
        uint32_t cache_slot;           /* cache slot (for RECV_INIT) */
        uint32_t opline_num;           /* opline number (for FAST_CALL) */
        uint32_t lineno;               /* line number (for ast nodes) */
        uint32_t num_args;             /* arguments number for EX(This) */
        uint32_t fe_pos;               /* foreach position */
        uint32_t fe_iter_idx;          /* foreach iterator index */
        uint32_t access_flags;         /* class constant access flags */
        uint32_t property_guard;       /* single property guard */
        uint32_t constant_flags;       /* constant flags */
        uint32_t extra;                /* not further specified */
    } u2;
};

这个结构看起来比实际情况要复杂一些。它的核心是存储一个 8 字节的值和一个单字节的类型标记,这两点我们在前面已经讨论过了。

理论上,zval 的大小为 9 字节。但是,为了实现高效访问,有必要对齐 8 字节边界的结构大小,这样总大小就变成了 16 字节。由于额外的空间无论如何都会被使用,PHP 对 “浪费” 的空间进行了一些利用:

类型标记是更大的 type_info 结构的一部分,该结构额外存储了 type_flags。从 PHP 7.4 开始,只有两个类型标志: IS_TYPE_REFCOUNTED 表示值是引用计数的,而 IS_TYPE_COLLECTABLE 表示它参与了循环垃圾回收。我们将在以后讨论这两种类型。

u2 成员是一个用于存储任意数据的 32 位空间,根据不同的上下文有不同的用途。哈希表用它来存储碰撞解决链,但正如上面的注释所示,它还有很多其他用途。需要注意的是,标准的 zval 宏永远不会修改或复制 u2 字段。

u1.v.u.extra(额外)字段是类型的一部分,很少用于存储额外信息。不过,只有在非常特殊的情况下才可能使用该字段,因为 PHP 通常会假定它为零。

访问宏

了解了 zval 结构,你就可以利用它来编写代码了:

zval *zv_ptr = /* ... get zval from somewhere */;

if (zv_ptr->u1.v.type == IS_LONG) {
    php_printf("Zval is a long with value " ZEND_LONG_FMT "\n", zv_ptr->value.lval);
} else /* ... handle other types */

虽然上面的代码可以工作,但这不是惯用的方式。它直接访问 zval 成员,而不是为此使用一组特殊的访问宏:

zval *zv_ptr = /* ... */;

if (Z_TYPE_P(zv_ptr) == IS_LONG) {
    php_printf("Zval is a long with value " ZEND_LONG_FMT "\n", Z_LVAL_P(zv_ptr));
} else /* ... */

上述代码使用 Z_TYPE_P() 宏来检索类型标签,使用 Z_LVAL_P() 来获取长整型值。所有访问宏都有带 _P(表示 “指针”)后缀或没有后缀的变体。使用哪一个取决于您使用的是 zval 还是 zval*

zval zv;
zval *zv_ptr;

Z_TYPE(zv);       // Same as Z_TYPE_P(&zv).
Z_TYPE_P(zv_ptr); // Same as Z_TYPE(*zv_ptr).

Z_LVAL 类似,还有用于获取所有其他类型的值的宏。为了演示它们的用法,我们将创建一个简单的函数来转储 zval

PHP_FUNCTION(dump)
{
    zval *zv_ptr;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "z", &zv_ptr) == FAILURE) {
        return;
    }

try_again:
    switch (Z_TYPE_P(zv_ptr)) {
        case IS_NULL:
            php_printf("NULL: null\n");
            break;
        case IS_TRUE:
            php_printf("BOOL: true\n");
            break;
        case IS_FALSE:
            php_printf("BOOL: false\n");
            break;
        case IS_LONG:
            php_printf("LONG: %ld\n", Z_LVAL_P(zv_ptr));
            break;
        case IS_DOUBLE:
            php_printf("DOUBLE: %g\n", Z_DVAL_P(zv_ptr));
            break;
        case IS_STRING:
            php_printf("STRING: value=\"");
            PHPWRITE(Z_STRVAL_P(zv_ptr), Z_STRLEN_P(zv_ptr));
            php_printf("\", length=%zd\n", Z_STRLEN_P(zv_ptr));
            break;
        case IS_RESOURCE:
            php_printf("RESOURCE: id=%d\n", Z_RES_HANDLE_P(zv_ptr));
            break;
        case IS_ARRAY:
            php_printf("ARRAY: hashtable=%p\n", Z_ARRVAL_P(zv_ptr));
            break;
        case IS_OBJECT:
            php_printf("OBJECT: object=%p\n", Z_OBJ_P(zv_ptr));
            break;
        case IS_REFERENCE:
            // For references, remove the reference wrapper and try again.
            // Yes, you are allowed to use goto for this purpose!
            php_printf("REFERENCE: ");
            zv_ptr = Z_REFVAL_P(zv_ptr);
            goto try_again;
        EMPTY_SWITCH_DEFAULT_CASE() // Assert that all types are handled.
    }
}

让我们尝试一下:

dump(null);                 // NULL: null
dump(true);                 // BOOL: true
dump(false);                // BOOL: false
dump(42);                   // LONG: 42
dump(4.2);                  // DOUBLE: 4.2
dump("foo");                // STRING: value="foo", length=3
dump(fopen(__FILE__, "r")); // RESOURCE: id=???
dump(array(1, 2, 3));       // ARRAY: hashtable=0x???
dump(new stdClass);         // OBJECT: object=0x???

下表总结了最常用的访问器宏,尽管还有很多。

返回类型 需要的 zval 类型 描述

Z_TYPE

unsigned char

zval 的类型。IS_* 常量之一。

Z_LVAL

zend_long

IS_LONG

整数值

Z_DVAL

double

IS_DOUBLE

浮点值

Z_STR

zend_string *

IS_STRING

指向完整 zend_string 结构的指针。

Z_STRVAL

char *

IS_STRING

zend_string 结构的字符串内容。

Z_STRLEN

size_t

IS_STRING

zend_string 结构的字符串长度。

Z_ARR

HashTable *

IS_ARRAY

指向 HashTable 结构的指针。

Z_ARRVAL

HashTable *

IS_ARRAY

Z_ARR 的别名。

Z_OBJ

zend_object *

IS_OBJECT

指向 zend_object 结构的指针。

Z_OBJCE

zend_class_entry *

IS_OBJECT

对象的类实体。

Z_RES

zend_resource *

IS_RESOURCE

指向 zend_resource 结构的指针。

Z_REF

zend_reference *

IS_REFERENCE

指向 zend_reference 结构的指针。

Z_REFVAL

zval *

IS_REFERENCE

指向引用包装的 zval 的指针。

当你想要访问 zval 的内容时,你应该始终通过这些宏,而不是直接访问其成员。这保持了一定程度的抽象,并且在某种程度上使你免受实现更改的影响。