基本结构
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 的内容时,你应该始终通过这些宏,而不是直接访问其成员。这保持了一定程度的抽象,并且在某种程度上使你免受实现更改的影响。