## 4.4 中斷及跳轉
PHP中的中斷及跳轉語句主要有break、continue、goto,這幾種語句的實現基礎都是跳轉。
### 4.4.1 break與continue
break用于結束當前for、foreach、while、do-while 或者 switch 結構的執行;continue用于跳過本次循環中剩余代碼,進行下一輪循環。break、continue是非常相像的,它們都可以接受一個可選數字參數來決定跳過的循環層數,兩者的不同點在于break是跳到循環結束的位置,而continue是跳到循環判斷條件的位置,本質在于跳轉位置的不同。
break、continue的實現稍微有些復雜,下面具體介紹下其編譯過程。
上一節我們已經介紹過循環語句的編譯,其中在各種循環編譯過程中有兩個特殊操作:zend_begin_loop()、zend_end_loop(),分別在循環編譯前以及編譯后調用,這兩步操作就是為break、continue服務的。
在每層循環編譯時都會創建一個`zend_brk_cont_element`的結構:
```c
typedef struct _zend_brk_cont_element {
int start;
int cont;
int brk;
int parent;
} zend_brk_cont_element;
```
cont記錄的是當前循環判斷條件opcode起始位置,brk記錄的是當前循環結束的位置,parent記錄的是父層循環`zend_brk_cont_element`結構的存儲位置,也就是說多層嵌套循環會生成一個`zend_brk_cont_element`的鏈表,每層循環編譯結束時更新自己的`zend_brk_cont_element`結構,所以break、continue的處理過程實際就是根據跳出的層級索引到那一層的`zend_brk_cont_element`結構,然后得到它的cont、brk進行相應的opcode跳轉。
各循環的`zend_brk_cont_element`結構保存在`zend_op_array->brk_cont_array`數組中,編譯各循環時依次申請一個`zend_brk_cont_element`,`zend_op_array->last_brk_cont`記錄此數組第一個可用位置,每申請一個元素last_brk_cont就相應的增加1,然后將數組擴容,parent記錄的就是父層循環結構在該數組中的存儲位置。
```c
zend_brk_cont_element *get_next_brk_cont_element(zend_op_array *op_array)
{
op_array->last_brk_cont++;
op_array->brk_cont_array = erealloc(op_array->brk_cont_array, sizeof(zend_brk_cont_element)*op_array->last_brk_cont);
return &op_array->brk_cont_array[op_array->last_brk_cont-1];
}
```
示例:
```php
$i = 0;
while(1){
while(1){
if($i > 10){
break 2;
}
++$i
}
}
```
循環編譯完以后對應的內存結構:

介紹完編譯循環結構時為break、continue做的準備,接下來我們具體分析下break、continue的編譯。
有了前面的準備,break、continue的編譯過程就比較簡單了,主要就是各生成一條臨時opcode:ZEND_BRK、ZEND_CONT,這條opcode記錄著兩個重要信息:
* __op1:__ 記錄著當前循環`zend_brk_cont_element`結構的存儲位置(在循環編譯過程中CG(context).current_brk_cont記錄著當前循環zend_brk_cont_element的位置)
* __op2:__ 記錄著要跳出循環的層級,如果break/continue沒有加數字,則默認為1
```c
void zend_compile_break_continue(zend_ast *ast)
{
zend_ast *depth_ast = ast->child[0];
zend_op *opline;
int depth;
if (depth_ast) {
zval *depth_zv;
...
depth = Z_LVAL_P(depth_zv);
} else {
depth = 1;
}
...
//生成opcode
opline = zend_emit_op(NULL, ast->kind == ZEND_AST_BREAK ? ZEND_BRK : ZEND_CONT, NULL, NULL);
opline->op1.num = CG(context).current_brk_cont; //break、continue所在循環層
opline->op2.num = depth; //要跳出的層數
}
```
`zend_compile_break_continue()`到這一步完成整個break、continue的編譯還沒有完成,因為`CG(active_op_array)->brk_cont_array`這個數組只是編譯期間使用的一個臨時結構,break、continue編譯生成的opcode:ZEND_BRK、ZEND_CONT并不是運行時直接執行的,這條opcode在整個腳本編譯完成后、執行前被優化為 __ZEND_JMP__ ,這個操作在`pass_two()`中完成,關于這個過程在《3.1.2.2 AST->zend_op_array》一節曾經介紹過。
```c
ZEND_API zend_op_array *compile_file(zend_file_handle *file_handle, int type)
{
//語法解析
zendparse();
//AST->opcodes
zend_compile_top_stmt(CG(ast));
pass_two(op_array);
...
}
```
```c
ZEND_API int pass_two(zend_op_array *op_array)
{
...
opline = op_array->opcodes;
end = opline + op_array->last;
while (opline < end) {
switch (opline->opcode) {
...
case ZEND_BRK:
case ZEND_CONT:
{
//計算跳轉位置
uint32_t jmp_target = zend_get_brk_cont_target(op_array, opline);
...
//將opcode修改為ZEND_JMP
opline->opcode = ZEND_JMP;
opline->op1.opline_num = jmp_target;
opline->op2.num = 0;
//將絕對跳轉opcode位置修改為相對當前opcode的位置
ZEND_PASS_TWO_UPDATE_JMP_TARGET(op_array, opline, opline->op1);
}
break;
...
}
}
op_array->fn_flags |= ZEND_ACC_DONE_PASS_TWO;
return 0;
}
```
從上面的過程可以看出,如果opcode為:ZEND_BRK或ZEND_CONT則統一設置opcode為`ZEND_JMP`,新opcode的op1記錄的是break、continue跳到opcode的位置,這個值根據編譯期間的`zend_brk_cont_element`計算得到,首先從op1、op2取出break、continue所在循環的zend_brk_cont_element結構以及要跳過的層級,然后根據`zend_brk_cont_element.parent`及層級數找到具體要跳出層的`zend_brk_cont_element`結構,從這個結構中獲得那層循環判斷條件及循環結束的opcode的位置。
```c
static uint32_t zend_get_brk_cont_target(const zend_op_array *op_array, const zend_op *opline) {
int nest_levels = opline->op2.num; //跳出的層級:break n;
int array_offset = opline->op1.num;//break、continue所屬循環zend_brk_cont_element的存儲下標
zend_brk_cont_element *jmp_to;
do {
//從break/continue所在循環層開始
jmp_to = &op_array->brk_cont_array[array_offset];
if (nest_levels > 1) {
//如果還沒到要跳出的層數則接著跳到上層
array_offset = jmp_to->parent;
}
} while (--nest_levels > 0);
return opline->opcode == ZEND_BRK ? jmp_to->brk : jmp_to->cont;
}
```
上面那個例子最終執行前的opcode如下圖:

