天天看点

[翻译][php扩展开发和嵌入式]第6章-返回值

全部翻译内容pdf文档下载地址: http://download.csdn.net/detail/lgg201/5107012

本书目前在github上由laruence(http://www.laruence.com)和walu(http://www.walu.cc)两位大牛组织翻译. 该翻译项目地址为: https://github.com/walu/phpbook

本书在github上的地址: https://github.com/goosman-lei/php-eae

未来本书将可能部分合并到phpbook项目中, 同时保留一份独立版本.

原书名: <Extending and Embedding PHP>

原作者: Sara Golemon

译者: goosman.lei(雷果国)

译者Email: [email protected]

译者Blog: http://blog.csdn.net/lgg201

返回值

用户空间函数利用return关键字向它的调用空间回传信息, 这一点和C语言的语法相同.

例如:

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

当sample_long()被调用时, 返回42并设置到$bar变量中. 在C语言中的等价代码如下:

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

当然, 在C语言中你总是知道被调用的函数是什么, 并且基于函数原型返回, 因此相应的你要定义返回结果存储的变量. 在php用户空间处理时, 变量类型是动态的, 转而依赖于第2章"变量的里里外外"中介绍的zval的类型.

return_value变量

你可能认为你的内部函数应该直接返回一个zval, 或者说分配一个zval的内存空间并如下返回zval *.

PHP_FUNCTION(sample_long_wrong)
{
    zval *retval;

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

    return retval;
}
           

不幸的是, 这样做是不正确的. 并不是强制每个函数实现分配zval并返回它. 而是Zend引擎在函数调用之前预先分配这个空间. 接着将zval的类型初始化为IS_NULL, 并将值作为参数名return_value传递. 下面是正确的做法:

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

要注意的是PHP_FUNCTION()实现并不会直接返回任何值. 取而代之的是直接将恰当的数据弹出到return_value参数中, Zend引擎会在内部函数执行完成后处理它.

友情提示: 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和refcount属性不应该被内部函数直接修改. 这些值由Zend引擎在调用你的函数时初始化并处理.

现在我们来看看这个特殊的函数, 将它增加到第5章"你的第一个扩展"中创建的sample扩展中. 只需要在sample_hello_world()函数下面添加这个函数, 并将sample_long()加入到php_sample_functions结构体中:

static function_entry php_sample_functions[] = {
    PHP_FE(sample_hello_world, NULL)
    PHP_FE(sample_long, NULL)
    { NULL, NULL, NULL }
};
           

现在我们就可以执行make重新构建扩展了.

如果一切OK, 可以运行php并测试新函数:

$ php -r 'var_dump(sample_long());
           

包装更紧凑的宏

在代码可读性和可维护性方面, ZVAL_*()宏中有重复的部分: return_value变量. 这种情况下, 将宏的ZVAL替换为RETVAL, 我们就可以在调用时省略return_value了.

前面的例子中, sample_long()的实现代码可以缩减到下面这样:

PHP_FUNCTION(sample_long)
{
    RETVAL_LONG(42);
    return;
}
           

下表列出了Zend引擎中RETVAL一族的宏. 除了两个特殊的, RETVAL宏除了删除了return_value参数之外, 和对应的ZVAL族宏相同.

普通的ZVAL宏 return_value专用宏
ZVAL_NULL(return_value) RETVAL_NULL()
ZVAL_BOOL(return_value, bval) RETVAL_BOOL(bval)
ZVAL_TRUE(return_value) RETVAL_TRUE
ZVAL_FALSE(return_value) RETVAL_FALSE
ZVAL_LONG(return_value, lval) RETVAL_LONG(lval)
ZVAL_DOUBLE(return_value, dval) RETVAL_DOUBLE(dval)
ZVAL_STRING(return_value, str, dup) RETVAL_STRING(str, dup)
ZVAL_STRINGL(return_value, str, len, dup) RETVAL_STRINGL(str, len, dup)
ZVAL_RESOURCE(return_value, rval) RETVAL_RESOURCE(rval)

要注意到, TRUE和FALSE宏没有括号. 这是考虑到了Zend/PHP代码标准的偏差, 保留了主要的一种以保持向后兼容. 如果你构建扩展失败, 收到了错误消息undefined macro RETVAL_TRUE(), 请确认你是否在代码中写这两个宏时误写了括号.

通常, 在你的函数处理返回值的时候, 它已经准备好退出并将控制返回给调用作用域了. 由于这个原因, 为内部函数设计了另外一些宏用于返回: RETURN_*()族宏.

PHP_FUNCTION(sample_long)
{
    RETURN_LONG(42);
}
           

尽管看不到, 但这个函数在RETURN_LONG()宏调用完后的确会返回. 我们可以在函数末尾增加php_printf()进行测试:

PHP_FUNCTION(sample_long)
{
    RETURN_LONG(42);
    php_printf("I will never be reached.\n");
}
           

php_printf(), 如内容所描述的, 因为RETURN_LONG()调用隐式的结束了函数.

和RETVAL系列一样, 前面表中列出的每个简单类型都有对应的RETURN宏. 同样和RETVAL系列一样, RETURN_TRUE和RETURN_FALSE宏不使用括号.

更加复杂的类型, 比如对象和数组, 同样是通过return_value参数返回的; 然而, 它们天生就不能通过简单的宏创建. 即便是资源类型, 虽然它有RETVAL宏, 但是真正要返回资源类型还需要额外的工作. 你将在第8章到第11章看到怎样返回这些类型.

值得这么麻烦吗?

一个尚未使用的Zend内部函数特性是return_value_used参数. 考虑下面的用户空间代码:

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

因为sample_array_range()调用的时候并没有将结果存储到变量中, 这里创建使用的1000个元素的数组空间将被完全浪费. 当然, 这样调用sample_array_range()是愚蠢的, 但是没有很好的办法预知未来你又能怎么样呢?

虽然无法访问用户空间函数, 内部函数可以依赖于所有内部函数公共的return_value_used参数设置, 条件式的跳开这样的无意义行为.

PHP_FUNCTION(sample_array_range)
{
    if (return_value_used) {
        int i;
        /* 返回从0到999的数组 */
        array_init(return_value);
        for(i = 0; i < 1000; i++) {
            add_next_index_long(return_value, i);
        }
        return;
    } else {
        /* 提示错误 */
        php_error_docref(NULL TSRMLS_CC, E_NOTICE,
               "Static return-only function called without processing output");
        RETURN_NULL();
    }
}
           

要看到这个函数的操作, 只需要在你的sample.c源文件中增加这个函数, 并将它暴露在php_sample_functions结构中:

PHP_FE(sample_array_range, NULL)
           

译注: 关于用户空间不使用的函数返回值怎么处理, 可以阅读Zend/zend_vm_execute.h中的zend_do_fcall_common_helper_SPEC函数, 它在处理完内部函数调用后, 会检查该函数的返回值是否被使用, 如果不使用, 则进行了相应的释放.

返回引用值

从用户空间的php工作中你可能已经知道了, php函数还可以以引用方式返回值. 由于实现问题, 在php 5.1之前应该避免在内部函数中返回引用, 因为它不能工作. 考虑下面的用户空间代码片段:

function &sample_reference_a() {
    /* 如果全局空间没有变量$a, 就以初始值NULL创建它 */
    if (!isset($GLOBALS['a'])) {
        $GLOBALS['a'] = NULL;
    }
    return $GLOBALS['a'];
}
$a = 'Foo';
$b = sample_reference_a();
$b = 'Bar';
           

在这个代码片段中, 就像使用$b = &$GLOBALS['a];或者由于在全局空间, 使用$b = &$a; 将$b创建为$a的一个引用.

回顾第3章"内存管理", 在到达最后一行时, $a和$b实际上包含相同的值'Bar'. 现在我们看看内部实现这个函数:

#if (PHP_MAJOR_VERSION > 5) || (PHP_MAJOR_VERSION == 5 && \
                          PHP_MINOR_VERSION > 0)
PHP_FUNCTION(sample_reference_a)
{
    zval **a_ptr, *a;


    /* 从全局符号表查找变量$a */
    if (zend_hash_find(&EG(symbol_table), "a", sizeof("a"),
                                          (void**)&a_ptr) == SUCCESS) {
        a = *a_ptr;
    } else {
        /* 如果不存在$GLOBALS['a']则创建它 */
        ALLOC_INIT_ZVAL(a);
        zend_hash_add(&EG(symbol_table), "a", sizeof("a"), &a,
                                              sizeof(zval*), NULL);
    }
    /* 废弃旧的返回值 */
    zval_ptr_dtor(return_value_ptr);
    if (!a->is_ref && a->refcount > 1) {
        /* $a需要写时复制, 在使用之前, 必选先隔离 */
        zval *newa;
        MAKE_STD_ZVAL(newa);
        *newa = *a;
        zval_copy_ctor(newa);
        newa->is_ref = 0;
        newa->refcount = 1;
        zend_hash_update(&EG(symbol_table), "a", sizeof("a"), &newa,
                                                 sizeof(zval*), NULL);
        a = newa;
    }
    /* 将新的返回值设置为引用方式并增加refcount */
    a->is_ref = 1;
    a->refcount++;
    *return_value_ptr = a;
}
#endif /* PHP >= 5.1.0 */
           

return_value_ptr参数是所有内部函数都会传递的另外一个公共参数, 它是zval **类型, 包含了指向return_value的指针. 通过在它上面调用zval_ptr_dtor(), 默认的return_value的zval *将被释放. 接着可以自由的选择一个新的zval *去替代它, 在这里选择了变量$a, 选择性的进行zval隔离后, 将它的is_ref设置为1, 设置到return_value_ptr中.

如果现在编译运行这段代码, 无论如何你会得到一个段错误. 为了使它可以工作, 你需要在php_sample.h中增加下面的代码:

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

译注: 译者使用的php-5.4.9中, ZEND_BEGIN_ARG_INFO_EX的宏定义中已经包含了static修饰符, 因此本书示例中相应的需要进行修改, 请读者在阅读过程中注意这一点.

接着, 在php_sample_functions中声明你的函数时使用这个结构:

#if (PHP_MAJOR_VERSION > 5) || (PHP_MAJOR_VERSION == 5 && \
                          PHP_MINOR_VERSION > 0)
    PHP_FE(sample_reference_a, php_sample_retref_arginfo)
