3.1. zval 基础结构

基本结构

zval("Zend Value" 的缩写) 代表任意 PHP 值。所以它可能是所有 PHP 中最重要的结构,并且您在使用 PHP 的时候,它也在进行大量工作。

本节描述 zval 及其使用背后的基本概念。

类型和值

每个 zval 都存储一些值和该值的类型。这是不可或缺的,因为 PHP 是一种动态类型语言,只有在运行时才知道变量类型,而不是在编译时。 此外,zval 的类型可以在其生命周期中更改,如果 zval 以前存储了一个整数,则它可能在以后的时间点包含一个字符串。

该类型存储为整数标记(无符号 int)。它可以是多个值之一。一些值对应于 PHP 中的八种类型,其他值仅用于内部引擎。这些值使用 IS_TYPE 形式的常量引用。例如。IS_NULL 对应于空类型,IS_STRING 对应于字符串类型。

实际值存储在 union 中,该定义如下:

  1. typedef union _zend_value {
  2. zend_long lval;
  3. double dval;
  4. zend_refcounted *counted;
  5. zend_string *str;
  6. zend_array *arr;
  7. zend_object *obj;
  8. zend_resource *res;
  9. zend_reference *ref;
  10. zend_ast_ref *ast;
  11. zval *zv;
  12. void *ptr;
  13. zend_class_entry *ce;
  14. zend_function *func;
  15. struct {
  16. uint32_t w1;
  17. uint32_t w2;
  18. } ww;
  19. } zend_value;

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

当使用 zval 时,type 标志用于确定 union 当前正在使用的成员。在查看用于实现此目的的 API 之前,让我们看看 PHP 支持的不同类型和它们是怎么存储的:

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

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

关于 zend_long 类型,应该注意一些事:第一,它是有符号整数类型,即它可以存储正整数和负整数,但通常不适合进行按位运算。第二, zend_long代表平台长的抽象,所以不管你使用哪种平台, zend_long 在32位平台上权重4字节,在64位平台上权重8字节。

另外,你可以使用与 long 相关的宏,SIZEOF_ZEND_LONG 或者 ZEND_LONG_MAX 。更多信息请参阅 Zend/zend_long.h 源码。

用于存储浮点数的 double 类型(通常)是遵循 IEEE-754 规范的8字节值。这个格式的细节不会在这里做讨论 ,但是你至少意识到:该类型精度有限,并且通常不会存储你想要的精确值。

布尔值使用 IS_TRUEIS_FALSE 标志,并且不需要存储其他信息。存在所谓的“假类型” _IS_BOOL 标记,但是你不应该将其作为 zval 使用,这是不正确的。这个假类型在一些罕见的内部情况下(类似类型提醒)才使用。

其余四个类型在这里快速提及下,在它们各自的章节会详细的讨论:

字符串(IS_STRING)存储在 zend_string 结构体中,即包括 char * 字符串和 size_t 长度。你可以查看更多关于 zend_string 结构体的信息和专用 API在 字符串 章节。

数组使用 IS_ARRAY 类型标志,并存储在 zend_array *arr 成员中。将在 哈希表 章节讨论 HashTable 结构体是如何工作的。

对象(IS_OBJECT) 使用 zend_object *obj 成员。PHP 的类和对象体系会在 对象 章节讲述。

资源(IS_RESOURCE)是使用 zend_resource *res 成员的特殊类型。在 资源 章节介绍。

总之,这里的表格列了所有变量类型标记和其值相应的存储位置:

类型标记 存储位置
IS_NULL none
IS_TRUE or 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

特殊类型

你可能看到 zval 包含了其他类型,我们尚未对其进行审查。这些是特殊类型,在 PHP 语言用户领域中并不存在,而只用于内部用例的引擎中。zval 结构体被认为是非常灵活的,并在内部用于承载几乎任何感兴趣的数据类型,并不仅仅是上面提及过 PHP 特殊类型。

