


In-depth analysis of php execution principle (2): Compilation of functions, in-depth analysis of php_PHP tutorial
Jul 12, 2016 am 08:53 AM深入剖析php執(zhí)行原理(2):函數(shù)的編譯,深入剖析php
本文只探討純粹的函數(shù),并不包含方法。對于方法,會放到類、對象中一起研究。
想講清楚在zend vm中,函數(shù)如何被正確的編譯成op指令、如何發(fā)生參數(shù)傳遞、如何模擬調用棧、如何切換作用域等等,的確是一個很大范疇的話題。但為了弄明白php的原理,必須要攻克它。
對函數(shù)的研究,大致可以分成兩塊。第一塊是函數(shù)體的編譯,主要涉及到如何將函數(shù)轉化成zend_op指令。第二塊是研究函數(shù)的調用,涉及到函數(shù)調用語句的編譯,以及函數(shù)如何被執(zhí)行等topic。這里先來看看函數(shù)如何被編譯,我們下一篇再講函數(shù)的調用。
函數(shù)的編譯
對函數(shù)進行編譯,最終目的是為了生成一份對應的op指令集,除了op指令集,編譯函數(shù)還會產生其他一些相關的數(shù)據(jù),比如說函數(shù)名稱、參數(shù)列表信息、compiled variables,甚至函數(shù)所在文件,起始行數(shù)等等。這些信息作為編譯的產出,都需要保存起來。保存這些產出的數(shù)據(jù)結構,正是上一節(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腳本中,最終其實會產生兩個op_array。一個是由函數(shù)foo編譯而來,另一個則是由除去foo之外代碼編譯生成的。同理可以推出,假如一份php腳本其中包含有2個函數(shù)和若干語句,則最終會產生3個op_array。也就是說,每個函數(shù)最終都會被編譯成一個對應的op_array。
剛才提到,op_array中有一些字段是和函數(shù)息息相關的。比如function_name代表著函數(shù)的名稱,比如num_args代表了函數(shù)的參數(shù)個數(shù),比如required_num_args代表了必須的參數(shù)個數(shù),比如arg_info代表著函數(shù)的參數(shù)信息...etc。
下面會繼續(xù)結合這段代碼,來研究foo函數(shù)詳細的編譯過程。
1、語法定義
從zend_language_parser.y文件中可以看出,函數(shù)的語法分析大致涉及如下幾個推導式:
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ù)體內語句的語法分析,但已經(jīng)足夠我們弄清楚編譯過程中的一些細節(jié)。
函數(shù)體內的語句,其對應的語法為inner_statement_list。inner_statement_list和函數(shù)體之外一般的語句并無二致,可以簡單當成普通的語句來編譯。
最重要的是看下unticked_function_declaration_statement,它定義了函數(shù)語法的骨架,同時還可以看出,函數(shù)編譯中會執(zhí)行zend_do_begin_function_declaration以及zend_do_end_function_declaration。這兩步分別對應著下文提到的開始編譯和結束編譯。我們先來看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ù)的編譯結果,當函數(shù)編譯完成之后,zend vm又需要將將CG(active_op_array)恢復成函數(shù)體外層的op_array。利用該znode保存函數(shù)體外的op_array,可以很方便的在函數(shù)編譯結束時進行CG(active_op_array)恢復,具體后面會講到。
研究下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的一些設置</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之前設置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> 設置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));
上面這段代碼很關鍵。有幾點要說明的:
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ù)所在文件路徑、代碼在內存中的地址等等。具體的實現(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ù)自身對應的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ù)體內的第一條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內聲明的函數(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ù)體內的語句并不屬于函數(shù)體外的label</span> zend_stack_push(&CG(labels_stack), (<span>void</span> *) &CG(labels), <span>sizeof</span>(HashTable*<span>)); CG(labels) </span>= NULL;
可能初學者會對CG(switch_cond_stack),CG(foreach_copy_stack),CG(labels_stack)等字段有疑惑。其實也很好理解。以CG(labels_stack)為例,由于進入函數(shù)體內之后,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)中恢復出原先的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)來恢復CG(labels),則CG(labels)再次變成“label1”。
至此,整個zend_do_begin_function_declaration過程已經(jīng)全部分析完成。最重要的是,一旦完成zend_do_begin_function_declaration,CG(active_op_array)就指向了函數(shù)自身對應的op_array。同時,也利用生成的“key”在CG(function_table)中替函數(shù)占了一個位。
3、編譯參數(shù)列表
函數(shù)可以定義為不接受任何參數(shù),對于參數(shù)列表為空的情況,其實不做任何處理。我們前文的例子foo函數(shù),接受了一個參數(shù)$arg1,我們下面還是分析有參數(shù)的情況。
根據(jù)語法推導式non_empty_parameter_list的定義,參數(shù)列表一共有8種,前4種對應的是一個參數(shù),后4種對應多個參數(shù)。我們只關心前4種,后4種編譯的過程,僅僅是重復前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ū)別并不大,它們的語法分析的處理過程也幾乎一致。都是先調用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ù)組存儲了變量,而并非內核中普遍使用的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的各個字段進行設置。opline->op1表明這是第幾個參數(shù),opline->op2可能為初始值,也可能被設置為UNUSED。
如果一個參數(shù)有默認值,那么在調用函數(shù)時,其實是可以不用傳遞該參數(shù)的。所以,required_num_args不會將這類非必須的參數(shù)算進去的??梢钥吹剑趏p == 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> 設置當前的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設置為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會被設置為IS_CONST,而u.constant.type會被設置為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會被設置為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ù),便會調用一次zend_do_receive_arg,生成一條RECV指令。因此,函數(shù)有幾個參數(shù),就會編譯出幾條RECV指令。
4、編譯函數(shù)體
當編譯完參數(shù)列表,zend vm便會進入函數(shù)內部了。函數(shù)體的編譯其實和正常語句的編譯一樣。zend vm只需要將函數(shù)體內部的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ù)體內部的語句,表示為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、結束編譯
我們最后來看下結束編譯的過程。結束函數(shù)編譯是通過zend_do_end_function_declaration來完成的。
zend_do_end_function_declaration接收的參數(shù)function_token,其實就是前面提到過的function字面對應的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指令設置對應的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ù)是否合法</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> 很關鍵!將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)結束了。zend vm會繼續(xù)看接下來函數(shù)之外的代碼,所以需要將CG(active_op_array)切換成原先的。
2,zend_do_return負責在函數(shù)最后添加上一條RETURN指令,因為我們傳進去的是NULL,所以這條RETURN指令的操作數(shù)被強制設置為UNUSED。注意,不管函數(shù)本身是否有return語句,最后這條RETURN指令是必然存在的。假如函數(shù)有return語句,return語句也會產生一條RETURN指令,所以會導致可能出現(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,我們剛才講解的所有步驟中,都只是設置了每條指令的opcode,而并沒有設置這條指令具體的handle函數(shù)。pass_two會負責遍歷每條zend_op指令,根據(jù)opcode,以及操作數(shù)op1和op2,去查找并且設置對應的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ù)組里找到對應的處理函數(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設置對應的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)綁定。這個名字容易讓人產生疑惑,其實只有在涉及到類和方法的時候,才會有早期綁定,與之相對的是延遲綁定,或者叫后期綁定。純粹函數(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)也很簡單,主要就是調用了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ā)生重復定義了</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、總結
這篇其實主要是為了弄清楚,函數(shù)如何被編譯成op_array。一些關鍵的步驟如下圖:
?
至于函數(shù)的調用,又是另外一個話題了。
?
?