#endif /* PHP >= 5.1.0 */
           

这个结构你将在本章后面详细学习, 用来向Zend引擎提供函数调用的重要暗示信息. 这里它告诉Zend引擎return_value需要被覆写, 应该从return_value_ptr中得到正确的地址. 如果没有这个暗示, Zend引擎会简单的在return_value_ptr中设置NULL, 这可能使得在执行到zval_ptr_dtor()时程序崩溃.

这段代码每一个片段都包裹在#if块中, 它指示编译器只有在PHP版本大于等于5.1时才启用这个支持. 如果没有这些条件指令, 这个扩展将不能在php4上编译(因为在return_value_ptr中包含的一些元素不存在), 在php5.0中不能提供正确的功能(有一个bug导致返回的引用被以值的方式拷贝)

引用方式返回值

使用return(语法)结构将值和变量回传给调用方是没有问题的, 但是, 有时你需要从一个函数返回多个值. 你可以使用数组(我们将在第8章"使用数组和哈希表工作")达到这个目的, 或者你可以使用参数栈返回值.

调用时引用传值

一种简单的引用传递变量方式是要求调用时在参数变量名前使用取地址符(&), 如下用户空间代码:

function sample_byref_calltime($a) {
    $a .= ' (modified by ref!)';
}
$foo = 'I am a string';
sample_byref_calltime(&$foo);
echo $foo;
           

