# 附錄2:defer推遲函數調用語法的實現
使用過Go語言的應該都知道defer這個語法,它用來推遲一個函數的執行,在函數執行返回前首先檢查當前函數內是否有推遲執行的函數,如果有則執行,然后再返回。defer是一個非常有用的語法,這個功能可以很方便的在函數結束前執行一些清理工作,比如關閉打開的文件、關閉連接、釋放資源、解鎖等等。這樣延遲一個函數有以下兩個好處:
* (1) 靠近使用位置,避免漏掉清理工作,同時比放在函數結尾要清晰
* (2) 如果有多處返回的地方可以避免代碼重復,比如函數中有很多處return
在一個函數中可以使用多個defer,其執行順序與棧類似:后進先出,先定義的defer后執行。另外,在返回之后定義的defer將不會被執行,只有返回前定義的才會執行,通過exit退出程序的情況也不會執行任何defer。
在PHP中并沒有實現類似的語法,本節我們將嘗試在PHP中實現類似Go語言中defer的功能。此功能的實現需要對PHP的語法解析、抽象語法樹/opcode的編譯、opcode指令的執行等環節進行改造,涉及的地方比較多,但是改動點比較簡單,可以很好的幫助大家完整的理解PHP編譯、執行兩個核心階段的實現。總體實現思路:
* __(1)語法解析:__ defer本質上還是函數調用,只是將調用時機移到了函數的最后,所以編譯時可以復用調用函數的規則,但是需要與普通的調用區分開,所以我們新增一個AST節點類型,其子節點為為正常函數調用編譯的AST,語法我們定義為:`defer function_name()`;
* __(2)opcode編譯:__ 編譯opcode時也復用調用函數的編譯邏輯,不同的地方在于把defer放在最后編譯,另外需要在編譯return前新增一條opcode,用于執行return前跳轉到defer開始的位置,在defer的最后也需要新增一條opcode,用于執行完defer后跳回return的位置;
* __(3)執行階段:__ 執行時如果發現是return前新增的opcode則跳轉到defer開始的位置,同時把return的位置記錄下來,執行完defer后再跳回return。
編譯后的opcode指令如下圖所示:

接下來我們詳細介紹下各個環節的改動,一步步實現defer功能。
__(1)語法解析__
想讓PHP支持`defer function_name()`的語法首先需要修改的是詞法解析規則,將"defer"關鍵詞解析為token:T_DEFER,這樣詞法掃描器在匹配token時遇到"defer"將告訴語法解析器這是一個T_DEFER。這一步改動比較簡單,PHP的詞法解析規則定義在zend_language_scanner.l中,加入以下代碼即可:
```c
<ST_IN_SCRIPTING>"defer" {
RETURN_TOKEN(T_DEFER);
}
```
完成詞法解析規則的修改后接著需要定義語法解析規則,這是非常關鍵的一步,語法解析器會根據配置的語法規則將PHP代碼解析為抽象語法樹(AST)。普通函數調用會被解析為ZEND_AST_CALL類型的AST節點,我們新增一種節點類型:ZEND_AST_DEFER_CALL,抽象語法樹的節點類型為enum,定義在zend_ast.h中,同時此節點只需要一個子節點,這個子節點用于保存ZEND_AST_CALL節點,因此zend_ast.h的修改如下:
```c
enum _zend_ast_kind {
...
/* 1 child node */
...
ZEND_AST_DEFER_CALL
....
}
```
定義完AST節點后就可以在配置語法解析規則了,把defer語法解析為ZEND_AST_DEFER_CALL節點,我們把這條語法規則定義在"statement:"節點下,if、echo、for等語法都定義在此節點下,語法解析規則文件為zend_language_parser.y:
```c
statement:
'{' inner_statement_list '}' { $$ = $2; }
...
| T_DEFER function_call ';' { $$ = zend_ast_create(ZEND_AST_DEFER_CALL, $2); }
;
```
修改完這兩個文件后需要分別調用re2c、yacc生成對應的C文件,具體的生成命令可以在Makefile.frag中看到:
```sh
$ re2c --no-generation-date --case-inverted -cbdFt Zend/zend_language_scanner_defs.h -oZend/zend_language_scanner.c Zend/zend_language_scanner.l
$ yacc -p zend -v -d Zend/zend_language_parser.y -oZend/zend_language_parser.c
```
執行完以后將在Zend目錄下重新生成zend_language_scanner.c、zend_language_parser.c兩個文件。到這一步已經完成生成抽象語法樹的工作了,重新編譯PHP后已經能夠解析defer語法了,將會生成以下節點:

