BeWithYou

胡搞的技术博客

  1. 首页
  2. PHP
  3. PHP内核中zval的拷贝

PHP内核中zval的拷贝


最近在学习PHP扩展开发,发现参考资料里有一些问题。案例代码无法按照预期的运行,经过一系列debug和查阅资料,发现问题在于zval的传递。

发现问题

参考《PHP核心技术与最佳扩展》,按照书中的代码,编写的双向链表扩展。使用如下测试代码测试时,却不能准确地遍历链表。

$list = list_create();
for($i=0;$i<10;$i++){
    list_add_head($list, "element $i");
}

$listNum = list_element_nums($list);
echo "list num:".$listNum."\n";

for($i=0;$i<$listNum;$i++){
    echo list_fetch_index($list, $i)."\n";
}

实际上,每个节点都有概率丢失它的value

将插入过程改为:

for($i=0;$i<10;$i++){
    $str = "element $i";
    list_add_head($list, $str);
}

可以遍历每个节点的值,但是值却一样。遂打印每个节点的内存地址,和value的内存地址,发现节点内存不同,但是指向的value地址相同。

php_printf("node address: %p\n", node);
php_printf("value address: %p\n", node->value);

于是大单猜测,与临时变量有关。

第一种情况,我们直接传入字符串,这时只要跳出for循环,所有的字符串都生效被gc了。我们在list_add_head时,由PHP向扩展函数里传入的valuezval *的形式,虽然没有引用传递,但是因为外部被gc了所以节点的value变成了NULL

第二种情况,我们用临时变量$str保存每次插入的值,但是实际上每个节点都被指向了这个同一个$str。所以当我们改变$str的值时,所有的节点值也会发生变化。

那么,怎么正常运行?

可以在外层定义变量数组,这样跳出循环也不会失效了。

$elements = [];
for($i=0;$i<10;$i++){
    $elements[] = "element $i";
    list_add_head($list, $elements[$i]);
}

但是同样也有问题,就是$elements里的内容改变时,链表的值也会改变。这不是我们希望看到的。

解决问题

其实说明白就是PHP中的变量指针直接赋值给了链表节点,我们需要的是一次深拷贝,之后再让节点指向新的zval变量。

首先先提一下引用传递的问题。在扩展开发中,我们可以指定函数入参是否是引用传递。

ZEND_BEGIN_ARG_INFO_EX(arginfo_list_add_head, 0, 0, 2)
                ZEND_ARG_INFO(1, lrc)
                ZEND_ARG_INFO(0, value)
ZEND_END_ARG_INFO()

const zend_function_entry linklist_functions[] = {
        PHP_FE(list_add_head, arginfo_list_add_head)
        PHP_FE_END    /* Must be the last line in linklist_functions[] */
};

可以用宏ZEND_BEGIN_ARG_INFO_EXlist_add_head方法指定了每个参数是否是引用传参。

当然这里即使我把value设为非引用传参也没有用,因为它是以zval *的形式传递的。

所以要解决问题,必须针对入参进行一次深拷贝。

PHP_FUNCTION (list_add_head) {
    zval *lrc ;
    zval *value,*node_value;
    list_head *list;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rz", &lrc, &value) == FAILURE) {
        RETURN_FALSE;
    }

    ZEND_FETCH_RESOURCE(list, list_head *, &lrc, -1, "list_resource", le_linklist);

    //下面两个语句 将value深拷贝给了node_value
    ALLOC_ZVAL(node_value);
    MAKE_COPY_ZVAL(&value, node_value);

    list_add_head(list, node_value);

    RETURN_TRUE;
}

这样我们的链表就可以正常工作了。

PHP内核中zval的拷贝

PHP的写时复制机制让内核不需要创建很多的变量。当变量的值改变时,才进行内存的复制。

PHP提供了很多zval之间拷贝的方法和宏。

  • ZVAL_COPY_VALUE

    zval *zv_src;
    MAKE_STD_ZVAL(zv_src);
    ZVAL_STRING(zv_src, "test", 1);
    
    zval *zv_dest;
    ALLOC_ZVAL(zv_dest);
    ZVAL_COPY_VALUE(zv_dest, zv_src);

    只拷贝valuetype。如果两个变量其中的任意一个的值发生了变化,另一个也会受影响。这显然是浅拷贝。

  • zval_copy_ctor()

    zval *zv_dest;
    ALLOC_ZVAL(zv_dest);
    ZVAL_COPY_VALUE(zv_dest, zv_src);
    zval_copy_ctor(zv_dest);

    这个宏会深拷贝变量的值。这样两个zval就不会互相影响了。

  • MAKE_COPY_ZVAL

    zval *zv_dest;
    ALLOC_ZVAL(zv_dest);
    MAKE_COPY_ZVAL(&zv_src, zv_dest);

    这个宏在深拷贝的同时,会初始化新变量的refcountis_ref(现在都加了__gc的后缀)。而且需要注意的是,它的入参顺序,以及第一个参数是二级指针。

  • ZVAL_ZVAL

    ZVAL_ZVAL(zv_dest, zv_src, 0, 0);
    /* equivalent to: */
    ZVAL_COPY_VALUE(zv_dest, zv_src)
    
    ZVAL_ZVAL(zv_dest, zv_src, 1, 0);
    /* equivalent to: */
    ZVAL_COPY_VALUE(zv_dest, zv_src);
    zval_copy_ctor(&zv_src);
    
    ZVAL_ZVAL(zv_dest, zv_src, 1, 1);
    /* equivalent to: */
    ZVAL_COPY_VALUE(zv_dest, zv_src);
    zval_copy_ctor(zv_dest);
    //下面用来检查是否需要free掉zv_src
    zval_ptr_dtor(&zv_src);
    
    ZVAL_ZVAL(zv_dest, zv_src, 0, 1);
    /* equivalent to: */
    ZVAL_COPY_VALUE(zv_dest, zv_src);
    ZVAL_NULL(zv_src);
    zval_ptr_dtor(&zv_src);
    //这种情况直接将src移动到dest上了 并且不会调用拷贝构造函数 如果src的refcount=1则会被free掉 否则被置为空

参考资料:

PHP Intenals Book

回到顶部