前一小節介紹了函數的定義,函數的定義是一個將函數名注冊到函數列表的過程,在了解了函數的定義后,我們來看看函數的參數。這一小節將包括用戶自定義函數的參數、內部函數的參數和參數的傳遞:
### 用戶自定義函數的參數[]()
在[<<第三章第五小節 類型提示的實現>>](#)中,我們對于參數的類型提示做了分析,這里我們在這一小節的基礎上,進行一些更詳細的說明。在經過詞語分析,語法分析后,我們知道對于函數的參數檢查是通過 **zend_do_receive_arg** 函數來實現的。在此函數中對于參數的關鍵代碼如下:
CG(active_op_array)->arg_info = erealloc(CG(active_op_array)->arg_info,
[sizeof](http://www.php.net/sizeof)(zend_arg_info)*(CG(active_op_array)->num_args));
cur_arg_info = &CG(active_op_array)->arg_info[CG(active_op_array)->num_args-1];
cur_arg_info->name = estrndup(varname->u.[constant](http://www.php.net/constant).value.str.val,
varname->u.[constant](http://www.php.net/constant).value.str.len);
cur_arg_info->name_len = varname->u.[constant](http://www.php.net/constant).value.str.len;
cur_arg_info->array_type_hint = 0;
cur_arg_info->allow_null = 1;
cur_arg_info->pass_by_reference = pass_by_reference;
cur_arg_info->class_name = NULL;
cur_arg_info->class_name_len = 0;
整個參數的傳遞是通過給中間代碼的arg_info字段執行賦值操作完成。關鍵點是在arg_info字段。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;
> 參數的值傳遞和參數傳遞的區分是通過 **pass_by_reference**參數在生成中間代碼時實現的。
對于參數的個數,中間代碼中包含的arg_nums字段在每次執行 **zend_do_receive_arg×× 時都會加1.如下代碼:
CG(active_op_array)->num_args++;
并且當前參數的索引為CG(active_op_array)->num_args-1 .如下代碼:
cur_arg_info = &CG(active_op_array)->arg_info[CG(active_op_array)->num_args-1];
以上的分析是針對函數定義時的參數設置,這些參數是固定的。而在實際編寫程序時可能我們會用到可變參數。此時我們會使用到函數 **func_num_args** 和 **func_get_args**。它們是以內部函數存在。在 Zend\zend_builtin_functions.c 文件中找到這兩個函數的實現。首先我們來看func_num_args函數的實現。其代碼如下:
/* {{{ proto int func_num_args(void) Get the number of arguments that were passed to the function */
ZEND_FUNCTION(func_num_args)
{
zend_execute_data *ex = EG(current_execute_data)->prev_execute_data;
?
if (ex && ex->function_state.arguments) {
RETURN_LONG((long)(zend_uintptr_t)*(ex->function_state.arguments));
} else {
zend_error(E_WARNING,
"func_num_args(): Called from the global scope - no function context");
RETURN_LONG(-1);
}
}
/* }}} */
在存在 ex->function_state.arguments的情況下,即函數調用時,返回ex->function_state.arguments轉化后的值 ,否則顯示錯誤并返回-1。這里最關鍵的一點是EG(current_execute_data)。這個變量存放的是當前執行程序或函數的數據。此時我們需要取前一個執行程序的數據,為什么呢?因為這個函數的調用是在進入函數后執行的。函數的相關數據等都在之前執行過程中。于是調用的是:
zend_execute_data *ex = EG(current_execute_data)->prev_execute_data;
> function_state等結構請參照本章第一小節。
在了解func_num_args函數的實現后,func_get_args函數的實現過程就簡單了,它們的數據源是一樣的,只是前面返回的是長度,而這里返回了一個創建的數組。數組中存放的是從ex->function_state.arguments轉化后的數據。
### 內部函數的參數[]()
以上我們所說的都是用戶自定義函數中對于參數的相關內容。下面我們開始講解內部函數是如何傳遞參數的。以常見的count函數為例。其參數處理部分的代碼如下:
/* {{{ proto int count(mixed var [, int mode]) Count the number of elements in a variable (usually an array) */
PHP_FUNCTION(count)
{
zval *array;
long mode = COUNT_NORMAL;
?
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z|l",
&array, &mode) == FAILURE) {
return;
}
... //省略
}
這包括了兩個操作:一個是取參數的個數,一個是解析參數列表。
**取參數的個數**
取參數的個數是通過ZEND_NUM_ARGS()宏來實現的。其定義如下:
#define ZEND_NUM_ARGS() (ht)
> PHP3 中使用的是宏 ARG_COUNT
ht是在 Zend/zend.h文件中定義的宏 **INTERNAL_FUNCTION_PARAMETERS** 中的ht,如下:
#define INTERNAL_FUNCTION_PARAMETERS int ht, zval *return_value,
zval **return_value_ptr, zval *this_ptr, int return_value_used TSRMLS_DC
**解析參數列表**
PHP內部函數在解析參數時使用的是 **zend_parse_parameters**。它可以大大簡化參數的接收處理工作,雖然它在處理可變參數時還有點弱。
其聲明如下:
ZEND_API int zend_parse_parameters(int num_args TSRMLS_DC, char *type_spec, ...)
- 第一個參數num_args表明表示想要接收的參數個數,我們經常使用ZEND_NUM_ARGS() 來表示對傳入的參數“有多少要多少”。
- 第二參數應該總是宏 TSRMLS_CC 。
- 第三個參數 type_spec 是一個字符串,用來指定我們所期待接收的各個參數的類型,有點類似于 printf 中指定輸出格式的那個格式化字符串。
- 剩下的參數就是我們用來接收PHP參數值的變量的指針。
zend_parse_parameters() 在解析參數的同時會盡可能地轉換參數類型,這樣就可以確保我們總是能得到所期望的類型的變量。任何一種標量類型都可以轉換為另外一種標量類型,但是不能在標量類型與復雜類型(比如數組、對象和資源等)之間進行轉換。如果成功地解析和接收到了參數并且在轉換期間也沒出現錯誤,那么這個函數就會返回 SUCCESS,否則返回 FAILURE。如果這個函數不能接收到所預期的參數個數或者不能成功轉換參數類型時就會拋出一些錯誤信息。
第三個參數指定的各個參數類型列表如下所示:
- l - 長整形
- d - 雙精度浮點類型
- s - 字符串 (也可能是空字節)和其長度
- b - 布爾型
- r - 資源,保存在 zval*
- a - 數組,保存在 zval*
- o - (任何類的)對象,保存在 zval *
- O - (由class entry 指定的類的)對象,保存在 zval *
- z - 實際的 zval*
除了各個參數類型,第三個參數還可以包含下面一些字符,它們的含義如下:
- | - 表明剩下的參數都是可選參數。如果用戶沒有傳進來這些參數值,那么這些值就會被初始化成默認值。
- / - 表明參數解析函數將會對剩下的參數以 SEPARATE_ZVAL_IF_NOT_REF() 的方式來提供這個參數的一份拷貝,除非這些參數是一個引用。
- ! - 表明剩下的參數允許被設定為 NULL(僅用在 a、o、O、r和z身上)。如果用戶傳進來了一個 NULL 值,則存儲該參數的變量將會設置為 NULL。
### 參數的傳遞[]()
在PHP的運行過程中,如果函數有參數,當執行參數傳遞時,所傳遞參數的引用計數會發生變化。如和Xdebug的作者[Derick Rethans](http://derickrethans.nl/who.html)在其文章[php variables](http://derickrethans.nl/talks/phparch-php-variables-article.pdf)中的示例的類似代碼:
function do_something($s) {
xdebug_debug_zval('s');
$s = 100;
return $s;
}
?
$a = 1111;
$b = do_something($a);
[echo](http://www.php.net/echo) $b;
如果你安裝了xdebug,此時會輸出s變量的refcount為3,如果使用debug_zval_dump,會輸出4。因為此內部函數調用也對refcount執行了加1操作。這里的三個引用計數分別是:
- function stack中的引用
- function symbol table中引用
- 原變量$a的引用。
這個函數符號表只有用戶定義的函數才需要,內置和擴展里的函數不需要此符號表。debug_zval_dump()是內置函數,并不需要符號表,所以只增加了1。 xdebug_debug_zval()傳遞的是變量名字符串,所以沒有增加refcount。
每個PHP腳本都有自己專屬的全局符號表,而每個用戶自定義的函數也有自己的符號表,這個符號表用來存儲在這個函數作用域下的屬于它自己的變量。當調用每個用戶自定義的函數時,都會為這個函數創建一個符號表,當這個函數返回時都會釋放這個符號表。
當執行一個擁有參數的用戶自定義的函數時,其實它相當于賦值一個操作,即$s = $a;只是這個賦值操作的引用計數會執行兩次,除了給函數自定義的符號表,還有一個是給函數棧。
參數的傳遞的第一步是SEND_VAR操作,這一步操作是在函數調用這一層級,如示例的PHP代碼通過VLD生成的中間代碼:
compiled vars: !0 = $a, !1 = $b
line # * op fetch ext return operands
--------------------------------------------------------------------------------
-
2 0 > EXT_STMT
1 NOP
7 2 EXT_STMT
3 ASSIGN !0, 1111
8 4 EXT_STMT
5 EXT_FCALL_BEGIN
6 SEND_VAR !0
7 DO_FCALL 1 'demo'
8 EXT_FCALL_END
9 ASSIGN !1, $1
9 10 EXT_STMT
11 ECHO !1
12 > RETURN 1
?
branch: # 0; line: 2- 9; sop: 0; eop: 12
path #1: 0,
Function demo:
函數調用是DO_FCALL,在此中間代碼之前有一個SEND_VAR操作,此操作的作用是將實參傳遞給函數,并且將它添加到函數棧中。最終調用的具體代碼參見zend_send_by_var_helper_SPEC_CV函數,在此函數中執行了引用計數加1(Z_ADDREF_P)操作和函數棧入棧操作(zend_vm_stack_push)。
與第一步的SEND操作對應,第二步是RECV操作。RECV操作和SEND_VAR操作不同,它是歸屬于當前函數的操作,僅為此函數服務。它的作用是接收SEND過來的變量,并將它們添加到當前函數的符號表。示例函數生成的中間代碼如下:
compiled vars: !0 = $s
line # * op fetch ext return operands
--------------------------------------------------------------------------------
-
2 0 > EXT_NOP
1 RECV 1
3 2 EXT_STMT
3 ASSIGN !0, 10
4 4 EXT_STMT
5 > RETURN !0
5 6* EXT_STMT
7* > RETURN null
?
branch: # 0; line: 2- 5; sop: 0; eop: 7
參數和普通局部變量一樣 ,都需要進行操作,都需要保存在符號表(或CVs里,不過查找一般都是直接從變量變量數組里查找的)。如果函數只是需要讀這個變量,如果我們將這個變量復制一份給當前函數使用的話,在內存使用和性能方面都會有問題,而現在的方案卻避免了這個問題,如我們的示例:使用類似于賦值的操作,將原變量的引用計數加一,將有變化時才將原變量引用計數減一,并新建變量。其最終調用是ZEND_RECV_SPEC_HANDLER。
參數的壓棧操作用戶自定義的函數和內置函數都需要,而RECV操作僅用戶自定義函數需要。
- 第一章 準備工作和背景知識
- 第一節 環境搭建
- 第二節 源碼結構、閱讀代碼方法
- 第三節 常用代碼
- 第四節 小結
- 第二章 用戶代碼的執行
- 第一節 生命周期和Zend引擎
- 第二節 SAPI概述
- Apache模塊
- 嵌入式
- FastCGI
- 第三節 PHP腳本的執行
- 詞法分析和語法分析
- opcode
- opcode處理函數查找
- 第四節 小結
- 第三章 變量及數據類型
- 第一節 變量的結構和類型
- 哈希表(HashTable)
- PHP的哈希表實現
- 鏈表簡介
- 第二節 常量
- 第三節 預定義變量
- 第四節 靜態變量
- 第五節 類型提示的實現
- 第六節 變量的生命周期
- 變量的賦值和銷毀
- 變量的作用域
- global語句
- 第七節 數據類型轉換
- 第八節 小結
- 第四章 函數的實現
- 第一節 函數的內部結構
- 函數的內部結構
- 函數間的轉換
- 第二節 函數的定義,傳參及返回值
- 函數的定義
- 函數的參數
- 函數的返回值
- 第三節 函數的調用和執行
- 第四節 匿名函數及閉包
- 第五節 小結
- 第五章 類和面向對象
- 第一節 類的結構和實現
- 第二節 類的成員變量及方法
- 第三節 訪問控制的實現
- 第四節 類的繼承,多態及抽象類
- 第五節 魔術方法,延遲綁定及靜態成員
- 第六節 PHP保留類及特殊類
- 第七節 對象
- 第八節 命名空間
- 第九節 標準類
- 第十節 小結
- 第六章 內存管理
- 第一節 內存管理概述
- 第二節 PHP中的內存管理
- 第三節 內存使用:申請和銷毀
- 第四節 垃圾回收
- 新的垃圾回收
- 第五節 內存管理中的緩存
- 第六節 寫時復制(Copy On Write)
- 第七節 內存泄漏
- 第八節 小結
- 第七章 Zend虛擬機
- 第一節 Zend虛擬機概述
- 第二節 語法的實現
- 詞法解析
- 語法分析
- 實現自己的語法
- 第三節 中間代碼的執行
- 第四節 PHP代碼的加密解密
- 第五節 小結
- 第八章 線程安全
- 第二節 線程,進程和并發
- 第三節 PHP中的線程安全
- 第九章 錯誤和異常處理
- 第十章 輸出緩沖
- 第十六章 PHP語言特性的實現
- 第一節 循環語句
- foreach的實現
- 第二十章 怎么樣系列(how to)
- 附錄
- 附錄A PHP及Zend API
- 附錄B PHP的歷史
- 附錄C VLD擴展使用指南
- 附錄D 怎樣為PHP貢獻
- 附錄E phpt測試文件說明
- 附錄F PHP5.4新功能升級解析
- 附錄G:re2c中文手冊