基本结构
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_TRUE
或 IS_FALSE
类型,也不需要存储值。为了提高效率,PHP 在内部将 true
和 false
表示为不同的类型,尽管从用户的角度来看,这两种类型都被认为是值。还有一种 _IS_BOOL
类型,但它从未作为 zval
类型使用。它在内部用于表示布尔类型的转换和类似用途。
为了存储数字,PHP 提供了 IS_LONG
和 IS_DOUBLE
,它们分别使用了 zend_long lval
和 double dval
成员。前者用于存储整数,后者用于存储浮点数。
关于 zend_long
类型,有几点需要注意: 首先,这是一个带符号的整数类型,即可以存储正整数和负整数,但通常不适合进行位操作。其次,zend_long
与 long
并不相同,因为它抽象了平台差异。在 32 位平台上,zend_long
总是 4 字节大,而在 64 位平台上则是 8 字节大,即使 long 类型的大小可能不同。
因此,使用专门为 zend_long
编写的宏很重要,例如 SIZEOF_ZEND_LONG
或 ZEND_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_UNDEF
和 IS_REFERENCE
是你会经常遇到的类型。
IS_UNDEF
类型用于表示未初始化的 zval
。该类型标签的值为零,因此使用 memset
将一个 zval
清零将导致一个 UNDEF zval
。IS_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 *ce
和 zend_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 的内容时,你应该始终通过这些宏,而不是直接访问其成员。这保持了一定程度的抽象,并且在某种程度上使你免受实现更改的影响。