函数返回值

你也许会认为扩展中定义的函数应该直接通过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

1.int ht

2.zval *return_value,我们在函数内部修改这个指针,函数执行完成后,内核将把这个指针指向的zval返回给用户端的函数调用者。

3.zval **return_value_ptr,

4.zval *this_ptr,如果此函数是一个类的方法,那么这个指针的含义和PHP语言中$this变量差不多。

5.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_value的is_ref__gc和refcount__gc属性,这两个属性的值会由PHP内核自动管理。

现在我们把它加到我们在第五章得到的那个扩展框架里,并把这个函数名称注册到函数入口数组里,就像下面这样:

static zend_function_entry walu_functions[] = {
    ZEND_FE(walu_hello,        NULL)
    PHP_FE(sample_long, NULL)
    { NULL, NULL, NULL }
};

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

<?php var_dump(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_ZVAL与RETURN_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();
    }
}

以引用的形式返回值

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

<?php
//关于PHP语言中引用形式返回值的详述,请参考PHP手册。
$a = 'china';
 
function &return_by_ref() {
    global $a;
    return $a;
}
 
$b = &return_by_ref();
$b = "php";
echo $a;
//此时程序输出php

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

#if (PHP_MAJOR_VERSION > 5) || (PHP_MAJOR_VERSION == 5 && PHP_MINOR_VERSION > 0)
ZEND_FUNCTION(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(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(return_by_ref, 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的时候才会被启用。如果没有这些if、endif,那我们的程序将无法在php4下通过编译,在php5.0上也会激活一些无法预测的错误。


引用与函数的执行结果

一个函数的执行结果要返回给调用者,除了使用return功能,还有一种办法,那就是以引用的形式传递参数,然后在内部修改这个参数的值。前一种方法往往只能返回一个值,如果我们的函数执行结果具有多种数据,便需要把这些数据打包到一个数组、类等复合类型的变量中才能得以实现;但后一种方法相比而言就简单一些了。


运行时传递引用:Call-time Pass-by-ref

标题有点绕口,其实很简单,功能如以下php代码所示:

<?php
function byref_calltime($a) {
    $a = '(modified by ref!)';
}
 
$foo = 'I am a string';
 
//使用&传递引用
byref_calltime(&$foo);
echo $foo;
//输出'(modified by ref!)'

我们在传递参数的时候使用&操作符,便可以传递$foo变量的引用过去,而不是copy一份。当我们在函数内核修改这个参数时,函数外部的$foo也跟着被一起修改了。同样的功能我们如何在扩展里实现呢,其实很简单,请看下面的源码:

ZEND_FUNCTION(byref_calltime){
    zval *a;
    //我们我接收的参数传给zval *a;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &a) == FAILURE) {
        RETURN_NULL();
    }
     
    //如果a不是以应用的方式传递的。
    if (!a->is_ref__gc) {
        return;
    }
     
    //将a转成字符串
    convert_to_string(a);
     
    //更改数据
    ZVAL_STRING(a," (modified by ref!)",1);
    return;
}

编译时的传递引用Compile-time Pass-by-ref

如果每一次都在调用函数时候都对参数加一个&符号真是太罗嗦了,有没有一个简单的办法呢,比如在定义函数的时候便声明这个参数是引用形式的,而不用用户自己加&符号表示引用,而由内核来完成这步操作?这个功能是有的,我们在PHP语言中可以这样实现。

<?php
在定义函数参数的时候加了引用符
function byref_compiletime(&$a) {
    $a = ' (modified by ref!)';
}
$foo = 'I am a string';
 
//这个地方我们没有加&引用符
byref_compiletime($foo);
echo $foo;
//输出 (modified by ref!)

上面的代码中,我们只是把引用符号从函数调用里转移到函数定义里。此功能在扩展里面实现的话就颇费周折了,我们需要提前为它定义一个arginfo结构体来向内核通知此函数的这个特定行为。添加此函数到module_entry里需要这样:

ZEND_FE(byref_compiletime, byref_compiletime_arginfo)

byref_compiletime_arginfo是一个arginfo结构体。

在Zend Engine 2 (PHP5+)中,arginfo的数据是由多个zend_arg_info结构体构成的数组,数组的每一个成员即每一个zend_arg_info结构体处理函数的一个参数。zend_arg_info结构体的定义如下:

typedef struct _zend_arg_info {
    const char *name;               /* 参数的名称*/
    zend_uint name_len;             /* 参数名称的长度*/
    const char *class_name;         /* 类名 */
    zend_uint class_name_len;       /* 类名长度*/
    zend_bool array_type_hint;      /* 数组类型提示 */
    zend_bool allow_null;           /* 是否允许为NULL */
    zend_bool pass_by_reference;    /* 是否引用传递 */
    zend_bool return_reference;     /* 返回值是否为引用形式 */
    int required_num_args;          /* 必要参数的数量 */
} zend_arg_info;

生成zend_arg_info结构的数组比较繁琐,为了方便PHP扩展开发者,内核已经准备好了相应的宏来专门处理此问题,首先先用一个宏函数来生成头部,然后用第二个宏生成具体的数据,最后用一个宏生成尾部代码。

#define ZEND_BEGIN_ARG_INFO(name, pass_rest_by_reference)   
ZEND_BEGIN_ARG_INFO_EX(name, pass_rest_by_reference, ZEND_RETURN_VALUE, -1)
#define ZEND_BEGIN_ARG_INFO_EX(name, pass_rest_by_reference, return_reference, required_num_args)   

static const zend_arg_info name[] = {                                                                
{ NULL, 0, NULL, 0, 0, 0, pass_rest_by_reference, return_reference, required_num_args },
#define ZEND_ARG_INFO(pass_by_ref, name){ 
     #name, sizeof(#name)-1, NULL, 0, 0, 0, pass_by_ref, 0, 0 
},
#define ZEND_ARG_PASS_INFO(pass_by_ref){ 
    NULL, 0, NULL, 0, 0, 0, pass_by_ref, 0, 0
},
#define ZEND_ARG_OBJ_INFO(pass_by_ref, name, classname, allow_null) { 
    #name, sizeof(#name)-1, #classname, sizeof(#classname)-1, 0, allow_null, pass_by_ref, 0, 0 
},
#define ZEND_ARG_ARRAY_INFO(pass_by_ref, name, allow_null) { 
    #name, sizeof(#name)-1, NULL, 0, 1, allow_null, pass_by_ref, 0, 0 
},
#define ZEND_END_ARG_INFO()     
};