特殊的 IS_UNDEF 类型具有特殊的含义。 这意味着“这个 zval 不包含感兴趣的数据,不要从中访问任何数据字段”。这用于 内存管理 的目的。如果你看见 IS_UNDEF 类型,意味着它不是特殊类型并且不包含任何有效信息。

zend_refcounted *counted 字段非常难理解。基本上,该字段用作其他引用可计数类型的标头。这个部分的详情在 Zval memory management and garbage collect… 章节介绍。

zend_reference *ref 用于代表 PHP 引用。然后使用 IS_REFERENCE 类型标记。同样在这里,我们有专用章节来介绍这样一个概念,请看 Zval 内存管理和垃圾回收 章节。

zend_ast_ref *ast 用于当你从编译器操作 AST 时。PHP 编译的详细介绍在 Zend 编译器 章节中。

zval *zv 仅在内部使用。你不必操作它。它配合 IS_INDIRECT 使用,并允许将 zval * 嵌入 zval 中。这种字段非常隐蔽的用法是用于表示 $GLOBALS[] PHP 超全局变量。

void *ptr 字段非常有用。同样在这里:没有 PHP 用户领域使用,只有在内部使用。基本上,当你想要存储“内容”到 zval 时才会使用。是的,它是 void *,在 C 中表示“指向任意大小的内存空间的指针,(希望)包含任何内容”。然后在 zval 中使用 IS_PTR 标志类型。

当你阅读 对象 章节时,你会学习关于 zend_class_entry 类型。zval 的 zend_class_entry *ce 字段用于将 PHP 类的引用携带到 zval 中。同样,它并不直接在 PHP 语言本身(用户区)使用,但是在内部,你将需要它。

最后,zend_function *func 字段用于将 PHP 函数嵌入 zval 中。函数 章节详细介绍了 PHP 函数。

访问宏

现在,让我们看下 zval 结构实际的样子:

  1. struct _zval_struct {
  2. zend_value value; /* 值 */
  3. union {
  4. struct {
  5. ZEND_ENDIAN_LOHI_4(
  6. zend_uchar type, /* 数据类型 */
  7. zend_uchar type_flags,
  8. zend_uchar const_flags,
  9. zend_uchar reserved) /* EX(This)的呼叫信息 */
  10. } v;
  11. uint32_t type_info;
  12. } u1;
  13. union {
  14. uint32_t next; /*哈希碰撞链*/
  15. uint32_t cache_slot; /*文字缓存插槽 */
  16. uint32_t lineno; /*行号(用于ast节点)*/
  17. uint32_t num_args; /* EX(this)的参数编号*/
  18. uint32_t fe_pos; /* foreach 位置 */
  19. uint32_t fe_iter_idx; /* foreach 迭代器索引*/
  20. uint32_t access_flags; /*类常量访问标志 */
  21. uint32_t property_guard; /* 单一属性监护 */
  22. uint32_t extra; /* 未进一步指定 */
  23. } u2;
  24. };

如上所述,zval 有存储 value 及其 type_info 的成员。value 存放在前面讨论过的 zvalue_value 联合体中,类型标记保存在 u1 联合体的 zend_uchar 部分中。 另外,结构体具有 u2 属性。现在我们先忽略它,在之后讨论它们的功能。

使用 type_info 访问 u1type_info 是缩小为详细的 typetype_flagsconst_flagsreserved 字段。记住,我们在这里是 u1 联合体。所以这四个在 u1.v 字段的信息的权重和存储在 u1.type_info 的信息的权重相同。此处使用了巧妙的内存对齐规则。u1 非常有用,因为它嵌入了有关存储在 zval 中的类型的信息。

u2还有其他的意义。现在我们不必详细说明 u2 字段,只需忽略它,稍后我们将继续进行讨论。

知道了 zval 的结构,现在你可以使用它来编写代码:

  1. zval zv_ptr = /* ... 从某处获取 zval */;
  2. if (zv_ptr->type == IS_LONG) {
  3. php_printf("Zval is a long with value %ld\n", zv_ptr->value.lval);
  4. } else /* ... 处理其他类型 */

