## 7.6 函數
### 7.6.1 內部函數注冊
通過擴展可以將C語言實現的函數提供給PHP腳本使用,如同大量PHP內置函數一樣,這些函數統稱為內部函數(internal function),與PHP腳本中定義的用戶函數不同,它們無需經歷用戶函數的編譯過程,同時執行時也不像用戶函數那樣每一個指令都調用一次C語言編寫的handler函數,因此,內部函數的執行效率更高。除了性能上的優勢,內部函數還可以擁有更高的控制權限,可發揮的作用也更大,能夠完成很多用戶函數無法實現的功能。
前面介紹PHP函數的編譯時曾經詳細介紹過PHP函數的實現,函數通過`zend_function`來表示,這是一個聯合體,用戶函數使用`zend_function.op_array`,內部函數使用`zend_function.internal_function`,兩者具有相同的頭部用來記錄函數的基本信息。不管是用戶函數還是內部函數,其最終都被注冊到EG(function_table)中,函數被調用時根據函數名稱向這個符號表中查找。從內部函數的注冊、使用過程可以看出,其定義實際非常簡單,我們只需要定義一個`zend_internal_function`結構,然后注冊到EG(function_table)中即可,接下來再重新看下內部函數的結構:
```c
typedef struct _zend_internal_function {
/* Common elements */
zend_uchar type;
zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
uint32_t fn_flags;
zend_string* function_name;
zend_class_entry *scope;
zend_function *prototype;
uint32_t num_args;
uint32_t required_num_args;
zend_internal_arg_info *arg_info;
/* END of common elements */
void (*handler)(INTERNAL_FUNCTION_PARAMETERS); //函數指針,展開:void (*handler)(zend_execute_data *execute_data, zval *return_value)
struct _zend_module_entry *module;
void *reserved[ZEND_MAX_RESERVED_RESOURCES];
} zend_internal_function;
```
Common elements就是與用戶函數相同的頭部,用來記錄函數的基本信息:函數類型、參數信息、函數名等,handler是此內部函數的具體實現,PHP提供了一個宏用于此handler的定義:`PHP_FUNCTION(function_name)`或`ZEND_FUNCTION()`,展開后:
```c
void *zif_function_name(zend_execute_data *execute_data, zval *return_value)
{
...
}
```
PHP為函數名加了"zif_"前綴,gdb調試時記得加上這個前綴;另外內部函數定義了兩個參數:execute_data、return_value,execute_data不用再說了,return_value是函數的返回值,這兩個值在擴展中會經常用到。
比如要在擴展中定義兩個函數:my_func_1()、my_func_2(),首先是編寫函數:
```c
PHP_FUNCTION(my_func_1)
{
printf("Hello, I'm my_func_1\n");
}
PHP_FUNCTION(my_func_2)
{
printf("Hello, I'm my_func_2\n");
}
```
函數定義完了就需要向PHP注冊了,這里并不需要擴展自己注冊,PHP提供了一個內部函數注冊結構:zend_function_entry,擴展只需要為每個內部函數生成這樣一個結構,然后把它們保存到擴展`zend_module_entry.functions`即可,在加載擴展中會自動向EG(function_table)注冊。
```c
typedef struct _zend_function_entry {
const char *fname; //函數名稱
void (*handler)(INTERNAL_FUNCTION_PARAMETERS); //handler實現
const struct _zend_internal_arg_info *arg_info;//參數信息
uint32_t num_args; //參數數目
uint32_t flags;
} zend_function_entry;
```
zend_function_entry結構可以通過`PHP_FE()`或`ZEND_FE()`定義:
```c
const zend_function_entry mytest_functions[] = {
PHP_FE(my_func_1, NULL)
PHP_FE(my_func_2, NULL)
PHP_FE_END //末尾必須加這個
};
```
這幾個宏的定義為:
```c
#define ZEND_FE(name, arg_info) ZEND_FENTRY(name, ZEND_FN(name), arg_info, 0)
#define ZEND_FENTRY(zend_name, name, arg_info, flags) { #zend_name, name, arg_info, (uint32_t) (sizeof(arg_info)/sizeof(struct _zend_internal_arg_info)-1), flags },
#define ZEND_FN(name) zif_##name
```
最后將`zend_module_entry->functions`設置為`mytest_functions`即可:
```c
zend_module_entry mytest_module_entry = {
STANDARD_MODULE_HEADER,
"mytest",
mytest_functions, //functions
NULL, //PHP_MINIT(mytest),
NULL, //PHP_MSHUTDOWN(mytest),
NULL, //PHP_RINIT(mytest),
NULL, //PHP_RSHUTDOWN(mytest),
NULL, //PHP_MINFO(mytest),
"1.0.0",
STANDARD_MODULE_PROPERTIES
};
```
下面來測試下這兩個函數能否使用,編譯安裝后在PHP腳本中調用這兩個函數:
```php
//test.php
my_func_1();
my_func_2();
```
cli模式下執行`php test.php`將輸出:
```
Hello, I'm my_func_1
Hello, I'm my_func_2
```
大功告成,函數已經能夠正常工作了,后續的工作就是不斷完善handler實現擴展自己的功能了。
### 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個,否則將報錯
* __+、* :__ 用于可變參數,`+、*`的區別在于 * 表示可以不傳可變參數,而 + 表示可變參數至少有一個。可變參數將被解析到zval數組,可以通過一個整形參數,用于獲取具體的數量,例如:
```c
PHP_FUNCTION(my_func_1)
{
zval *args;
int argc;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "+", &args, &argc) == FAILURE) {
return;
}
//...
}
```
argc獲取的就是可變參數的數量,args為參數數組,指向第一個參數,可以通過args[i]獲取其它參數,比如這樣傳參:
```php
my_func_1(array(), 1, false, "ddd");
```
那么傳入的4個參數就可以在解析后通過args[0]、args[1]、args[2]、args[3]獲取。
### 7.6.3 引用傳參
上一節介紹了如何在內部函數中解析參數,這里還有一種情況沒有講到,那就是引用傳參:
```php
$a = array();
function my_func(&$a){
$a[] = 1;
}
```
上面這個例子在函數中對$a的修改將反映到原變量上,那么這種用法如何在內部函數中實現呢?上一節介紹參數解析的過程中并沒有提到用戶函數中參數的zend_arg_info結構,內部函數中也有類似的一個結構用于函數注冊時指定參數的一些信息:zend_internal_arg_info。
```c
typedef struct _zend_internal_arg_info {
const char *name; //參數名
const char *class_name;
zend_uchar type_hint; //顯式聲明的類型
zend_uchar pass_by_reference; //是否引用傳參
zend_bool allow_null; //是否允許參數為NULL,類似"!"的用法
zend_bool is_variadic; //是否為可變參數
} zend_internal_arg_info;
```
這個結構幾乎與zend_arg_info完全一樣,不同的地方只在于name、class_name的類型,zend_arg_info這兩個成員的類型都是zend_string。如果函數需要使用引用類型的參數或返回引用就需要創建函數的參數數組,這個數組通過:`ZEND_BEGIN_ARG_INFO()或ZEND_BEGIN_ARG_INFO_EX()`、`ZEND_END_ARG_INFO()`宏定義:
```c
#define ZEND_BEGIN_ARG_INFO_EX(name, _unused, return_reference, required_num_args)
#define ZEND_BEGIN_ARG_INFO(name, _unused)
```
* __name:__ 參數數組名,注冊函數`PHP_FE(function, arg_info)`會用到
* ___unused:__ 保留值,暫時無用
* __return_reference:__ 返回值是否為引用,一般很少會用到
* __required_num_args:__ required參數數
這兩個宏需要與`ZEND_END_ARG_INFO()`配合使用:
```c
ZEND_BEGIN_ARG_INFO_EX(arginfo_my_func_1, 0, 0, 2)
...
ZEND_END_ARG_INFO()
```
接著就是在上面兩個宏中間定義每一個參數的zend_internal_arg_info,PHP提供的宏有:
```c
//pass_by_ref表示是否引用傳參,name為參數名稱
#define ZEND_ARG_INFO(pass_by_ref, name) { #name, NULL, 0, pass_by_ref, 0, 0 },
//只聲明此參數為引用傳參
#define ZEND_ARG_PASS_INFO(pass_by_ref) { NULL, NULL, 0, pass_by_ref, 0, 0 },
//顯式聲明此參數的類型為指定類的對象,等價于PHP中這樣聲明:MyClass $obj
#define ZEND_ARG_OBJ_INFO(pass_by_ref, name, classname, allow_null) { #name, #classname, IS_OBJECT, pass_by_ref, allow_null, 0 },
//顯式聲明此參數類型為數組,等價于:array $arr
#define ZEND_ARG_ARRAY_INFO(pass_by_ref, name, allow_null) { #name, NULL, IS_ARRAY, pass_by_ref, allow_null, 0 },
//顯式聲明為callable,將檢查函數、成員方法是否可調
#define ZEND_ARG_CALLABLE_INFO(pass_by_ref, name, allow_null) { #name, NULL, IS_CALLABLE, pass_by_ref, allow_null, 0 },
//通用宏,自定義各個字段
#define ZEND_ARG_TYPE_INFO(pass_by_ref, name, type_hint, allow_null) { #name, NULL, type_hint, pass_by_ref, allow_null, 0 },
//聲明為可變參數
#define ZEND_ARG_VARIADIC_INFO(pass_by_ref, name) { #name, NULL, 0, pass_by_ref, 0, 1 },
```
舉個例子來看:
```php
function my_func_1(&$a, Exception $c){
...
}
```
用內核實現則可以這么定義:
```c
ZEND_BEGIN_ARG_INFO_EX(arginfo_my_func_1, 0, 0, 1)
ZEND_ARG_INFO(1, a) //引用
ZEND_ARG_OBJ_INFO(0, b, Exception, 0) //注意:這里不要把字符串加""
ZEND_END_ARG_INFO()
```
展開后:
```c
static const zend_internal_arg_info name[] = {
//多出來的這個是給返回值用的
{ (const char*)(zend_uintptr_t)(2), NULL, 0, 0, 0, 0 },
{ "a", NULL, 0, 0, 0, 0 },
{ "b", "Exception", 8, 1, 0, 0 },
}
```
第一個數組元素用于記錄必傳參數的數量以及返回值是否為引用。定義完這個數組接下來就需要把這個數組告訴函數:
```c
const zend_function_entry mytest_functions[] = {
PHP_FE(my_func_1, arginfo_my_func_1)
PHP_FE(my_func_2, NULL)
PHP_FE_END //末尾必須加這個
};
```
引用參數通過`zend_parse_parameters()`解析時只能使用"z"解析,不能再直接解析為zend_value了,否則引用將失效:
```c
PHP_FUNCTION(my_func_1)
{
zval *lval; //必須為zval,定義為zend_long也能解析出,但不是引用
zval *obj;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "zo", &lval, &obj) == FAILURE){
RETURN_FALSE;
}
//lval的類型為IS_REFERENCE
zval *real_val = Z_REFVAL_P(lval); //獲取實際引用的zval地址:&(lval.value->ref.val)
Z_LVAL_P(real_val) = 100; //設置實際引用的類型
}
```
```php
$a = 90;
$b = new Exception;
my_func_1($a, $b);
echo $a;
==========[output]===========
100
```
> __Note:__ 參數數組與zend_parse_parameters()有很多功能重合,兩者都會生效,對zend_internal_arg_info驗證在zend_parse_parameters()之前,為避免混亂兩者應該保持一致;另外,雖然內部函數的參數數組并不強制定義聲明,但還是建議聲明。
### 7.6.4 函數返回值
調用內部函數時其返回值指針作為參數傳入,這個參數為`zval *return_value`,如果函數有返回值直接設置此指針即可,需要特別注意的是設置返回值時需要增加其引用計數,舉個例子來看:
```c
PHP_FUNCTION(my_func_1)
{
zval *arr;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "a", &arr) == FAILURE){
RETURN_FALSE;
}
//增加引用計數
Z_ADDREF_P(arr);
//設置返回值為數組:
ZVAL_ARR(return_value, Z_ARR_P(arr));
}
```
此函數接收一個數組,然后直接返回該數組,相當于:
```php
function my_func_1($arr){
return $arr;
}
```
調用該函數:
```php
$a = array(); //$a -> zend_array(refcount:1)
$b = my_func_1($a); //傳參后:參數arr -> zend_array(refcount:2)
//然后函數內部賦給了返回值:$b,$a,arr -> zend_array(refcount:3)
//函數return階段釋放了參數:$b,$a -> zend_array(refcount:2)
var_dump($b);
=============[output]===========
array(0) {
}
```
雖然可以直接設置return_value,但實際使用時并不建議這么做,因為PHP提供了很多專門用于設置返回值的宏,這些宏定義在`zend_API.h`中:
```c
//返回布爾型,b:IS_FALSE、IS_TRUE
#define RETURN_BOOL(b) { RETVAL_BOOL(b); return; }
//返回NULL
#define RETURN_NULL() { RETVAL_NULL(); return;}
//返回整形,l類型:zend_long
#define RETURN_LONG(l) { RETVAL_LONG(l); return; }
//返回浮點值,d類型:double
#define RETURN_DOUBLE(d) { RETVAL_DOUBLE(d); return; }
//返回字符串,可返回內部字符串,s類型為:zend_string *
#define RETURN_STR(s) { RETVAL_STR(s); return; }
//返回內部字符串,這種變量將不會被回收,s類型為:zend_string *
#define RETURN_INTERNED_STR(s) { RETVAL_INTERNED_STR(s); return; }
//返回普通字符串,非內部字符串,s類型為:zend_string *
#define RETURN_NEW_STR(s) { RETVAL_NEW_STR(s); return; }
//拷貝字符串用于返回,這個會自己加引用計數,s類型為:zend_string *
#define RETURN_STR_COPY(s) { RETVAL_STR_COPY(s); return; }
//返回char *類型的字符串,s類型為char *
#define RETURN_STRING(s) { RETVAL_STRING(s); return; }
//返回char *類型的字符串,s類型為char *,l為字符串長度,類型為size_t
#define RETURN_STRINGL(s, l) { RETVAL_STRINGL(s, l); return; }
//返回空字符串
#define RETURN_EMPTY_STRING() { RETVAL_EMPTY_STRING(); return; }
//返回資源,r類型:zend_resource *
#define RETURN_RES(r) { RETVAL_RES(r); return; }
//返回數組,r類型:zend_array *
#define RETURN_ARR(r) { RETVAL_ARR(r); return; }
//返回對象,r類型:zend_object *
#define RETURN_OBJ(r) { RETVAL_OBJ(r); return; }
//返回zval
#define RETURN_ZVAL(zv, copy, dtor) { RETVAL_ZVAL(zv, copy, dtor); return; }
//返回false
#define RETURN_FALSE { RETVAL_FALSE; return; }
//返回true
#define RETURN_TRUE { RETVAL_TRUE; return; }
```
### 7.6.5 函數調用
實際應用中,擴展可能需要調用用戶自定義的函數或者其他擴展定義的內部函數,前面章節已經介紹過函數的執行過程,這里不再重復,本節只介紹下PHP提供的函數調用API的使用:
```c
ZEND_API int call_user_function(HashTable *function_table, zval *object, zval *function_name, zval *retval_ptr, uint32_t param_count, zval params[]);
```
各參數的含義:
* __function_table:__ 函數符號表,普通函數是EG(function_table),如果是成員方法則是zend_class_entry.function_table
* __object:__ 調用成員方法時的對象
* __function_name:__ 調用的函數名稱
* __retval_ptr:__ 函數返回值地址
* __param_count:__ 參數數量
* __params:__ 參數數組
從接口的定義看其使用還是很簡單的,不需要我們關心執行過程中各階段復雜的操作。下面從一個具體的例子看下其使用:
(1)在PHP中定義了一個普通的函數,將參數$i加上100后返回:
```php
function mySum($i){
return $i+100;
}
```
(2)接下來在擴展中調用這個函數:
```c
PHP_FUNCTION(my_func_1)
{
zend_long i;
zval call_func_name, call_func_ret, call_func_params[1];
uint32_t call_func_param_cnt = 1;
zend_string *call_func_str;
char *func_name = "mySum";
if(zend_parse_parameters(ZEND_NUM_ARGS(), "l", &i) == FAILURE){
RETURN_FALSE;
}
//分配zend_string:調用完需要釋放
call_func_str = zend_string_init(func_name, strlen(func_name), 0);
//設置到zval
ZVAL_STR(&call_func_name, call_func_str);
//設置參數
ZVAL_LONG(&call_func_params[0], i);
//call
if(SUCCESS != call_user_function(EG(function_table), NULL, &call_func_name, &call_func_ret, call_func_param_cnt, call_func_params)){
zend_string_release(call_func_str);
RETURN_FALSE;
}
zend_string_release(call_func_str);
RETURN_LONG(Z_LVAL(call_func_ret));
}
```
(3)最后調用這個內部函數:
```php
function mySum($i){
return $i+100;
}
echo my_func_1(60);
===========[output]===========
160
```
`call_user_function()`并不是只能調用PHP腳本中定義的函數,內核或其它擴展注冊的函數同樣可以通過此函數調用,比如:array_merge()。
```c
PHP_FUNCTION(my_func_1)
{
zend_array *arr1, *arr2;
zval call_func_name, call_func_ret, call_func_params[2];
uint32_t call_func_param_cnt = 2;
zend_string *call_func_str;
char *func_name = "array_merge";
if(zend_parse_parameters(ZEND_NUM_ARGS(), "hh", &arr1, &arr2) == FAILURE){
RETURN_FALSE;
}
//分配zend_string
call_func_str = zend_string_init(func_name, strlen(func_name), 0);
//設置到zval
ZVAL_STR(&call_func_name, call_func_str);
ZVAL_ARR(&call_func_params[0], arr1);
ZVAL_ARR(&call_func_params[1], arr2);
if(SUCCESS != call_user_function(EG(function_table), NULL, &call_func_name, &call_func_ret, call_func_param_cnt, call_func_params)){
zend_string_release(call_func_str);
RETURN_FALSE;
}
zend_string_release(call_func_str);
RETURN_ARR(Z_ARRVAL(call_func_ret));
}
```
```php
$arr1 = array(1,2);
$arr2 = array(3,4);
$arr = my_func_1($arr1, $arr2);
var_dump($arr);
```
你可能會注意到,上面的例子通過`call_user_function()`調用函數時并沒有增加兩個數組參數的引用計數,但根據前面介紹的內容:函數傳參時不會硬拷貝value,而是增加參數value的引用計數,然后在函數return階段再把引用減掉。實際是`call_user_function()`替我們完成了這個工作,下面簡單看下其處理過程。
```c
int call_user_function(HashTable *function_table, zval *object, zval *function_name, zval *retval_ptr, uint32_t param_count, zval params[])
{
return call_user_function_ex(function_table, object, function_name, retval_ptr, param_count, params, 1, NULL);
}
int call_user_function_ex(HashTable *function_table, zval *object, zval *function_name, zval *retval_ptr, uint32_t param_count, zval params[], int no_separation, zend_array *symbol_table)
{
zend_fcall_info fci;
fci.size = sizeof(fci);
fci.function_table = function_table;
fci.object = object ? Z_OBJ_P(object) : NULL;
ZVAL_COPY_VALUE(&fci.function_name, function_name);
fci.retval = retval_ptr;
fci.param_count = param_count;
fci.params = params;
fci.no_separation = (zend_bool) no_separation;
fci.symbol_table = symbol_table;
return zend_call_function(&fci, NULL);
}
```
`call_user_function()`將我們提供的參數組裝為`zend_fcall_info`結構,然后調用`zend_call_function()`進行處理,還記得`zend_parse_parameters()`那個"f"解析符嗎?它也是將輸入的函數名稱解析為一個`zend_fcall_info`,可以更方便的調用函數,同時我們也可以自己創建一個`zend_fcall_info`結構,然后使用`zend_call_function()`完成函數的調用。
```c
int zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_cache)
{
...
for (i=0; i<fci->param_count; i++) {
zval *param;
zval *arg = &fci->params[i];
...
//為參數添加引用
if (Z_OPT_REFCOUNTED_P(arg)) {
Z_ADDREF_P(arg);
}
}
...
//調用的是用戶函數
if (func->type == ZEND_USER_FUNCTION) {
//執行
zend_init_execute_data(call, &func->op_array, fci->retval);
zend_execute_ex(call);
}else if (func->type == ZEND_INTERNAL_FUNCTION){ //內部函數
if (EXPECTED(zend_execute_internal == NULL)) {
func->internal_function.handler(call, fci->retval);
} else {
zend_execute_internal(call, fci->retval);
}
}
...
}
```
- 目錄
- 第1章 PHP基本架構
- 1.1 PHP簡介
- 1.2 PHP7的改進
- 1.3 FPM
- 1.4 PHP執行的幾個階段
- 第2章 變量
- 2.1 變量的內部實現
- 2.2 數組
- 2.3 靜態變量
- 2.4 全局變量
- 2.5 常量
- 3.1 PHP代碼的編譯
- 3.1.1 詞法解析、語法解析
- 3.1.2 抽象語法樹編譯流程
- 第3章 Zend虛擬機
- 3.2.1 內部函數
- 3.2.2 用戶函數的實現
- 3.3 Zend引擎執行流程
- 3.3.1 基本結構
- 3.2 函數實現
- 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.2 線程安全資源管理器
- 第7章 擴展開發
- 7.1 概述
- 6.1 什么是線程安全
- 7.2 擴展的實現原理
- 7.3 擴展的構成及編譯
- 7.4 鉤子函數
- 7.5 運行時配置
- 7.6 函數
- 7.7 zval的操作
- 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.2 命名空間的定義
- 8.2.1 定義語法
- 8.2.2 內部實現
- 8.3 命名空間的使用
- 8.3.1 基本用法
- 8.3.2 use導入
- 8.3.3 動態用法
- 附錄
- 附錄1:break/continue按標簽中斷語法實現
- 附錄2:defer推遲函數調用語法的實現
- 8.1 概述