//这里我们先看
ZEND_BEGIN_ARG_INFO(name, pass_rest_by_reference)
ZEND_BEGIN_ARG_INFO_EX(name, pass_rest_by_reference, return_reference,required_num_args)


这两个宏函数的前两个参数的含义是一样的,name便是这个zend_arg_info数组变量的名字,这里我们定义它为:byref_compiletime_arginfo。pass_rest_by_reference如果被赋值为1,则代表着所有的参数默认都是需要以引用的方式传递的(在arginfo中单独声明的除外)。而对于ZEND_BEGIN_ARG_INFO_EX的后两个参数:


1.name和pass_rest_by_reference的含义同上。

2.return_reference:声明这个函数的返回值需要以引用的形式返回,这个参数已经在前面章节用过了。

3.required_num_args:函数被调用时,传递参数至少为前N个函数(也就是后面参数都有默认值),当设置为-1时,必须传递所有参数


接下来让我们看生成具体数据的宏:

ZEND_ARG_PASS_INFO(by_ref)
//强制所有参数使用引用的方式传递
 
ZEND_ARG_INFO(by_ref, name)
//如果by_ref为1,则名称为name的参数必须以引用的方式传递,
  
ZEND_ARG_ARRAY_INFO(by_ref, name, allow_null)
ZEND_ARG_OBJ_INFO(by_ref, name, classname, allow_null)
这两个宏实现了类型绑定,也就是说我们在传递某个参数时,必须是数组类型或者某个类的实例。如果最后的参数为真,则除了绑定的数据类型,还可以传递一个NULL数据。
 
//我们组合起来使用:
ZEND_BEGIN_ARG_INFO(byref_compiletime_arginfo, 0)
    ZEND_ARG_PASS_INFO(1)
ZEND_END_ARG_INFO()

为了使我们的扩展能够兼容PHP4,还需要使用#ifdef进行特殊处理。

#ifdef ZEND_ENGINE_2
    ZEND_BEGIN_ARG_INFO(byref_compiletime_arginfo, 0)
        ZEND_ARG_PASS_INFO(1)
    ZEND_END_ARG_INFO()
#else /* ZE 1 */
static unsigned char byref_compiletime_arginfo[] =   { 1, BYREF_FORCE };
#endif

我们copy一份ZEND_FUNCTION(byref_calltime)的实现,并重名成ZEND_FUNCTION(byref_compiletime)就行了。或者直接弄个ZEND_FALIAS就行了:

ZEND_FALIAS(byref_compiletime,byref_calltime,byref_compiletime_arginfo)