### 7.6.2 函數參數解析
上面我們定義的函數沒有接收任何參數,那么擴展定義的內部函數如何讀取參數呢?首先回顧下函數參數的實現:用戶自定義函數在編譯時會為每個參數創建一個`zend_arg_info`結構,這個結構用來記錄參數的名稱、是否引用傳參、是否為可變參數等,在存儲上函數參數與局部變量相同,都分配在zend_execute_data上,且最先分配的就是函數參數,調用函數時首先會進行參數傳遞,按參數次序依次將參數的value從調用空間傳遞到被調函數的zend_execute_data,函數內部像訪問普通局部變量一樣通過存儲位置訪問參數,這是用戶自定義函數的參數實現。
內部函數與用戶自定義函數最大的不同在于內部函數就是一個普通的C函數,除函數參數以外在zend_execute_data上沒有其他變量的分配,函數參數是從PHP用戶空間傳到函數的,它們與用戶自定義函數完全相同,包括參數的分配方式、傳參過程,也是按照參數次序依次分配在zend_execute_data上,所以在擴展中定義的函數直接按照順序從zend_execute_data上讀取對應的值即可,PHP中通過`zend_parse_parameters()`這個函數解析zend_execute_data上保存的參數:
```c
zend_parse_parameters(int num_args, const char *type_spec, ...);
```
* num_args為實際傳參數,通過`ZEND_NUM_ARGS()`獲取:zend_execute_data->This.u2.num_args,前面曾介紹過`zend_execute_data->This`這個zval的用途;
* type_spec是一個字符串,用來標識解析參數的類型,比如:"la"表示第一個參數為整形,第二個為數組,將按照這個解析到指定變量;
* 后面是一個可變參數,用來指定解析到的變量,這個值與type_spec配合使用,即type_spec用來指定解析的變量類型,可變參數用來指定要解析到的變量,這個值必須是指針。
i解析的過程也比較容易理解,調用函數時首先會把參數拷貝到調用函數的zend_execute_data上,所以解析的過程就是按照type_spec指定的各個類型,依次從zend_execute_data上獲取參數,然后將參數地址賦給目標變量,比如下面這個例子:
```c
PHP_FUNCTION(my_func_1)
{
zend_long lval;
zval *arr;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "la", &lval, &arr) == FAILURE){
RETURN_FALSE;
}
...
}
```
對應的內存關系:

注意:解析時除了整形、浮點型、布爾型是直接硬拷貝value外,其它解析到的變量只能是指針,arr為zend_execute_data上param_1的地址,即:`zval *arr = ¶m_1`,也就是說參數始終存儲在zend_execute_data上,解析獲取的是這些參數的地址。`zend_parse_parameters()`調用了`zend_parse_va_args()`進行處理,簡單看下解析過程:
```c
//va就是定義的要解析到的各個變量的地址
static int zend_parse_va_args(int num_args, const char *type_spec, va_list *va, int flags)
{
const char *spec_walk;
int min_num_args = -1; //最少參數數
int max_num_args = 0; //要解析的參數總數
int post_varargs = 0;
zval *arg;
int arg_count; //實際傳參數
//遍歷type_spec計算出min_num_args、max_num_args
for (spec_walk = type_spec; *spec_walk; spec_walk++) {
...
}
...
//檢查數目是否合法
if (num_args < min_num_args || (num_args > max_num_args && max_num_args >= 0)) {
...
}
//獲取實際傳參數:zend_execute_data.This.u2.num_args
arg_count = ZEND_CALL_NUM_ARGS(EG(current_execute_data));
...
i = 0;
//逐個解析參數
while (num_args-- > 0) {
...
//獲取第i個參數的zval地址:arg就是在zend_execute_data上分配的局部變量
arg = ZEND_CALL_ARG(EG(current_execute_data), i + 1);
//解析第i個參數
if (zend_parse_arg(i+1, arg, va, &type_spec, flags) == FAILURE) {
if (varargs && *varargs) {
*varargs = NULL;
}
return FAILURE;
}
i++;
}
}
```
接下來詳細看下不同類型的解析方式。
#### 7.6.2.1 整形:l、L
整形通過"l"、"L"標識,表示解析的參數為整形,解析到的變量類型必須是`zend_long`,不能解析其它類型,如果輸入的參數不是整形將按照類型轉換規則將其轉為整形:
```c
zend_long lval;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "l", &lval){
...
}
printf("lval:%d\n", lval);
```
如果在標識符后加"!",即:"l!"、"L!",則必須再提供一個zend_bool變量的地址,通過這個值可以判斷傳入的參數是否為NULL,如果為NULL則將要解析到的zend_long值設置為0,同時zend_bool設置為1:
```c
zend_long lval; //如果參數為NULL則此值被設為0
zend_bool is_null; //如果參數為NULL則此值為1,否則為0
if(zend_parse_parameters(ZEND_NUM_ARGS(), "l!", &lval, &is_null){
...
}
```
具體的解析過程:
```c
//zend_API.c #line:519
case 'l':
case 'L':
{
//這里獲取解析到的變量地址取的是zend_long *,所以只能解析到zend_long
zend_long *p = va_arg(*va, zend_long *);
zend_bool *is_null = NULL;
//后面加"!"時check_null為1
if (check_null) {
is_null = va_arg(*va, zend_bool *);
}
if (!zend_parse_arg_long(arg, p, is_null, check_null, c == 'L')) {
return "integer";
}
}
```
```c
static zend_always_inline int zend_parse_arg_long(zval *arg, zend_long *dest, zend_bool *is_null, int check_null, int cap)
{
if (check_null) {
*is_null = 0;
}
if (EXPECTED(Z_TYPE_P(arg) == IS_LONG)) {
//傳參為整形,無需轉化
*dest = Z_LVAL_P(arg);
} else if (check_null && Z_TYPE_P(arg) == IS_NULL) {
//傳參為NULL
*is_null = 1;
*dest = 0;
} else if (cap) {
//"L"的情況
return zend_parse_arg_long_cap_slow(arg, dest);
} else {
//"l"的情況
return zend_parse_arg_long_slow(arg, dest);
}
return 1;
}
```
> __Note:__ "l"與"L"的區別在于,當傳參不是整形且轉為整形后超過了整形的大小范圍時,"L"將值調整為整形的最大或最小值,而"l"將報錯,比如傳的參數是字符串"9223372036854775808"(0x7FFFFFFFFFFFFFFF + 1),轉整形后超過了有符號int64的最大值:0x7FFFFFFFFFFFFFFF,所以如果是"L"將解析為0x7FFFFFFFFFFFFFFF。
#### 7.6.2.2 布爾型:b
通過"b"標識符表示將傳入的參數解析為布爾型,解析到的變量必須是zend_bool:
```c
zend_bool ok;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "b", &ok, &is_null) == FAILURE){
...
}
```
"b!"的用法與整形的完全相同,也必須再提供一個zend_bool的地址用于獲取傳參是否為NULL,如果為NULL,則zend_bool為0,用于獲取是否NULL的zend_bool為1。
#### 7.6.2.3 浮點型:d
通過"d"標識符表示將參數解析為浮點型,解析的變量類型必須為double:
```c
double dval;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "d", &dval) == FAILURE){
...
}
```
具體解析過程不再展開,"d!"與整形、布爾型用法完全相同。
#### 7.6.2.4 字符串:s、S、p、P
字符串解析有兩種形式:char*、zend_string,其中"s"將參數解析到`char*`,且需要額外提供一個size_t類型的變量用于獲取字符串長度,"S"將解析到zend_string:
```c
char *str;
size_t str_len;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "s", &str, &str_len) == FAILURE){
...
}
```
```c
zend_string *str;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "S", &str) == FAILURE){
...
}
```
"s!"、"S!"與整形、布爾型用法不同,字符串時不需要額外提供zend_bool的地址,如果參數為NULL,則char*、zend_string將設置為NULL。除了"s"、"S"之外還有兩個類似的:"p"、"P",從解析規則來看主要用于解析路徑,實際與普通字符串沒什么區別,尚不清楚這倆有什么特殊用法。
#### 7.6.2.5 數組:a、A、h、H
數組的解析也有兩類,一類是解析到zval層面,另一類是解析到HashTable,其中"a"、"A"解析到的變量必須是zval,"h"、"H"解析到HashTable,這兩類是等價的:
```c
zval *arr; //必須是zval指針,不能是zval arr,因為參數保存在zend_execute_data上,arr為此空間上參數的地址
HashTable *ht;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "ah", &arr, &ht) == FAILURE){
...
}
```
具體解析過程:
```c
case 'A':
case 'a':
{
//解析到zval *
zval **p = va_arg(*va, zval **);
if (!zend_parse_arg_array(arg, p, check_null, c == 'A')) {
return "array";
}
}
break;
case 'H':
case 'h':
{
//解析到HashTable *
HashTable **p = va_arg(*va, HashTable **);
if (!zend_parse_arg_array_ht(arg, p, check_null, c == 'H')) {
return "array";
}
}
break;
```
"a!"、"A!"、"h!"、"H!"的用法與字符串一致,也不需要額外提供別的地址,如果傳參為NULL,則對應解析到的zval*、HashTable*也為NULL。
> __Note:__
>
> 1、"a"與"A"當傳參為數組時沒有任何差別,它們的區別在于:如果傳參為對象"A"將按照對象解析到zval,而"a"將報錯
>
> 2、"h"與"H"當傳參為數組時同樣沒有差別,當傳參為對象時,"H"將把對象的成員參數數組解析到目標變量,"h"將報錯
#### 7.6.2.6 對象:o、O
如果參數是一個對象則可以通過"o"、"O"將其解析到目標變量,注意:只能解析為zval*,無法解析為zend_object*。
```c
zval *obj;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "o", &obj) == FAILURE){
...
}
```
"O"是要求解析指定類或其子類的對象,類似傳參時顯式的聲明了參數類型的用法:`function my_func(MyClass $obj){...}`,如果參數不是指定類的實例化對象則無法解析。
"o!"、"O!"與字符串用法相同。
#### 7.6.2.7 資源:r
如果參數為資源則可以通過"r"獲取其zval的地址,但是無法直接解析到zend_resource的地址,與對象相同。
```c
zval *res;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "r", &res) == FAILURE){
...
}
```
"r!"與字符串用法相同。
#### 7.6.2.8 類:C
如果參數是一個類則可以通過"C"解析出zend_class_entry地址:`function my_func(stdClass){...}`,這里有個地方比較特殊,解析到的變量可以設定為一個類,這種情況下解析時將會找到的類與指定的類之間的父子關系,只有存在父子關系才能解析,如果只是想根據參數獲取類型的zend_class_entry地址,記得將解析到的地址初始化為NULL,否則將會不可預料的錯誤。
```c
zend_class_entry *ce = NULL; //初始為NULL
if(zend_parse_parameters(ZEND_NUM_ARGS(), "C", &ce) == FAILURE){
RETURN_FALSE;
}
```
#### 7.6.2.9 callable:f
callable指函數或成員方法,如果參數是函數名稱字符串、array(對象/類,成員方法),則可以通過"f"標識符解析出`zend_fcall_info`結構,這個結構是調用函數、成員方法時的唯一輸入。
```c
zend_fcall_info callable; //注意,這兩個結構不能是指針
zend_fcall_info_cache call_cache;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "f", &callable, &call_cache) == FAILURE){
RETURN_FALSE;
}
```
函數調用:
```php
my_func_1("func_name");
//或
my_func_1(array('class_name', 'static_method'));
//或
my_func_1(array($object, 'method'));
```
解析出`zend_fcall_info`后就可以通過`zend_call_function()`調用函數、成員方法了,提供"f"解析到`zend_fcall_info`的用意是簡化函數調用的操作,否則需要我們自己去查找函數、檢查是否可被調用等工作,關于這個結構稍后介紹函數調用時再作詳細說明。
#### 7.6.2.10 任意類型:z
"z"表示按參數實際類型解析,比如參數為字符串就解析為字符串,參數為數組就解析為數組,這種實際就是將zend_execute_data上的參數地址拷貝到目的變量了,沒有做任何轉化。
"z!"與字符串用法相同。
#### 7.6.2.11 其它標識符
除了上面介紹的這些解析符號以外,還有幾個有特殊用法的標識符:"|"、"+"、"*",它們并不是用來表示某種數據類型的。
* __|:__ 表示此后的參數為可選參數,可以不傳,比如解析規則為:"al|b",則可以傳2個或3個參數,如果是:"alb",則必須傳3個,否則將報錯;
* __+/*:__ 用于可變參數,注意這里與PHP函數...的用法不太一樣,PHP中可以把函數最后一個參數前加...,表示調用時可以傳多個參數,這些參數都會插入...參數的數組中,"*/+"也表示這個參數是可變的,但內核中只能接收一個值,即使傳了多個后面那些也解析不到,"*"、"+"的區別在于"*"表示可以不傳可變參數,而"+"表示可變參數至少有一個。
- 前言
- 第1章 PHP基本架構
- 1.1 PHP簡介
- 1.2 PHP7的改進
- 1.3 FPM
- 1.3.1 概述
- 1.3.2 基本實現
- 1.3.3 FPM的初始化
- 1.3.4 請求處理
- 1.3.5 進程管理
- 1.4 PHP執行的幾個階段
- 第2章 變量
- 2.1 變量的內部實現
- 2.2 數組
- 2.3 靜態變量
- 2.4 全局變量
- 2.5 常量
- 第3章 Zend虛擬機
- 3.1 PHP代碼的編譯
- 3.1.1 詞法解析、語法解析
- 3.1.2 抽象語法樹編譯流程
- 3.2 函數實現
- 3.2.1 內部函數
- 3.2.2 用戶函數的實現
- 3.3 Zend引擎執行流程
- 3.3.1 基本結構
- 3.3.2 執行流程
- 3.3.3 函數的執行流程
- 3.3.4 全局execute_data和opline
- 3.4 面向對象實現
- 3.4.1 類
- 3.4.2 對象
- 3.4.3 繼承
- 3.4.4 動態屬性
- 3.4.5 魔術方法
- 3.4.6 類的自動加載
- 3.5 運行時緩存
- 3.6 Opcache
- 3.6.1 opcode緩存
- 3.6.2 opcode優化
- 3.6.3 JIT
- 第4章 PHP基礎語法實現
- 4.1 類型轉換
- 4.2 選擇結構
- 4.3 循環結構
- 4.4 中斷及跳轉
- 4.5 include/require
- 4.6 異常處理
- 第5章 內存管理
- 5.1 Zend內存池
- 5.2 垃圾回收
- 第6章 線程安全
- 6.1 什么是線程安全
- 6.2 線程安全資源管理器
- 第7章 擴展開發
- 7.1 概述
- 7.2 擴展的實現原理
- 7.3 擴展的構成及編譯
- 7.3.1 擴展的構成
- 7.3.2 編譯工具
- 7.3.3 編寫擴展的基本步驟
- 7.3.4 config.m4
- 7.4 鉤子函數
- 7.5 運行時配置
- 7.5.1 全局變量
- 7.5.2 ini配置
- 7.6 函數
- 7.6.1 內部函數注冊
- 7.6.2 函數參數解析
- 7.6.3 引用傳參
- 7.6.4 函數返回值
- 7.6.5 函數調用
- 7.7 zval的操作
- 7.7.1 新生成各類型zval
- 7.7.2 獲取zval的值及類型
- 7.7.3 類型轉換
- 7.7.4 引用計數
- 7.7.5 字符串操作
- 7.7.6 數組操作
- 7.8 常量
- 7.9 面向對象
- 7.9.1 內部類注冊
- 7.9.2 定義成員屬性
- 7.9.3 定義成員方法
- 7.9.4 定義常量
- 7.9.5 類的實例化
- 7.10 資源類型
- 7.11 經典擴展解析
- 7.8.1 Yaf
- 7.8.2 Redis
- 第8章 命名空間
- 8.1 概述
- 8.2 命名空間的定義
- 8.2.1 定義語法
- 8.2.2 內部實現
- 8.3 命名空間的使用
- 8.3.1 基本用法
- 8.3.2 use導入
- 8.3.3 動態用法
- 附錄
- break/continue按標簽中斷語法實現
- defer推遲函數調用語法的實現
- 一起線上事故引發的對PHP超時控制的思考