深入剖析php執(zhí)行原理(2):函數(shù)的編譯,深入剖析php_PHP教程
Jul 12, 2016 am 08:53 AM深入剖析php執(zhí)行原理(2):函數(shù)的編譯,深入剖析php
本文只探討純粹的函數(shù),并不包含方法。對于方法,會放到類、對象中一起研究。
想講清楚在zend vm中,函數(shù)如何被正確的編譯成op指令、如何發(fā)生參數(shù)傳遞、如何模擬調(diào)用棧、如何切換作用域等等,的確是一個很大范疇的話題。但為了弄明白php的原理,必須要攻克它。
對函數(shù)的研究,大致可以分成兩塊。第一塊是函數(shù)體的編譯,主要涉及到如何將函數(shù)轉(zhuǎn)化成zend_op指令。第二塊是研究函數(shù)的調(diào)用,涉及到函數(shù)調(diào)用語句的編譯,以及函數(shù)如何被執(zhí)行等topic。這里先來看看函數(shù)如何被編譯,我們下一篇再講函數(shù)的調(diào)用。
函數(shù)的編譯
對函數(shù)進行編譯,最終目的是為了生成一份對應(yīng)的op指令集,除了op指令集,編譯函數(shù)還會產(chǎn)生其他一些相關(guān)的數(shù)據(jù),比如說函數(shù)名稱、參數(shù)列表信息、compiled variables,甚至函數(shù)所在文件,起始行數(shù)等等。這些信息作為編譯的產(chǎn)出,都需要保存起來。保存這些產(chǎn)出的數(shù)據(jù)結(jié)構(gòu),正是上一節(jié)中所描述的zend_op_array。下文會以op_array作為簡稱。
下面列出了一個簡單的例子:
<?<span>php </span><span>function</span> foo(<span>$arg1</span><span>) { </span><span>print</span>(<span>$arg1</span><span>); } </span><span>$bar</span> = 'hello php'<span>; foo(</span><span>$bar</span>);
這段代碼包含了一個最簡單的函數(shù)示例。
在這樣一份php腳本中,最終其實會產(chǎn)生兩個op_array。一個是由函數(shù)foo編譯而來,另一個則是由除去foo之外代碼編譯生成的。同理可以推出,假如一份php腳本其中包含有2個函數(shù)和若干語句,則最終會產(chǎn)生3個op_array。也就是說,每個函數(shù)最終都會被編譯成一個對應(yīng)的op_array。
剛才提到,op_array中有一些字段是和函數(shù)息息相關(guān)的。比如function_name代表著函數(shù)的名稱,比如num_args代表了函數(shù)的參數(shù)個數(shù),比如required_num_args代表了必須的參數(shù)個數(shù),比如arg_info代表著函數(shù)的參數(shù)信息...etc。
下面會繼續(xù)結(jié)合這段代碼,來研究foo函數(shù)詳細的編譯過程。
1、語法定義
從zend_language_parser.y文件中可以看出,函數(shù)的語法分析大致涉及如下幾個推導(dǎo)式:
top_statement:<br /> statement { zend_verify_namespace(TSRMLS_C); }<br /> | function_declaration_statement { zend_verify_namespace(TSRMLS_C); zend_do_early_binding(TSRMLS_C); }<br /> | class_declaration_statement { zend_verify_namespace(TSRMLS_C); zend_do_early_binding(TSRMLS_C); }<br /> ...<br /><br />function_declaration_statement: unticked_function_declaration_statement { DO_TICKS(); } ; unticked_function_declaration_statement: function is_reference T_STRING { zend_do_begin_function_declaration(&$1, &$3, 0, $2.op_type, NULL TSRMLS_CC); } '(' parameter_list ')' '{' inner_statement_list '}' { zend_do_end_function_declaration(&$1 TSRMLS_CC); } ;<br /><br />is_reference:<br /> /* empty */ { $$.op_type = ZEND_RETURN_VAL; }<br /> | '&' { $$.op_type = ZEND_RETURN_REF; }<br />;<br /><br />parameter_list:<br /> non_empty_parameter_list<br /> | /* empty */<br />; non_empty_parameter_list: optional_class_type T_VARIABLE { znode tmp; fetch_simple_variable(&tmp, &$2, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$1, &$2, 0 TSRMLS_CC); } | optional_class_type '&' T_VARIABLE { znode tmp; fetch_simple_variable(&tmp, &$3, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$1, &$3, 1 TSRMLS_CC); } | optional_class_type '&' T_VARIABLE '=' static_scalar { znode tmp; fetch_simple_variable(&tmp, &$3, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$5, &$1, &$3, 1 TSRMLS_CC); } | optional_class_type T_VARIABLE '=' static_scalar { znode tmp; fetch_simple_variable(&tmp, &$2, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$4, &$1, &$2, 0 TSRMLS_CC); } | non_empty_parameter_list ',' optional_class_type T_VARIABLE { znode tmp; fetch_simple_variable(&tmp, &$4, 0 TSRMLS_CC); $$=$1; Z_LVAL($$.u.constant)++; zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$3, &$4, 0 TSRMLS_CC); } | non_empty_parameter_list ',' optional_class_type '&' T_VARIABLE { znode tmp; fetch_simple_variable(&tmp, &$5, 0 TSRMLS_CC); $$=$1; Z_LVAL($$.u.constant)++; zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$3, &$5, 1 TSRMLS_CC); } | non_empty_parameter_list ',' optional_class_type '&' T_VARIABLE '=' static_scalar { znode tmp; fetch_simple_variable(&tmp, &$5, 0 TSRMLS_CC); $$=$1; Z_LVAL($$.u.constant)++; zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$7, &$3, &$5, 1 TSRMLS_CC); } | non_empty_parameter_list ',' optional_class_type T_VARIABLE '=' static_scalar { znode tmp; fetch_simple_variable(&tmp, &$4, 0 TSRMLS_CC); $$=$1; Z_LVAL($$.u.constant)++; zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$6, &$3, &$4, 0 TSRMLS_CC); } ;
這里并沒有截取完整,主要是缺少函數(shù)體內(nèi)語句的語法分析,但已經(jīng)足夠我們弄清楚編譯過程中的一些細節(jié)。
函數(shù)體內(nèi)的語句,其對應(yīng)的語法為inner_statement_list。inner_statement_list和函數(shù)體之外一般的語句并無二致,可以簡單當成普通的語句來編譯。
最重要的是看下unticked_function_declaration_statement,它定義了函數(shù)語法的骨架,同時還可以看出,函數(shù)編譯中會執(zhí)行zend_do_begin_function_declaration以及zend_do_end_function_declaration。這兩步分別對應(yīng)著下文提到的開始編譯和結(jié)束編譯。我們先來看zend_do_begin_function_declaration。
2、開始編譯
當解析器遇到一段函數(shù)聲明時,會嘗試開始編譯函數(shù),這是通過執(zhí)行zend_do_begin_function_declaration來完成的。
有兩點:
1,函數(shù)是否返回引用,通過is_reference判斷??梢钥吹皆趯s_reference進行語法分析時,可能會將op_type賦予ZEND_RETURN_VAL或ZEND_RETURN_REF。根據(jù)我們文章開始給出的php代碼示例,函數(shù)foo并不返回引用,因此這里$2.op_type為ZEND_RETURN_VAL。話說由 function & func_name(){ ... } 這種形式來決定是否返回引用,已經(jīng)很古老了,還是在CI框架中見過,現(xiàn)在很少有類似需求。
2,zend_do_begin_function_declaration接受的第一個參數(shù),是對function字面進行詞法分析生成的znode。這個znode被使用得非常巧妙,因為在編譯函數(shù)時,zend vm必須將CG(active_op_array)切換成函數(shù)自己的op_array,以便于存儲函數(shù)的編譯結(jié)果,當函數(shù)編譯完成之后,zend vm又需要將將CG(active_op_array)恢復(fù)成函數(shù)體外層的op_array。利用該znode保存函數(shù)體外的op_array,可以很方便的在函數(shù)編譯結(jié)束時進行CG(active_op_array)恢復(fù),具體后面會講到。
研究下zend_do_begin_function_declaration的實現(xiàn),比較長,我們分段來看:
<span>//</span><span> 聲明函數(shù)會變編譯成的op_array</span> <span>zend_op_array op_array; </span><span>//</span><span> 函數(shù)名、長度、起始行數(shù)</span> <span>char</span> *name = function_name-><span>u.constant.value.str.val; </span><span>int</span> name_len = function_name-><span>u.constant.value.str.len; </span><span>int</span> function_begin_line = function_token-><span>u.opline_num; zend_uint fn_flags; </span><span>char</span> *<span>lcname; zend_bool orig_interactive; ALLOCA_FLAG(use_heap) </span><span>if</span><span> (is_method) { ... } </span><span>else</span><span> { fn_flags </span>= <span>0</span><span>; } </span><span>//</span><span> 對函數(shù)來說,fn_flags沒用,對方法來說,fn_flags指定了方法的修飾符</span> <span>if</span> ((fn_flags & ZEND_ACC_STATIC) && (fn_flags & ZEND_ACC_ABSTRACT) && !(CG(active_class_entry)->ce_flags &<span> ZEND_ACC_INTERFACE)) { zend_error(E_STRICT, </span><span>"</span><span>Static function %s%s%s() should not be abstract</span><span>"</span>, is_method ? CG(active_class_entry)->name : <span>""</span>, is_method ? <span>"</span><span>::</span><span>"</span> : <span>""</span>, Z_STRVAL(function_name-><span>u.constant)); }</span>
這段代碼一開始就印證了我們先前的說法,每個函數(shù)都有一份自己的op_array。所以會在開頭先聲明一個op_array變量。
<span>//</span><span> 第一個znode參數(shù)的妙處,它記錄了當前的CG(active_op_array)</span> function_token->u.op_array =<span> CG(active_op_array); lcname </span>=<span> zend_str_tolower_dup(name, name_len); </span><span>//</span><span> 對op_array進行初始化,強制op_array.fn_flags會被初始化為0</span> orig_interactive =<span> CG(interactive); CG(interactive) </span>= <span>0</span><span>; init_op_array(</span>&<span>op_array, ZEND_USER_FUNCTION, INITIAL_OP_ARRAY_SIZE TSRMLS_CC); CG(interactive) </span>=<span> orig_interactive; </span><span>//</span><span> 對op_array的一些設(shè)置</span> op_array.function_name =<span> name; op_array.return_reference </span>=<span> return_reference; op_array.fn_flags </span>|=<span> fn_flags; op_array.pass_rest_by_reference </span>= <span>0</span><span>; op_array.scope </span>= is_method ?<span> CG(active_class_entry):NULL; op_array.prototype </span>=<span> NULL; op_array.line_start </span>= zend_get_compiled_lineno(TSRMLS_C);
function_token便是對function字面進行詞法分析而生成的znode。這段代碼一開始,就讓它保存當前的CG(active_op_array),即函數(shù)體之外的op_array。保存好CG(active_op_array)之后,便會開始對函數(shù)自己的op_array進行初始化。
op_array.fn_flags是個多功能字段,還記得上一篇中提到的交互式么,如果php以交互式打開,則op_array.fn_flags會被初始化為ZEND_ACC_INTERACTIVE,否則會被初始化為0。這里在init_op_array之前設(shè)置CG(interactive) = 0,便是確保op_array.fn_flags初始化為0。隨后會進一步執(zhí)行op_array.fn_flags |= fn_flags,如果是在方法中,則op_array.fn_flags含義為static、abstract、final等修飾符,對函數(shù)來講,op_array.fn_flags依然是0。
zend_op *opline =<span> get_next_op(CG(active_op_array) TSRMLS_CC); </span><span>//</span><span> 如果處于命名空間,則函數(shù)名還需要加上命名空間</span> <span>if</span><span> (CG(current_namespace)) { </span><span>/*</span><span> Prefix function name with current namespace name </span><span>*/</span><span> znode tmp; tmp.u.constant </span>= *<span>CG(current_namespace); zval_copy_ctor(</span>&<span>tmp.u.constant); zend_do_build_namespace_name(</span>&tmp, &<span>tmp, function_name TSRMLS_CC); op_array.function_name </span>=<span> Z_STRVAL(tmp.u.constant); efree(lcname); name_len </span>=<span> Z_STRLEN(tmp.u.constant); lcname </span>=<span> zend_str_tolower_dup(Z_STRVAL(tmp.u.constant), name_len); } </span><span>//</span><span> 設(shè)置opline</span> opline->opcode =<span> ZEND_DECLARE_FUNCTION; </span><span>//</span><span> 第一個操作數(shù)</span> opline->op1.op_type =<span> IS_CONST; build_runtime_defined_function_key(</span>&opline-><span>op1.u.constant, lcname, name_len TSRMLS_CC); </span><span>//</span><span> 第二個操作數(shù)</span> opline->op2.op_type =<span> IS_CONST; opline</span>->op2.u.constant.type =<span> IS_STRING; opline</span>->op2.u.constant.value.str.val =<span> lcname; opline</span>->op2.u.constant.value.str.len =<span> name_len; Z_SET_REFCOUNT(opline</span>->op2.u.constant, <span>1</span><span>); opline</span>->extended_value =<span> ZEND_DECLARE_FUNCTION; </span><span>//</span><span> 切換CG(active_op_array)成函數(shù)自己的op_array</span> zend_hash_update(CG(function_table), opline->op1.u.constant.value.str.val, opline->op1.u.constant.value.str.len, &op_array, <span>sizeof</span>(zend_op_array), (<span>void</span> **) &CG(active_op_array));
上面這段代碼很關(guān)鍵。有幾點要說明的:
1,如果函數(shù)是處于命名空間中,則其名稱會被擴展成命名空間\函數(shù)名。比如:
<?<span>php namespace MyProject; </span><span>function</span> foo(<span>$arg1</span>, <span>$arg2</span> = 100<span>) { </span><span>print</span>(<span>$arg1</span><span>); }</span>
則會將函數(shù)名改為MyProject\foo。擴展工作由zend_do_build_namespace_name來完成。
2,build_runtime_defined_function_key會生成一個“key”。除了用到函數(shù)名稱之外,還用到了函數(shù)所在文件路徑、代碼在內(nèi)存中的地址等等。具體的實現(xiàn)可以自行閱讀。將函數(shù)放進CG(function_table)時,用的鍵便是這個“key”。
3,代碼中的op_line獲取時,尚未發(fā)生CG(active_op_array)的切換。也就是說,op_line依然是外層op_array的一條指令。該指令具體為ZEND_DECLARE_FUNCTION,有兩個操作數(shù),第一個操作數(shù)保存了第二點中提到的“key”,第二個操作數(shù)則保存了形如"myproject\foo"這樣的函數(shù)名(小寫)。
4,這段代碼的最后,將函數(shù)自身對應(yīng)的op_array存放進了CG(function_table),同時,完成了CG(active_op_array)的切換。從這條語句開始,CG(active_op_array)便開始指向函數(shù)自己的op_array,而不再是函數(shù)體外層的op_array了。
繼續(xù)來看zend_do_begin_function_declaration的最后一段:
<span>//</span><span> 需要debuginfo,則函數(shù)體內(nèi)的第一條zend_op,為ZEND_EXT_NOP</span> <span>if</span> (CG(compiler_options) &<span> ZEND_COMPILE_EXTENDED_INFO) { zend_op </span>*opline =<span> get_next_op(CG(active_op_array) TSRMLS_CC); opline</span>->opcode =<span> ZEND_EXT_NOP; opline</span>->lineno =<span> function_begin_line; SET_UNUSED(opline</span>-><span>op1); SET_UNUSED(opline</span>-><span>op2); } </span><span>//</span><span> 控制switch和foreach內(nèi)聲明的函數(shù)</span> <span>{ </span><span>/*</span><span> Push a seperator to the switch and foreach stacks </span><span>*/</span><span> zend_switch_entry switch_entry; switch_entry.cond.op_type </span>=<span> IS_UNUSED; switch_entry.default_case </span>= <span>0</span><span>; switch_entry.control_var </span>= <span>0</span><span>; zend_stack_push(</span>&CG(switch_cond_stack), (<span>void</span> *) &switch_entry, <span>sizeof</span><span>(switch_entry)); { </span><span>/*</span><span> Foreach stack separator </span><span>*/</span><span> zend_op dummy_opline; dummy_opline.result.op_type </span>=<span> IS_UNUSED; dummy_opline.op1.op_type </span>=<span> IS_UNUSED; zend_stack_push(</span>&, (<span>void</span> *) &dummy_opline, <span>sizeof</span><span>(zend_op)); } } </span><span>//</span><span> 保存函數(shù)的注釋語句</span> <span>if</span><span> (CG(doc_comment)) { CG(active_op_array)</span>->doc_comment =<span> CG(doc_comment); CG(active_op_array)</span>->doc_comment_len =<span> CG(doc_comment_len); CG(doc_comment) </span>=<span> NULL; CG(doc_comment_len) </span>= <span>0</span><span>; } </span><span>//</span><span> 作用和上面switch,foreach是一樣的,函數(shù)體內(nèi)的語句并不屬于函數(shù)體外的label</span> zend_stack_push(&CG(labels_stack), (<span>void</span> *) &CG(labels), <span>sizeof</span>(HashTable*<span>)); CG(labels) </span>= NULL;
可能初學(xué)者會對CG(switch_cond_stack),CG(foreach_copy_stack),CG(labels_stack)等字段有疑惑。其實也很好理解。以CG(labels_stack)為例,由于進入函數(shù)體內(nèi)之后,op_array發(fā)生了切換,外層的CG(active_op_array)被保存到function znode的u.op_array中(如果記不清楚了回頭看上文:-))。因此函數(shù)外層已經(jīng)被parse出的一些label也需要被保存下來,用的正是CG(labels_stack)來保存。當函數(shù)體完成編譯之后,zend vm可以從CG(labels_stack)中恢復(fù)出原先的label。舉例來說,
<?<span>php label1</span>: <span>function</span> foo(<span>$arg1</span><span>) { </span><span>print</span>(<span>$arg1</span><span>); goto label2; label2</span>: <span>exit</span><span>; } </span><span>$bar</span> = 'hello php'<span>; foo(</span><span>$bar</span>);
解釋器在進入zend_do_begin_function_declaration時,CG(labels)中保存的是“label1”。當解釋器開始編譯函數(shù)foo,則需要將“label1”保存到CG(labels_stack)中,同時清空CG(labels)。因為在編譯foo的過程中,CG(labels)會保存“labe2”。當foo編譯完成,會利用CG(labels_stack)來恢復(fù)CG(labels),則CG(labels)再次變成“label1”。
至此,整個zend_do_begin_function_declaration過程已經(jīng)全部分析完成。最重要的是,一旦完成zend_do_begin_function_declaration,CG(active_op_array)就指向了函數(shù)自身對應(yīng)的op_array。同時,也利用生成的“key”在CG(function_table)中替函數(shù)占了一個位。
3、編譯參數(shù)列表
函數(shù)可以定義為不接受任何參數(shù),對于參數(shù)列表為空的情況,其實不做任何處理。我們前文的例子foo函數(shù),接受了一個參數(shù)$arg1,我們下面還是分析有參數(shù)的情況。
根據(jù)語法推導(dǎo)式non_empty_parameter_list的定義,參數(shù)列表一共有8種,前4種對應(yīng)的是一個參數(shù),后4種對應(yīng)多個參數(shù)。我們只關(guān)心前4種,后4種編譯的過程,僅僅是重復(fù)前4種的步驟而已。
optional_class_type T_VARIABLE { znode tmp; fetch_simple_variable(&tmp, &$2, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$1, &$2, 0 TSRMLS_CC); } optional_class_type '&' T_VARIABLE { znode tmp; fetch_simple_variable(&tmp, &$3, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV, &tmp, &$$, NULL, &$1, &$3, 1 TSRMLS_CC); } optional_class_type '&' T_VARIABLE '=' static_scalar { znode tmp; fetch_simple_variable(&tmp, &$3, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$5, &$1, &$3, 1 TSRMLS_CC); } optional_class_type T_VARIABLE '=' static_scalar { znode tmp; fetch_simple_variable(&tmp, &$2, 0 TSRMLS_CC); $$.op_type = IS_CONST; Z_LVAL($$.u.constant)=1; Z_TYPE($$.u.constant)=IS_LONG; INIT_PZVAL(&$$.u.constant); zend_do_receive_arg(ZEND_RECV_INIT, &tmp, &$$, &$4, &$1, &$2, 0 TSRMLS_CC); }
前4種情況,具體又可以分為2類,1類沒有默認值,區(qū)別只在于參數(shù)的傳遞是否采用引用,而另1類,都有默認值“static_scalar”。
實際上區(qū)別并不大,它們的語法分析的處理過程也幾乎一致。都是先調(diào)用fetch_simple_variable,再執(zhí)行zend_do_receive_arg。有沒有默認值,區(qū)別也僅僅在于zend_do_receive_arg的參數(shù),會不會將默認值傳遞進去。先來看fetch_simple_variable。
3.1 fetch_simple_variable
fetch_simple_variable是用來獲取compiled variables索引的。compiled variables被視作php的性能提升手段之一,因為它利用數(shù)組存儲了變量,而并非內(nèi)核中普遍使用的HashTable。這里可以看出,函數(shù)的任何一個參數(shù),均會被編譯為compiled variables,compiled variables被保存在函數(shù)體op_array->vars數(shù)組中。雖然根據(jù)變量名稱去HashTable查詢,效率并不低。但顯然根據(jù)索引去op_array->vars數(shù)組中獲取變量,會更加高效。
<span>void</span> fetch_simple_variable_ex(znode *result, znode *varname, <span>int</span> bp, zend_uchar op TSRMLS_DC) <span>/*</span><span> {{{ </span><span>*/</span><span> { zend_op opline; ... </span><span>if</span> (varname->op_type ==<span> IS_CONST) { </span><span>if</span> (Z_TYPE(varname->u.constant) !=<span> IS_STRING) { convert_to_string(</span>&varname-><span>u.constant); } </span><span>if</span> (!zend_is_auto_global(varname->u.constant.value.str.val, varname->u.constant.value.str.len TSRMLS_CC) && !(varname->u.constant.value.str.len == (<span>sizeof</span>(<span>"</span><span>this</span><span>"</span>)-<span>1</span>) && !memcmp(varname->u.constant.value.str.val, <span>"</span><span>this</span><span>"</span>, <span>sizeof</span>(<span>"</span><span>this</span><span>"</span>))) &&<span> (CG(active_op_array)</span>->last == <span>0</span> || CG(active_op_array)->opcodes[CG(active_op_array)->last-<span>1</span>].opcode !=<span> ZEND_BEGIN_SILENCE)) { </span><span>//</span><span> 節(jié)點的類型為IS_CV,表明是compiled variables</span> result->op_type =<span> IS_CV; </span><span>//</span><span> 用u.var來記錄compiled variables在CG(active_op_array)->vars中的索引</span> result->u.<span>var</span> = lookup_cv(CG(active_op_array), varname->u.constant.value.str.val, varname-><span>u.constant.value.str.len); result</span>->u.EA.type = <span>0</span><span>; varname</span>->u.constant.value.str.val = CG(active_op_array)->vars[result->u.<span>var</span><span>].name; </span><span>return</span><span>; } } ... }</span>
這里不做詳細的分析了。當fetch_simple_variable獲取索引之后,znode中就不必再保存變量的名稱,取而代之的是變量在vars數(shù)組中的索引,即znode->u.var,其類型為int。fetch_simple_variable完成,會進入zend_do_receive_arg。
3.2 zend_do_receive_arg
zend_do_receive_arg目的是生成一條zend op指令,可以稱作RECV。
一般而言,除非函數(shù)不存在參數(shù),否則RECV是函數(shù)的第一條指令(這里表述不準,有extend info時也不是第一條)。該指令的opcode可能為ZEND_RECV或者ZEND_RECV_INIT,取決于是否有默認值。如果參數(shù)沒有默認值,指令等于ZEND_RECV,有默認值,則為ZEND_RECV_INIT。zend_do_receive_arg的第二個參數(shù),就是上面提到的compiled variables節(jié)點。
分析下zend_do_receive_arg的源碼,也是分幾段來看:
zend_op *<span>opline; zend_arg_info </span>*<span>cur_arg_info; </span><span>//</span><span> class_type主要用于限制函數(shù)參數(shù)的類型</span> <span>if</span> (class_type->op_type == IS_CONST && Z_TYPE(class_type->u.constant) == IS_STRING && Z_STRLEN(class_type->u.constant) == <span>0</span><span>) { </span><span>/*</span><span> Usage of namespace as class name not in namespace </span><span>*/</span><span> zval_dtor(</span>&class_type-><span>u.constant); zend_error(E_COMPILE_ERROR, </span><span>"</span><span>Cannot use 'namespace' as a class name</span><span>"</span><span>); </span><span>return</span><span>; } </span><span>//</span><span> 對靜態(tài)方法來說,參數(shù)不能為this</span> <span>if</span> (<span>var</span>->op_type == IS_CV && <span>var</span>->u.<span>var</span> == CG(active_op_array)->this_var && (CG(active_op_array)->fn_flags & ZEND_ACC_STATIC) == <span>0</span><span>) { zend_error(E_COMPILE_ERROR, </span><span>"</span><span>Cannot re-assign $this</span><span>"</span><span>); } </span><span>else</span> <span>if</span> (<span>var</span>->op_type == IS_VAR && CG(active_op_array)->scope && ((CG(active_op_array)->fn_flags & ZEND_ACC_STATIC) == <span>0</span>) && (Z_TYPE(varname->u.constant) == IS_STRING) && (Z_STRLEN(varname->u.constant) == <span>sizeof</span>(<span>"</span><span>this</span><span>"</span>)-<span>1</span>) && (memcmp(Z_STRVAL(varname->u.constant), <span>"</span><span>this</span><span>"</span>, <span>sizeof</span>(<span>"</span><span>this</span><span>"</span>)) == <span>0</span><span>)) { zend_error(E_COMPILE_ERROR, </span><span>"</span><span>Cannot re-assign $this</span><span>"</span><span>); } </span><span>//</span><span> CG(active_op_array)此時已經(jīng)是函數(shù)體的op_array了,這里拿一條指令</span> opline =<span> get_next_op(CG(active_op_array) TSRMLS_CC); CG(active_op_array)</span>->num_args++<span>; opline</span>->opcode =<span> op; opline</span>->result = *<span>var</span><span>; </span><span>//</span><span> op1節(jié)點表明是第幾個參數(shù)</span> opline->op1 = *<span>offset; </span><span>//</span><span> op2節(jié)點可能為初始值,也可能為UNUSED</span> <span>if</span> (op ==<span> ZEND_RECV_INIT) { opline</span>->op2 = *<span>initialization; } </span><span>else</span><span> { CG(active_op_array)</span>->required_num_args = CG(active_op_array)-><span>num_args; SET_UNUSED(opline</span>-><span>op2); }</span>
上面這段代碼,首先通過get_next_op(CG(active_op_array) TSRMLS_CC)一句獲取了opline,opline是未被使用的一條zend_op指令。緊接著,會對opline的各個字段進行設(shè)置。opline->op1表明這是第幾個參數(shù),opline->op2可能為初始值,也可能被設(shè)置為UNUSED。
如果一個參數(shù)有默認值,那么在調(diào)用函數(shù)時,其實是可以不用傳遞該參數(shù)的。所以,required_num_args不會將這類非必須的參數(shù)算進去的。可以看到,在op == ZEND_RECV_INIT這段邏輯分支中,并沒有處理required_num_args。
繼續(xù)來看:
<span>//</span><span> 這里采用erealloc進行分配,因為期望最終會形成一個參數(shù)信息的數(shù)組</span> CG(active_op_array)->arg_info = erealloc(CG(active_op_array)->arg_info, <span>sizeof</span>(zend_arg_info)*(CG(active_op_array)-><span>num_args)); </span><span>//</span><span> 設(shè)置當前的zend_arg_info</span> cur_arg_info = &CG(active_op_array)->arg_info[CG(active_op_array)->num_args-<span>1</span><span>]; cur_arg_info</span>->name = estrndup(varname->u.constant.value.str.val, varname-><span>u.constant.value.str.len); cur_arg_info</span>->name_len = varname-><span>u.constant.value.str.len; cur_arg_info</span>->array_type_hint = <span>0</span><span>; cur_arg_info</span>->allow_null = <span>1</span><span>; cur_arg_info</span>->pass_by_reference =<span> pass_by_reference; cur_arg_info</span>->class_name =<span> NULL; cur_arg_info</span>->class_name_len = <span>0</span><span>; </span><span>//</span><span> 如果需要對參數(shù)做類型限定</span> <span>if</span> (class_type->op_type !=<span> IS_UNUSED) { cur_arg_info</span>->allow_null = <span>0</span><span>; </span><span>//</span><span> 限定為類</span> <span>if</span> (class_type->u.constant.type ==<span> IS_STRING) { </span><span>if</span> (ZEND_FETCH_CLASS_DEFAULT == zend_get_class_fetch_type(Z_STRVAL(class_type->u.constant), Z_STRLEN(class_type-><span>u.constant))) { zend_resolve_class_name(class_type, </span>&opline->extended_value, <span>1</span><span> TSRMLS_CC); } cur_arg_info</span>->class_name = class_type-><span>u.constant.value.str.val; cur_arg_info</span>->class_name_len = class_type-><span>u.constant.value.str.len; </span><span>//</span><span> 如果限定為類,則參數(shù)的默認值只能為NULL</span> <span>if</span> (op ==<span> ZEND_RECV_INIT) { </span><span>if</span> (Z_TYPE(initialization->u.constant) == IS_NULL || (Z_TYPE(initialization->u.constant) == IS_CONSTANT && !strcasecmp(Z_STRVAL(initialization->u.constant), <span>"</span><span>NULL</span><span>"</span><span>))) { cur_arg_info</span>->allow_null = <span>1</span><span>; } </span><span>else</span><span> { zend_error(E_COMPILE_ERROR, </span><span>"</span><span>Default value for parameters with a class type hint can only be NULL</span><span>"</span><span>); } } } </span><span>//</span><span> 限定為數(shù)組</span> <span>else</span><span> { </span><span>//</span><span> 將array_type_hint設(shè)置為1</span> cur_arg_info->array_type_hint = <span>1</span><span>; cur_arg_info</span>->class_name =<span> NULL; cur_arg_info</span>->class_name_len = <span>0</span><span>; </span><span>//</span><span> 如果限定為數(shù)組,則參數(shù)的默認值只能為數(shù)組或NULL</span> <span>if</span> (op ==<span> ZEND_RECV_INIT) { </span><span>if</span> (Z_TYPE(initialization->u.constant) == IS_NULL || (Z_TYPE(initialization->u.constant) == IS_CONSTANT && !strcasecmp(Z_STRVAL(initialization->u.constant), <span>"</span><span>NULL</span><span>"</span><span>))) { cur_arg_info</span>->allow_null = <span>1</span><span>; } </span><span>else</span> <span>if</span> (Z_TYPE(initialization->u.constant) != IS_ARRAY && Z_TYPE(initialization->u.constant) !=<span> IS_CONSTANT_ARRAY) { zend_error(E_COMPILE_ERROR, </span><span>"</span><span>Default value for parameters with array type hint can only be an array or NULL</span><span>"</span><span>); } } } } opline</span>->result.u.EA.type |= EXT_TYPE_UNUSED;
這部分代碼寫的很清晰。注意,對于限定為數(shù)組的情況,class_type的op_type會被設(shè)置為IS_CONST,而u.constant.type會被設(shè)置為IS_NULL:
optional_class_type: /* empty */ { $$.op_type = IS_UNUSED; } | fully_qualified_class_name { $$ = $1; } | T_ARRAY { $$.op_type = IS_CONST; Z_TYPE($$.u.constant)=IS_NULL;}
因此,zend_do_receive_arg中區(qū)分限定為類還是數(shù)組,是利用class_type->u.constant.type == IS_STRING來判斷的。如果類型限定為數(shù)組,則cur_arg_info->array_type_hint會被設(shè)置為1。
還有另一個地方需要了解,zend_resolve_class_name函數(shù)會修正類名。舉例來說:
<?<span>php namespace A; </span><span>class</span><span> B { } </span><span>function</span> foo(B <span>$arg1</span>, <span>$arg2</span> = 100<span>) { </span><span>print</span>(<span>$arg1</span><span>); }</span>
我們期望參數(shù)arg1的類型為B,class_type中也保存了B。但是因為位于命名空間A下,所以,zend_resolve_class_name會將class_type中保存的類名B,修正為A\B。
OK,到這里,zend_do_receive_arg已經(jīng)全部分析完。zend vm在分析函數(shù)參數(shù)時,每遇見一個參數(shù),便會調(diào)用一次zend_do_receive_arg,生成一條RECV指令。因此,函數(shù)有幾個參數(shù),就會編譯出幾條RECV指令。
4、編譯函數(shù)體
當編譯完參數(shù)列表,zend vm便會進入函數(shù)內(nèi)部了。函數(shù)體的編譯其實和正常語句的編譯一樣。zend vm只需要將函數(shù)體內(nèi)部的php語句,按照正常的statment,進行詞法分析、語法分析來處理,最終形成一條條zend_op指令。
來看下語法文件:
unticked_function_declaration_statement: function is_reference T_STRING { zend_do_begin_function_declaration(&$1, &$3, 0, $2.op_type, NULL TSRMLS_CC); } '(' parameter_list ')' '{' inner_statement_list '}' { zend_do_end_function_declaration(&$1 TSRMLS_CC); }<br />;
函數(shù)體內(nèi)部的語句,表示為inner_statement_list。
inner_statement_list: inner_statement_list { zend_do_extended_info(TSRMLS_C); } inner_statement { HANDLE_INTERACTIVE(); } | /* empty */ ;
而inner_statment正是由語句、函數(shù)聲明、類聲明組成的。
inner_statement: statement | function_declaration_statement | class_declaration_statement | T_HALT_COMPILER '(' ')' ';' { zend_error(E_COMPILE_ERROR, "__HALT_COMPILER() can only be used from the outermost scope"); } ;
inner_statement并非專門用于函數(shù),其他譬如foreach,while循環(huán)等有block語句塊中,都會被識別為inner_statement。從這里其實還能看到一些有意思的語法,比如說我們可以在函數(shù)里聲明一個類。inner_statement就不展開敘述了,否則相當于將整個php的語法捋一遍,情況太多了。
5、結(jié)束編譯
我們最后來看下結(jié)束編譯的過程。結(jié)束函數(shù)編譯是通過zend_do_end_function_declaration來完成的。
zend_do_end_function_declaration接收的參數(shù)function_token,其實就是前面提到過的function字面對應(yīng)的znode。根據(jù)我們在“開始編譯”一節(jié)所述,function_token中保留了函數(shù)體之外的op_array。
<span>char</span> lcname[<span>16</span><span>]; </span><span>int</span><span> name_len; zend_do_extended_info(TSRMLS_C); </span><span>//</span><span> 返回NULL</span> zend_do_return(NULL, <span>0</span><span> TSRMLS_CC); </span><span>//</span><span> 通過op指令設(shè)置對應(yīng)的handler函數(shù)</span> <span>pass_two(CG(active_op_array) TSRMLS_CC); </span><span>//</span><span> 釋放當前函數(shù)的CG(labels),并從CG(labels_stack)中還原之前的CG(labels)</span> <span>zend_release_labels(TSRMLS_C); </span><span>if</span><span> (CG(active_class_entry)) { </span><span>//</span><span> 檢查魔術(shù)方法的參數(shù)是否合法</span> zend_check_magic_method_implementation(CG(active_class_entry), (zend_function*<span>)CG(active_op_array), E_COMPILE_ERROR TSRMLS_CC); } </span><span>else</span><span> { </span><span>/*</span><span> we don't care if the function name is longer, in fact lowercasing only * the beginning of the name speeds up the check process </span><span>*/</span><span> name_len </span>= strlen(CG(active_op_array)-><span>function_name); zend_str_tolower_copy(lcname, CG(active_op_array)</span>->function_name, MIN(name_len, <span>sizeof</span>(lcname)-<span>1</span><span>)); lcname[</span><span>sizeof</span>(lcname)-<span>1</span>] = <span>'</span><span>\0</span><span>'</span>; <span>/*</span><span> zend_str_tolower_copy won't necessarily set the zero byte </span><span>*/</span> <span>//</span><span> 檢查__autoload函數(shù)的參數(shù)是否合法</span> <span>if</span> (name_len == <span>sizeof</span>(ZEND_AUTOLOAD_FUNC_NAME) - <span>1</span> && !memcmp(lcname, ZEND_AUTOLOAD_FUNC_NAME, <span>sizeof</span>(ZEND_AUTOLOAD_FUNC_NAME)) && CG(active_op_array)->num_args != <span>1</span><span>) { zend_error(E_COMPILE_ERROR, </span><span>"</span><span>%s() must take exactly 1 argument</span><span>"</span><span>, ZEND_AUTOLOAD_FUNC_NAME); } } CG(active_op_array)</span>->line_end =<span> zend_get_compiled_lineno(TSRMLS_C); </span><span>//</span><span> 很關(guān)鍵!將CG(active_op_array)還原成函數(shù)外層的op_array</span> CG(active_op_array) = function_token-><span>u.op_array; </span><span>/*</span><span> Pop the switch and foreach seperators </span><span>*/</span><span> zend_stack_del_top(</span>&<span>CG(switch_cond_stack)); zend_stack_del_top(</span>&CG(foreach_copy_stack));
有3處值得注意:
1,zend_do_end_function_declaration中會對CG(active_op_array)進行還原。用的正是function_token->u.op_array。一旦zend_do_end_function_declaration完成,函數(shù)的整個編譯過程就已經(jīng)結(jié)束了。zend vm會繼續(xù)看接下來函數(shù)之外的代碼,所以需要將CG(active_op_array)切換成原先的。
2,zend_do_return負責在函數(shù)最后添加上一條RETURN指令,因為我們傳進去的是NULL,所以這條RETURN指令的操作數(shù)被強制設(shè)置為UNUSED。注意,不管函數(shù)本身是否有return語句,最后這條RETURN指令是必然存在的。假如函數(shù)有return語句,return語句也會產(chǎn)生一條RETURN指令,所以會導(dǎo)致可能出現(xiàn)多條RETURN指令。舉例來說:
<span>function</span> foo(<span>)<br /><span>{</span> </span><span>return</span> <span>true</span><span>; }</span>
編譯出來的OP指令最后兩條如下:
RETURN true RETURN null
我們可以很明顯在最后看到兩條RETURN。一條是通過return true編譯出來的。另一條,就是在zend_do_end_function_declaration階段,強制插入的RETURN。
3,我們剛才講解的所有步驟中,都只是設(shè)置了每條指令的opcode,而并沒有設(shè)置這條指令具體的handle函數(shù)。pass_two會負責遍歷每條zend_op指令,根據(jù)opcode,以及操作數(shù)op1和op2,去查找并且設(shè)置對應(yīng)的handle函數(shù)。這項工作,是通過ZEND_VM_SET_OPCODE_HANDLER(opline)宏來完成的。
<span>#define</span> ZEND_VM_SET_OPCODE_HANDLER(opline) zend_vm_set_opcode_handler(opline)
zend_vm_set_opcode_handler的實現(xiàn)很簡單:
<span>void</span> zend_init_opcodes_handlers(<span>void</span><span>) { </span><span>//</span><span> 超大的數(shù)組,里面存放了所有的handler</span> <span>static</span> <span>const</span> opcode_handler_t labels[] =<span> { ZEND_NOP_SPEC_HANDLER, ZEND_NOP_SPEC_HANDLER, ZEND_NOP_SPEC_HANDLER, ZEND_NOP_SPEC_HANDLER, ZEND_NOP_SPEC_HANDLER, ZEND_NOP_SPEC_HANDLER, ... }; zend_opcode_handlers </span>= (opcode_handler_t*<span>)labels; } </span><span>static</span> opcode_handler_t zend_vm_get_opcode_handler(zend_uchar opcode, zend_op*<span> op) { </span><span>static</span> <span>const</span> <span>int</span> zend_vm_decode[] =<span> { _UNUSED_CODE, </span><span>/*</span><span> 0 </span><span>*/</span><span> _CONST_CODE, </span><span>/*</span><span> 1 = IS_CONST </span><span>*/</span><span> _TMP_CODE, </span><span>/*</span><span> 2 = IS_TMP_VAR </span><span>*/</span><span> _UNUSED_CODE, </span><span>/*</span><span> 3 </span><span>*/</span><span> _VAR_CODE, </span><span>/*</span><span> 4 = IS_VAR </span><span>*/</span><span> _UNUSED_CODE, </span><span>/*</span><span> 5 </span><span>*/</span><span> _UNUSED_CODE, </span><span>/*</span><span> 6 </span><span>*/</span><span> _UNUSED_CODE, </span><span>/*</span><span> 7 </span><span>*/</span><span> _UNUSED_CODE, </span><span>/*</span><span> 8 = IS_UNUSED </span><span>*/</span><span> _UNUSED_CODE, </span><span>/*</span><span> 9 </span><span>*/</span><span> _UNUSED_CODE, </span><span>/*</span><span> 10 </span><span>*/</span><span> _UNUSED_CODE, </span><span>/*</span><span> 11 </span><span>*/</span><span> _UNUSED_CODE, </span><span>/*</span><span> 12 </span><span>*/</span><span> _UNUSED_CODE, </span><span>/*</span><span> 13 </span><span>*/</span><span> _UNUSED_CODE, </span><span>/*</span><span> 14 </span><span>*/</span><span> _UNUSED_CODE, </span><span>/*</span><span> 15 </span><span>*/</span><span> _CV_CODE </span><span>/*</span><span> 16 = IS_CV </span><span>*/</span><span> }; </span><span>//</span><span> 去handler數(shù)組里找到對應(yīng)的處理函數(shù)</span> <span>return</span> zend_opcode_handlers[opcode * <span>25</span> + zend_vm_decode[op->op1.op_type] * <span>5</span> + zend_vm_decode[op-><span>op2.op_type]]; } ZEND_API </span><span>void</span> zend_vm_set_opcode_handler(zend_op*<span> op) { </span><span>//</span><span> 給zend op設(shè)置對應(yīng)的handler函數(shù)</span> op->handler = zend_vm_get_opcode_handler(zend_user_opcodes[op-><span>opcode], op); }</span>
所有的opcode都定義在zend_vm_opcodes.h里,從php5.3-php5.6,大概從150增長到170個opcode。上面可以看到通過opcode查找handler的準確算法:
zend_opcode_handlers[opcode * <span>25</span> + zend_vm_decode[op->op1.op_type] * <span>5</span> + zend_vm_decode[op->op2.op_type]
不過zend_opcode_handlers數(shù)組太大了...找起來很麻煩。
下面回到文章開始的那段php代碼,我們將函數(shù)foo進行編譯,最終得到的指令如下:
可以看出,因為foo指接受一個參數(shù),所以這里只有一條RECV指令。
print語句的參數(shù)為!0,!0是一個compiled variables,其實就是參數(shù)中的arg1。0代表著索引,回憶一下,函數(shù)的op_array有一個數(shù)組專門用于保存compiled variables,0表明arg1位于該數(shù)組的開端。
print語句有返回值,所以會存在一個臨時變量保存其返回值,即~0。由于我們在函數(shù)中并未使用~0,所以隨即便會有一條FREE指令對其進行釋放。
在函數(shù)的最后,是一條RETURN指令。
6、綁定
函數(shù)編譯完成之后,還需要進行的一步是綁定。zend vm通過zend_do_early_binding來實現(xiàn)綁定。這個名字容易讓人產(chǎn)生疑惑,其實只有在涉及到類和方法的時候,才會有早期綁定,與之相對的是延遲綁定,或者叫后期綁定。純粹函數(shù)談不上這種概念,不過zend_do_early_binding是多功能的,并非僅僅為綁定方法而實現(xiàn)。
來看下zend_do_early_binding:
<span>//</span><span> 拿到的是最近一條zend op,對于函數(shù)來說,就是ZEND_DECLARE_FUNCTION</span> zend_op *opline = &CG(active_op_array)->opcodes[CG(active_op_array)->last-<span>1</span><span>]; HashTable </span>*<span>table; </span><span>while</span> (opline->opcode == ZEND_TICKS && opline > CG(active_op_array)-><span>opcodes) { opline</span>--<span>; } </span><span>switch</span> (opline-><span>opcode) { </span><span>case</span><span> ZEND_DECLARE_FUNCTION: </span><span>//</span><span> 真正綁定函數(shù)</span> <span>if</span> (do_bind_function(opline, CG(function_table), <span>1</span>) ==<span> FAILURE) { </span><span>return</span><span>; } table </span>=<span> CG(function_table); </span><span>break</span><span>; </span><span>case</span><span> ZEND_DECLARE_CLASS: ... </span><span>case</span><span> ZEND_DECLARE_INHERITED_CLASS: ... } </span><span>//</span><span> op1中保存的是函數(shù)的key,這里其從將CG(function_table)中刪除</span> zend_hash_del(table, opline->op1.u.constant.value.str.val, opline-><span>op1.u.constant.value.str.len); zval_dtor(</span>&opline-><span>op1.u.constant); zval_dtor(</span>&opline-><span>op2.u.constant); </span><span>//</span><span> opline置為NOP</span> MAKE_NOP(opline);
這個函數(shù)實現(xiàn)也很簡單,主要就是調(diào)用了do_bind_function。
ZEND_API <span>int</span> do_bind_function(zend_op *opline, HashTable *function_table, zend_bool compile_time) <span>/*</span><span> {{{ </span><span>*/</span><span> { zend_function </span>*<span>function; </span><span>//</span><span> 找出函數(shù)</span> zend_hash_find(function_table, opline->op1.u.constant.value.str.val, opline->op1.u.constant.value.str.len, (<span>void</span> *) &<span>function); </span><span>//</span><span> 以函數(shù)名稱作為key,重新加入function_table</span> <span>if</span> (zend_hash_add(function_table, opline->op2.u.constant.value.str.val, opline->op2.u.constant.value.str.len+<span>1</span>, function, <span>sizeof</span>(zend_function), NULL)==<span>FAILURE) { </span><span>int</span> error_level = compile_time ?<span> E_COMPILE_ERROR : E_ERROR; zend_function </span>*<span>old_function; </span><span>//</span><span> 加入失敗,可能發(fā)生重復(fù)定義了</span> <span>if</span> (zend_hash_find(function_table, opline->op2.u.constant.value.str.val, opline->op2.u.constant.value.str.len+<span>1</span>, (<span>void</span> *) &old_function)==<span>SUCCESS </span>&& old_function->type ==<span> ZEND_USER_FUNCTION </span>&& old_function->op_array.last > <span>0</span><span>) { zend_error(error_level, </span><span>"</span><span>Cannot redeclare %s() (previously declared in %s:%d)</span><span>"</span><span>, function</span>->common.function_name, old_function->op_array.filename, old_function->op_array.opcodes[<span>0</span><span>].lineno); } </span><span>else</span><span> { zend_error(error_level, </span><span>"</span><span>Cannot redeclare %s()</span><span>"</span>, function-><span>common.function_name); } </span><span>return</span><span> FAILURE; } </span><span>else</span><span> { (</span>*function->op_array.refcount)++<span>; function</span>->op_array.static_variables = NULL; <span>/*</span><span> NULL out the unbound function </span><span>*/</span> <span>return</span><span> SUCCESS; } }</span>
在進入do_bind_function之前,其實CG(function_table)中已經(jīng)有了函數(shù)的op_array。不過用的鍵并非函數(shù)名,而是build_runtime_defined_function_key生成的“key”,這點在前面“開始編譯”一節(jié)中有過介紹。do_bind_function所做的事情,正是利用這個“key”,將函數(shù)查找出來,并且以真正的函數(shù)名為鍵,重新插入到CG(function_table)中。
因此當do_bind_function完成時,function_table中有2個鍵可以查詢到該函數(shù)。一個是“key”為索引的,另一個是以函數(shù)名為索引的。在zend_do_early_binding的最后,會通過zend_hash_del來刪除“key”,從而保證function_table中,該函數(shù)只能夠以函數(shù)名為鍵值查詢到。
7、總結(jié)
這篇其實主要是為了弄清楚,函數(shù)如何被編譯成op_array。一些關(guān)鍵的步驟如下圖:
?
至于函數(shù)的調(diào)用,又是另外一個話題了。
?
?

? AI ??

Undress AI Tool
??? ???? ??

Undresser.AI Undress
???? ?? ??? ??? ?? AI ?? ?

AI Clothes Remover
???? ?? ???? ??? AI ?????.

Clothoff.io
AI ? ???

Video Face Swap
??? ??? AI ?? ?? ??? ???? ?? ???? ??? ?? ????!

?? ??

??? ??

???++7.3.1
???? ?? ?? ?? ???

SublimeText3 ??? ??
??? ??, ???? ?? ????.

???? 13.0.1 ???
??? PHP ?? ?? ??

???? CS6
??? ? ?? ??

SublimeText3 Mac ??
? ??? ?? ?? ?????(SublimeText3)

jQuery ?? ??? ?? ??? ??: ?? ?? ??? jQuery? ? ??? ??? ?? ???? JavaScript ??????, JavaScript ?????? ????? ????? ??? ??? ?????. ? ????? jQuery? ?? ??? ??? ???? ??? ??? ??? ? ??? ???? ?? ??? ?????. jQuery ?? ?? HTML ??? jQuery ?????? ???? ???. CDN ??? ?? ????? ????? ? ????.

jQuery?? PUT ?? ??? ???? ??? ?????? jQuery?? PUT ??? ??? ??? ?? ??? ??? ??? ?? ????? ? ?? ?? ??? ?? ?? ??? ???? ???. PUT ??? ????? ??????? ??? ???? ?? ??? ?? ????? ?? ???? ?????? ? ?????. ??? jQuery?? PUT ?? ???? ???? ???? ?? ?????. ?? jQuery ????? ??? ????? ??? ?? $.ajax({u? ?? PUT ??? ?? ? ????.

??: jQuery ?: ???? ?? ?? ??? ???? ??? ?????. ? ????? ???? ??? ???? ???? ?? ??? ????. jQuery? ??? ? ???? ?? ?? ??? ??? ??? ? ?? ???? ?? ??? ???, ?? ??? ???? ??? ? ????. ??? jQuery? ???? ???? ?? ?? ???? ??? ???? ??? ???? ???? ?? ??? ?????. ?? jQuery ????? ??? ???? ?? ??? ???? ?????? ???? ???. <

??: jQuery? ???? ?? ??? ??? ??? ?????. jQuery? DOM ??? ???? ? ?? ???? ?? ?? JavaScript ????????. ? ??? ?? ?? ???? ?? ?? ??(??)? ??? ??? ???? ?? ??? ?? ????. ? ????? jQuery? ???? ? ??? ???? ??? ???? ???? ?? ??? ?????. ?? ???? jQuery ?????? ???? ???. HTML ??? ?? ??? ?????.

jQuery? ???? ??? ?? ??? ???? ??? ?????? ????? ????? ??? ?? ??? ???? ?? ??? ?? ????. ??? ??? ??? ???? ???? ? ?? ?? ??? ?? ??? ???? ?? ??? ????. ? ????? jQuery? ???? ??? ?? ??? ???? ??? ???? ???? ?? ??? ?????. jQuery? ???? ?? ??? ???? ?? ?? CSS? ?? ??? ???? ???. height ??? ??? ??? ???? ? ?????.

jQuery? ? ????? DOM ?? ? ??? ??? ???? ? ?? ???? ?? ?? JavaScript ????????. jQuery?? eq() ???? ??? ??? ???? ??? ???? ? ?????. ???? ?? ? ?? ????? ??? ????. jQuery?? eq() ???? ??? ??? ??? ?? ??? ?????. ??? ??? 0?? ???? ?????. ?, ? ?? ??? ???? 0?? ? ?? ??? ???? 1???. eq() ???? ??? ??? ????: $("s

jQuery? ? ??? ?? ???? ?? ?? JavaScript ????????. ? ?? ?? JavaScript? ?? ???? ? ?? ???? ???? ?? ??? ????. ? ????? jQuery? ???? ???? ? ?? ???? ??? ???? ?? ?? ??? ?????. ?? HTML ???? jQuery ?????? ???? ???. jQuery ?????? ?? ??? ?? ??? ??? ? ????.

jQuery ??? ?? ??? ??? ??? ? ? ???? jQuery? ???? DOM ??? ??? ? ??? ?? ??? ??? ???? ?? ??? ?? ?????. ? ?? jQuery?? ???? ???? ???? ? ??? ?? ??? ? ????. ??? jQuery ??? ?? ??? ??? ???? ?? ????? ???? ? ?? ??? ?? ?? ??? ?? ?????. ?? 1: attr() ???? typeof ???? // ???? ??? ?? ??? ??? ??