__(2)編譯ZEND_AST_DEFER_CALL__
生成抽象語法樹后接下來就是編譯生成opcodes的操作,即從AST->Opcodes。編譯ZEND_AST_DEFER_CALL節點時不能立即進行編譯,需要等到當前腳本或函數全部編譯完以后再進行編譯,所以在編譯過程需要把ZEND_AST_DEFER_CALL節點先緩存下來,參考循環結構編譯時生成的zend_brk_cont_element的存儲位置,我們也把ZEND_AST_DEFER_CALL節點保存在zend_op_array中,通過數組進行存儲,將ZEND_AST_DEFER_CALL節點依次存入該數組,zend_op_array中加入以下幾個成員:
* __last_defer:__ 整形,記錄當前編譯的defer數
* __defer_start_op:__ 整形,用于記錄defer編譯生成opcode指令的起始位置
* __defer_call_array:__ 保存ZEND_AST_DEFER_CALL節點的數組,用于保存ast節點的地址
```c
struct _zend_op_array {
...
int last_defer;
uint32_t defer_start_op;
zend_ast **defer_call_array;
}
```
修改完數據結構后接著對應修改zend_op_array初始化的過程:
```c
//zend_opcode.c
void init_op_array(zend_op_array *op_array, zend_uchar type, int initial_ops_size)
{
...
op_array->last_defer = 0;
op_array->defer_start_op = 0;
op_array->defer_call_array = NULL;
...
}
```
完成依賴的這些數據結構的改造后接下來開始編寫具體的編譯邏輯,也就是編譯ZEND_AST_DEFER_CALL的處理。抽象語法樹的編譯入口函數為zend_compile_top_stmt(),然后根據不同節點的類型進行相應的編譯,我們在zend_compile_stmt()函數中對ZEND_AST_DEFER_CALL節點進行編譯:
```c
void zend_compile_stmt(zend_ast *ast)
{
...
switch (ast->kind) {
...
case ZEND_AST_DEFER_CALL:
zend_compile_defer_call(ast);
break
...
}
}
```
編譯過程只是將ZEND_AST_DEFER_CALL的子節點(即:ZEND_AST_CALL)保存到zend_op_array->defer_call_array數組中,注意這里defer_call_array數組還沒有分配內存,參考循環結構的實現,這里我們定義了一個函數用于數組的分配:
```c
//zend_compile.c
void zend_compile_defer_call(zend_ast *ast)
{
if(!ast){
return;
}
zend_ast **call_ast = NULL;
//將普通函數調用的ast節點保存到defer_call_array數組中
call_ast = get_next_defer_call(CG(active_op_array));
*call_ast = ast->child[0];
}
//zend_opcode.c
zend_ast **get_next_defer_call(zend_op_array *op_array)
{
op_array->last_defer++;
op_array->defer_call_array = erealloc(op_array->defer_call_array, sizeof(zend_ast*)*op_array->last_defer);
return &op_array->defer_call_array[op_array->last_defer-1];
}
```
既然分配了defer_call_array數組的內存就需要在zend_op_array銷毀時釋放:
```c
//zend_opcode.c
ZEND_API void destroy_op_array(zend_op_array *op_array)
{
...
if (op_array->defer_call_array) {
efree(op_array->defer_call_array);
}
...
}
```
編譯完整個腳本或函數后,最后還會編譯一條ZEND_RETURN,也就是返回指令,相當于ret指令,注意:這條opcode并不是我們在腳本中定義的return語句的,而是PHP內核為我們加的一條指令,這就是為什么有些函數我們沒有寫return也能返回的原因,任何函數或腳本都會生成這樣一條指令。我們緩存在zend_op_array->defer_call_array數組中defer就是要在這時進行編譯,也就是把defer的指令編譯在最后。內核最后編譯返回的這條指令由zend_emit_final_return()方法完成,我們把defer的編譯放在此方法的末尾:
```c
//zend_compile.c
void zend_emit_final_return(zval *zv)
{
...
ret = zend_emit_op(NULL, returns_reference ? ZEND_RETURN_BY_REF : ZEND_RETURN, &zn, NULL);
ret->extended_value = -1;
//編譯推遲執行的函數調用
zend_emit_defer_call();
}
```
前面已經說過,defer本質上就是函數調用,所以編譯的過程直接復用普通函數調用的即可。另外,在編譯時把起始位置記錄到zend_op_array->defer_start_op中,因為在執行return前需要知道跳轉到什么位置,這個值就是在那時使用的,具體的用法稍后再作說明。編譯時按照倒序的順序進行編譯:
```c
//zend_compile.c
void zend_emit_defer_call()
{
if (!CG(active_op_array)->defer_call_array) {
return;
}
zend_ast *call_ast;
zend_op *nop;
znode result;
uint32_t opnum = get_next_op_number(CG(active_op_array));
int defer_num = CG(active_op_array)->last_defer;
//記錄推遲的函數調用指令開始位置
CG(active_op_array)->defer_start_op = opnum;
while(--defer_num >= 0){
call_ast = CG(active_op_array)->defer_call_array[defer_num];
if (call_ast == NULL) {
continue;
}
nop = zend_emit_op(NULL, ZEND_NOP, NULL, NULL);
nop->op1.var = -2;
//編譯函數調用
zend_compile_call(&result, call_ast, BP_VAR_R);
}
//compile ZEND_DEFER_CALL_END
zend_emit_op(NULL, ZEND_DEFER_CALL_END, NULL, NULL);
}
```
編譯完推遲的函數調用之后,編譯一條ZEND_DEFER_CALL_END指令,該指令用于執行完推遲的函數后跳回return的位置進行返回,opcode定義在zend_vm_opcodes.h中:
```c
//zend_vm_opcodes.h
#define ZEND_DEFER_CALL_END 174
```
還有一個地方你可能已經注意到,在逐個編譯defer的函數調用前都生成了一條ZEND_NOP的指令,這個的目的是什么呢?開始的時候已經介紹過defer語法的特點,函數中定義的defer并不是全部執行,在return之后定義的defer是不會執行的,比如:
```go
func main(){
defer fmt.Println("A")
if 1 == 1{
return
}
defer fmt.Println("B")
}
```
這種情況下第2個defer就不會生效,因此在return前跳轉的位置就不一定是zend_op_array->defer_start_op,有可能會跳過幾個函數的調用,所以這里我們通過ZEND_NOP這條空指令對多個defer call進行隔離,同時為避免與其它ZEND_NOP指令混淆,增加一個判斷條件:op1.var=-2。這樣在return前跳轉時就根據此前定義的defer數跳過部分函數的調用,如下圖所示。

