### 3.2.1 用戶自定義函數的實現
用戶自定義函數是指我們在PHP腳本通過function定義的函數:
```php
function my_func(){
...
}
```
匯編中函數對應的是一組獨立的匯編指令,然后通過call指令實現函數的調用。前面已經說過PHP編譯的結果是opcode數組,與匯編指令對應。PHP用戶自定義函數的實現就是將函數編譯為獨立的opcode數組,調用時分配獨立的執行棧依次執行opcode,所以自定義函數對于zend而言并沒有什么特別之處,只是將opcode進行了打包封裝。PHP腳本中函數之外的指令,整個可以認為是一個函數(或者理解為main函數更直觀)。
```php
/* function main(){ */
$a = 123;
echo $a;
/* } */
```
#### 3.2.1.1 函數的存儲結構
下面具體看下PHP中函數的結構:
```c
typedef union _zend_function zend_function;
//zend_compile.h
union _zend_function {
zend_uchar type; /* MUST be the first element of this struct! */
struct {
zend_uchar type; /* never used */
zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
uint32_t fn_flags;
zend_string *function_name;
zend_class_entry *scope; //成員方法所屬類,面向對象實現中用到
union _zend_function *prototype;
uint32_t num_args; //參數數量
uint32_t required_num_args; //必傳參數數量
zend_arg_info *arg_info; //參數信息
} common;
zend_op_array op_array; //函數實際編譯為普通的zend_op_array
zend_internal_function internal_function;
};
```
這是一個union,因為PHP中函數除了用戶自定義函數還有一種:內部函數,內部函數是通過擴展或者內核提供的C函數,比如time、array系列等等,內部函數稍后再作分析。
內部函數主要用到`internal_function`,而用戶自定義函數編譯完就是一個普通的opcode數組,用的是`op_array`(注意:op_array、internal_function是嵌入的兩個結構,而不是一個單獨的指針),除了這兩個上面還有一個`type`跟`common`,這倆是做什么用的呢?
經過比較發現`zend_op_array`與`zend_internal_function`結構的起始位置都有`common`中的幾個成員,如果你對C的內存比較了解應該會馬上想到它們的用法,實際`common`可以看作是`op_array`、`internal_function`的header,不管是什么哪種函數都可以通過`zend_function.common.xx`快速訪問到`zend_function.zend_op_array.xx`及`zend_function.zend_internal_function.xx`,下面幾個,`type`同理,可以直接通過`zend_function.type`取到`zend_function.op_array.type`及`zend_function.internal_function.type`。

