PHP内核原理(一)Zvals基本结构

2/22/2017来源:ASP.NET技巧人气:1135

转载请注明出处http://blog.csdn.net/fanhengguang_php

Zvals 基本结构

php内核中使用zval表示一个php变量。

一个zval(zend value 的简写)结构可以表示一个任意的php变量,这是整个php内核中最重要的数据结构,本章将会介绍zval的基本概念以及如何使用。

Types and values

每个zval中存储了一个变量的值以及变量的类型。 这点非常必要,因为php是一个动态类型的语言,变量的类型是在运行阶段确定的,而不是编译阶段。另外变量的类型在zval的声明周期内是可以改变的,所以一个zval可能开始的时候存储的是int整型,过一会又变成了字符串string类型。

zval的类型用一个整形来标记(unsigned char)。 它可以有8种值,对应php中的8中变量类型, 变量的值可以通过一个常量的范式IS_TYPE来访问, 如IS_NULL代表null类型, IS_STRING代表string类型。

zval的实际的值存储在一个unin结构中,如下:

typedef union _zvalue_value { long lval; double dval; struct { char *val; int len; } str; HashTable *ht; zend_object_value obj; } zvalue_value;

对于不熟悉unin结构的同学来说:unin定义了多个成员变量, 但是任意时刻只有一个成员变量被使用, 不熟悉的同学可以自行GOOGle

通过zvals的type 标记就可以知道当前union中那个成员正在被使用, 在介绍相关api之前, 我们先简单看下php支持哪些不同类型的变量,以及是如何存储的。

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

php通过long lval 和double dval 成员变量来存储IS_LONG 和IS_DOUBLE类型, 前者表示整型,后者表示浮点型。

对于long型需要注意的是,首先它是一个有符号类型,也就是说既可以存储正整数也可以存储负数,但是其对于按位运算不太合适; 其次long型在不同平台上会有不同长度,对于32位系统为4bytes,对于64位系统长度可能是4或者8bytes。

基于以上原因,你不能依赖特定长度的long型, long型的最大最小值可以通过常量LONG_MAXLONG_MIN来获的。 长度可以通过SIZEOF_LONG得到。

double类型用来存储浮点型,通常为8types,但是你应该意识到这个类型的精度也是有局限的,有时存储的并不是你想要的结果。

布尔类型通过IS_BOOL常量标记,其值存储在long lval中, 0表示false, 1表示true。 由于布尔类型只有2个值,理论上可以用一个更小的类型如unsigned char 来表示, 但是由于zvalue_value是一个union, 其占用内存的大小是由最大的成员决定的, 所有这里复用的lval。

字符串Strings(IS_STRING)类型的值存储在

struct { char *val; int len; } str;

可见其包含一个char* 字符串指针,和int型字符串长度。 为保证二进制安全php字符串需要存储字符串的明确长度, 因为字符串中可能包含NUL(‘\0’)。 但是PHP底层字符串仍是用NUL(‘\0’)结尾的c字符串,这样的好处是很多现成的库并不支持显示传递字符串长度参数,当然这样的话就不是二进制安全的了,如果其中包含NUL字符,将会被截断。例如很多系统相关函数都是这样处理的,包括libc中的大部分函数。

字符串长度以bytes计算,是不包括结尾的NUL字符的,"foo"长度为3,但是其占用了4个bytes, 如果你使用sizeof计算字符串长度,需要记得-1:strlen("foo") == sizeof("foo") -1

而且string长度是用int而不是long型存储的, 这是一个不幸的历史问题,这导致字符串的长度最大为2147483647, 超过限制会导致溢出(长度会变成负值).

余下的几个类型,将会简单介绍下, 后面会有独立的章节详细介绍:

Array 通过IS_ARRAY标记, 其值存储在HashTable *ht成员变量中。 Objects (IS_OBJECT) 值存储在 zend_object_value 中, 他包含了一个用来查找这个对象的实际的数据的int object handle 对象句柄和一系列的用于决定这个对象行为的句柄。php 类和对象系统将会在后面介绍。

Resource(IS_RESOURCE)和 objects类似,页存储了一个唯一ID用来查找实际的value。 这个ID存储在long lval成员变量中,后面有具体章节介绍resource。

