PHP7扩展开发教程[4] – zval的工作原理
确定你已经读完上一章《PHP7扩展开发教程[3] – 怎样定义函数?》。
回顾前篇
在章节3中,我们没有讲解myext_strtolower和myext_strtoupper两个函数的实现,因为涉及了zval的使用。
在本章节中,将讲述zval的数据结构,同时通过示例来演示zval的使用方法以及背后原理。最终,将回到之前的2个函数体,看看zval是如何实际应用的。
前言
zval是Zend对各种数据类型的包装,像string,int,array等都可以包装成zval。这样Zend只需要依赖zval即可操作各种数据类型,算是一种抽象和解耦的设计。
理论先行
先来看一下zval数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
struct _zval_struct { zend_value value; /* value */ union { struct { ZEND_ENDIAN_LOHI_4( zend_uchar type, /* active type */ zend_uchar type_flags, zend_uchar const_flags, zend_uchar reserved) /* call info for EX(This) */ } v; uint32_t type_info; } u1; union { uint32_t next; /* hash collision chain */ uint32_t cache_slot; /* literal cache slot */ 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 extra; /* not further specified */ } u2; }; |
这个数据结构有3个字段。
- value:保存了具体类型的值,稍后再说。
- u1:是一个4字节的union,内部的type_info和v共享union的4字节,提供了两种访问方式而已。u1主要保存了value的类型信息,这也是zval包装多种数据类型的原理,具体来说type说明数据类型是字符串还是数组等等(对应宏IS_STRING,IS_DOUBLE等等…),而type_flags说明数据的属性,比如是否需要引用计数(对应宏IS_TYPE_CONSTANT,IS_TYPE_REFCOUNTED等等)。
- u2:也是union,它也是4字节,里面的若干字段同一时刻只有1个有效,例如:当zval被保存在哈希表中时,u2.next用于维护哈希表的链表下一节点标识。
接着看一下value,它的类型是zend_value:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
typedef union _zend_value { zend_long lval; /* long value */ double dval; /* double value */ zend_refcounted *counted; zend_string *str; zend_array *arr; zend_object *obj; zend_resource *res; zend_reference *ref; zend_ast_ref *ast; zval *zv; void *ptr; zend_class_entry *ce; zend_function *func; struct { uint32_t w1; uint32_t w2; } ww; } zend_value; |
zend_value本身就是个union,它同一时刻就保存某一种数据类型的具体值。zval做了一个优化,就是像lval,dval这种简单类型,直接占用union自身内存来存储,而复杂类型例如arr,str才会通过指针指向堆分配的具体数据类型。
直接存储在zval本身的数据类型,例如:zend_long,zend_double是不需要引用计数的,因为zval存储在栈空间,彼此直接浅拷贝即可完成复制,资源也无需释放,因为没有堆内存分配。而对于复杂类型,例如zend_string,zend_array结构复杂,所以作为独立的底层数据类型在堆中分配内存,多个zval引用同一份底层数据,需要维护引用计数,以便保证底层数据生命期不会被提前结束。
在php7中,zval将复杂类型的引用计数保存在底层数据类型的结构体中,而不是放在zval本身,这样的设计更加清晰与易于理解,下面以zend_string,zend_array为例,它们内部都保存了一个叫做gc的引用计数字段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
struct _zend_string { zend_refcounted_h gc; zend_ulong h; /* hash value */ size_t len; char val[1]; }; struct _zend_array { zend_refcounted_h gc; union { struct { ZEND_ENDIAN_LOHI_4( zend_uchar flags, zend_uchar nApplyCount, zend_uchar nIteratorsCount, zend_uchar consistency) } v; uint32_t flags; } u; uint32_t nTableMask; Bucket *arData; uint32_t nNumUsed; uint32_t nNumOfElements; uint32_t nTableSize; uint32_t nInternalPointer; zend_long nNextFreeElement; dtor_func_t pDestructor; }; |
而gc字段的结构体如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
typedef struct _zend_refcounted_h { uint32_t refcount; /* reference counter 32-bit */ union { struct { ZEND_ENDIAN_LOHI_3( zend_uchar type, zend_uchar flags, /* used for strings & objects */ uint16_t gc_info) /* keeps GC root number (or 0) and color */ } v; uint32_t type_info; } u; } zend_refcounted_h; |
这个引用计数的结构体有2个字段,通常我们只会用到refcount字段,它用于维护一个底层数据结构的引用计数。当一个数据对象的引用计数减少为0时,说明资源没有人引用,则可以释放对象的堆内存,意思就是释放zend_string*自身的内存即可。而u字段的type_info实际上等于zval的u1字段,记录的都是底层数据的类型信息。
至于怎么操作zval,zend_string,zend_array等,它们之间通过什么API交互,引用计数怎么管理,内存怎么分配与释放,在下面的例子中会逐一讲解。
在开始示例之前,我最后要特别提到一点就是zend_value是一个union(如果忘记请回头看),而所有的底层数据结构的头部都是zend_refcounted_h。假设当前zend_value保存了str字段,我们知道zend_string的第一个字段是zend_ref_counted_h,所以zend_value这个union里的zend_refcounted *counted与str保存的是同一个地址,这个地址开始的sizeof(zend_refcounted_h)字节就是引用计数对象,对C语言熟悉的朋友应该很容易理解。在这样的设计下,我们可以通过zval.value.counted直接访问到zval.value.str.gc里的引用计数信息,算是一个捷径,巧妙吧。
1 2 3 4 5 6 7 8 9 10 |
typedef union _zend_value { zend_long lval; /* long value */ double dval; /* double value */ zend_refcounted *counted; // counted保存的地址和str相同 zend_string *str; // 而str地址开始的内存就是一个zend_refcounted_h // 而zend_refcounted其实就是zend_refcounted_h struct _zend_refcounted { zend_refcounted_h gc; }; |
实例讲解
打开本章代码:https://github.com/owenliang/php7-extension-explore/tree/master/course4-how-the-zval-works。
在myext.c中,我按照第3章的方式注册了一个新的PHP函数叫做my_testzval,对应的C实现是zif_testzval。在这个函数中,我们将常见的底层数据类型与zval操作方式尽可能的展示出来,让大家对zval与底层数据结构的关系,引用计数的管理有比较清晰的认识。
下面采用分段讲解,如果需要翻看完整代码,请你打开上面的链接。
1 2 3 4 |
// undef zval undef_zval; ZVAL_UNDEF(&undef_zval); assert(Z_TYPE(undef_zval) == IS_UNDEF); |
PHP7的zval只需要放在栈内存里,一个没有承载任何底层数据的zval类型叫做IS_UNDEF,相当于在PHP中对一个变量执行unset。
ZVAL_UNDEF是初始化这个zval为IS_UNDEF类型,看一下这个宏:
1 2 3 |
#define ZVAL_UNDEF(z) do { \ Z_TYPE_INFO_P(z) = IS_UNDEF; \ } while (0) |
为了方便你加深zval的数据结构理解,进一步展开:
1 2 |
#define Z_TYPE_INFO(zval) (zval).u1.type_info #define Z_TYPE_INFO_P(zval_p) Z_TYPE_INFO(*(zval_p)) |
正如在理论环节所说,zval.u1.type_info是4字节整形,保存了数据的各种类型信息(zval.u1.v.type、zval.u1.v.type_flags、zval.u1.v.const_flags),赋值IS_UNDEF即可标明zval的数据类型。
这样就完成了一个简单zval的初始化,至于zval有哪些类型呢?先列举一下,你心里有个数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
/* regular data types */ #define IS_UNDEF 0 #define IS_NULL 1 #define IS_FALSE 2 #define IS_TRUE 3 #define IS_LONG 4 #define IS_DOUBLE 5 #define IS_STRING 6 #define IS_ARRAY 7 #define IS_OBJECT 8 #define IS_RESOURCE 9 #define IS_REFERENCE 10 /* constant expressions */ #define IS_CONSTANT 11 #define IS_CONSTANT_AST 12 /* fake types */ #define _IS_BOOL 13 #define IS_CALLABLE 14 #define IS_ITERABLE 19 #define IS_VOID 18 /* internal types */ #define IS_INDIRECT 15 #define IS_PTR 17 #define _IS_ERROR 20 |
IS_NULL类型也是类似的:
1 2 3 4 |
// null zval null_zval; ZVAL_NULL(&null_zval); assert(Z_TYPE(null_zval) == IS_NULL); |
上面用的Z_TYPE用于获取zval的类型:
1 2 3 4 5 6 |
#define Z_TYPE(zval) zval_get_type(&(zval)) #define Z_TYPE_P(zval_p) Z_TYPE(*(zval_p)) static zend_always_inline zend_uchar zval_get_type(const zval* pz) { return pz->u1.v.type; } |
可以看出,数据类型信息是保存在zval.u1.v.type这个字节中的,而数据的属性信息主要保存在zval.u1.v.type_flags中,后面会看到相关用法。
接下来是LONG类型,也就是PHP的整形类型:
1 2 3 4 5 |
// long zval long_zval; ZVAL_LONG(&long_zval, 1024); assert(Z_TYPE(long_zval) == IS_LONG); assert(Z_LVAL_P(&long_zval) == 1024); |
ZVAL_LONG这个宏除了设置zval的type信息,还需要将1024这个值保存到zval.value.lval中去:
1 2 3 4 5 6 7 8 |
#define ZVAL_LONG(z, l) { \ zval *__z = (z); \ Z_LVAL_P(__z) = l; \ Z_TYPE_INFO_P(__z) = IS_LONG; \ } #define Z_LVAL(zval) (zval).value.lval #define Z_LVAL_P(zval_p) Z_LVAL(*(zval_p)) |
通过这3个简单类型,我们对zval的真实面目有了更加具体的了解。这些简单类型直接保存在zval自身,所以不需要释放内存,用起来还算比较安心,下面进入复杂类型。
首先从zend_string开始:
1 2 3 4 5 6 7 8 9 |
// string zval str_zval; zend_string *str = zend_string_init("IS_STRING", sizeof("IS_STRING") - 1, 0); ZVAL_STR(&str_zval, str); assert(Z_TYPE(str_zval) == IS_STRING); assert(strncmp(Z_STRVAL_P(&str_zval), "IS_STRING", Z_STRLEN_P(&str_zval)) == 0); zval_addref_p(&str_zval); // add 1 ref for arr_zval below zval_addref_p(&str_zval); // add 1 ref for ref_zval below zval_ptr_dtor(&str_zval); |
首先zval str_zval是一个未经初始化的zval,复杂类型的zval需要底层数据结构作为支撑,所以首先通过zend_string(Zend/zend_string.h)的api来创建一个zend_string对象,传入字符串和字符串长度,最后一个参数是分配临时内存(后面会讲到临时内存和持久内存的意义)。然后,就像ZVAL_LONG一样,将这个zend_string初始化到str_zval上:
1 2 3 4 5 6 7 8 9 |
#define ZVAL_STR(z, s) do { \ zval *__z = (z); \ zend_string *__s = (s); \ Z_STR_P(__z) = __s; \ /* interned strings support */ \ Z_TYPE_INFO_P(__z) = ZSTR_IS_INTERNED(__s) ? \ IS_INTERNED_STRING_EX : \ IS_STRING_EX; \ } while (0) |
1 2 |
#define Z_STR(zval) (zval).value.str #define Z_STR_P(zval_p) Z_STR(*(zval_p)) |
这个宏除了给zval设置IS_STRING类型信息外,还把zend_string保存到了zval.value.str字段,仅此而已。zend_string_init返回的zend_string引用计数为1,所以无需额外增加引用计数。
1 |
assert(Z_TYPE(str_zval) == IS_STRING); |
通过上面的断言,判断zval是字符串类型。
1 |
assert(strncmp(Z_STRVAL_P(&str_zval), "IS_STRING", Z_STRLEN_P(&str_zval)) == 0); |
通过上面的断言,避免zval底层保存的字符串等于”IS_STRING”,Z_STRVAL_P的实现再简单不过,它取出zval.value.str,它是一个zend_string*,接着取出这个zend_string的val字段,也就是保存字符串内容的内存段,相关的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#define Z_STR(zval) (zval).value.str #define Z_STR_P(zval_p) Z_STR(*(zval_p)) #define Z_STRVAL(zval) ZSTR_VAL(Z_STR(zval)) #define Z_STRVAL_P(zval_p) Z_STRVAL(*(zval_p)) #define ZSTR_VAL(zstr) (zstr)->val struct _zend_string { zend_refcounted_h gc; zend_ulong h; /* hash value */ size_t len; char val[1]; }; |
接下来,我对这个保存了zend_string的zval做了3次引用计数操作:
1 2 3 |
zval_addref_p(&str_zval); // add 1 ref for arr_zval below zval_addref_p(&str_zval); // add 1 ref for ref_zval below zval_ptr_dtor(&str_zval); |
首先,我通过zval_addref_p为其增加了2个引用计数,此时底层zend_string的引用计数是3,这个函数的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
static zend_always_inline uint32_t zval_addref_p(zval* pz) { ZEND_ASSERT(Z_REFCOUNTED_P(pz)); return ++GC_REFCOUNT(Z_COUNTED_P(pz)); } #define Z_TYPE_FLAGS(zval) (zval).u1.v.type_flags #define Z_COUNTED(zval) (zval).value.counted #define Z_COUNTED_P(zval_p) Z_COUNTED(*(zval_p)) #define GC_REFCOUNT(p) (p)->gc.refcount #define Z_REFCOUNTED(zval) ((Z_TYPE_FLAGS(zval) & IS_TYPE_REFCOUNTED) != 0) #define Z_REFCOUNTED_P(zval_p) Z_REFCOUNTED(*(zval_p)) |
这个函数首先断言当前的zval保存的底层数据类型是支持引用计数的复杂类型,否则这个调用将失败(对于long,double类型不能使用该方法)。我们知道zval.u1.v.type_flags保存了数据类型的一些属性信息,比如是否支持引用计数(IS_TYPE_REFCOUNTED)。接着就是对引用计数做+1的操作,我们知道zval.value.counted实际上就是对zval.value.str中引用计数对象的直接访问,上面的宏其实就是在做这个事情。
增加这2个引用计数是因为下面的代码中有2处需要用到这个zval,但是当前这段代码对这个zval的使用已经结束,因此调用zval_ptr_dtor来释放这个zval,它的对应实现如下:
1 2 3 4 5 6 |
static zend_always_inline void _zval_ptr_dtor_nogc(zval *zval_ptr ZEND_FILE_LINE_DC) { if (Z_REFCOUNTED_P(zval_ptr) && !Z_DELREF_P(zval_ptr)) { _zval_dtor_func(Z_COUNTED_P(zval_ptr) ZEND_FILE_LINE_RELAY_CC); } } |
如果zval是复杂类型(支持引用计数),并且减少1个引用计数后为0,那么说明没有人持有这个zval,所以可以调用_zval_dtor_func彻底释放这个zval的底层数据对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
ZEND_API void ZEND_FASTCALL _zval_dtor_func(zend_refcounted *p ZEND_FILE_LINE_DC) { switch (GC_TYPE(p)) { case IS_STRING: case IS_CONSTANT: { zend_string *str = (zend_string*)p; CHECK_ZVAL_STRING_REL(str); zend_string_free(str); break; } case IS_ARRAY: { zend_array *arr = (zend_array*)p; zend_array_destroy(arr); break; } case IS_CONSTANT_AST: { zend_ast_ref *ast = (zend_ast_ref*)p; zend_ast_destroy_and_free(ast->ast); efree_size(ast, sizeof(zend_ast_ref)); break; } |
我们知道,zval.value.counted和zval.value.str/arr等共享一个地址,所以在这个释放函数里对p指针做了类型转换,通过switch case释放了对应的资源类型,例如string类型调用了zend_string_free,与zend_string_init相对。
当然,因为当前的str_zval有3个引用计数,所以调用一次只会释放掉1个引用计数,仍旧保留其生命期,并且预留了2个引用计数为后面的代码使用。
接下来看一个不太一样的数据结构:zend_array。
它相当于PHP里的array,既能充当哈希表,有能充当普通数组。当然,今天不是详细讲这个数据结构的实现(后面有必要会单独讲),而是介绍基本用法与引用计数相关的注意点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
typedef struct _zend_array HashTable; struct _zend_array { zend_refcounted_h gc; union { struct { ZEND_ENDIAN_LOHI_4( zend_uchar flags, zend_uchar nApplyCount, zend_uchar nIteratorsCount, zend_uchar consistency) } v; uint32_t flags; } u; uint32_t nTableMask; Bucket *arData; uint32_t nNumUsed; uint32_t nNumOfElements; uint32_t nTableSize; uint32_t nInternalPointer; zend_long nNextFreeElement; dtor_func_t pDestructor; }; |
zend_array有个别名HashTable。字段dtor_func_t pDestructor对引用计数的管理非常重要,它保存了一个资源销毁函数指针,是本章关注的重点。当你向zend_array覆盖一个value、删除一个value、或者释放zend_array时,它都会将value传给这个函数指针进行销毁,下面会讲到。
1 2 3 |
zval arr_zval; zend_array *arr = emalloc(sizeof(*arr)); zend_hash_init(arr, 0, NULL, ZVAL_PTR_DTOR, 0); |
在最初,arr_zval是未经初始化的。
接下来分配底层数据对象,这里通过emalloc分配临时内存,所谓临时内存就是从Zend的内存池中分配的,当请求结束时Zend可以得知这个内存是否被归还,如果因为开发者疏漏没有释放,Zend会发出警告错误并帮我们释放掉这个内存,因此它应该只用于分配请求期间的内存,请求结束它就无效了。
之后调用了zend_hash_init初始化这个未经初始化的zend_array:
1 2 |
#define zend_hash_init(ht, nSize, pHashFunction, pDestructor, persistent) \ _zend_hash_init((ht), (nSize), (pDestructor), (persistent) ZEND_FILE_LINE_CC) |
它等价于_zend_hash_init,我们传入的nSize=0也就是初始数组大小为0,pHashFunction传NULL因为用不到,pDestructor传ZVAL_PTR_DTOR这个宏(稍后看),persistant=0表示这个hash表内部分配内存应该使用临时内存(也就是请求结束就失效)。
那么ZVAL_PTR_DTOR宏是什么呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#define ZVAL_PTR_DTOR zval_ptr_dtor_wrapper #define zval_ptr_dtor_wrapper _zval_ptr_dtor_wrapper ZEND_API void _zval_ptr_dtor_wrapper(zval *zval_ptr) { i_zval_ptr_dtor(zval_ptr ZEND_FILE_LINE_CC); } static zend_always_inline void i_zval_ptr_dtor(zval *zval_ptr ZEND_FILE_LINE_DC) { if (Z_REFCOUNTED_P(zval_ptr)) { if (!Z_DELREF_P(zval_ptr)) { _zval_dtor_func(Z_COUNTED_P(zval_ptr) ZEND_FILE_LINE_RELAY_CC); } else { GC_ZVAL_CHECK_POSSIBLE_ROOT(zval_ptr); } } } |
_zval_dtor_func似曾相识,上面的代码做的事情等价于调用一次:zval_ptr_dtor,先对数组里的value减少一个引用计数,如果为0就释放资源。
完成了底层zend_array的初始化,现在将其赋值给zval即可:
1 2 |
ZVAL_ARR(&arr_zval, arr); assert(Z_TYPE(arr_zval) == IS_ARRAY); |
接下来,把之前的str_zval更新到这个zend_array中去:
1 |
zend_symtable_str_update(arr, "str_zval", sizeof("str_zval") - 1, &str_zval); |
需要传入的就是zend_array,key,key的长度,还有zval。还记得之前已经对str_zval增加过1次额外的引用计数,就是因为这个str_zval要托付给这个zend_array才需要增加计数。
现在,从zend_array中读出这个zval:
1 2 3 4 |
zval *zval_in_arr = zend_symtable_str_find(arr, "str_zval", sizeof("str_zval") - 1); assert(zval_in_arr); assert(Z_TYPE_P(zval_in_arr) == IS_STRING); assert(GC_REFCOUNT(Z_COUNTED_P(zval_in_arr)) == 2); |
没有什么特殊的,它返回的zval指针就是zend_array中保存的value(zval)的地址,这个value底层引用的zend_string与str_zval的底层引用是相同的。
第一个断言确认返回值不是NULL说明找到了这个Key,第二个断言判断它是string类型的zval,第三个断言判定当前zval(zend_string)有2个引用计数,因为它就是str_val的一个副本而已。
接下来:
1 2 3 |
uint32_t num_elems = zend_hash_num_elements(Z_ARRVAL_P(&arr_zval)); assert(num_elems == 1); zval_ptr_dtor(&arr_zval); |
获取一下zend_array的元素个数,通过Z_ARRVAL_P可以拿到zval.value.arr,就不列举这个宏了。
最后通过zval_ptr_dtor释放这个arr_zval,因为我不再使用它了。因为arr_zval只有初始化时的唯一1个引用计数,所以zend_array将要析构自身,最终将进入这个函数释放array:
1 |
zend_array_destroy(zval.value.arr) |
该函数内部会遍历所有value,将其传入ZVAL_PTR_DTOR进行资源释放,也就是对zval调用zval_ptr_dtor。因此,在arr_zval销毁后,我们的str_zval的引用计数将减1,还剩余1。
接下来是reference,也就是PHP中的&引用,连续的引用一个变量,对任意一个引用修改都可以反馈给原始变量,这一点在zval中有对应的设计表现。
1 2 3 4 5 6 |
// reference zval ref_zval; zend_reference *ref = emalloc(sizeof(*ref)); GC_REFCOUNT(ref) = 1; GC_TYPE_INFO(ref) = IS_REFERENCE; memcpy(&(ref->val), &str_zval, sizeof(str_zval)); |
ref_zval未经初始化,先分配底层数据结构zend_reference:
1 2 3 4 |
struct _zend_reference { zend_refcounted_h gc; zval val; }; |
zend_reference并没有类似zend_hash_init的函数来初始化自身,只能手动处理。主要是设置2个信息,一个是初始化gc的引用计数为1,再就是设置gc内的type=IS_REFERENCE,之前提到过:zend_refcounted_h内也保存了和zval.u1.type_info一样的值,用于垃圾回收时判定数据类型用,了解即可。
接下来,将str_zval拷贝到zend_reference.val上,之前给str_zval额外增加的另外1个引用计数就是为此准备的。
现在,可以将zend_reference对象赋值给zval了:
1 2 |
ZVAL_REF(&ref_zval, ref); assert(Z_TYPE(ref_zval) == IS_REFERENCE); |
和之前的其他类型一样,ZVAL_REF将底层数据对象赋值给zval:
1 2 3 4 5 6 7 8 |
#define ZVAL_REF(z, r) do { \ zval *__z = (z); \ Z_REF_P(__z) = (r); \ Z_TYPE_INFO_P(__z) = IS_REFERENCE_EX; \ } while (0) #define Z_REF(zval) (zval).value.ref #define Z_REF_P(zval_p) Z_REF(*(zval_p)) |
现在的ref_zval.value保存了一个zend_reference* ref,而ref里保存了str_zval的副本。所以,ref_zval就是对str_zval的一个引用。
因为下面我还要继续使用ref_zval,所以我给它增加1个额外的引用计数,此时有2个引用计数,然后尝试销毁自身,剩余1个引用计数。
1 2 |
zval_addref_p(&ref_zval); // add 1 ref for ref_ref_zval zval_ptr_dtor(&ref_zval); |
接下来,我定义另一个zval,它也是reference类型,复制自ref_zval,共享同样的底层zend_reference,因为此前ref_zval已经额外增加过1个引用计数,所以这个复制操作只需要这样:
1 2 3 |
// zval ref to reference zval ref_ref_zval; memcpy(&ref_ref_zval, &ref_zval, sizeof(ref_zval)); |
现在ref_ref_zval拥有对zend_reference的唯一引用计数,我决定对底层的zend_reference引用的zval进行一次偷梁换柱:
1 |
zval *real_zval = Z_REFVAL_P(&ref_ref_zval); |
先取出ref_ref_zval.value.ref.val,也就是zend_reference.val,它其实是str_zval的副本。
然后从这个zval中取出底层的zend_string:
1 2 |
zend_string *real_str = Z_STR_P(real_zval); assert(real_str == str); |
释放掉它最后的1个引用计数,令其析构:
1 |
zend_string_release(real_str); |
其实现:
1 2 3 4 5 6 7 8 |
static zend_always_inline void zend_string_release(zend_string *s) { if (!ZSTR_IS_INTERNED(s)) { if (--GC_REFCOUNT(s) == 0) { pefree(s, GC_FLAGS(s) & IS_STR_PERSISTENT); } } } |
然后,重新分配一个zend_string,并赋值到ref_ref_zval.value.ref.val,完成”换柱”:
1 |
ZVAL_STR(real_zval, zend_string_init("IS_STRING_TOO", sizeof("IS_STRING_TOO") - 1, 0)); |
现在ref_ref_zval的reference引用的zval是一个全新的字符串数据,引用计数为1。同时,ref_ref_zval也只有1个引用计数,我们最后对其进行析构:
1 |
zval_ptr_dtor(&ref_ref_zval); |
这个ref_ref_zval底层的zend_reference引用计数降低为0,内存将被释放,释放前它会对zend_reference.val也进行一次析构,因为这个新字符串zval的底层zend_string引用计数也降为0,所以也被释放。
接下来的例子,告诉我们只有复杂类型可以支持引用计数:
1 2 3 4 5 6 7 |
// tips: // zval_addref_p/zval_delref_p can only be applied to zval with ref count zval double_zval; ZVAL_DOUBLE(&double_zval, 13.14); assert(!Z_REFCOUNTED_P(&double_zval)); // the following line will trigger assert-coredump // zval_delref_p(&double_zval); |
对于double_zval来说,double_zval.u1.v.type_flags中没有IS_REFCOUNTED属性,所以不属于复杂类型,不能使用zval_addref_p/zval_delref_p。
接下来,我们初始化一个string类型的zval:
1 2 3 |
// zval copy zval copy_from; ZVAL_STR(©_from, zend_string_init("test ZVAL_COPY", sizeof("test ZVAL_COPY") - 1, 0)); |
使用一个方便的宏来完成zval的复制(并非真的副本,而是基于引用计数复制):
1 2 |
zval copy_to1; ZVAL_COPY(©_to1, ©_from); |
这个宏的实现如下:
1 2 3 4 5 6 7 8 9 10 11 |
#define ZVAL_COPY(z, v) \ do { \ zval *_z1 = (z); \ const zval *_z2 = (v); \ zend_refcounted *_gc = Z_COUNTED_P(_z2); \ uint32_t _t = Z_TYPE_INFO_P(_z2); \ ZVAL_COPY_VALUE_EX(_z1, _z2, _gc, _t); \ if ((_t & (IS_TYPE_REFCOUNTED << Z_TYPE_FLAGS_SHIFT)) != 0) { \ GC_REFCOUNT(_gc)++; \ } \ } while (0) |
其实等价于将copy_from通过memcpy拷贝给copy_to,然后判断一下如果是复杂类型,那么引用计数+1。
上面的这段代码等价于下面的这段代码,当然这个Z_REFCOUNTED_P判定在此是多余的,因为我们知道这是一个带引用计数的复杂类型:
1 2 3 4 5 6 |
// same as ZVAL_COPY above zval copy_to2; memcpy(©_to2, ©_from, sizeof(copy_from)); // same as ZVAL_COPY_VALUE if (Z_REFCOUNTED_P(©_from)) { zval_addref_p(©_to2); } |
因为copy_from,copy_to1,copy_to2都保存了同一份zend_string,所以最终释放3次就会对zend_string资源完成最终的清理:
1 2 3 4 |
// release all reference count zval_ptr_dtor(©_from); zval_ptr_dtor(©_from); zval_ptr_dtor(©_from); |
这里你也许奇怪的是为什么对同一个copy_from做了3次释放,一个调用过zval_ptr_dtor的zval还可以访问吗?其实观察zval_ptr_dtor的实现可知,它并不会修改zval本身的任何信息,仅仅是对其底层的zend_string进行释放,所以对copy_from多次调用zval_ptr_dtor等价于先后为copy_from,copy_to1,copy_to2调用zval_ptr_dtor。
实现真正有意义的函数
在章节3中,我们实现了my_strtolower和my_strtoupper函数,但是因为还没有介绍zval所以没有深入讲解。
现在我们回头看它们的实现就变得很简单了:
1 2 3 4 5 6 7 8 9 |
void zif_strtolower(zend_execute_data *execute_data, zval *return_value) { TRACE("zif_strtolower"); int num_args = ZEND_CALL_NUM_ARGS(execute_data); zval *args = ZEND_CALL_ARG(execute_data, 1); TRACE("num_args=%d", num_args); *return_value = strcase_convert(&args[0], 1); } |
这里zend_execute_data是PHP调用函数时的信息,可以简单看一下其数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
struct _zend_execute_data { const zend_op *opline; /* executed opline */ zend_execute_data *call; /* current call */ zval *return_value; zend_function *func; /* executed function */ zval This; /* this + call_info + num_args */ zend_execute_data *prev_execute_data; zend_array *symbol_table; #if ZEND_EX_USE_RUN_TIME_CACHE void **run_time_cache; /* cache op_array->run_time_cache */ #endif #if ZEND_EX_USE_LITERALS zval *literals; /* cache op_array->literals */ #endif }; |
获取函数参数是通过zval This获取的,我们知道zval有一个u2字段,这里使用了u2.num_args保存了函数的参数个数:
1 2 |
#define ZEND_CALL_NUM_ARGS(call) \ (call)->This.u2.num_args |
而真正的参数列表是一个zval数组,它保存在zend_execute_data结构体末尾之后的内存区域中,大概了解一下即可:
1 2 3 4 5 6 7 8 |
#define ZEND_CALL_FRAME_SLOT \ ((int)((ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data)) + ZEND_MM_ALIGNED_SIZE(sizeof(zval)) - 1) / ZEND_MM_ALIGNED_SIZE(sizeof(zval)))) #define ZEND_CALL_VAR_NUM(call, n) \ (((zval*)(call)) + (ZEND_CALL_FRAME_SLOT + ((int)(n)))) #define ZEND_CALL_ARG(call, n) \ ZEND_CALL_VAR_NUM(call, ((int)(n)) - 1) |
所以,ZEND_CALL_ARG(execute_data, 1)就是第一个参数的zval地址,后续的zval连续排列成数组。
接下来将第一个参数传递给自定义的C函数strcase_convert,实现大小写的转换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
zval strcase_convert(const zval *zv, int lowercase) { zval retval; zend_uchar zv_type = zval_get_type(zv); // equals to Z_TYPE_P(zv) if (zv_type == IS_STRING) { zend_string *raw = zv->value.str; // Z_STR_P(zv) zend_string *dup = zend_string_init(raw->val, raw->len, 0); size_t i; for (i = 0; i < dup->len/*ZSTR_LEN*/; ++i) { if (lowercase) { dup->val[i] = tolower(dup->val[i]); } else { dup->val[i] = toupper(dup->val[i]); } } ZVAL_STR(&retval, dup); } else { ZVAL_BOOL(&retval, 0); } return retval; } |
首先判断zv是否为字符串类型,如果不是就通过ZVAL_BOOL设置retval为false。
否则首先获取zv的底层zend_string对象叫做raw,通过zend_string_init产生一个相同内容的拷贝zend_string叫做dup,之后遍历dup的字符串内存,逐字节转换大小写,最后通过ZVAL_STR赋值dup给retval这个zval。
赋值给zif返回值return_value的zval要求至少有1个引用计数,在Zend引擎层使用完成后会帮我们释放掉这个计数并试图析构。对于zif函数的参数来说,一定不要直接对其进行修改,因为在开启zend opcache的情况下,传入的参数极有可能被多个php-fpm进程保存到共享内存(不是并发安全的),因此正确的做法是先对函数参数进行拷贝,然后对拷贝的内存进行修改。
结语
本章你应该掌握:
- zval的数据结构。
- 常见的底层数据类型的结构。
- 底层数据结构的引用计数与操作。
- zval与底层数据结构之间的协作关系。
- zif(Zend Internal Function)函数的参数处理和返回值处理。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

One thought on “PHP7扩展开发教程[4] – zval的工作原理”