函數是在編譯階段確定的,那么它們存在哪呢?
在PHP腳本的生命周期中有一個非常重要的值`executor_globals`(非ZTS下),類型是`struct _zend_executor_globals`,它記錄著PHP生命周期中所有的數據,如果你寫過PHP擴展一定用到過`EG`這個宏,這個宏實際就是對`executor_globals`的操作:`define EG(v) (executor_globals.v)`
`EG(function_table)`是一個哈希表,記錄的就是PHP中所有的函數。
PHP在編譯階段將用戶自定義的函數編譯為獨立的opcodes,保存在`EG(function_table)`中,調用時重新分配新的zend_execute_data(相當于運行棧),然后執行函數的opcodes,調用完再還原到舊的`zend_execute_data`,繼續執行,關于zend引擎execute階段后面會詳細分析。
#### 3.2.1.2 函數參數
函數參數在內核實現上與函數內的局部變量實際是一樣的,上一篇我們介紹編譯的時候提供局部變量會有一個單獨的 __編號__ ,而函數的參數與之相同,參數名稱也在zend_op_array.vars中,編號首先是從參數開始的,所以按照參數順序其編號依次為0、1、2...(轉化為相對內存偏移量就是96、112、128...),然后函數調用時首先會在調用位置將參數的value復制到各參數各自的位置,詳細的傳參過程我們在執行一篇再作說明。
比如:
```php
function my_function($a, $b = "aa"){
$ret = $a . $b;
return $ret;
}
```
編譯完后各變量的內存偏移量編號:
```
$a => 96
$b => 112
$ret => 128
```
與下面這么寫一樣:
```php
function my_function(){
$a = NULL;
$b = "aa";
$ret = $a . $b;
return $ret;
}
```
另外參數還有其它的信息,這些信息通過`zend_arg_info`結構記錄:
```c
typedef struct _zend_arg_info {
zend_string *name; //參數名
zend_string *class_name;
zend_uchar type_hint; //顯式聲明的參數類型,比如(array $param_1)
zend_uchar pass_by_reference; //是否引用傳參,參數前加&的這個值就是1
zend_bool allow_null; //是否允許為NULL,注意:這個值并不是用來表示參數是否為必傳的
zend_bool is_variadic; //是否為可變參數,即...用法,與golang的用法相同,5.6以上新增的一個用法:function my_func($a, ...$b){...}
} zend_arg_info;
```
每個參數都有一個上面的結構,所有參數的結構保存在`zend_op_array.arg_info`數組中,這里有一個地方需要注意:`zend_op_array->arg_info`數組保存的并不全是輸入參數,如果函數聲明了返回值類型則也會為它創建一個`zend_arg_info`,這個結構在arg_info數組的第一個位置,這種情況下`zend_op_array->arg_info`指向的實際是數組的第二個位置,返回值的結構通過`zend_op_array->arg_info[-1]`讀取,這里先單獨看下編譯時的處理:
```c
//函數參數的編譯
void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast)
{
zend_ast_list *list = zend_ast_get_list(ast);
uint32_t i;
zend_op_array *op_array = CG(active_op_array);
zend_arg_info *arg_infos;
if (return_type_ast) {
//聲明了返回值類型:function my_func():array{...}
//多分配一個zend_arg_info
arg_infos = safe_emalloc(sizeof(zend_arg_info), list->children + 1, 0);
...
arg_infos->allow_null = 0;
...
//arg_infos指向了下一個位置
arg_infos++;
op_array->fn_flags |= ZEND_ACC_HAS_RETURN_TYPE;
} else {
//沒有聲明返回值類型
if (list->children == 0) {
return;
}
arg_infos = safe_emalloc(sizeof(zend_arg_info), list->children, 0);
}
...
op_array->num_args = list->children;
//聲明了返回值的情況下arg_infos已經指向了數組的第二個元素
op_array->arg_info = arg_infos;
}
```
#### 3.2.1.3 函數的編譯
我們在上一篇文章介紹過PHP代碼的編譯過程,主要是PHP->AST->Opcodes的轉化,上面也說了函數其實就是將一組PHP代碼編譯為單獨的opcodes,函數的調用就是不同opcodes間的切換,所以函數的編譯過程與普通PHP代碼基本一致,只是會有一些特殊操作,我們以3.2.1.2開始那個例子簡單看下編譯過程。
普通函數的語法解析規則:
```c
function_declaration_statement:
function returns_ref T_STRING backup_doc_comment '(' parameter_list ')' return_type
'{' inner_statement_list '}'
{ $$ = zend_ast_create_decl(ZEND_AST_FUNC_DECL, $2, $1, $4,
zend_ast_get_str($3), $6, NULL, $10, $8); }
;
```
規則主要由五部分組成:
* __returns_ref:__ 是否返回引用,在函數名前加&,比如function &test(){...}
* __T_STRING:__ 函數名
* __parameter_list:__ 參數列表
* __return_type:__ 返回值類型
* __inner_statement_list:__ 函數內部代碼
函數生成的抽象語法樹根節點類型是zend_ast_decl,所有函數相關的信息都記錄在這個節點中(除了函數外類也是用的這個):
```c
typedef struct _zend_ast_decl {
zend_ast_kind kind; //函數就是ZEND_AST_FUNC_DECL,類則是ZEND_AST_CLASS
zend_ast_attr attr; /* Unused - for structure compatibility */
uint32_t start_lineno; //函數起始行
uint32_t end_lineno; //函數結束行
uint32_t flags; //其中一個標識位用來標識返回值是否為引用,是則為ZEND_ACC_RETURN_REFERENCE
unsigned char *lex_pos;
zend_string *doc_comment;
zend_string *name; //函數名
zend_ast *child[4]; //child有4個子節點,分別是:參數列表節點、use列表節點、函數內部表達式節點、返回值類型節點
} zend_ast_decl;
```
上面的例子最終生成的語法樹:

具體編譯為opcodes的過程在`zend_compile_func_decl()`中:
```c
void zend_compile_func_decl(znode *result, zend_ast *ast)
{
zend_ast_decl *decl = (zend_ast_decl *) ast;
zend_ast *params_ast = decl->child[0]; //參數列表
zend_ast *uses_ast = decl->child[1]; //use列表
zend_ast *stmt_ast = decl->child[2]; //函數內部
zend_ast *return_type_ast = decl->child[3]; //返回值類型
zend_bool is_method = decl->kind == ZEND_AST_METHOD; //是否為成員函數
//這里保存當前正在編譯的zend_op_array:CG(active_op_array),然后重新為函數生成一個新的zend_op_array,
//函數編譯完再將舊的還原
zend_op_array *orig_op_array = CG(active_op_array);
zend_op_array *op_array = zend_arena_alloc(&CG(arena), sizeof(zend_op_array)); //新分配zend_op_array
...
if (is_method) {
zend_bool has_body = stmt_ast != NULL;
zend_begin_method_decl(op_array, decl->name, has_body);
} else {
zend_begin_func_decl(result, op_array, decl); //注意這里會在當前zend_op_array(不是新生成的函數那個)生成一條ZEND_DECLARE_FUNCTION的opcode
}
CG(active_op_array) = op_array;
...
zend_compile_params(params_ast, return_type_ast); //編譯參數
if (uses_ast) {
zend_compile_closure_uses(uses_ast);
}
zend_compile_stmt(stmt_ast); //編譯函數內部語法
...
pass_two(CG(active_op_array));
...
CG(active_op_array) = orig_op_array; //還原之前的
}
```
> __編譯過程主要有這么幾個處理:__
> __(1)__ 保存當前正在編譯的zend_op_array,新分配一個結構,因為每個函數、include的文件都對應獨立的一個zend_op_array,通過CG(active_op_array)記錄當前編譯所屬zend_op_array,所以開始編譯函數時就需要將這個值保存下來,等到函數編譯完成再還原回去;另外還有一個關鍵操作:`zend_begin_func_decl`,這里會在當前zend_op_array(不是新生成的函數那個)生成一條 __ZEND_DECLARE_FUNCTION__ 的opcode,也就是函數聲明操作。
```php
$a = 123; //當前為CG(active_op_array) = zend_op_array_1,編譯到這時此opcode加到zend_op_array_1
//新分配一個zend_op_array_2,并將當前CG(active_op_array)保存到origin_op_array,
//然后將CG(active_op_array)=zend_op_array_2
function test(){
$b = 234; //編譯到zend_op_array_2
}//函數編譯結束,將CG(active_op_array) = origin_op_array,切回zend_op_array_1
$c = 345; //編譯到zend_op_array_1
```
> __(2)__ 編譯參數列表,函數的參數我們在上一小節已經介紹,完整的參數會有三個組成:參數類型(可選)、參數名、默認值(可選),這三部分分別保存在參數節點的三個child節點中,編譯參數的過程有兩個關鍵操作:
>> __操作1:__ 為每個參數編號
>> __操作2:__ 每個參數生成一條opcode,如果是可變參數其opcode=ZEND_RECV_VARIADIC,如果有默認值則為ZEND_RECV_INIT,否則為ZEND_RECV
> 上面的例子中$a編號為96,$b為112,同時生成了兩條opcode:ZEND_RECV、ZEND_RECV_INIT,調用的時候會根據具體傳參數量跳過部分opcode,比如這個函數我們這么調用`my_function($a)`則ZEND_RECV這條opcode就直接跳過了,然后執行ZEND_RECV_INIT將默認值寫到112位置,具體的編譯過程在`zend_compile_params()`中,上面已經介紹過。
>
> 參數默認值的保存與普通變量賦值相同:`$a = array()`,`array()`保存在literals,參數的默認值也是如此。
>
> __(3)__ 編譯函數內部語法,這個跟普通PHP代碼編譯過程無異。
> __(4)__ pass_two(),上一篇介紹過,不再贅述。
最終生成兩個zend_op_array:

總體來看,PHP在逐行編譯時發現一個function則生成一條ZEND_DECLARE_FUNCTION的opcode,然后調到函數中編譯函數,編譯完再跳回去繼續下面的編譯,這里多次提到ZEND_DECLARE_FUNCTION這個opcode是因為在函數編譯結束后還有一個重要操作:`zend_do_early_binding()`,前面我們說過總的編譯入口在`zend_compile_top_stmt()`,這里會對每條語法逐條編譯,而函數、類在編譯完成后還有后續的操作:
```c
void zend_compile_top_stmt(zend_ast *ast)
{
...
if (ast->kind == ZEND_AST_STMT_LIST) {
for (i = 0; i < list->children; ++i) {
zend_compile_top_stmt(list->child[i]);
}
}
zend_compile_stmt(ast); //編譯各條語法,函數也是在這里編譯完成
//函數編譯完成后
if (ast->kind == ZEND_AST_FUNC_DECL || ast->kind == ZEND_AST_CLASS) {
CG(zend_lineno) = ((zend_ast_decl *) ast)->end_lineno;
zend_do_early_binding();
}
}
```
`zend_do_early_binding()`核心工作就是 __將function、class加到CG(function_table)、CG(class_table)中__ ,加入成功了就直接把 __ZEND_DECLARE_FUNCTION__ 這條opcode干掉了,加入失敗的話則保留,這個相當于 __有一部分opcode在『編譯時』提前執行了__ ,這也是為什么PHP中可以先調用函數再聲明函數的原因,比如:
```php
$a = 1234;
echo my_function($a);
function my_function($a){
...
}
```
實際原始的opcode以及執行順序:

類的情況也是如此,后面我們再作說明。
#### 3.2.1.4 匿名函數
匿名函數(Anonymous functions),也叫閉包函數(closures),允許臨時創建一個沒有指定名稱的函數。最經常用作回調函數(callback)參數的值。當然,也有其它應用的情況。
官網的示例:
```php
$greet = function($name)
{
printf("Hello %s\r\n", $name);
};
$greet('World');
$greet('PHP');
```
這里提匿名函數只是想說明編譯函數時那個use的用法:
__匿名函數可以從父作用域中繼承變量。 任何此類變量都應該用 use 語言結構傳遞進去。__
```php
$message = 'hello';
$example = function () use ($message) {
var_dump($message);
};
$example();
```
- 前言
- 第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超時控制的思考