虽然上面的代码有效,但不是编写代码的惯用方式。为此,它直接访问 zval 成员,而不是使用一组特殊的访问宏:

  1. zval *zv_ptr = /* ... */;
  2. if (Z_TYPE_P(zv_ptr) == IS_LONG) {
  3. php_printf("Zval is a long with value %ld\n", Z_LVAL_P(zv_ptr));
  4. } else /* ... */

上面的代码使用了 Z_TYPE_P() 宏检索类型标志,并使用 Z_LVAL_P() 则获得长(整型)值。所有的访问宏的变体都带有 _P 后缀或者完全没有后缀。使用哪种取决于你是在使用 zval 还是 zval*

  1. zval zv;
  2. zval *zv_ptr;
  3. zval **zv_ptr_ptr; /* 罕见 */
  4. Z_TYPE(zv); // = zv.type
  5. Z_TYPE_P(zv_ptr); // = zv_ptr->type

基本上,P 代表“指针”。仅对 zval* 才有效,即并没有特殊的宏可用于 zval** ,因为这个在练习中真的没必要(只需首先使用 * 运算符,取消引用值)。

Z_LVAL 类似,也存在取得所有其他类型的值的宏。为了演示它们的用法,我们将创建一个简单的函数打印 zval:

  1. PHP_FUNCTION(dump)
  2. {
  3. zval *zv_ptr;
  4. if (zend_parse_parameters(ZEND_NUM_ARGS(), "z", &zv_ptr) == FAILURE) {
  5. return;
  6. }
  7. switch (Z_TYPE_P(zv_ptr)) {
  8. case IS_NULL:
  9. php_printf("NULL: null\n");
  10. break;
  11. case IS_TRUE:
  12. php_printf("BOOL: true\n");
  13. break;
  14. case IS_FALSE:
  15. php_printf("BOOL: false\n");
  16. break;
  17. case IS_LONG:
  18. php_printf("LONG: %ld\n", Z_LVAL_P(zv_ptr));
  19. break;
  20. case IS_DOUBLE:
  21. php_printf("DOUBLE: %g\n", Z_DVAL_P(zv_ptr));
  22. break;
  23. case IS_STRING:
  24. php_printf("STRING: value=\"");
  25. PHPWRITE(Z_STRVAL_P(zv_ptr), Z_STRLEN_P(zv_ptr));
  26. php_printf("\", length=%zd\n", Z_STRLEN_P(zv_ptr));
  27. break;
  28. case IS_RESOURCE:
  29. php_printf("RESOURCE: id=%d\n", Z_RES_HANDLE_P(zv_ptr));
  30. break;
  31. case IS_ARRAY:
  32. php_printf("ARRAY: hashtable=%p\n", Z_ARRVAL_P(zv_ptr));
  33. break;
  34. case IS_OBJECT:
  35. php_printf("OBJECT: object=%p\n", Z_OBJ_P(zv_ptr));
  36. break;
  37. }
  38. }
  39. const zend_function_entry funcs[] = {
  40. PHP_FE(dump, NULL)
  41. PHP_FE_END
  42. };

让我们尝试一下:

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

访问值的宏非常直接:Z_LVAL 用于长整型,Z_DVAL 用于浮点数。对于字符串 Z_STR 返回实际的 zend_string * 字符串,ZSTR_VAL 将 char 放入其中,而 Z_STRLEN 给我们提供了长度。资源的 ID 可以使用 Z_RES_HANDLE 获取,并通过 Z_ARRVAL 访问数组的 `zend_array ` 。

当你想要访问 zval 的内容时,你应始终头通过这些宏,而不是直接访问它的成员。这样可以保持抽象水平,并使意图更加清晰。使用宏还可以防止在将来的 PHP 版本中更改内部 zval 表示形式。

设定值

