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