到這一步我們已經完成defer函數調用的編譯,此時重新編譯PHP后可以看到通過defer推遲的函數調用已經被編譯在最后了,只不過這個時候它們不能被執行。
__(3)編譯return__
編譯return時需要插入一條指令用于跳轉到推遲執行的函數調用指令處,因此這里需要再定義一條opcode:ZEND_DEFER_CALL,在編譯過程中defer call還未編譯,因此此時還無法知道具體的跳轉值。
```c
//zend_vm_opcodes.h
#define ZEND_DEFER_CALL 173
#define ZEND_DEFER_CALL_END 174
```
PHP腳本中聲明的return語句由zend_compile_return()方法完成編譯,在編譯生成ZEND_DEFER_CALL指令時還需要將當前已定義的defer數(即在return前聲明的defer)記錄下來,用于計算具體的跳轉值。
```c
void zend_compile_return(zend_ast *ast)
{
...
//在return前編譯ZEND_DEFER_CALL:用于在執行retur前跳轉到defer call
if (CG(active_op_array)->defer_call_array) {
defer_zn.op_type = IS_UNUSED;
defer_zn.u.op.num = CG(active_op_array)->last_defer;
zend_emit_op(NULL, ZEND_DEFER_CALL, NULL, &defer_zn);
}
//編譯正常返回的指令
opline = zend_emit_op(NULL, by_ref ? ZEND_RETURN_BY_REF : ZEND_RETURN,
&expr_node, NULL);
...
}
```
除了這種return外還有一種我們上面已經提過的return,即PHP內核編譯的return指令,當PHP腳本中沒有聲明return語句時將執行內核添加的那條指令,因此也需要在zend_emit_final_return()加上上面的邏輯。
```c
void zend_emit_final_return(zval *zv)
{
...
//在return前編譯ZEND_DEFER_CALL:用于在執行retur前跳轉到defer call
if (CG(active_op_array)->defer_call_array) {
//當前return之前定義的defer數
defer_zn.op_type = IS_UNUSED;
defer_zn.u.op.num = CG(active_op_array)->last_defer;
zend_emit_op(NULL, ZEND_DEFER_CALL, NULL, &defer_zn);
}
//編譯返回指令
ret = zend_emit_op(NULL, returns_reference ? ZEND_RETURN_BY_REF : ZEND_RETURN, &zn, NULL);
ret->extended_value = -1;
//編譯推遲執行的函數調用
zend_emit_defer_call();
}
```
__(4)計算ZEND_DEFER_CALL指令的跳轉位置__
前面我們已經完成了推遲調用函數以及return編譯過程的改造,在編譯完成后ZEND_DEFER_CALL指令已經能夠知道具體的跳轉位置了,因為推遲調用的函數已經編譯完成了,所以下一步就是為全部的ZEND_DEFER_CALL指令計算跳轉值。前面曾介紹過,在編譯完成有一個pass_two()的環節,我們就在這里完成具體跳轉位置的計算,并把跳轉位置保存到ZEND_DEFER_CALL指令的操作數中,在執行階段直接跳轉到對應位置。
```c
ZEND_API int pass_two(zend_op_array *op_array)
{
zend_op *opline, *end;
...
//遍歷opcode
opline = op_array->opcodes;
end = opline + op_array->last;
while (opline < end) {
switch (opline->opcode) {
...
case ZEND_DEFER_CALL: //設置jmp
{
uint32_t defer_start = op_array->defer_start_op;
//skip_defer為當前return之后聲明的defer數,也就是不需要執行的defer
uint32_t skip_defer = op_array->last_defer - opline->op2.num;
//defer_opline為推遲的函數調用起始位置
zend_op *defer_opline = op_array->opcodes + defer_start;
uint32_t n = 0;
while(n <= skip_defer){
if (defer_opline->opcode == ZEND_NOP && defer_opline->op1.var == -2) {
n++;
}
defer_opline++;
defer_start++;
}
//defer_start為opcode在op_array->opcodes數組中的位置
opline->op1.opline_num = defer_start;
//將跳轉位置保存到操作數op1中
ZEND_PASS_TWO_UPDATE_JMP_TARGET(op_array, opline, opline->op1);
}
break;
}
...
}
...
}
```
這里我們并沒有直接編譯為ZEND_JMP跳轉指令,雖然ZEND_JMP可以跳轉到后面的指令位置,但是最后的那條跳回return位置的指令(即:ZEND_DEFER_CALL_END)由于可能存在多個return的原因無法在編譯期間確定具體的跳轉值,只能在運行期間執行ZEND_DEFER_CALL時才能確定,所以需要在ZEND_DEFER_CALL指令的handler中將return的位置記錄下來,執行ZEND_DEFER_CALL_END時根據這個值跳回。
__(5)定義ZEND_DEFER_CALL、ZEND_DEFER_CALL_END指令的handler__
ZEND_DEFER_CALL指令執行時需要將return的位置保存下來,我們把這個值保存到zend_execute_data結構中:
```c
//zend_compile.h
struct _zend_execute_data {
...
const zend_op *return_opline;
...
}
```
opcode的handler定義在zend_vm_def.h文件中,定義完成后需要執行`php zend_vm_gen.php`腳本生成具體的handler函數。
```c
ZEND_VM_HANDLER(173, ZEND_DEFER_CALL, ANY, ANY)
{
USE_OPLINE
//1) 將return指令的位置保存到EX(return_opline)
EX(return_opline) = opline + 1;
//2) 跳轉
ZEND_VM_SET_OPCODE(OP_JMP_ADDR(opline, opline->op1));
ZEND_VM_CONTINUE();
}
ZEND_VM_HANDLER(174, ZEND_DEFER_CALL_END, ANY, ANY)
{
USE_OPLINE
ZEND_VM_SET_OPCODE(EX(return_opline));
ZEND_VM_CONTINUE();
}
```
到目前為止我們已經完成了全部的修改,重新編譯PHP后就可以使用defer語法了:
```php
function shutdown($a){
echo $a."\n";
}
function test(){
$a = 1234;
defer shutdown($a);
$a = 8888;
if(1){
return "mid end\n";
}
defer shutdown("9999");
return "last end\n";
}
echo test();
```
執行后將顯示:
```sh
8888
mid end
```
這里我們只實現了普通函數調用的方式,關于成員方法、靜態方法、匿名函數等調用方式并未實現,留給有興趣的讀者自己去實現。
完整代碼:[https://github.com/pangudashu/php-7.0.12](https://github.com/pangudashu/php-7.0.12)
- 前言
- 第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超時控制的思考