参数变量名前的取地址符(&)使得发送给函数的是$foo实际的zval, 而不是它的内容拷贝.这就使得函数可以通过传递的这个参数修改这个值来返回信息. 如果调用sample_byref_calltime()时没有在$foo前面使用取地址符(&), 则函数内的修改并不会影响原来的变量.

在C层面重演这个行为并不需要特殊的编码. 在你的sample.c源码文件中sample_long()后面创建下面的函数:

PHP_FUNCTION(sample_byref_calltime)
{
    zval *a;
    int addtl_len = sizeof(" (modified by ref!)") - 1;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &a) == FAILURE) {
        RETURN_NULL();
    }
    if (!a->is_ref) {
        /* 不是以引用方式传值则不做任何事离开函数 */
        return;
    }
    /* 确保变量是字符串 */
    convert_to_string(a);
    /* 扩大a的缓冲区以使可以保存要追加的数据 */
    Z_STRVAL_P(a) = erealloc(Z_STRVAL_P(a),
        Z_STRLEN_P(a) + addtl_len + 1);
    memcpy(Z_STRVAL_P(a) + Z_STRLEN_P(a),
    " (modified by ref!)", addtl_len + 1);
    Z_STRLEN_P(a) += addtl_len;
}
           

和往常一样, 这个函数需要增加到php_sample_functions结构中.

