# 6.1 函數返回值
你也許會認為擴展中定義的函數應該直接通過return關鍵字來返回一個值,比如由你自己來生成一個zval并返回,就像下面這樣:
````c
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宏的時候了。
````c
#define INTERNAL_FUNCTION_PARAMETERS int ht, zval *return_value, zval **return_value_ptr, zval *this_ptr, int return_value_used TSRMLS_DC
````
<ul>
<li>int ht</li>
<li>zval *return_value,我們在函數內部修改這個指針,函數執行完成后,內核將把這個指針指向的zval返回給用戶端的函數調用者。</li>
<li>zval **return_value_ptr,</li>
<li>zval *this_ptr,如果此函數是一個類的方法,那么這個指針的含義和PHP語言中$this變量差不多。</li>
<li>int return_value_used,代表用戶端在調用此函數時有沒有使用到它的返回值。</li>
</ul>
下面讓我們先試驗一個非常簡單的例子,我先給出PHP語言中的實現,然后給出我們在擴展中用C語言完成相同功能的代碼。
````php
<?php
function sample_long()
{
return 42;
}
/*
這個函數非常簡單.
$a = sample_long();
那此時$a的值便是42了,這個我們大家肯定都明白。
*/
?>
````
下面是我們在編寫擴展時的實現。
````c
ZEND_FUNCTION(sample_long)
{
ZVAL_LONG(return_value, 42);
return;
}
````
需要注意的是,ZEND_FUNCTION本身并沒有通過return關鍵字返回任何有價值的東西,它只不過是在運行時修改了return_value指針所指向的變量的值而已,而內核則會把return_value指向的變量作為用戶端調用此函數后的得到的返回值。回想一下,ZVAL_LONG()宏是對一類操作的封裝,展開后應該就是下面這樣:
````c
Z_TYPE_P(return_value) = IS_LONG;
Z_LVAL_P(return_value) = 42;
//更徹底的講,應該是這樣的:
return_value->type = IS_LONG;
return_value->value.lval = 42;
````
<div class="tip-warning">我們千萬不要自己去修改return_value的is_ref__gc和refcount__gc屬性,這兩個屬性的值會由PHP內核自動管理。</div>
現在我們把它加到我們在第五章得到的那個擴展框架里,并把這個函數名稱注冊到函數入口數組里,就像下面這樣:
````c
static zend_function_entry walu_functions[] = {
ZEND_FE(walu_hello, NULL)
PHP_FE(sample_long, NULL)
{ NULL, NULL, NULL }
};
````
現在我們編譯我們的擴展,便可以在用戶端通過調用sample_long函數來得到一個整型的返回值了:
````php
<?php var_dump(sample_long());?>
````
### 與return_value有關的宏
return_value如此重要,內核肯定早已經為它準備了大量的宏,來簡化我們的操作,提高程序的質量。
在前幾章我們接觸的宏大多都是以ZVAL_開頭的,而接下來我們要介紹的宏的名字是:RETVAL。
再回到上面的那個例子,我們用RETVAL來重寫一下:
````c
PHP_FUNCTION(sample_long)
{
RETVAL_LONG(42);
//展開后相當與ZVAL_LONG(return_value, 42);
return;
}
````
大多數情況下,我們在處理完return_value后所做的便是用return語句結束我們的函數執行,幫人幫到底,送佛送到西,為了減少我們的工作量,內核中還提供了RETURN_*系列宏來為我們自動補上return;如:
````c
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_***宏,供大家查閱使用。
````c
//這些宏都定義在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
<?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這個參數:
````c
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語言中引用形式返回值的詳述,請參考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,讓我們以內核的角度重新觀察這一切:
````c
#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的話,便要進行分離。
不幸的是,如果你編譯上面的代碼,使用的時候便會得到一個段錯誤。為了使它能夠正常的工作,需要在源文件中加一些東西:
````c
#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()函數時便會使程序崩潰。
<div class="tip-warning">這一些代碼都包含在了一個宏里面,只有在php版本大于等于5.1的時候才會被啟用。如果沒有這些if、endif,那我們的程序將無法在php4下通過編譯,在php5.0上也會激活一些無法預測的錯誤。</div>
<hr/>
<ul>
<li>zone(zqx10104#163.com)于2011-10-20提供了一個Bug,:-)</li>
</ul>
## links
* 6 [函數返回值](<6.md>)
* 6.2 [引用與函數的執行結果](<6.2.md>)
- about
- 開始閱讀
- 目錄
- 1 PHP的生命周期
- 1.讓我們從SAPI開始
- 2.PHP的啟動與終止
- 3.PHP的生命周期
- 4.線程安全
- 5.小結
- 2 PHP變量在內核中的實現
- 1. 變量的類型
- 2. 變量的值
- 3. 創建PHP變量
- 4. 變量的存儲方式
- 5. 變量的檢索
- 6. 類型轉換
- 7. 小結
- 3 內存管理
- 1. 內存管理
- 2. 引用計數
- 3. 總結
- 4 動手編譯PHP
- 1. 編譯前的準備
- 2. PHP編譯前的config配置
- 3. Unix/Linux平臺下的編譯
- 4. 在Win32平臺上編譯PHP
- 5. 小結
- 5 Your First Extension
- 1. 一個擴展的基本結構
- 2. 編譯我們的擴展
- 3. 靜態編譯
- 4. 編寫函數
- 5. 小結
- 6 函數返回值
- 1. 一個特殊的參數:return_value
- 2. 引用與函數的執行結果
- 3. 小結
- 7 函數的參數
- 1. zend_parse_parameters
- 2. Arg Info 與類型綁定
- 3. 小結
- 8 使用HashTable與{數組}
- 1. 數組(C中的)與鏈表
- 2. 操作HashTable的API
- 3. 在內核中操作PHP語言中數組
- 4. 小結
- 9 PHP中的資源類型
- 1. 復合類型的數據——{資源}
- 2. Persistent Resources
- 3. {資源}自有的引用計數
- 4. 小結
- 10 PHP中的面向對象(一)
- 1. zend_class_entry
- 2. 定義一個類
- 3. 定義一個接口
- 4. 類的繼承與接口的實現
- 5. 小結
- 11 PHP中的面向對象(二)
- 1. 生成對象的實例與調用方法
- 2. 讀寫對象的屬性
- 3. 小結
- 12 啟動與終止的那點事
- 2. 小結
- 1. 關于生命周期
- 2. MINFO與phpinfo
- 3. 常量
- 4. PHP擴展中的全局變量
- 5. PHP語言中的超級全局變量
- 6. 小結
- 13 INI設置
- 1. 聲明和訪問ini設置
- 2. 小結
- 2. 小結
- 14 流式訪問
- 1. 概覽
- 2. 打開流
- 3. 訪問流
- 4. 靜態資源操作
- 5. 小結
- 15 流的實現
- 1. php流的表象之下
- 2. 包裝器操作
- 3. 實現一個包裝器
- 4. 操縱
- 5. 檢查
- 6. 小結
- 16 有趣的流
- 1. 上下文
- 2. 過濾器
- 3. 小結
- 17 配置和鏈接
- 1. autoconf
- 2. 庫的查找
- 3. 強制模塊依賴
- 4. Windows方言
- 5. 小結
- 18 擴展生成
- 1. ext_skel
- 2. PECL_Gen
- 3. 小結
- 19 設置宿主環境
- 1. 嵌入式SAPI
- 2. 構建并編譯一個宿主應用
- 3. 通過嵌入包裝重新創建cli
- 4. 老技術新用
- 5. 小結
- 20 高級嵌入式
- 1. 回調到php中
- 2. 錯誤處理
- 3. 初始化php
- 4. 覆寫INI_SYSTEM和INI_PERDIR選項
- 5. 捕獲輸出
- 6. 同時擴展和嵌入
- 7. 小結
- 約定