Hot AI Tools

Undress AI Tool
Undress images for free

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Clothoff.io
AI clothes remover

Video Face Swap
Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Article

Hot Tools

Notepad++7.3.1
Easy-to-use and free code editor

SublimeText3 Chinese version
Chinese version, very easy to use

Zend Studio 13.0.1
Powerful PHP integrated development environment

Dreamweaver CS6
Visual web development tools

SublimeText3 Mac version
God-level code editing software (SublimeText3)

Hot Topics

Detailed explanation of jQuery reference method: Quick start guide jQuery is a popular JavaScript library that is widely used in website development. It simplifies JavaScript programming and provides developers with rich functions and features. This article will introduce jQuery's reference method in detail and provide specific code examples to help readers get started quickly. Introducing jQuery First, we need to introduce the jQuery library into the HTML file. It can be introduced through a CDN link or downloaded

How to use PUT request method in jQuery? In jQuery, the method of sending a PUT request is similar to sending other types of requests, but you need to pay attention to some details and parameter settings. PUT requests are typically used to update resources, such as updating data in a database or updating files on the server. The following is a specific code example using the PUT request method in jQuery. First, make sure you include the jQuery library file, then you can send a PUT request via: $.ajax({u

jQuery is a fast, small, feature-rich JavaScript library widely used in front-end development. Since its release in 2006, jQuery has become one of the tools of choice for many developers, but in practical applications, it also has some advantages and disadvantages. This article will deeply analyze the advantages and disadvantages of jQuery and illustrate it with specific code examples. Advantages: 1. Concise syntax jQuery's syntax design is concise and clear, which can greatly improve the readability and writing efficiency of the code. for example,

Title: jQuery Tips: Quickly modify the text of all a tags on the page In web development, we often need to modify and operate elements on the page. When using jQuery, sometimes you need to modify the text content of all a tags in the page at once, which can save time and energy. The following will introduce how to use jQuery to quickly modify the text of all a tags on the page, and give specific code examples. First, we need to introduce the jQuery library file and ensure that the following code is introduced into the page: <

Title: Use jQuery to modify the text content of all a tags. jQuery is a popular JavaScript library that is widely used to handle DOM operations. In web development, we often encounter the need to modify the text content of the link tag (a tag) on ??the page. This article will explain how to use jQuery to achieve this goal, and provide specific code examples. First, we need to introduce the jQuery library into the page. Add the following code in the HTML file:

How to remove the height attribute of an element with jQuery? In front-end development, we often encounter the need to manipulate the height attributes of elements. Sometimes, we may need to dynamically change the height of an element, and sometimes we need to remove the height attribute of an element. This article will introduce how to use jQuery to remove the height attribute of an element and provide specific code examples. Before using jQuery to operate the height attribute, we first need to understand the height attribute in CSS. The height attribute is used to set the height of an element

jQuery is a popular JavaScript library that is widely used to handle DOM manipulation and event handling in web pages. In jQuery, the eq() method is used to select elements at a specified index position. The specific usage and application scenarios are as follows. In jQuery, the eq() method selects the element at a specified index position. Index positions start counting from 0, i.e. the index of the first element is 0, the index of the second element is 1, and so on. The syntax of the eq() method is as follows: $("s

jQuery is a popular JavaScript library widely used in web development. During web development, it is often necessary to dynamically add new rows to tables through JavaScript. This article will introduce how to use jQuery to add new rows to a table, and provide specific code examples. First, we need to introduce the jQuery library into the HTML page. The jQuery library can be introduced in the tag through the following code:
