<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                合規國際互聯網加速 OSASE為企業客戶提供高速穩定SD-WAN國際加速解決方案。 廣告
                # 附錄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指令如下圖所示: ![](../img/defer.png) 接下來我們詳細介紹下各個環節的改動,一步步實現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語法了,將會生成以下節點: ![](../img/defer_ast.png) __(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數跳過部分函數的調用,如下圖所示。 ![](../img/defer_call.png) 到這一步我們已經完成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)
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看