[ PHP 内核与扩展开发系列] 函数返回值:一个特殊的参数 —— return_value

引入 return_value

PHP 语言中函数返回值是通过 return 来完成的,就像下面的程序:

<?php
function sample_long() {
    return 42;
}
$bar = sample_long();

C 语言也一样使用 return 关键字:

int sample_long(void) {
    return 42;
}

int main(void) {
    int bar = sample_long();
    return 1;
}

那我们在扩展中编写的 PHP 函数如何把返回值回馈给用户端的函数调用者呢?

你也许会认为扩展中定义的函数应该直接通过 return 关键字来返回一个值,比如由你自己来生成一个 zval 并返回,就像下面这样:

ZEND_FUNCTION(sample_long_wrong)
{
    zval *retval;

    MAKE_STD_ZVAL(retval);
    ZVAL_LONG(retval, 42);

    return retval;
}

但是,上面的写法是无效的!与其让扩展开发人员每次都初始化一个 zval 并 return,Zend 引擎早就准备好了一个更好的方法。它在每个 zif 函数声明里加了一个 zval* 类型的形参,名为 return_value,专门来解决函数返回值的问题。在前面我们已经知道了 ZEND_FUNCTION 宏展开后是void name(INTERNAL_FUNCTION_PARAMETERS) 的形式,现在是我们展开代表参数声明的INTERNAL_FUNCTION_PARAMETERS 宏的时候了。

#define INTERNAL_FUNCTION_PARAMETERS int ht, zval *return_value, zval **return_value_ptr, zval *this_ptr, int return_value_used TSRMLS_DC
  • int ht,用户实际传递参数的数量。
  • zval *return_value,我们在函数内部修改这个指针,函数执行完成后,内核将把这个指针指向的zval 返回给用户端的函数调用者。
  • zval **return_value_ptr,当返回引用时,PHP 将其设为变量的指针。
  • zval *this_ptr,如果此函数是一个类的方法,那么这个指针的含义和 PHP 语言中 $this 变量差不多。
  • int return_value_used,代表用户端在调用此函数时有没有使用到它的返回值。

下面让我们先试验一个非常简单的例子,我先给出PHP语言中的实现,然后给出我们在扩展中用 C 语言完成相同功能的代码。

<?php
function sample_long()
{
    return 42;
}
/*
    这个函数非常简单.
    $a = sample_long();
    那此时$a的值便是42了,这个我们大家肯定都明白。
*/
?>

下面是我们在编写扩展时的实现:

ZEND_FUNCTION(sample_long)
{
    ZVAL_LONG(return_value, 42);
    return;
}

需要注意的是,ZEND_FUNCTION 本身并没有通过 return 关键字返回任何有价值的东西,它只不过是在运行时修改了 return_value 指针所指向的变量的值而已,而内核则会把 return_value 指向的变量作为用户端调用此函数后的得到的返回值。回想一下,ZVAL_LONG() 宏是对一类操作的封装,展开后应该就是下面这样:

Z_TYPE_P(return_value) = IS_LONG;
Z_LVAL_P(return_value) = 42;

//更彻底的讲,应该是这样的:
return_value->type = IS_LONG;
return_value->value.lval = 42;

我们千万不要自己去修改 return_valueis_ref__gcrefcount__gc 属性,这两个属性的值会由 PHP 内核自动管理。

现在我们把它加到我们在前面章节编写的那个扩展里,并把这个函数名称注册到函数入口数组里,就像下面这样:

static zend_function_entry academy_functions[] = {
    ZEND_FE(academy_hello, NULL)
    ZEND_FE(sample_long, NULL)
    { NULL, NULL, NULL }
};

现在我们编译我们的扩展,便可以在用户端通过调用 sample_long 函数来得到一个整型的返回值了:

与 return_value 有关的宏

return_value 如此重要,内核肯定早已经为它准备了大量的宏,来简化我们的操作,提高程序的质量。 在前几章我们接触的宏大多都是以 ZVAL_ 开头的,而接下来我们要介绍的宏的名字是:RETVAL。再回到上面的那个例子,我们用 RETVAL 来重写一下:

PHP_FUNCTION(sample_long)
{
    RETVAL_LONG(42);
    //展开后相当与ZVAL_LONG(return_value, 42);
    return;
}

大多数情况下,我们在处理完 return_value 后所做的便是用 return 语句结束我们的函数执行,帮人帮到底,送佛送到西,为了减少我们的工作量,内核中还提供了 RETURN_* 系列宏来为我们自动补上return;,如:

PHP_FUNCTION(sample_long)
{
    RETURN_LONG(42);
    //#define RETURN_LONG(l) { RETVAL_LONG(l); return; }
    php_printf("I will never be reached.\n"); //这一行代码永远不会被执行。
}

下面,我们给出目前所有的 RETVAL_*** 宏和 RETURN_*** 宏,供大家查阅使用,这些宏都定义在Zend/zend_API.h 文件里:

#define RETVAL_RESOURCE(l)              ZVAL_RESOURCE(return_value, l)
#define RETVAL_BOOL(b)                  ZVAL_BOOL(return_value, b)
#define RETVAL_NULL()                   ZVAL_NULL(return_value)
#define RETVAL_LONG(l)                  ZVAL_LONG(return_value, l)
#define RETVAL_DOUBLE(d)                ZVAL_DOUBLE(return_value, d)
#define RETVAL_STRING(s, duplicate)         ZVAL_STRING(return_value, s, duplicate)
#define RETVAL_STRINGL(s, l, duplicate)     ZVAL_STRINGL(return_value, s, l, duplicate)
#define RETVAL_EMPTY_STRING()           ZVAL_EMPTY_STRING(return_value)
#define RETVAL_ZVAL(zv, copy, dtor)     ZVAL_ZVAL(return_value, zv, copy, dtor)
#define RETVAL_FALSE                    ZVAL_BOOL(return_value, 0)
#define RETVAL_TRUE                     ZVAL_BOOL(return_value, 1)

#define RETURN_RESOURCE(l)              { RETVAL_RESOURCE(l); return; }
#define RETURN_BOOL(b)                  { RETVAL_BOOL(b); return; }
#define RETURN_NULL()                   { RETVAL_NULL(); return;}
#define RETURN_LONG(l)                  { RETVAL_LONG(l); return; }
#define RETURN_DOUBLE(d)                { RETVAL_DOUBLE(d); return; }
#define RETURN_STRING(s, duplicate)     { RETVAL_STRING(s, duplicate); return; }
#define RETURN_STRINGL(s, l, duplicate) { RETVAL_STRINGL(s, l, duplicate); return; }
#define RETURN_EMPTY_STRING()           { RETVAL_EMPTY_STRING(); return; }
#define RETURN_ZVAL(zv, copy, dtor)     { RETVAL_ZVAL(zv, copy, dtor); return; }
#define RETURN_FALSE                    { RETVAL_FALSE; return; }
#define RETURN_TRUE                     { RETVAL_TRUE; return; }

其实,除了这些标量类型,还有很多 PHP 语言中的复合类型我们需要在函数中返回,如数组和对象,我们可以通过 RETVAL_ZVALRETURN_ZVAL 来操作它们,有关它们的详细介绍我们将在后续章节中叙述。

不返回值可以么?

其实,Zend Internal Function 的形参中还有一个比较常用的名为 return_value_used 的参数,它是干嘛使的呢?它用来标志这个函数的返回值在用户端有没有用到。看下面的代码:

<?php 
function sample_array_range() {
    $ret = array();
    for($i = 0; $i < 1000; $i++) {
        $ret[] = $i;
    }
    return $ret;
}
sample_array_range();

sample_array_range() 仅仅是执行了一下而已,并没有使用到函数的返回值。函数的返回值 $ret 初始化并返回给调用者后根本就没有发挥作用,却白白浪费了很多内存来存储它的 1000 个元素。虽然这个例子有点极端,但是却提醒了我们,如果返回值没有被用到,有没有办法在函数中提前知晓并进行一些有利于性能的操作呢?这个想法在 PHP 脚本语言里简直就是异想天开,肯定是无法实现的,但是如果我们所处的环境是内核,即 zif,便可以轻松实现这个愿望了,而我们所需要做的便是充分利用 return_value_used 这个参数:

ZEND_FUNCTION(sample_array_range)
{
    if (return_value_used) {
        int i;

        //把返回值初始化成一个PHP语言中的数组
        array_init(return_value);
        for(i = 0; i < 1000; i++)
        {
            //向retrun_value里不断的添加新元素,值为i
            add_next_index_long(return_value, i);
        }
        return;
    }
    else
    {
        //抛出一个E_NOTICE级错误
        php_error_docref(NULL TSRMLS_CC, E_NOTICE, "猫了个咪的,我就知道你没用我的劳动成果!");
        RETURN_NULL();
    }
}

以引用的形式返回值