執行時直接跳到對應的opcode位置即可。
> __Note:__
>
> 在多層循環中break、continue直接根據層級數字跳轉很不方便,這點PHP可以借鑒Golang的語法:break/continue + LABEL,支持按標簽break、continue,根據上一節及本節介紹的內容這一個實現起來并不復雜,有興趣的可以思考下如何實現。
### 4.4.2 goto
goto 操作符可以用來跳轉到程序中的另一位置。該目標位置可以用目標名稱加上冒號來標記,而跳轉指令是 goto 之后接上目標位置的標記。PHP 中的 goto 有一定限制,目標位置只能位于同一個文件和作用域,也就是說無法跳出一個函數或類方法,也無法跳入到另一個函數,可以跳出循環但無法跳入循環(可以在同一層循環中跳轉),多層循環中通常會用goto代替多層break。
goto語法:
```php
goto LABEL;
LABEL:
statement;
```
goto與label需要組合使用,其實現與break、continue類似,最終也是被優化為`ZEND_JMP`,首先看下定義一個label時都有哪些操作:
```c
statement:
...
| T_STRING ':' { $$ = zend_ast_create(ZEND_AST_LABEL, $1); }
;
```
label的編譯過程非常簡單,與循環結構的編譯類似,編譯時會把label插入`CG(context).labels`哈希表中,key就是label名稱,value是一個`zend_label`結構:
```c
typedef struct _zend_label {
int brk_cont; //當前label所在循環
uint32_t opline_num; //下一條opcode位置
} zend_label;
```
brk_cont用于記錄當前label所在的循環,這個值就是上面介紹的每個循環在`zend_op_array->brk_cont_array`數組中的位置;opline_num比較容易理解,就是label下面第一條opcode的位置。到這里你應該能猜得到goto的工作過程了,首先根據label名稱在`CG(context).labels`查找到跳轉label的`zend_label`結構,然后jmp到`zend_label.opline_num`的位置,brk_cont的作用是用來判斷是不是goto到了另一層循環中去。label具體的編譯過程:
```c
void zend_compile_label(zend_ast *ast)
{
zend_string *label = zend_ast_get_str(ast->child[0]);
zend_label dest;
//編譯時會將label插入CG(context).labels哈希表
if (!CG(context).labels) {
ALLOC_HASHTABLE(CG(context).labels);
zend_hash_init(CG(context).labels, 8, NULL, label_ptr_dtor, 0);
}
//設置label信息:當前所在循環、下一條opcode編號
dest.brk_cont = CG(context).current_brk_cont;
dest.opline_num = get_next_op_number(CG(active_op_array));
if (!zend_hash_add_mem(CG(context).labels, label, &dest, sizeof(zend_label))) {
zend_error_noreturn(E_COMPILE_ERROR, "Label '%s' already defined", ZSTR_VAL(label));
}
}
```
goto的編譯過程:
```c
void zend_compile_goto(zend_ast *ast)
{
zend_ast *label_ast = ast->child[0];
znode label_node;
zend_op *opline;
uint32_t opnum_start = get_next_op_number(CG(active_op_array));
zend_compile_expr(&label_node, label_ast);
//如果當前在一個循環內則有的情況下是不能簡單跳出循環的
zend_handle_loops_and_finally();
//編譯一條臨時opcode:ZEND_GOTO
opline = zend_emit_op(NULL, ZEND_GOTO, NULL, &label_node);
opline->op1.num = get_next_op_number(CG(active_op_array)) - opnum_start - 1;
opline->extended_value = CG(context).current_brk_cont;
}
```
goto初步被編譯為`ZEND_GOTO`,其中label名稱保存在op2,extended_value記錄的是goto所在循環,如果沒有在循環中這個值就等于-1,op1比較特殊,從上面編譯的過程分析,它的值等于goto之間的opcode數,goto只編譯了一條`ZEND_GOTO`哪來的其他opcode呢?這種情況就是goto在一個循環中,上一節介紹的循環結構中有一個比較特殊:foreach,它在遍歷前會新生成一個zval用于遍歷,這個zval是在循環結束時才被釋放,假如foreach循環體中執行了goto,直接像普通跳轉一樣跳到了別的位置,那么這個zval就無法釋放了,所以這種情況下在goto跳轉前需要先執行這些收尾的opcode,這些opcode就是上面`zend_handle_loops_and_finally()`編譯的,具體的細節這里不再展開,有興趣的可以仔細研究下foreach編譯時`zend_begin_loop()`的特殊處理。
后面的處理就與break、continue一樣了,在`pass_two()`中`ZEND_GOTO`被重置為`ZEND_JMP`,具體的處理過程在`zend_resolve_goto_label()`,比較簡單,不再贅述。
- 目錄
- 第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 概述