PHP_FE(sample_byref_calltime,        NULL)
           

译注: 运行时引用传值在译者使用的版本(php-5.4.9 ZendEngine 2.4.0)中已经被完全废弃. 早前的版本可以在php.ini中使用allow_call_time_pass_reference指令启用. 测试时请注意版本问题.

编译期引用传值

更常用的方式是编译期引用传值. 这里函数的参数定义为只能以引用方式使用, 传递常量或其他中间值(比如函数调用的结果)将导致错误, 因为那样函数就没有地方可以存储结果值去回传了. 用户空间的编译期引用传值代码如下:

function sample_byref_compiletime(&$a) {
    $a .= ' (modified by ref!)';
}
$foo = 'I am a string';
sample_byref_compiletime($foo);
echo $foo;
           

如你所见, 这和调用时引用传值的差别仅在于取地址符的位置不同. 在C的层面上去看这个函数时, 函数代码上是完全相同的. 唯一的区别在于php_sample_functions块中对函数的声明:

PHP_FE(sample_byref_compiletime, php_sample_byref_arginfo)
           

php_sample_byref_arginfo是一个常量结构, 你需要在使用之前先定义它.

实际上, 编译期引用传值中, 对is_ref检查的代码可以删除, 因为总能保证它是引用方式的. 不过这里留着它也不会有什么危害.

在Zend引擎1(php4)中, 这个结构是一个简单的char *列表, 第一个元素指定了长度, 接下来是描述每个函数参数的标记集合.

static unsigned char php_sample_byref_arginfo[] =
                                { 1, BYREF_FORCE };
           

这里, 1表示向量只包含了一个参数的信息. 后面的元素就顺次描述参数特有的标记信息, 第二个元素描述第一个参数. 如果涉及到第二个或第三个参数, 对应的就需要在这个向量中增加第三个和第四个元素. 参数信息的可选值如下表:

标记类型 含义
BYREF_NONE 这个参数永远都不允许引用传值.尝试使用调用时引用传值将被忽略,参数仍然会被拷贝.
BYREF_FORCE 参数永远都是引用传值,无论怎样调用.这等价于在用户空间函数定义时使用取地址符(&)
BYREF_ALLOW 参数是否引用传值取决于调用时的语义.这等价于普通的用户空间函数定义.
BYREF_FORCE_REST 当前参数以及后面所有参数都将应用BYREF_FORCE.这个标记只能作为列表的最后一个标记.在BYREF_FORCE_REST后面放置其他标记可能导致未定义行为.

在Zend引擎2(php 5+)中, 你将使用一种更加可扩展的结构, 它包含类更多的信息, 比如最小和最大参数要求, 类型暗示, 是否强制引用等.

首先, 参数信息结构使用两个宏中的一个定义. 较简单的一个是ZEND_BEGIN_ARG_INFO(), 它需要两个参数:

ZEND_BEGIN_ARG_INFO(name, pass_rest_by_reference)
           

