<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>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                ## 3.2 函數實現 函數,通俗的講就是一組操作的集合,給予特定的輸入將對應特定的輸出。 ### 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 function](https://box.kancloud.cn/1613b76340b7dc4098f0170e15d3d405_860x655.jpg) 函數是在編譯階段確定的,那么它們存在哪呢? 在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; ``` 上面的例子最終生成的語法樹: ![](https://box.kancloud.cn/ee9c33e2366ca443710346f161843ca9_730x552.png) 具體編譯為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: ![](https://box.kancloud.cn/f174cab8d6af8b22f61d77dcedea9362_422x255.png) 總體來看,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以及執行順序: ![](https://box.kancloud.cn/5142256301f0a8bedd00c4d7ba696191_265x93.png) 類的情況也是如此,后面我們再作說明。 #### 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(); ``` ### 3.2.2 內部函數 上一節已經提過,內部函數指的是由內核、擴展提供的C語言編寫的function,這類函數不需要經歷opcode的編譯過程,所以效率上要高于PHP用戶自定義的函數,調用時與普通的C程序沒有差異。 Zend引擎中定義了很多內部函數供用戶在PHP中使用,比如:define、defined、strlen、method_exists、class_exists、function_exists......等等,除了Zend引擎中定義的內部函數,PHP擴展中也提供了大量內部函數,我們也可以靈活的通過擴展自行定制。 #### 3.2.2.1 內部函數結構 上一節介紹`zend_function`為union,其中`internal_function`就是內部函數用到的,具體結構: ```c //zend_complie.h 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; ``` `zend_internal_function`頭部是一個與`zend_op_array`完全相同的common結構。 下面看下如何定義一個內部函數。 #### 3.2.2.2 定義與注冊 內部函數與用戶自定義函數沖突,用戶無法在PHP代碼中覆蓋內部函數,執行PHP腳本時會提示error錯誤。 內部函數的定義非常簡單,我們只需要創建一個普通的C函數,然后創建一個`zend_internal_function`結構添加到 __EG(function_table)__ (也可能是CG(function_table),取決于在哪一階段注冊)中即可使用,內部函數 __通常__ 情況下是在php_module_startup階段注冊的,這里之所以說通常是按照標準的擴展定義,除了擴展提供的方式我們可以在任何階段自由定義內部函數,當然并不建議這樣做。下面我們先不討論擴展標準的定義方式,我們先自己嘗試下如何注冊一個內部函數。 根據`zend_internal_function`的結構我們知道需要定義一個handler: ```c void qp_test(INTERNAL_FUNCTION_PARAMETERS) { printf("call internal function 'qp_test'\n"); } ``` 然后創建一個內部函數結構(我們在擴展PHP_MINIT_FUNCTION方法中注冊,也可以在其他位置): ```c PHP_MINIT_FUNCTION(xxxxxx) { zend_string *lowercase_name; zend_function *reg_function; //函數名轉小寫,因為php的函數不區分大小寫 lowercase_name = zend_string_alloc(7, 1); zend_str_tolower_copy(ZSTR_VAL(lowercase_name), "qp_test", 7); lowercase_name = zend_new_interned_string(lowercase_name); reg_function = malloc(sizeof(zend_internal_function)); reg_function->internal_function.type = ZEND_INTERNAL_FUNCTION; //定義類型為內部函數 reg_function->internal_function.function_name = lowercase_name; reg_function->internal_function.handler = qp_test; zend_hash_add_ptr(CG(function_table), lowercase_name, reg_function); //注冊到CG(function_table)符號表中 } ``` 接著編譯、安裝擴展,測試: ```php qp_test(); ``` 結果輸出: `call internal function 'qp_test'` 這樣一個內部函數就定義完成了。這里有一個地方需要注意的我們把這個函數注冊到 __CG(function_table)__ 中去了,而不是 __EG(function_table)__ ,這是因為在`php_request_startup`階段會把 __CG(function_table)__ 賦值給 __EG(function_table)__ 。 上面的過程看著比較簡單,但是在實際應用中不要這樣做,PHP提供給我們一套標準的定義方式,接下來看下如何在擴展中按照官方方式提供一個內部函數。 首先也是定義C函數,這個通過`PHP_FUNCTION`宏定義: ```c PHP_FUNCTION(qp_test) { printf("call internal function 'qp_test'\n"); } ``` 然后是注冊過程,這個只需要我們將所有的函數數組添加到擴展結構`zend_module_entry.functions`即可,擴展加載過程中會自動進行函數注冊(見1.2節),不需要我們干預: ```c const zend_function_entry xxxx_functions[] = { PHP_FE(qp_test, NULL) PHP_FE_END }; zend_module_entry xxxx_module_entry = { STANDARD_MODULE_HEADER, "擴展名稱", xxxx_functions, PHP_MINIT(timeout), PHP_MSHUTDOWN(timeout), PHP_RINIT(timeout), /* Replace with NULL if there's nothing to do at request start */ PHP_RSHUTDOWN(timeout), /* Replace with NULL if there's nothing to do at request end */ PHP_MINFO(timeout), PHP_TIMEOUT_VERSION, STANDARD_MODULE_PROPERTIES }; ``` 關于更多擴展中函數相關的用法會在后面擴展開發一章中詳細介紹,這里不再展開。
                  <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>

                              哎呀哎呀视频在线观看