PHP 源代码中的 zval
在 PHP 中定义变量不需要声明类型。 一开始,变量$a被赋予一个整数值,以后可以很容易地更改为其他类型。
那么PHP源码中如何存储这个变量$a呢? 带着这个疑问,我们来看看PHP的源码。
PHP的源代码是由C编译的,PHP源代码中使用了一个zval结构来存储PHP代码中创建的变量。 我们把zval结构体的定义拿出来简单分析一下。
这是 PHP 在 Github 上的官方仓库:
github.com/php/php-src,本文使用的分支是 PHP-7.4.29。
zval结构
在PHP源码中找到这个文件:php-src/Zend/zend_types.h,可以看到zval结构体的定义如下,源码如右。
源码中使用了PHP自己定义的类型zend_uchar、uint16_t、uint32_t等,这个类型在平台和不同平台的编译器下会转换为charshorint。
为了便于理解,我将其翻译成通用类型php 浮点型,并显示在源码右侧。 同时还扩展了宏函数ZEND_ENDIAN_LOHI_3()。
typedef struct _zval_struct zval;
...
《源代码》 《翻译后》
-------------------------------------------------------------------------------------------
struct _zval_struct { | struct _zval_struct {
zend_value value; | zend_value value;
union { | union {
struct { | struct {
ZEND_ENDIAN_LOHI_3( | unsigned char type;
zend_uchar type, | unsigned char type_flags;
zend_uchar type_flags, | union {
union { | unsigned short extra;
uint16_t extra; | } u;
} u | } v;
) | unsigned int type_info;
} v; | } u1;
uint32_t type_info; | union {
} u1; | unsigned int next;
union { | unsigned int cache_slot;
uint32_t next; | unsigned int opline_num;
uint32_t cache_slot; | unsigned int lineno;
uint32_t opline_num; | unsigned int num_args;
uint32_t lineno; | unsigned int fe_pos;
uint32_t num_args; | unsigned int fe_iter_idx;
uint32_t fe_pos; | unsigned int access_flags;
uint32_t fe_iter_idx; | unsigned int property_guard;
uint32_t access_flags; | unsigned int constant_flags;
uint32_t property_guard; | unsigned int extra;
uint32_t constant_flags; | } u2;
uint32_t extra; | };
} u2; |
}; |
在zval结构中,变量的值存储在zend_value类型的value属性中。 并用u1.v.type来记录这个值的类型,例如IS_LONG对应整型,IS_STRING对应字符串类型。
zend_value 联合
zend_value类型也在php-src/Zend/zend_types.h中定义,它是一个联合体。 下面是zend_value union的定义,左边是源码。 同样在左侧,我也做了一个简单的翻译php 浮点型,将zend_longuint32_t翻译成常见的类型以供查看。
《源代码》 《翻译后》
------------------------------------------------------------------------------------
typedef union _zend_value { | typedef union _zend_value {
zend_long lval; /* long value */ | long lval;
double dval; /* double value */ | double dval;
zend_refcounted *counted; | zend_refcounted *counted;
zend_string *str; | zend_string *str;
zend_array *arr; | zend_array *arr;
zend_object *obj; | zend_object *obj;
zend_resource *res; | zend_resource *res;
zend_reference *ref; | zend_reference *ref;
zend_ast_ref *ast; | zend_ast_ref *ast;
zval *zv; | zval *zv;
void *ptr; | void *ptr;
zend_class_entry *ce; | zend_class_entry *ce;
zend_function *func; | zend_function *func;
struct { | struct {
uint32_t w1; | unsigned int w1;
uint32_t w2; | unsigned int w2;
} ww; | } ww;
} zend_value; | } zend_value;
union的一个特点是它占用的显存是其属性中最大的类型对应的厚度。
其中zend_long为long类型。 可以看到long类型的lval和double类型的dval占用的宽度都是8个字节。 上面其他指针类型也是8字节。 最后一个结构体属性ww由两个int类型组成,厚度相乘也是8个字节。
因此,该联合的厚度为 8 字节。
在我们编写的PHP代码中,整型和浮点型数据的值会直接存储在lval和dval中。
如果是字符串、数组或者其他类型,就会开辟一块空间来存储数据,并将其地址存储在zend_value中,即zval.value属性,如:zval.value.zend_long=9527,zval .value.zend_string=字符串地址,zval.value.zend_array=链表地址。
然后将zval.u1.v.type上的这个zval.value标记为整数,或者浮点,或者字符串,或者其他类型。
zval.u1.v.type类型定义也在php-src/Zend/zend_types.h文件中,所有定义如下:
/* regular data types */
/* constant expressions */
/* internal types */
/* fake types used only for type hinting (Z_TYPE(zv) can not use them) */
zval 结构的内存使用情况
接下来我们来分析一下zval所需的显存。
也就是说,当我们编写PHP代码时,如果创建一个整型变量,那么它在运行时实际上会占用16字节的显存,显存成本至少是C语言的两倍。 事实上,这两倍的成本也带来了PHP处理变量的灵活性。