在上一章我们讲到PHP 的 zval 的结构, 接下来我们讲解下 PHP 的zval 操作函数
1、zval 的创建
MAKE_STD_ZVAL(pzv). 这个宏将会以一种优化的方式为zval分配空间, 自动的处理超出内存错误,并初始化新zval的refcount和is_ref属性,除此之外,还有宏 ALLOC_INIT_ZVAL(). 这个宏和MAKE_STD_ZVAL唯一的区别是它会将zval *的数据类型初始化为IS_NULL。
MAKE_STD_ZVAL例子(注意:在PHP7下,已经不允许我们在堆上去分配 zval 空间,我们通常的做法是, 定义一个临时变量(栈上),然后将 p 的指针指向这个临时变量的地址,注意在使用完之后销毁zval,通常,我们可以在.h文件中做一个php5和php7版本的适配, 让你的代码能同时在php5 和 php7 上编译通过):
----php7_wrapper.h----
#if PHP_MAJOR_VERSION < 7 /* PHP Version 5*/
#define SW_MAKE_STD_ZVAL(p) MAKE_STD_ZVAL(p)
#define sw_zval_ptr_dtor zval_ptr_dtor //zval销毁
#else /* PHP Version 7 */
#define SW_MAKE_STD_ZVAL(p) zval _stack_zval_##p; p = &(_stack_zval_##p)
#define sw_zval_ptr_dtor(p) zval_ptr_dtor(*p) //zval销毁
#endif
----test.c----
PHP_METHOD(create_zval)
{
zval *result = NULL;
SW_MAKE_STD_ZVAL(result);
sw_zval_ptr_dtor(&result); //销毁zval
RETURN_TRUE;
}
ALLOC_INIT_ZVAL例子(注意:在PHP7下,我们手动为p分配堆上内存,并初始化为NULL,在使用完成之后,记得手动销毁zval):
----php_wrapper.h----
#if PHP_MAJOR_VERSION < 7 /* PHP Version 5*/
#define SW_ALLOC_INIT_ZVAL(p) ALLOC_INIT_ZVAL(p)
#define sw_zval_ptr_dtor zval_ptr_dtor //zval销毁
#else /* PHP Version 7 */
#define SW_ALLOC_INIT_ZVAL(p) do{p = (zval *)emalloc(sizeof(zval)); bzero(p, sizeof(zval));}while(0)
#define sw_zval_ptr_dtor(p) zval_ptr_dtor(*p) ////zval销毁
#endif
static inline void sw_zval_free(zval *val)
{
sw_zval_ptr_dtor(&val); //销毁zval
efree(val); //释放zval内存
}
----test.c----
PHP_METHOD(create_zval)
{
zval *result = NULL;
SW_ALLOC_INIT_ZVAL(result);
...
sw_zval_free(result); //注意使用完成之后主动销毁zval
RETURN_TRUE;
}
2、zval 的销毁
zval 的销毁有两个函数,zval_dtor 和 zval_ptr_dtor ,两个是不同的
//zval_dtor 是宏函数,最终展开后
ZEND_API void _zval_dtor_func(zval *zvalue ZEND_FILE_LINE_DC)
//zval_ptr_dtor 是宏函数,最终展开后
ZEND_API void _zval_ptr_dtor(zval **zval_ptr ZEND_FILE_LINE_DC)
两者的工作都与释放zval有关,但又有很大的区别。
比如我们有一个zval *tmp,而且我们已经对它进行了MAKE_STD_ZVAL等一系列操作了。
zval_dtor会直接把我们的tmp的value部分即tmp->value所指的内存释放掉。当然也可能不是直接释放掉,而是重新交给ZendMM,由ZendMM进行重新分配或者释放。
如果我们对它使用zval_ptr_dtor会发生什么事情呢?
zval_ptr_dtor首先会将它的refcount减一,如果减一后refcount为0了,便会再调用 zval_dtor 把tmp->value给释放掉,然后再调用efree_rel()函数把自己tmp所指的zval类型结构体所占的内存空间给释放掉。
如果减一后不为0呢?那zval_ptr_dtor便不会释放tmp->value和tmp本身,而是通知一下GC垃圾回收器,然后返回而已。
zval_dtor例子:
----test.c----
PHP_METHOD(delete_zval)
{
zval *zset = NULL;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &zset) == FAILURE)
{
return;
}
zval_dtor(zset);
RETURN_TRUE;
}
zval_ptr_dtor 例子:
----php7_wrapper.h----
#if PHP_MAJOR_VERSION < 7 /* PHP Version 5*/
#define sw_zval_ptr_dtor zval_ptr_dtor
#else /* PHP Version 7 */
#define sw_zval_ptr_dtor(p) zval_ptr_dtor(*p)
#endif
------test.c----
PHP_METHOD(delete_zval)
{
zval *zset = NULL;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &zset) == FAILURE)
{
return;
}
sw_zval_ptr_dtor(&zset); //销毁zval
RETURN_TRUE;
}
3、为基础变量赋值
ZVAL_NULL(pvz); 等价操作: Z_TYPE_P(pzv) = IS_NULL;
4、zval 类型获取
利用宏 Z_TYPE_P 可以获取 zval 的类型, 这个宏针对的是指针 zval ,当然还有,Z_TYPE 和 Z_TYPE_PP
例子:
void display_zval(zval *value) {
switch (Z_TYPE_P(value)) {
case IS_NULL:
/* NULLs are echoed as nothing */
break;
case IS_BOOL:
if (Z_BVAL_P(value)) {
php_printf("1");
}
break;
case IS_LONG:
php_printf("%ld", Z_LVAL_P(value));
break;
case IS_DOUBLE:
php_printf("%f", Z_DVAL_P(value));
break;
case IS_STRING:
PHPWRITE(Z_STRVAL_P(value), Z_STRLEN_P(value));
break;
case IS_RESOURCE:
php_printf("Resource #%ld", Z_RESVAL_P(value));
break;
case IS_ARRAY:
php_printf("Array");
break;
case IS_OBJECT:
php_printf("Object");
break;
...
}
}
6、获取 zval 的 value 值
数组类型的HashTable: Z_ARRVAL_P
zset* zval;
HashTable *vht;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &zset) == FAILURE)
{
return;
}
vht = Z_ARRVAL_P(zset);
5、引用计数 Z_REFCOUNT_P(p)
我们用宏Z_REFCOUNT_P(p)来获取 zval 的引用计数,参数 p 为 zval 指针。
例子:
PHP_METHOD(print_refcount)
{
zval *zset = NULL;
//参数传入数组
if (zend_parse_parameters(ZEND_NUM_ARGS()TSRMLS_CC, "z", &zset) == FAILURE)
{
return;
}
if (Z_TYPE_P(zset) != IS_ARRAY)
{
RETURN_FALSE;
}
//下面两个函数获取的值是一样的
php_printf("refcount=%d --\n", zset->value.arr->gc.refcount);? //php数组的引用计数值存储位置
php_printf("refcount=%d --\n", Z_REFCOUNT_P(zset));
}
6、增加引用计数
增加引用计数在 PHP5 和 PHP7 的函数是不一样的,在 PHP 5 是 zval_add_ref(zval* v),而PHP7 是一个宏 Z_TRY_ADDREF_P(zval* v) , 通常,我们可以在.h文件中做一个php5和php7版本的适配, 让你的代码能同时在php5 和 php7 上编译通过, 如下:
----php7_wrapper.h----
#if PHP_MAJOR_VERSION < 7 /* PHP Version 5*/
#define sw_zval_add_ref zval_add_ref
#else /* PHP Version 7 */
#define sw_zval_add_ref(p) Z_TRY_ADDREF_P(*p)
#endif
----test.c----
PHP_METHOD(add_refcount)
{
zval *zset = NULL;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &zset) == FAILURE)
{
return;
}
php_printf("refcount=%d --\n", Z_REFCOUNT_P(zset));
sw_zval_add_ref(&zset); //增加 zset 的引用计数
php_printf("refcount=%d --\n", Z_REFCOUNT_P(zset));
RETURN_TRUE;
}
7、传入参数分离
有的时候,我们需要在扩展空间去使用传入的参数,而不影响PHP用户空间传入的参数,我们可以自己定义参数分离,来讲用户空间与扩展空间的 zval 隔离开来,在下面函数,我定义了隔离宏 php_ch_array_separate(arr)
----php_swoole.h----
#define php_swoole_array_separate(arr) zval *_new_##arr;\
SW_MAKE_STD_ZVAL(_new_##arr);\
array_init(_new_##arr);\
php_array_merge(Z_ARRVAL_P(_new_##arr), Z_ARRVAL_P(arr));\
arr = _new_##arr;
----test.c----
PHP_METHOD(php_array_separate)
{
zval *zset = NULL;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &zset) == FAILURE)
{
return;
}
if (Z_TYPE_P(zset) != IS_ARRAY)
{
RETURN_FALSE;
}
php_swoole_array_separate(zset); //经过隔离之后, zset 的引用计数变成 1
...
RETURN_TRUE;
}