name非常简单, 就是扩展中其他地方使用这个结构时的名字, 当前这个例子我们使用的名字是: php_sample_byref_arginfo

pass_rest_by_reference的含义和BYREF_FORCE_REST用在Zend引擎1的参数信息向量最后一个元素时一致. 如果这个参数设置为1, 所有没有在结构中显式描述的参数都被认为是编译期引用传值的参数.

还有一种可选的开始宏, 它引入了两个Zend引擎1没有的新选项, 它是ZEND_BEGIN_ARG_INFO_EX():

ZEND_BEGIN_ARG_INFO_EX(name, pass_rest_by_reference, return_reference,
                             required_num_args)
           

当然, name和pass_rest_by_reference和前面所说的是相同的含义. 本章前面也提到了, return_reference是告诉Zend你的函数需要用自己的zval覆写return_value_ptr.

最后一个参数, required_num_args, 它是另外一种类型的暗示, 用来告诉Zend在某种被认为是不完整的调用时完全的跳过函数调用.

在你拥有了一个恰当的开始宏后, 接下来就可以是0个或多个ZEND_ARG_*INFO元素. 这些宏的类型和用法如下表. 

用途
ZEND_ARG_PASS_INFO(by_ref) by_ref和所有后面的宏一样是一个二选一的选项,它用来标识是否对应的参数应该被强制为引用传值.设置这个选项为1等同于在Zend引擎1中使用BYREF_FORCE.
ZEND_ARG_INFO(by_ref, name) 这个宏提供了一个附加的name属性,用于内部生成的错误消息和反射API.它应该被设置成一些非加密的帮助信息.
ZEND_ARG_ARRAY_INFO(by_ref, name, allow_null) 这两个宏提供了内部函数的参数类型暗示,用来描述参数只能是数组或某个特定类型的实例.将allow_null设置为非0值将允许调用时在放置array/object的地方传递NULL值.
ZEND_ARG_OBJ_INFO(by_ref, name, classname, allow_null)

最后, 所有使用Zend引擎2的宏设置的参数信息结构必须使用ZEND_END_ARG_INFO()结束. 对于你的sample函数, 你需要选择一个如下的结构:

ZEND_BEGIN_ARG_INFO(php_sample_byref_arginfo, 0)
    ZEND_ARG_PASS_INFO(1)
ZEND_END_ARG_INFO()
           

为了让扩展兼容Zend引擎1和2, 需要使用#ifdef语句为两者均定义arg_info结构:

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

注意, 这些代码片段是集中在一起的, 现在是时候创建一个真正的编译期引用传值实现了. 首先, 我们将为Zend引擎1和2定义的php_sample_byref_arginfo块放到头文件php_sample.h中.

接下来, 可以有两种选择, 一种是将PHP_FUNCTION(sample_byref_calltime)拷贝一份, 并重命名为PHP_FUNCTION(sample_byref_compiletime), 接着在php_sample_functions中增加一行PHP_FE(sample_byref_compiletime, php_sample_byref_arginfo)

这种方式简单移动, 并且在一段时候后修改时, 更加不容易产生混淆. 因为这里只是示例代码, 因此, 我们可以稍微放松点, 使用你在上一章学的PHP_FALIAS()避免代码重复.

这样, 就不是赋值PHP_FUNCTION(sample_byref_calltime), 而是在php_sample_functions中直接增加一行:

PHP_FALIAS(sample_byref_compiletime, sample_byref_calltime,
    php_sample_byref_arginfo)
           

回顾第5章, 这将创建一个名为sample_byref_compiletime()的用户空间函数, 它对应的内部实现是sample_byref_calltime()的代码. php_sample_byref_arginfo是这个版本的特殊之处.

小结

本章你看到了怎样从一个内部函数返回值, 包括直接返回值和引用方式返回, 以及通过参数栈引用返回. 此外还简单了解了Zend引擎2的参数类型暗示结构zend_arg_info.

下一章你将会继续探究接受基本的zval参数以及使用zend_parse_parameters()强大的类型戏法.

目录 上一章: 您的第一个扩展

下一章: 接受参数