《PHP扩展开发--引用计数的使用》

感谢twosee大佬的点拨。

这篇文章,我们来讲一下PHP扩展开发的常见问题,引用计数管理。因为现有的书籍有些老旧,跟不上PHP的发展,所以很有必要学习一下。

PHP版本是7.3.5

我们来写一个测试扩展函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
ZEND_BEGIN_ARG_INFO_EX(arginfo_study_test, 0, 0, 1)
ZEND_ARG_ARRAY_INFO(0, arr, 0)
ZEND_END_ARG_INFO()

PHP_FUNCTION(test)
{
zval *arr;

if(zend_parse_parameters(ZEND_NUM_ARGS(), "a", &arr) == FAILURE){
RETURN_FALSE;
}
RETURN_ARR(Z_ARR_P(arr));
}

这个函数做的事情很简单,接收一个数组,然后直接返回回去。

(小伙伴们自己记得注册一下创建的这个测试函数)

OK。我们编译、安装扩展:

1
2
3
4
5
6
7
8
9
~/codeDir/cppCode/study # make clean ; make ; make install
----------------------------------------------------------------------

Build complete.
Don't forget to run 'make test'.

Installing shared extensions: /usr/local/lib/php/extensions/no-debug-non-zts-20180731/
Installing header files: /usr/local/include/php/
~/codeDir/cppCode/study #

然后编写测试脚本:

1
2
3
4
5
6
7
8
9
10
11
<?php

$a = array("time" => time());
xdebug_debug_zval("a");
$b = test($a);
xdebug_debug_zval("a");
xdebug_debug_zval("b");

unset($a);

var_dump($b);

执行:

1
2
3
4
5
6
7
~/codeDir/cppCode/study # php test.php
a: (refcount=1, is_ref=0)=array ('time' => (refcount=0, is_ref=0)=1569290383)
a: (refcount=1, is_ref=0)=array ('time' => (refcount=0, is_ref=0)=1569290383)
b: (refcount=1, is_ref=0)=array ('time' => (refcount=0, is_ref=0)=1569290383)
/root/codeDir/cppCode/study/test.php:11:
&array
~/codeDir/cppCode/study #

我们发现,没有打印出$b这个数组。出现了bug,我们分析一下。

因为PHP对于复杂类型,例如字符串、数组、对象都是通过引用计数来管理的,不会拷贝出一个副本。所以,当我们在PHP脚本传递一个数组进入PHP扩展的时候,实际上只是增加了这个数组的引用计数。我们可以用gdb来调试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 34│ PHP_FUNCTION(test)
35│ {
36│ zval *arr;
37│
38│ if(zend_parse_parameters(ZEND_NUM_ARGS(), "a", &arr) == FAILURE){
39│ RETURN_FALSE;
40│ }
41├───> RETURN_ARR(Z_ARR_P(arr));
42│ }

(gdb) p *arr.value.arr
$3 = {gc = {refcount = 2, u = {type_info = 23}}, u = {v = {flags = 24 '\030', _unused = 0 '\000', nIteratorsCount = 0 '\000', _unused2 = 0 '\000'}, flags = 24}, nTableMask = 4294967280, arData = 0x7ffff76
66680, nNumUsed = 1, nNumOfElements = 1, nTableSize = 8, nInternalPointer = 0, nNextFreeElement = 0, pDestructor = 0x555555a46860 <zval_ptr_dtor>}
(gdb)

我们发现,在调用了zend_parse_parameters解析出PHP脚本传递过来的数组之后,这个数组的引用计数变成了2。这一步增加引用计数的操作是PHP底层自动帮我们做的。

然后,通过xdebug打印发现,从扩展里面返回数组到PHP脚本后,它的引用计数又变回了1。这一步也是PHP帮我们做的。

接下来,我们unset$a,使得这个数组的引用计数变成了0。又因为$b$a指向的是一个数组,所以,此时我们再使用$b,就会报错了。

以上bug会在动态生成数组时出现。如果我们初始的数组是一个不可变数组,那么,同样的代码是不会出现bug的。因为不可变数组的初始引用计数是2,而不是1。我们可以测试一下:

1
2
3
4
5
6
7
8
9
10
11
<?php

$a = array("time" => 1111);
xdebug_debug_zval("a");
$b = test($a);
xdebug_debug_zval("a");
xdebug_debug_zval("b");

unset($a);

var_dump($b);

我们把time对应的value写死为1111。然后执行脚本:

1
2
3
4
5
6
7
8
9
10
~/codeDir/cppCode/study # php test.php
a: (refcount=2, is_ref=0)=array ('time' => (refcount=0, is_ref=0)=1111)
a: (refcount=2, is_ref=0)=array ('time' => (refcount=0, is_ref=0)=1111)
b: (refcount=2, is_ref=0)=array ('time' => (refcount=0, is_ref=0)=1111)
/root/codeDir/cppCode/study/test.php:11:
array(1) {
'time' =>
int(1111)
}
~/codeDir/cppCode/study #

我们发现,因为不可变数组初始的引用计数是2,当我们对unset($a)之后,它的引用计数变成了1。此时,这个数组还是可以用的,所以后面使用$b不会报错。

正是因为从PHP脚本传入到扩展,以及从扩展传出到PHP脚本的那个数组是同一个,所以,$a$b都是指向同一个数组的,因此,我们需要在扩展层面手动为这个数组的引用计数+1。所以,扩展代码应该改为:

1
2
3
4
5
6
7
8
9
10
PHP_FUNCTION(test)
{
zval *arr;

if(zend_parse_parameters(ZEND_NUM_ARGS(), "a", &arr) == FAILURE){
RETURN_FALSE;
}
Z_TRY_ADDREF_P(arr);
RETURN_ARR(Z_ARR_P(arr));
}

重新编译、安装扩展:

1
2
3
4
5
6
7
8
9
~/codeDir/cppCode/study # make clean ; make ; make install
----------------------------------------------------------------------

Build complete.
Don't forget to run 'make test'.

Installing shared extensions: /usr/local/lib/php/extensions/no-debug-non-zts-20180731/
Installing header files: /usr/local/include/php/
~/codeDir/cppCode/study #

测试脚本为:

1
2
3
4
5
6
7
8
9
10
11
<?php

$a = array("time" => time());
xdebug_debug_zval("a");
$b = test($a);
xdebug_debug_zval("a");
xdebug_debug_zval("b");

unset($a);

var_dump($b);

指向脚本:

1
2
3
4
5
6
7
8
9
10
~/codeDir/cppCode/study # php test.php
a: (refcount=1, is_ref=0)=array ('time' => (refcount=0, is_ref=0)=1569291353)
a: (refcount=2, is_ref=0)=array ('time' => (refcount=0, is_ref=0)=1569291353)
b: (refcount=2, is_ref=0)=array ('time' => (refcount=0, is_ref=0)=1569291353)
/root/codeDir/cppCode/study/test.php:11:
array(1) {
'time' =>
int(1569291353)
}
~/codeDir/cppCode/study #

我们发现,从扩展传回到PHP的时候,这个数组的引用计数变成了2。因此,当我们unset($a)之后,这个数组的引用计数变成了1。此时,我们再次使用$b就不会出错了。