PHP 引用

通常,当你向函数传递参数时,是通过值来传递的,这意味着被调用的函数不能修改它。一般来说,在 PHP 扩展中可以修改通过值传递的参数,但很可能会导致内存错误和崩溃。如果需要修改函数的参数(如 sort() 函数),则必须通过引用传递。

通过引用传递是 php 引用的主要用途,但在其他数据结构(如数组的元素)中也随处可见。

在内部,引用由 zval 表示,IS_REFERENCE 为类型,zend_reference 结构指针为值。与所有引用计数类型一样,它继承自 zend_refcounted 结构,该结构定义了第一个 64 位字的格式。结构的其余部分是另一个嵌入的 zval。

image 2023 11 14 13 38 12 630

检查或检索引用 zvals 的字段的 C 宏很少(后缀为 _P 的宏也会指向 zvals):

  • Z_ISREF(zv) – 检查该值是否为 PHP 引用(类型为 IS_REFERENCE)。

  • Z_REF(zv) – 返回依赖的 zend_reference 结构(类型必须是 IS_REFERENCE)。

  • Z_REFVAL(zv) – 返回指向引用值(zval)的指针。

此外,还有一些用于构建引用和去引用的宏:

  • ZVAL_REF(zv, ref) - 通过 IS_REFERENCE 类型和 zend_reference 指针初始化 zval。

  • ZVAL_NEW_EMPTY_REF(zv) - 通过 IS_REFERENCE 类型和一个新的 zend_reference 结构来初始化 zval。Z_REFVAL_P(zv) 需要在调用后进行初始化。

  • ZVAL_NEW_REF(zv, value) - 使用 IS_REFERENCE 类型初始化 zval,并使用给定的值初始化一个新的 zend_reference 结构。

  • ZVAL_MAKE_REF_EX(zv, refcount) - 使用给定的引用计数器将 "zv" 转换为 PHP 引用。

  • ZVAL_DEREF(zv) - 如果 "zv" 是一个引用,则取消引用(指向引用值的指针被赋值给 "zv")。

在我们的扩展实例中使用 php 引用

让我们尝试向 test_scale() 函数传递一个引用。

$ php -r '$a = 5; var_dump(test_scale([&$a], 2));'
Warning: test_scale(): unexpected argument type in Command line code on line 1
NULL

不支持引用。

要解决这个问题,我们只需添加取消引用即可。

static int do_scale(zval *return_value, zval *x, zend_long factor)
{
    ZVAL_DEREF(x);
    if (Z_TYPE_P(x) == IS_LONG) {
        RETVAL_LONG(Z_LVAL_P(x) * factor);

现在一切都很好:

$ php -r '$a = 5; var_dump(test_scale([&$a], 2));'
array(1) {
    [0]=>
    int(10)
}

我们还要将 test_scale() 转换为函数 test_scale_ref(),它不会返回任何值,但会通过引用接收参数,并将传递的值就地相乘。

static int do_scale_ref(zval *x, zend_long factor)
{
    ZVAL_DEREF(x);
    if (Z_TYPE_P(x) == IS_LONG) {
        Z_LVAL_P(x) *= factor;
    } else if (Z_TYPE_P(x) == IS_DOUBLE) {
        Z_DVAL_P(x) *= factor;
    } else if (Z_TYPE_P(x) == IS_STRING) {
        size_t len = Z_STRLEN_P(x);
        char *p;

        ZVAL_STR(x, zend_string_safe_realloc(Z_STR_P(x), len, factor, 0, 0));
        p = Z_STRVAL_P(x) + len;
        while (--factor > 0) {
            memcpy(p, Z_STRVAL_P(x), len);
            p += len;
        }
        *p = '\000';
    } else if (Z_TYPE_P(x) == IS_ARRAY) {
        zval *val;
        ZEND_HASH_FOREACH_VAL(Z_ARR_P(x), val) {
            if (do_scale_ref(val, factor) != SUCCESS) {
                return FAILURE;
            }
        } ZEND_HASH_FOREACH_END();
    } else {
        php_error_docref(NULL, E_WARNING, "unexpected argument type");
        return FAILURE;
    }
    return SUCCESS;
}

PHP_FUNCTION(test_scale_ref)
{
    zval * x;
    zend_long factor = TEST_G(scale); // default value

    ZEND_PARSE_PARAMETERS_START(1, 2)
        Z_PARAM_ZVAL(x)
        Z_PARAM_OPTIONAL
        Z_PARAM_LONG(factor)
    ZEND_PARSE_PARAMETERS_END();

    do_scale_ref(x, factor);
}

ZEND_BEGIN_ARG_INFO(arginfo_test_scale_ref, 1)
    ZEND_ARG_INFO(1, x) // pass by reference
    ZEND_ARG_INFO(0, factor)
ZEND_END_ARG_INFO()

static const zend_function_entry test_functions[] = {
    PHP_FE(test_test1, arginfo_test_test1)
    PHP_FE(test_test2, arginfo_test_test2)
    PHP_FE(test_scale_ref, arginfo_test_scale_ref)
    PHP_FE_END
};

测试:

$ php -r '$x=5; test_scale_ref($x, 2); var_dump($x);'
int(10)
$ php -r '$x=5.0; test_scale_ref($x, 2);
var_dump($x);'
float(10)
$ php -r '$x="5"; test_scale_ref($x, 2); var_dump($x);'
string(2) "55"
$ php -r '$x=[[5]]; test_scale_ref($x, 2); var_dump($x);'
array(1) {
[0]=> array(1) {
        [0]=>
        int(10)
    }
}

一切看起来都是正确的,但在某些情况下,我们的函数会表现不正确。有关如何通过使用 "写入时复制" 避免出现问题的详细信息,请参阅下一节。