天天看點

六十二、PHP核心探索:形參return value ☞ 每個zif函數聲明裡加了一個zval*類型的形參

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~

你也許會認為擴充中定義的函數應該直接通過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,
  • 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_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 */