总结一下:下表列出了php中的类型以及相应的value的实际存储位置:

Type tag Storage location IS_NULL none IS_BOOL long lval IS_LONG long lval IS_DOUBLE double dval IS_STRING struct { char *val; int len; } str IS_ARRAY HashTable *ht IS_OBJECT zend_object_value obj IS_RESOURCE long lval

变量存取宏

我们看下zval 结构体的结构

typedef struct _zval_struct { zvalue_value value; zend_uint refcount__gc; zend_uchar type; zend_uchar is_ref__gc; } zval;

前面已经提到过,zval的成员变量中存储了变量的值value 和变量的类型,value存储在我们上面讨论过的zvalue_value value;union中,变量类型存储在zend_uchar中。 另外有两个成员变量以__gc结尾, 这两个变量用来进行垃圾回收处理的,这里暂且布标,后面再讨论。

在了解了zval结构之后, 你可以编码使用它了:

zval *zv_ptr = /* ... get zval from somewhere */; if (zv_ptr->type == IS_LONG) { php_PRintf("Zval is a long with value %ld\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 %ld\n", Z_LVAL_P(zv_ptr)); } else /* ... */

上面的代码中使用了Z_TYPE_P() 宏来获取类型标记, 用Z_LVAL_P()来获取zval中的long型value。 所有这些存取宏都类似于这样_P后缀, _PP后缀, 或者没有后缀。使用哪一种形式取决于你当前是在处理zval, zval* 还是zval**

zval zv; zval *zv_ptr; zval **zv_ptr_ptr; zval ***zv_ptr_ptr_ptr; Z_TYPE(zv); // = zv.type Z_TYPE_P(zv_ptr); // = zv_ptr->type Z_TYPE_PP(zv_ptr_ptr); // = (*zv_ptr_ptr)->type Z_TYPE_PP(*zv_ptr_ptr_ptr); // = (**zv_ptr_ptr_ptr)->type

基本上P后缀的数量和*的数量是一致的, 这个规则适用于zval**以内,对于zval***是没有特定访问宏的, 因为其极少用到。

Z_LVAL,还有很多其他的访问宏用于访问其他类型的value,为了演示他们的用法,我们看下面这个zval打印函数。

PHP_FUNCTION(dump) { zval *zv_ptr; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &zv_ptr) == FAILURE) { return; } switch (Z_TYPE_P(zv_ptr)) { case IS_NULL: php_printf("NULL: null\n"); break; case IS_BOOL: if (Z_BVAL_P(zv_ptr)) { php_printf("BOOL: true\n"); } else { 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=%d\n", Z_STRLEN_P(zv_ptr)); break; case IS_RESOURCE: php_printf("RESOURCE: id=%ld\n", Z_RESVAL_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: ???\n"); break; } } const zend_function_entry funcs[] = { PHP_FE(dump, NULL) PHP_FE_END };

运行结果如下:

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: ???

这些zval 访问宏是非常直接了当的: Z_BVAL bools对应bool类型 Z_LVAL 对应long型, Z_DVAL 对应double类型。 Z_STRVAL返回实际的字符串测char*指针,Z_STRLEN返回字符串长度。资源id通过Z_RESVAL获取。数组类型zval的HashTable*通过Z_ARRVAL宏获取。object类型value的获取暂且不表,因为涉及到有些背景知识。

当你想要访问zval的value的时候, 你应当使用这些宏定义,而不是直接访问结构体、联合体的成员。通过这些宏定义访问zval的内容可以避免歧义和错误,而且可以保证PHP内核升级的兼容性。

zval value的设置

上面介绍的一些存取宏,仅仅提供了某些zval结构成员变量的存取方法。可以通过这些访问对成员变量进行读写。对于一些常见的操作来说,通过上面的宏操作是比较复杂的,考虑以下函数,几年返回一个字符串hello world。

PHP_FUNCTION(hello_world) { Z_TYPE_P(return_value) = IS_STRING; Z_STRVAL_P(return_value) = estrdup("hello world!"); Z_STRLEN_P(return_value) = strlen("hello world!"); }; /* ... */ PHP_FE(hello_world, NULL) /* ... */

执行php -r "echo hello_world();", 控制台输出hello world。

上线的例子我们设置了return_value变量, 这是一个由PHP_FUNCTION提供的zval*指针。 后面我们会详细介绍它, 在这里你只需要知道这个值将会是这个函数的返回值,默认情况下它被初始化为IS_NULL.

通过之前介绍的访问宏来操作zval非常简单,但是需要注意的是:你需要单独去设置zval的类型。仅仅设置内容(通过Z_STRVAL and Z_STRLEN here)是不行的。 你总是要注意自己手动设置类型标记。

另外你需要注意大多数情况下zval以及其包含的值的的生命周期会比你操作这个zval的上下文还要长, 有时也有例外,比如你操作一个临时的zvals。

对于上面的例子意味着return_value在函数结束后会依然存在(这是显而易见的, 否则不会有人去使用return_value了). 所以这里不能使用临时的变量,例如Z_STRVAL_P(return_value) = "hello world!"是非法的, 以为字符串hello Word将会随着函数退出而消失(对于c语言每个栈上分配的变量都是这样).

因此我们需要使用estrdup()拷贝字符串,这将会在堆上创建一个独立的字符串拷贝, 由于这个字符串是zval的成员,当zval销毁的时候,需要确保这个字符串也被释放掉, 这个特性也适用于其他复杂的zval。 例如当你设置一个zval的HashTable*后,zval将会接管这个变量,当zval被释放的时候,它也会被随之释放掉。对于那些简单类型例如整型或者double型来说不需要考虑这个问题。

最后,并不是所有的访问宏都会返回一个成员,Z_BVAL定义如下:

#define Z_BVAL(zval) ((zend_bool)(zval).value.lval)

由于其包含一个类型转换操作,所以你不能这样操作Z_BVAL_P(return_value) = 1 除去一些对象操作的宏,这是唯一的例外,其他的访问宏,可以用来设置value。

实际上你无需考虑字符串的结尾符, 因为设置zval的value是一个常用操作,所以php提供了另外的一些宏来提供这个功能, 通过这些宏你可以同时设定类型标记和zval的值,通过宏来重写上面的例子:

PHP_FUNCTION(hello_world) { ZVAL_STRINGL(return_value, estrdup("hello world!"), strlen("hello world!"), 0); }

由于设置zval值时通常需要对字符串进行拷贝,你可以直接使用ZVAL_STRINGL的最后一个参数完成这个操作,如果传递0则直接使用这个字符串, 如果传递1,字符串会通过estrndup()进行拷贝, 这样我们的例子可以重写如下:

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

更进一步,我们无需手动计算字符串长度strlen, 可以使用ZVAL_STRING 后面没有L

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

如果你知道字符串长度(因为字符串长度可能会传递给你), 你应该使用ZVAL_STRINGL宏来保证二进制安全。如果你不知道字符串长度(或者知道字符串中不包含NUL字节)你可以使用ZVAL_STRING代替。

除了ZVAL_STRING(L)之外,还有一些用于设置zval 值的宏,如下:

ZVAL_NULL(return_value); ZVAL_BOOL(return_value, 0); ZVAL_BOOL(return_value, 1); /* or better */ ZVAL_FALSE(return_value); ZVAL_TRUE(return_value); ZVAL_LONG(return_value, 42); ZVAL_DOUBLE(return_value, 4.2); ZVAL_RESOURCE(return_value, resource_id); ZVAL_EMPTY_STRING(return_value); /* = ZVAL_STRING(return_value, "", 1); */ ZVAL_STRING(return_value, "string", 1); /* = ZVAL_STRING(return_value, estrdup("string"), 0); */ ZVAL_STRINGL(return_value, "nul\0string", 10, 1); /* = ZVAL_STRINGL(return_value, estrndup("nul\0string", 10), 10, 0); */

注意:这些宏只负责设置zval的值, 不会销毁之前的已经存在的值,对于return_value是没问题的,因为它被初始化为IS_NULL类型(没有任何value需要被释放). 但是其他情况下你需要先释放掉老的value值,具体方法下节介绍。