你肯定已经在手册中看到过有关将函数的返回值以引用的形式返回的功能了。但是因为某些历史原因,在为扩展编写函数时如果想让返回值以引用的形式返回一定要慎之又慎,因为在 PHP 5.1 之前,根本就没法真正的实现这个功能,看一下下面的代码:

<?php
//关于PHP语言中引用形式返回值的详述,请参考PHP手册。
$a = 'php extension';

function &return_by_ref()
{
    global $a;
    return $a;
}

$b = &return_by_ref();
$b = "Laravel Academy";
echo $a;
//此时程序输出Laravel Academy

在上面的代码中,$b 其实是 $a 的一个引用,当最后一行代码执行后,$a$b 都开始寻找'bar' 这个字符串对应的 zval,让我们从内核的角度重新观察这一切:

#if (PHP_MAJOR_VERSION > 5) || (PHP_MAJOR_VERSION == 5 && PHP_MINOR_VERSION > 0)
ZEND_FUNCTION(academy_return_by_ref)
{
    zval **a_ptr;
    zval *a;

    //检查全局作用域中是否有$a这个变量,如果没有则添加一个
    if (zend_hash_find(&EG(symbol_table), "a", sizeof("a"), (void **)&a_ptr) == SUCCESS)
    {
            a = *a_ptr;
    }
    else
    {
            ALLOC_INIT_ZVAL(a);
        zend_hash_add(&EG(symbol_table), "a", sizeof("a"), &a, sizeof(zval*), NULL);
    }

    //废弃return_value,使用return_value_ptr来接替它的工作
    zval_ptr_dtor(return_value_ptr);
    if ( !a->is_ref__gc && a->refcount__gc > 1 )
    {
            zval *tmp;
            MAKE_STD_ZVAL(tmp);
            *tmp = *a;
            zval_copy_ctor(tmp);
            tmp->is_ref__gc = 0;
            tmp->refcount__gc = 1;
            zend_hash_update(&EG(symbol_table), "a", sizeof("a"), &tmp, sizeof(zval *), NULL);
            a = tmp;
    }
       a->is_ref__gc = 1;
       a->refcount__gc++;
       *return_value_ptr = a;
}
#endif /* PHP >= 5.1.0 */

return_value_ptr 是定义 Zend Internal Function 时的另外一个重要参数,它是一个 zval**类型的指针,并且指向函数的返回值。我们调用 zval_ptr_dtor() 函数后,默认的 return_value 便被废弃了。这里的 $a 变量如果是与某个非引用形式的变量共用一个 zval 的话,便要进行分离。不幸的是,如果你编译上面的代码,使用的时候便会得到一个段错误。为了使它能够正常的工作,需要在源文件中加一些东西:

#if (PHP_MAJOR_VERSION > 5) || (PHP_MAJOR_VERSION == 5 && PHP_MINOR_VERSION > 0)
    ZEND_BEGIN_ARG_INFO_EX(academy_return_by_ref_arginfo, 0, 1, 0)
    ZEND_END_ARG_INFO ()
#endif /* PHP >= 5.1.0 */

然后使用下面的代码来申明我们的定义的函数:

#if (PHP_MAJOR_VERSION > 5) || (PHP_MAJOR_VERSION == 5 && PHP_MINOR_VERSION > 0)
    ZEND_FE(academy_return_by_ref, academy_return_by_ref_arginfo)
#endif /* PHP >= 5.1.0 */

arginfo 是一种特殊的结构体,用来提前向内核告知此函数具有的一些特定的性质,如本例,其将告诉内核本函数需要引用形式的返回值,所以内核不再通过 return_value 来获取执行结果,而是通过return_value_ptr。如果没有 arginfo,那内核会预先把 return_value_ptr 置为 NULL,当我们对其调用 zval_ptr_dtor() 函数时便会使程序崩溃。

这些代码都包含在了一个宏里面,只有在 PHP 版本大于等于 5.1 的时候才会被启用。如果没有这些 ifendif,那我们的程序将无法在 PHP 4 下通过编译,在 PHP 5.0 上也会激活一些无法预测的错误。

编写一段验证脚本如下:

<?php
    global $a;
    $a = 'php extension';
    $b = &academy_return_by_ref();
    $b = 'Laravel Academy';
    echo $a."\n";

执行该脚本,输出如下:

Laravel Academy

学院君 has written 716 articles

资深PHP工程师,Laravel学院院长

发表评论

标记为*的字段是必填项(邮箱地址不会被公开)

你可以使用这些HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>