上面提及到的大多数宏仅仅访问 zval 结构体的一些成员,因此,你也可以使用它们读取和写入相应的值。例如,考虑以下函数,该函数仅返回字符串“hello world!”:

  1. PHP_FUNCTION(hello_world) {
  2. Z_TYPE_P(return_value) = IS_STRING;
  3. Z_STR_P(return_value) = zend_string_init("hello world!", strlen("hello world!"), 0);
  4. };
  5. /* ... */
  6. PHP_FE(hello_world, NULL)
  7. /* ... */

运行 php -r "echo hello_world();",终端应该会打印出 hello world!

在上面的例子中,我们设置了 return_value 变量,这是 PHP_FUNCTION 宏提供的 zval * 。 我们将在下一章中更详细地介绍该变量,现在足以知道该变量的值就是函数的返回值。 默认情况下,它被初始化为 IS_NULL 类型。

使用访问宏设定 zval 的值是真的简单直接,但是我们也要记住:首先你要记住类型标志决定了 zval 的类型。仅仅设置值(通过 Z_STR_P )是不够的,你还需要设置类型标签。

此外你要需要意识到大多数的案例中,zval “拥有”它的值,且 zval 的寿命将比你设置的值的范围更长。有时候这并不适合处理临时 zval,但是大部分情况下是这样的。

使用上面的例子意味着 return_value 将在函数体离开后继续存在(很明显的,否则没有人可以使用该返回值),所以它不可以使用任何函数的临时变量。

因此我们必须使用 zend_string_init() 创建一个新的 zend_string。这会在堆上创建一个单独的字符串副本。因为 zval “携带”其值,当 zval 被销毁时,它会确保释放该副本,或者至少减去它的引用计数。这同样适用 zval 的其它“复杂”值。例如,如果你为数组设置了 zend_array*,则zval 会在稍后携带它,并在销毁 zval 时将其释放。“释放”是指递减引用计数器,或在引用计数器为零时释放结构。使用整数或者双精度数之类的原始类型时,很明显你无需关注这个,因为它们总是被复制的。所有这些内存管理步骤,像分配、空闲或者引用计数,都在 Zval 内存管理和垃圾回收 章节中详细介绍。

设置 zval 的值是很常见的任务,PHP 为此提供了另一设置宏。它们允许你同时设置类型标志和值。使用这样的宏重写之前的例子:

  1. PHP_FUNCTION(hello_world) {
  2. ZVAL_STRINGL(return_value, "hello world!", strlen("hello world!"));
  3. }

此外,我们无需手动计数 strlen ,而可以使用 ZVAL_STRING 宏(结尾没有L):

  1. PHP_FUNCTION(hello_world) {
  2. ZVAL_STRING(return_value, "hello world!");
  3. }

如果你知道字符串的长度(因为它是以某种方式传递给你),你应该总是使用 ZVAL_STRINGL 宏,以保持二进制安全。如果你不知道长度(或者知道字符串不包含 NULL 字节,这通常与文字一样),你可以使用 ZVAL_STRING 代替。

除了 ZVAL_STRING(L),还有其他一些设置值的宏,在下面例子列出了这些:

  1. ZVAL_NULL(return_value);
  2. ZVAL_FALSE(return_value);
  3. ZVAL_TRUE(return_value);
  4. ZVAL_LONG(return_value, 42);
  5. ZVAL_DOUBLE(return_value, 4.2);
  6. ZVAL_RES(return_value, zend_resource *);
  7. ZVAL_EMPTY_STRING(return_value);
  8. /* 管理空字符串的特殊方式 */
  9. ZVAL_STRING(return_value, "string");
  10. /* = ZVAL_NEW_STR(z, zend_string_init("string", strlen("string"), 0)); */
  11. ZVAL_STRINGL(return_value, "nul\0string", 10);
  12. /* = ZVAL_NEW_STR(z, zend_string_init("nul\0string", 10, 0)); */

请注意这些宏将设置值,但是不会销毁 zval 之前可能保留的任何值。对于 return_value zval 而已,这并不会怎么样,因为它已初始为 IS_NULL(它并没有需要释放的值),但在其他情况下,你必须先使用以下部分中介绍的函数销毁旧值。