ホームページ > バックエンド開発 > PHPチュートリアル > [翻訳] [php拡張機能と埋め込み] 第6章 - 戻り値

[翻訳] [php拡張機能と埋め込み] 第6章 - 戻り値

WBOY
リリース: 2016-06-13 12:53:17
オリジナル
837 人が閲覧しました

[翻訳] [php拡張機能と埋め込み] 第6章 - 戻り値

この本は現在、laruence (http://www.laruence.com) と walu (http://www.walu.cc) によって github 上で翻訳されています。翻訳プロジェクトのアドレスは https://github.com です。 /walu/phpbook

Github 上のこの本のアドレス: https://github.com/goosman-lei/php-eae

将来的には、この本は独立したバージョンを保持したまま、部分的に phpbook プロジェクトに統合される可能性があります。


元のタイトル:

原作者:サラ・ゴーレモン

翻訳者: goosman.lei (Lei Guo)

翻訳者のメールアドレス: lgg860911@yahoo.com.cn

翻訳者ブログ: 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 ユーザー空間で処理するときに返された結果が格納される変数を定義する必要があります。変数の型は、dynamic であり、代わりに第 2 章「変数のインとアウト」で紹介した zval の型に依存します。

戻り値変数

内部関数は 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(戻り値)

RETVAL_TRUE

ZVAL_FALSE(戻り値)

RETVAL_FALSE

ZVAL_LONG(return_value, lval)

RETVAL_LONG(lval)

ZVAL_DOUBLE(戻り値, dval)

RETVAL_DOUBLE(dval)

ZVAL_STRING(return_value, str, dup)

RETVAL_STRING(str, dup)

ZVAL_STRINGL(戻り値、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 に設定することは、このオプションを Zendengine に設定することと同じです。 1 BYREF_FORCE.

を使用します。

ZEND_ARG_INFO(by_ref, name)

このマクロは、追加の name 属性 を提供します。 内部生成されたエラー メッセージとリフレクション用の API。 暗号化されていないヘルプ メッセージに設定する必要があります

ZEND_ARG_ARRAY_INFO(by_ref、name、allow_null)

これら 2 つのマクロは、内部関数のパラメータ型のヒントを提供します は、パラメータが特定の型 の配列またはインスタンスのみであることを説明するために使用されます。 allow_null0 以外の値に設定すると、 を配置するときに呼び出しが許可されます。配列 /object が渡される NULL value.

ZEND_ARG_OBJ_INFO(by_ref、名前、クラス名、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()强大的类型戏法.


目录

上一章: 您的第一个扩展
下一章: 待续

関連ラベル:
ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート