国产av日韩一区二区三区精品,成人性爱视频在线观看,国产,欧美,日韩,一区,www.成色av久久成人,2222eeee成人天堂

首頁 > php教程 > php手冊 > 正文

深入剖析php執(zhí)行原理(4):函數(shù)的調(diào)用,深入剖析php

php中文網(wǎng)
發(fā)布: 2016-07-06 14:25:31
原創(chuàng)
1091人瀏覽過

深入剖析php執(zhí)行原理(4):函數(shù)的調(diào)用,深入剖析php

本章開始研究php中函數(shù)的調(diào)用和執(zhí)行,先來看函數(shù)調(diào)用語句是如何被編譯的。

我們前面的章節(jié)弄明白了函數(shù)體會被編譯生成哪些zend_op指令,本章會研究函數(shù)調(diào)用語句會生成哪些zend_op指,等后面的章節(jié)再根據(jù)這些op指令,來剖析php運行時的細節(jié)。

源碼依然取自php5.3.29。

函數(shù)調(diào)用

回顧之前用的php代碼示例:

<?<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>);
登錄后復(fù)制

在函數(shù)編譯一章里已經(jīng)分析過,函數(shù)foo最終會編譯生成對應(yīng)的zend_function,存放于函數(shù)表(CG(function_table))中。

立即學(xué)習(xí)PHP免費學(xué)習(xí)筆記(深入)”;

現(xiàn)在開始看?foo($bar); 一句,這應(yīng)該是最簡單的函數(shù)調(diào)用語句了。其他還有一些形式更為復(fù)雜的函數(shù)調(diào)用,例如以可變變量作為函數(shù)名,例如導(dǎo)入的函數(shù)以別名進行調(diào)用(涉及到命名空間),再例如以引用作為參數(shù),以表達式作為參數(shù),以函數(shù)調(diào)用本身作為參數(shù)等等。

我們從簡單的來入手,弄清楚調(diào)用語句的編譯過程及產(chǎn)出,對于復(fù)雜的一些調(diào)用,下文也爭取都能談到一些。

1、語法推導(dǎo)

就?foo($bar); 而言,其主要部分語法樹為:

綠色的節(jié)點表示最后對應(yīng)到php代碼中的字面。紅色的部分是語法推導(dǎo)過程中最重要的幾步,特別是function_call。

我們從語法分析文件zend_language_parser.y中挑出相關(guān)的:

function_call:
		namespace_name '(' { $2.u.opline_num = zend_do_begin_function_call(&$1, 1 TSRMLS_CC); }
				function_call_parameter_list
				')' { zend_do_end_function_call(&$1, &$$, &$4, 0, $2.u.opline_num TSRMLS_CC); zend_do_extended_fcall_end(TSRMLS_C); }
	|	T_NAMESPACE T_NS_SEPARATOR namespace_name '(' { $1.op_type = IS_CONST; ZVAL_EMPTY_STRING(&$1.u.constant);  zend_do_build_namespace_name(&$1, &$1, &$3 TSRMLS_CC); $4.u.opline_num = zend_do_begin_function_call(&$1, 0 TSRMLS_CC); }
				function_call_parameter_list
				')' { zend_do_end_function_call(&$1, &$$, &$6, 0, $4.u.opline_num TSRMLS_CC); zend_do_extended_fcall_end(TSRMLS_C); }
	|	T_NS_SEPARATOR namespace_name '(' { $3.u.opline_num = zend_do_begin_function_call(&$2, 0 TSRMLS_CC); }
				function_call_parameter_list
				')' { zend_do_end_function_call(&$2, &$$, &$5, 0, $3.u.opline_num TSRMLS_CC); zend_do_extended_fcall_end(TSRMLS_C); }
	|	class_name T_PAAMAYIM_NEKUDOTAYIM T_STRING '(' { $4.u.opline_num = zend_do_begin_class_member_function_call(&$1, &$3 TSRMLS_CC); }
			function_call_parameter_list
			')' { zend_do_end_function_call($4.u.opline_num?NULL:&$3, &$$, &$6, $4.u.opline_num, $4.u.opline_num TSRMLS_CC); zend_do_extended_fcall_end(TSRMLS_C);}
	|	class_name T_PAAMAYIM_NEKUDOTAYIM variable_without_objects '(' { zend_do_end_variable_parse(&$3, BP_VAR_R, 0 TSRMLS_CC); zend_do_begin_class_member_function_call(&$1, &$3 TSRMLS_CC); }
			function_call_parameter_list
			')' { zend_do_end_function_call(NULL, &$$, &$6, 1, 1 TSRMLS_CC); zend_do_extended_fcall_end(TSRMLS_C);}
	|	variable_class_name T_PAAMAYIM_NEKUDOTAYIM T_STRING '(' { zend_do_begin_class_member_function_call(&$1, &$3 TSRMLS_CC); }
			function_call_parameter_list
			')' { zend_do_end_function_call(NULL, &$$, &$6, 1, 1 TSRMLS_CC); zend_do_extended_fcall_end(TSRMLS_C);}
	|	variable_class_name T_PAAMAYIM_NEKUDOTAYIM variable_without_objects '(' { zend_do_end_variable_parse(&$3, BP_VAR_R, 0 TSRMLS_CC); zend_do_begin_class_member_function_call(&$1, &$3 TSRMLS_CC); }
			function_call_parameter_list
			')' { zend_do_end_function_call(NULL, &$$, &$6, 1, 1 TSRMLS_CC); zend_do_extended_fcall_end(TSRMLS_C);}
	|	variable_without_objects  '(' { zend_do_end_variable_parse(&$1, BP_VAR_R, 0 TSRMLS_CC); zend_do_begin_dynamic_function_call(&$1, 0 TSRMLS_CC); }
			function_call_parameter_list ')'
			{ zend_do_end_function_call(&$1, &$$, &$4, 0, 1 TSRMLS_CC); zend_do_extended_fcall_end(TSRMLS_C);}
;<br /><br />function_call_parameter_list:<br />        non_empty_function_call_parameter_list    { $$ = $1; }<br />    |    /* empty */                        { Z_LVAL($$.u.constant) = 0; }<br />;<br /><br /><br />non_empty_function_call_parameter_list:<br />        expr_without_variable    { Z_LVAL($$.u.constant) = 1;  zend_do_pass_param(&$1, ZEND_SEND_VAL, Z_LVAL($$.u.constant) TSRMLS_CC); }<br />    |    variable                { Z_LVAL($$.u.constant) = 1;  zend_do_pass_param(&$1, ZEND_SEND_VAR, Z_LVAL($$.u.constant) TSRMLS_CC); }<br />    |    '&' w_variable          { Z_LVAL($$.u.constant) = 1;  zend_do_pass_param(&$2, ZEND_SEND_REF, Z_LVAL($$.u.constant) TSRMLS_CC); }<br />    |    non_empty_function_call_parameter_list ',' expr_without_variable    { Z_LVAL($$.u.constant)=Z_LVAL($1.u.constant)+1;  zend_do_pass_param(&$3, ZEND_SEND_VAL, Z_LVAL($$.u.constant) TSRMLS_CC); }<br />    |    non_empty_function_call_parameter_list ',' variable                 { Z_LVAL($$.u.constant)=Z_LVAL($1.u.constant)+1;  zend_do_pass_param(&$3, ZEND_SEND_VAR, Z_LVAL($$.u.constant) TSRMLS_CC); }<br />    |    non_empty_function_call_parameter_list ',' '&' w_variable           { Z_LVAL($$.u.constant)=Z_LVAL($1.u.constant)+1;  zend_do_pass_param(&$4, ZEND_SEND_REF, Z_LVAL($$.u.constant) TSRMLS_CC); }<br />;
登錄后復(fù)制

其結(jié)構(gòu)并不復(fù)雜:

1)function_call這條推導(dǎo),代表了一個完整的函數(shù)調(diào)用。

2)namespace_name是指經(jīng)過命名空間修飾過之后的函數(shù)名,由于我們的例子中,函數(shù)foo并沒有處于任何一個命名空間里,所以namespace_name其實就是foo。如果我們的函數(shù)定義在命名空間中,則namespace_name是一個類似“全路徑”的fullname。

<span>namespace MyProject
{
</span><span>function</span> foo(<span>$arg1</span><span>)
{
    </span><span>print</span>(<span>$arg1</span><span>);
}
}

namespace
{
</span><span>$bar</span> = 'hello php'<span>;</span>
MyProjectoo(<span>$bar</span><span>);<span>//<span> 以類似&ldquo;全路徑&rdquo;的fullname來調(diào)用函數(shù),則namespace_name為MyProjectoo</span></span> 
}</span>
登錄后復(fù)制

3)function_call_parameter_list是函數(shù)的參數(shù)列表,而non_empty_function_call_parameter_list則代表了非空參數(shù)列表。

4)從這些推導(dǎo)產(chǎn)生式里,我們還能看出編譯時的所運用的一些關(guān)鍵處理:

zend_do_begin_function_call-->zend_do_pass_param-->zend_do_end_function_call<br /><br />        開始                        解析參數(shù)                     結(jié)束
登錄后復(fù)制

和編譯function語句塊時的幾步(zend_do_begin_function_declaration->zend_do_receive_arg->zend_do_end_function_declaration等)順序上比較類似。

上面提到語法樹我們僅僅畫了一部分,準(zhǔn)確講,沒有將namespace以及function_call_parameter_list以下的推導(dǎo)過程進一步畫出來。原因一是namespace的推導(dǎo)比較簡單。第二,由于function_call_parameter_list-->variable這步會回到variable上,而variable經(jīng)過若干步一直到產(chǎn)生變量$bar的推導(dǎo)比較復(fù)雜,也不是本文的重點,所以這里就不一進步探究了。

2、開始編譯

看下function_call的推導(dǎo)式,一開始,zend vm會執(zhí)行zend_do_begin_function_call做一些函數(shù)調(diào)用的準(zhǔn)備。

2.1、 zend_do_begin_function_call

代碼注解如下:

zend_function *<span>function;
</span><span>char</span> *<span>lcname;
</span><span>char</span> *is_compound = memchr(Z_STRVAL(function_name->u.constant), <span>'</span><span>\</span><span>'</span>, Z_STRLEN(function_name-><span>u.constant));

</span><span>//</span><span> 將函數(shù)名進行修正,例如帶上命名空間作為前綴等</span>
<span>zend_resolve_non_class_name(function_name, check_namespace TSRMLS_CC);

</span><span>//</span><span> 能進入該分支,說明在一個命名空間下以shortname調(diào)用函數(shù),會生成一條DO_FCALL_BY_NAME指令</span>
<span>if</span> (check_namespace && CG(current_namespace) && !<span>is_compound) {
        </span><span>/*</span><span> We assume we call function from the current namespace
        if it is not prefixed. </span><span>*/</span>

        <span>/*</span><span> In run-time PHP will check for function with full name and
        internal function with short name </span><span>*/</span><span>
        zend_do_begin_dynamic_function_call(function_name, </span><span>1</span><span> TSRMLS_CC);
        </span><span>return</span> <span>1</span><span>;
} 

</span><span>//</span><span> 轉(zhuǎn)成小寫,因為CG(function_table)中的函數(shù)名都是小寫</span>
lcname = zend_str_tolower_dup(function_name->u.constant.value.str.val, function_name-><span>u.constant.value.str.len);

</span><span>//</span><span> 如果function_table中找不到該函數(shù),則也嘗試生成DO_FCALL_BY_NAME指令</span>
<span>if</span> ((zend_hash_find(CG(function_table), lcname, function_name->u.constant.value.str.len+<span>1</span>, (<span>void</span> **) &function) == FAILURE) ||<span>
    ((CG(compiler_options) </span>& ZEND_COMPILE_IGNORE_INTERNAL_FUNCTIONS) && (function->type ==<span> ZEND_INTERNAL_FUNCTION))) {
        zend_do_begin_dynamic_function_call(function_name, </span><span>0</span><span> TSRMLS_CC);
        efree(lcname);
        </span><span>return</span> <span>1</span>; <span>/*</span><span> Dynamic </span><span>*/</span><span>
} 
efree(function_name</span>-><span>u.constant.value.str.val);
function_name</span>->u.constant.value.str.val =<span> lcname;

</span><span>//</span><span> 壓入CG(function_call_stack)</span>
zend_stack_push(&CG(function_call_stack), (<span>void</span> *) &function, <span>sizeof</span>(zend_function *<span>));
zend_do_extended_fcall_begin(TSRMLS_C);
</span><span>return</span> <span>0</span>;
登錄后復(fù)制

有幾點需要理解的:

1,zend_resolve_non_class_name。由于php支持命名空間、也支持別名/導(dǎo)入等特性,因此首先要做的是將函數(shù)名稱進行修正,否則在CG(function_table)中找不到。例如,函數(shù)處于一個命名空間中,則可能需要將函數(shù)名添加上命名空間作為前綴,最終形成完整的函數(shù)名,也就是我們前文提到的以一種類似“全路徑”的fullname作為函數(shù)名。再例如,函數(shù)名只是一個設(shè)置的別名,它實際指向了另一個命名空間中的某個函數(shù),則需要將其改寫成真正被調(diào)用函數(shù)的名稱。這些工作,均由zend_resolve_non_class_name完成。命名空間添加了不少復(fù)雜度,下面是一些簡單的例子:

<?<span>php
namespace MyProject;

</span><span>function</span> foo(<span>$arg1</span><span>)
{
    </span><span>print</span>(<span>$arg1</span><span>);
}

</span><span>$bar</span> = 'hello php'<span>;<br />
foo(</span><span>$bar</span>);                 <span>//</span><span> zend_resolve_non_class_name會將foo處理成MyProjectoo</span><span>
namespaceoo(</span><span>$bar</span>);       <span>//</span><span> 在進入zend_do_begin_function_call之前,<span>函數(shù)名已經(jīng)</span>被擴展成<span><span><span>MyProjectoo</span></span></span>,再經(jīng)過zend_resolve_non_class_name,<span>將<span>MyProjectoo</span></span>處理成MyProjectoo</span><span>
MyProjectoo(</span><span>$bar</span>);      <span>//</span><span> zend_resolve_non_class_name會將<span><span><span>MyProjectoo</span></span></span>處理成MyProjectoo</span>
登錄后復(fù)制

總之,zend_resolve_non_class_name是力圖生成一個最精確、最完整的函數(shù)名。

2,CG(current_namespace)存儲了當(dāng)前的命名空間。check_namespace和!is_compound一起說明被調(diào)用函數(shù)在當(dāng)前命名空間下的,并且以shortname名稱被調(diào)用。所謂shortname,是和上述的fullname相對,shorname的函數(shù)名,不存在""。

就像上面的例子中,我們在MyProject命名空間下,以foo為函數(shù)名來調(diào)用。這種情況下,check_namespace=1,is_compound = NULL,CG(current_namespace) = MyProject。因此,會走到zend_do_begin_dynamic_function_call里進一步處理。zend_do_begin_dynamic_function_call我們下面再具體描述。

<?<span>php
namespace MyProjectsub;

</span><span>function</span> foo(<span>$arg1</span><span>)
{
    </span><span>print</span>(<span>$arg1</span><span>);
}

namespace MyProject;
</span><span>$bar</span> = 'hello php'<span>;
suboo(</span><span>$bar</span>);      <span>//</span><span> 以suboo調(diào)用函數(shù),并不算shortname,因為存在</span>
登錄后復(fù)制

注意上述例子,我們以sub oo來調(diào)用函數(shù)。zend_resolve_non_class_name會將函數(shù)名處理成MyProjectsub oo。不過is_compound是在zend_resolve_non_class_name之前算的,由于sub oo存在"",所以is_compound為" oo",!is_compound是false,因而不能進入zend_do_begin_dynamic_function_call。

3,同樣,如果CG(function_table)中找不到函數(shù),也會進入zend_do_begin_dynamic_function_call進一步處理。為什么在函數(shù)表中找不到函數(shù),因為php允許我們先調(diào)用,再去定義函數(shù)。例如:

<?<span>php
</span><span>$bar</span> = 'hello php'<span>;

</span><span>//</span><span> 先調(diào)用</span>
foo(<span>$bar</span><span>);

</span><span>//</span><span> 后定義</span>
<span>function</span> foo(<span>$arg1</span><span>)
{
    </span><span>print</span>(<span>$arg1</span><span>);
}</span>
登錄后復(fù)制

4,在zend_do_begin_function_call的最后,我們將函數(shù)壓入CG(function_call_stack)。這是一個棧,因為在后續(xù)對傳參的編譯,我們?nèi)匀恍枰玫胶瘮?shù),所以這里將其壓亞入棧中,方便后面獲取使用。之所以用棧,是因為調(diào)用函數(shù)傳遞的參數(shù),可能是另一次函數(shù)調(diào)用。為了確保參數(shù)總是能找到對應(yīng)的函數(shù),所以用棧。

<?<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>strlen</span>(<span>$bar</span>));   <span>//</span><span> 首先foo入棧,然后分析參數(shù)strlen($bar),發(fā)現(xiàn)依然是個函數(shù),于是strlen入棧,再分析參數(shù)$bar,此時彈出對應(yīng)的函數(shù)正好為strlen。</span>
登錄后復(fù)制

2.2、 zend_do_begin_dynamic_function_call

前面提到,正常的調(diào)用,會先執(zhí)行zend_do_begin_function_call,在zend_do_begin_function_call中有兩種情況會進一步調(diào)用zend_do_begin_dynamic_function_call來處理。

一是,在命名空間中,以shortname調(diào)用函數(shù);

二是,在調(diào)用函數(shù)時,尚未定義函數(shù)。

其實還有第三種情況會走到zend_do_begin_dynamic_function_call,就是當(dāng)我們調(diào)用函數(shù)的時候,函數(shù)名并非直接寫成字面,而是通過變量等形式來間接確定。這種情況下,zend vm會直接執(zhí)行zend_do_begin_dynamic_function_call。

舉例1:

<?<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>;
</span><span>$func</span> = 'foo'<span>;
</span><span>$func</span>(<span>$bar</span>);          <span>//</span><span> 我們以變量$func作為函數(shù)名,試圖調(diào)用函數(shù)foo,$func類型是IS_CV</span>
登錄后復(fù)制

此時, $func($bar)?對應(yīng)function_call語法推導(dǎo)式的最后一條:

function_call:<br />        ...
	|	variable_without_objects  '(' { zend_do_end_variable_parse(&$1, BP_VAR_R, 0 TSRMLS_CC); zend_do_begin_dynamic_function_call(&$1, 0 TSRMLS_CC); }
			function_call_parameter_list ')'
			{ zend_do_end_function_call(&$1, &$$, &$4, 0, 1 TSRMLS_CC); zend_do_extended_fcall_end(TSRMLS_C);}
登錄后復(fù)制

推導(dǎo)式中的variable_without_objects對應(yīng)的就是變量?$func?。$func其實是一個compiled_variable,并且在op_array->vars數(shù)組中索引為1,索引為0的是在它之前定義的變量?$bar

舉例2:

<span>function</span> foo(<span>$arg1</span><span>)
{
    </span><span>print</span>(<span>$arg1</span><span>);
}

</span><span>$bar</span> = 'hello php'<span>;
</span><span>$func</span> = 'foo'<span>;
</span><span>$ref_func</span> = 'func'<span>;
$</span><span>$ref_func</span>(<span>$bar</span>); <span>//</span><span> 以可變變量的形式來調(diào)用函數(shù),$$ref_func類型是IS_VAR</span>
登錄后復(fù)制

該例是以可變變量來調(diào)用函數(shù),和例1一樣, $$ref_func($bar)也是對應(yīng)function_call語法推導(dǎo)式的最后一條,所以不會走進zend_do_begin_function_call,而是直接進入zend_do_begin_dynamic_function_call。不同的點在于 $$ref_func 節(jié)點類型不再是compiled_variable,而是普通的variable,標(biāo)識為IS_VAR。

下面的圖畫出了5種case,第1種不經(jīng)過zend_do_begin_dynamic_function_call,而后4種會調(diào)用zend_do_begin_dynamic_function_call處理,注意最后2種不經(jīng)過zend_do_begin_function_call:

具體看下zend_do_begin_dynamic_function_call的代碼:

<span>void</span> zend_do_begin_dynamic_function_call(znode *function_name, <span>int</span> ns_call TSRMLS_DC) <span>/*</span><span> {{{ </span><span>*/</span><span>
{
    unsigned </span><span>char</span> *ptr =<span> NULL;
    zend_op </span>*opline, *<span>opline2;

    </span><span>//</span><span> 拿一條zend_op</span>
    opline =<span> get_next_op(CG(active_op_array) TSRMLS_CC);
    
    </span><span>//</span><span> 參數(shù)ns_call表名是否以shortname在命名空間中調(diào)用函數(shù)</span>
    <span>if</span><span> (ns_call) {
        </span><span>char</span> *<span>slash;
        </span><span>int</span><span> prefix_len, name_len;
        </span><span>/*</span><span> In run-time PHP will check for function with full name and
           internal function with short name </span><span>*/</span>
        
        <span>//</span><span> 第一條指令是ZEND_INIT_NS_FCALL_BY_NAME</span>
        opline->opcode =<span> ZEND_INIT_NS_FCALL_BY_NAME;
        opline</span>->op2 = *<span>function_name;
        opline</span>->extended_value = <span>0</span><span>;
        opline</span>->op1.op_type =<span> IS_CONST;
        Z_TYPE(opline</span>->op1.u.constant) =<span> IS_STRING;
        Z_STRVAL(opline</span>->op1.u.constant) = zend_str_tolower_dup(Z_STRVAL(opline->op2.u.constant), Z_STRLEN(opline-><span>op2.u.constant));
        Z_STRLEN(opline</span>->op1.u.constant) = Z_STRLEN(opline-><span>op2.u.constant);
        opline</span>->extended_value = zend_hash_func(Z_STRVAL(opline->op1.u.constant), Z_STRLEN(opline->op1.u.constant) + <span>1</span><span>);
        
        </span><span>//</span><span> 再拿一條zend_op,指令為ZEND_OP_DATA</span>
        slash = zend_memrchr(Z_STRVAL(opline->op1.u.constant), <span>'</span><span>\</span><span>'</span>, Z_STRLEN(opline-><span>op1.u.constant));
        prefix_len </span>= slash-Z_STRVAL(opline->op1.u.constant)+<span>1</span><span>;
        name_len </span>= Z_STRLEN(opline->op1.u.constant)-<span>prefix_len;
        opline2 </span>=<span> get_next_op(CG(active_op_array) TSRMLS_CC);
        opline2</span>->opcode =<span> ZEND_OP_DATA;
        opline2</span>->op1.op_type =<span> IS_CONST;
        Z_TYPE(opline2</span>->op1.u.constant) =<span> IS_LONG;
        </span><span>if</span>(!<span>slash) {
            zend_error(E_CORE_ERROR, </span><span>"</span><span>Namespaced name %s should contain slash</span><span>"</span>, Z_STRVAL(opline-><span>op1.u.constant));
        }
        </span><span>/*</span><span> this is the length of namespace prefix </span><span>*/</span><span>
        Z_LVAL(opline2</span>->op1.u.constant) =<span> prefix_len;
        </span><span>/*</span><span> this is the hash of the non-prefixed part, lowercased </span><span>*/</span><span>
        opline2</span>->extended_value = zend_hash_func(slash+<span>1</span>, name_len+<span>1</span><span>);
        SET_UNUSED(opline2</span>-><span>op2);
    } </span><span>else</span><span> {
        </span><span>//</span><span> 第一條指令是ZEND_INIT_FCALL_BY_NAME</span>
        opline->opcode =<span> ZEND_INIT_FCALL_BY_NAME;
        opline</span>->op2 = *<span>function_name;
        
        </span><span>//</span><span> 先調(diào)用,再定義</span>
        <span>if</span> (opline->op2.op_type ==<span> IS_CONST) {
            opline</span>->op1.op_type =<span> IS_CONST;
            Z_TYPE(opline</span>->op1.u.constant) =<span> IS_STRING;
            Z_STRVAL(opline</span>->op1.u.constant) = zend_str_tolower_dup(Z_STRVAL(opline->op2.u.constant), Z_STRLEN(opline-><span>op2.u.constant));
            Z_STRLEN(opline</span>->op1.u.constant) = Z_STRLEN(opline-><span>op2.u.constant);
            opline</span>->extended_value = zend_hash_func(Z_STRVAL(opline->op1.u.constant), Z_STRLEN(opline->op1.u.constant) + <span>1</span><span>);
        }
        </span><span>//</span><span> 以變量當(dāng)函數(shù)名來調(diào)用</span>
        <span>else</span><span> {
            opline</span>->extended_value = <span>0</span><span>;
            SET_UNUSED(opline</span>-><span>op1);
        }
    }

    </span><span>//</span><span> 將NULL壓入CG(function_call_stack)</span>
    zend_stack_push(&CG(function_call_stack), (<span>void</span> *) &ptr, <span>sizeof</span>(zend_function *<span>));
    zend_do_extended_fcall_begin(TSRMLS_C);
}</span>
登錄后復(fù)制

ns_call參數(shù)取值為0或者1。如果在命名空間中,以shortname調(diào)用函數(shù),則ns_call = 1,并且會生成2條指令。如果是先調(diào)用再定義,或者以變量作函數(shù)名,則ns_call = 0,并且只會生成1條指令。

以ns_call = 1為例:

<?<span>php
namespace MyProject;

</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>);
登錄后復(fù)制

?生成的op指令如下所示:

php $bar = 'hello php'; foo($bar); function foo($arg1) { print($arg1); }

生成的op指令如下所示:

php function foo($arg1) { print($arg1); } $bar = 'hello php'; $func = 'foo'; $func($bar);

生成的op指令如下所示:

void zend_do_pass_param(znode *param, zend_uchar op, int offset TSRMLS_DC) /* {{{ */ { zend_op *opline; int original_op = op; zend_function **function_ptr_ptr, *function_ptr; int send_by_reference; int send_function = 0; // 從CG(function_call_stack)獲取當(dāng)前函數(shù),注意可能拿出的是NULL zend_stack_top(&CG(function_call_stack), (void **) &function_ptr_ptr); function_ptr = *function_ptr_ptr; // 調(diào)用的地方以引用傳參,但是php.ini中配置不允許這樣,則拋錯 if (original_op == ZEND_SEND_REF && !CG(allow_call_time_pass_reference)) { if (function_ptr && function_ptr->common.function_name && function_ptr->common.type == ZEND_USER_FUNCTION && !ARG_SHOULD_BE_SENT_BY_REF(function_ptr, (zend_uint) offset)) { zend_error(E_DEPRECATED, "Call-time pass-by-reference has been deprecated; " "If you would like to pass it by reference, modify the declaration of %s(). " "If you would like to enable call-time pass-by-reference, you can set " "allow_call_time_pass_reference to true in your INI file", function_ptr->common.function_name); } else { zend_error(E_DEPRECATED, "Call-time pass-by-reference has been deprecated"); } }

1,首先是從CG(function_call_stack)中獲取當(dāng)前參數(shù)對應(yīng)的函數(shù)。注意,可能拿到的只是一個NULL。因為php的語法允許我們先函數(shù)調(diào)用,再接著對函數(shù)進行定義。如前文所述,這種情況下zend_do_begin_function_call中會向CG(function_call_stack)中壓入NULL,同時會產(chǎn)生DO_FCALL_BY_NAME指令。

2,在傳參的語法推導(dǎo)式中,op可能會有3種,分別是ZEND_SEND_VAL、ZEND_SEND_VAR、ZEND_SEND_REF。

expr_without_variable    { Z_LVAL($$.u.constant) = <span>1</span>;  zend_do_pass_param(&$<span>1</span><span>, ZEND_SEND_VAL, Z_LVAL($$.u.constant) TSRMLS_CC); }
variable                 { Z_LVAL($$.u.constant) </span>= <span>1</span>;  zend_do_pass_param(&$<span>1</span><span>, ZEND_SEND_VAR, Z_LVAL($$.u.constant) TSRMLS_CC); }
</span><span>'</span><span>&</span><span>'</span> w_variable           { Z_LVAL($$.u.constant) = <span>1</span>;  zend_do_pass_param(&$<span>2</span>, ZEND_SEND_REF, Z_LVAL($$.u.constant) TSRMLS_CC); }
登錄后復(fù)制

這三種op分別對應(yīng)的語法是expr_without_variable、variable、'&'w_variable,簡單來說就是“不含變量的表達式”、“變量”、“引用”。

zend_do_pass_param會判斷,如果用戶傳遞的是引用,但同時在php.INI中配置了形如 allow_call_time_pass_reference = Off ,則需要產(chǎn)生一條E_DEPRECATED錯誤信息,告知用戶傳遞的時候不建議強制寫成引用。

其實,還有第4種傳參的opcode,即ZEND_SEND_VAR_NO_REF。我們接下來會提到。

<span>//</span><span> 函數(shù)已定義,則根據(jù)函數(shù)的定義,來決定send_by_reference是否傳引用</span>
<span>if</span><span> (function_ptr) {
    </span><span>if</span><span> (ARG_MAY_BE_SENT_BY_REF(function_ptr, (zend_uint) offset)) {
        ...
    } </span><span>else</span><span> {
        </span><span>//</span><span> 要么為0,要么為ZEND_ARG_SEND_BY_REF</span>
        send_by_reference = ARG_SHOULD_BE_SENT_BY_REF(function_ptr, (zend_uint) offset) ? ZEND_ARG_SEND_BY_REF : <span>0</span><span>;
    }
}
</span><span>//</span><span> 函數(shù)為定義,先統(tǒng)一將send_by_reference置為0</span>
<span>else</span><span> {
    send_by_reference </span>= <span>0</span><span>;
}

</span><span>//</span><span> 如果用戶傳遞的參數(shù),本身就是一次函數(shù)調(diào)用,則將op改成ZEND_SEND_VAR_NO_REF</span>
<span>if</span> (op == ZEND_SEND_VAR &&<span> zend_is_function_or_method_call(param)) {
    </span><span>/*</span><span> Method call </span><span>*/</span><span>
    op </span>=<span> ZEND_SEND_VAR_NO_REF;
    send_function </span>=<span> ZEND_ARG_SEND_FUNCTION;
}
</span><span>//</span><span> 如果用戶傳遞的參數(shù),是一個表達式,并且結(jié)果會產(chǎn)生中間變量,則也將op改成ZEND_SEND_VAR_NO_REF</span>
<span>else</span> <span>if</span> (op == ZEND_SEND_VAL && (param->op_type & (IS_VAR|<span>IS_CV))) {
    op </span>=<span> ZEND_SEND_VAR_NO_REF;
}</span>
登錄后復(fù)制

1,send_by_reference表示根據(jù)函數(shù)的定義,參數(shù)是不是引用。ARG_MAY_BE_SENT_BY_REF和ARG_SHOULD_BE_SENT_BY_REF兩個宏這里就不具體敘述了,感興趣的朋友可以自己閱讀代碼。

2,op == ZEND_SEND_VAR對應(yīng)的是variable,假如參數(shù)是一個函數(shù)調(diào)用,也可能會被編譯成variable,但是函數(shù)調(diào)用并不存在顯式定義的變量,所以不能直接編譯成SEND_VAR指令,因此這里就涉及到了上文提到的第4種opcode,即ZEND_SEND_VAR_NO_REF。例如:

3,op == ZEND_SEND_VAL對應(yīng)的是一個表達式,如果該表達式產(chǎn)生了一個變量作為結(jié)果,則也需要將op改成ZEND_SEND_VAR_NO_REF。例如:

繼續(xù)來看zend_do_pass_param:

<span>//</span><span> 如果根據(jù)函數(shù)定義需要傳遞引用,且實際傳遞的參數(shù)是變量,則將op改成ZEND_SEND_REF</span>
<span>if</span> (op!=ZEND_SEND_VAR_NO_REF && send_by_reference==<span>ZEND_ARG_SEND_BY_REF) {
    </span><span>/*</span><span> change to passing by reference </span><span>*/</span>
    <span>switch</span> (param-><span>op_type) {
        </span><span>case</span><span> IS_VAR:
        </span><span>case</span><span> IS_CV:
            op </span>=<span> ZEND_SEND_REF;
            </span><span>break</span><span>;
        </span><span>default</span><span>:
            zend_error(E_COMPILE_ERROR, </span><span>"</span><span>Only variables can be passed by reference</span><span>"</span><span>);
            </span><span>break</span><span>;
    }
}

</span><span>//</span><span> 如果實際傳遞的參數(shù)是變量,調(diào)用zend_do_end_variable_parse處理鏈?zhǔn)秸{(diào)用</span>
<span>if</span> (original_op ==<span> ZEND_SEND_VAR) {
    </span><span>switch</span><span> (op) {
        </span><span>case</span><span> ZEND_SEND_VAR_NO_REF:
            zend_do_end_variable_parse(param, BP_VAR_R, </span><span>0</span><span> TSRMLS_CC);
            </span><span>break</span><span>;
        </span><span>case</span><span> ZEND_SEND_VAR:
            </span><span>if</span><span> (function_ptr) {
                zend_do_end_variable_parse(param, BP_VAR_R, </span><span>0</span><span> TSRMLS_CC);
            } </span><span>else</span><span> {
                zend_do_end_variable_parse(param, BP_VAR_FUNC_ARG, offset TSRMLS_CC);
            }
            </span><span>break</span><span>;
        </span><span>case</span><span> ZEND_SEND_REF:
            zend_do_end_variable_parse(param, BP_VAR_W, </span><span>0</span><span> TSRMLS_CC);
            </span><span>break</span><span>;
    }
}</span>
登錄后復(fù)制

這里注意param->op_type是傳遞的參數(shù)經(jīng)過編譯得到znode的op_type,如果不屬于變量(IS_VAR、IS_CV),就直接報錯了。舉例來說:

<span>function</span> foo(&<span>$a</span><span>)
{
    </span><span>print</span>(<span>$a</span><span>);
}

foo(</span><span>$bar</span> == 1);  // 拋錯 <span>"<span>Only variables can be passed by reference<span>"</span></span></span>
登錄后復(fù)制

上面 $bar == 1 表達式的編譯結(jié)果,op_type為IS_TMP_VAR,可以看做一種臨時的中間結(jié)果,并非IS_VAR,IS_CV,因此無法編譯成功??粗壿嬘悬c繞,其實很好理解。因為我們傳遞引用,實際目的是希望能夠在函數(shù)中,對這個參數(shù)的值進行修改,需要參數(shù)是可寫的。然而?$bar == 1 產(chǎn)生的中間結(jié)果,我們無法做出修改,是只讀的。

來看zend_do_pass_param的最后一段:

<span>//</span><span> 獲取下一條zend op指令</span>
opline =<span> get_next_op(CG(active_op_array) TSRMLS_CC);

</span><span>//</span><span> extended_value加上不同的附加信息</span>
<span>if</span> (op ==<span> ZEND_SEND_VAR_NO_REF) {
    </span><span>if</span><span> (function_ptr) {
        opline</span>->extended_value = ZEND_ARG_COMPILE_TIME_BOUND | send_by_reference |<span> send_function;
    } </span><span>else</span><span> {
        opline</span>->extended_value =<span> send_function;
    }
} </span><span>else</span><span> {
    </span><span>if</span><span> (function_ptr) {
        opline</span>->extended_value =<span> ZEND_DO_FCALL;
    } </span><span>else</span><span> {
        opline</span>->extended_value =<span> ZEND_DO_FCALL_BY_NAME;
    }
}

</span><span>//</span><span> 設(shè)置opcode、op1、op2等</span>
opline->opcode =<span> op;
opline</span>->op1 = *<span>param;
opline</span>->op2.u.opline_num =<span> offset;
SET_UNUSED(opline</span>->op2);
登錄后復(fù)制

上面這段代碼生成了一條SEND指令。如果我們調(diào)用函數(shù)時候傳遞了多個參數(shù),則會調(diào)用多次zend_do_pass_param,最終會生成多條SEND指令。

至于指令具體是SEND_VAR,SEND_VAL,還是SEND_RE,亦或是ZEND_SEND_VAR_NO_REF,則依靠zend_do_pass_param中的判斷。zend_do_pass_param中的邏輯分支比較多,一下子不能弄明白所有分支也沒關(guān)系,最重要的是知道它會根據(jù)函數(shù)的定義以及實際傳遞的參數(shù),產(chǎn)生最合適的SEND指令。

還是回到我們開始的例子,對于?foo($bar)?,則經(jīng)過zend_do_pass_param之后,產(chǎn)生的SEND指令細節(jié)如下:

?

4、結(jié)束編譯

結(jié)束函數(shù)調(diào)用是通過zend_do_end_function_call來完成的。根據(jù)前文所述,zend_do_begin_function_call并不產(chǎn)生一條實際的調(diào)用指令,但它確定了最終函數(shù)調(diào)用走的是DO_FCALL還是DO_FCALL_BY_NAME,并且據(jù)此來生成ZEND_INIT_NS_FCALL_BY_NAME或ZEND_INIT_FCALL_BY_NAME指令。

實際的調(diào)用指令是放在zend_do_end_function_call中來生成的。

具體分析下zend_do_end_function_call

zend_op *<span>opline;

</span><span>//</span><span> 這段邏輯分支現(xiàn)在已經(jīng)走不到了</span>
<span>if</span> (is_method && function_name && function_name->op_type ==<span> IS_UNUSED) {
    </span><span>/*</span><span> clone </span><span>*/</span>
    <span>if</span> (Z_LVAL(argument_list->u.constant) != <span>0</span><span>) {
        zend_error(E_WARNING, </span><span>"</span><span>Clone method does not require arguments</span><span>"</span><span>);
    }
    opline </span>= &CG(active_op_array)->opcodes[Z_LVAL(function_name-><span>u.constant)];
} </span><span>else</span><span> {
    opline </span>=<span> get_next_op(CG(active_op_array) TSRMLS_CC);
    
    </span><span>//</span><span> 函數(shù),名稱確定,非dynamic_fcall,函數(shù)則生成ZEND_DO_FCALL指令</span>
    <span>if</span> (!is_method && !is_dynamic_fcall && function_name->op_type==<span>IS_CONST) {
        opline</span>->opcode =<span> ZEND_DO_FCALL;
        opline</span>->op1 = *<span>function_name;
        ZVAL_LONG(</span>&opline->op2.u.constant, zend_hash_func(Z_STRVAL(function_name->u.constant), Z_STRLEN(function_name->u.constant) + <span>1</span><span>));
    }
    </span><span>//</span><span> 否則生成ZEND_DO_FCALL_BY_NAME指令</span>
    <span>else</span><span> {
        opline</span>->opcode =<span> ZEND_DO_FCALL_BY_NAME;
        SET_UNUSED(opline</span>-><span>op1);
    }
}

</span><span>//</span><span> 生成臨時變量索引,函數(shù)的調(diào)用,返回的znode必然是IS_VAR</span>
opline->result.u.<span>var</span> =<span> get_temporary_variable(CG(active_op_array));
opline</span>->result.op_type =<span> IS_VAR;
</span>*result = opline-><span>result;
SET_UNUSED(opline</span>-><span>op2);

</span><span>//</span><span> 從CG(function_call_stack)彈出當(dāng)前被調(diào)用的函數(shù)</span>
zend_stack_del_top(&<span>CG(function_call_stack));

</span><span>//</span><span> 傳參個數(shù)</span>
opline->extended_value = Z_LVAL(argument_list->u.constant);
登錄后復(fù)制

其中有一段if邏輯分支已經(jīng)走不到了,可以忽略。

具體考據(jù):這段邏輯在462eff3中被添加,主要用于當(dāng)調(diào)用__clone魔術(shù)方法時傳參進行拋錯,但在8e30d96中,已經(jīng)不允許直接調(diào)用__clone方法了,在進入zend_do_end_function_call之前便會終止編譯,所以實際上已經(jīng)再也走不到該分支了。

直接看else部分,else生成了一條zend op指令。如果函數(shù)名確定,函數(shù)已被定義,并且不屬于動態(tài)調(diào)用等,則生成的op指令為ZEND_DO_FCALL,否則生成ZEND_DO_FCALL_BY_NAME。對于ZEND_DO_FCALL指令,其操作數(shù)比較明確,為函數(shù)名,但是對于ZEND_DO_FCALL_BY_NAME來說,由于被調(diào)的函數(shù)尚未明確,所以將操作數(shù)置為UNUSED。

5、總結(jié)

用一張圖總結(jié)一下函數(shù)調(diào)用大致的編譯流程:

紅色的方框為生成的op指令。特別是編譯傳參的地方,情況比較多,可能會產(chǎn)出4種SEND指令。

?

PHP速學(xué)教程(入門到精通)
PHP速學(xué)教程(入門到精通)

PHP怎么學(xué)習(xí)?PHP怎么入門?PHP在哪學(xué)?PHP怎么學(xué)才快?不用擔(dān)心,這里為大家提供了PHP速學(xué)教程(入門到精通),有需要的小伙伴保存下載就能學(xué)習(xí)啦!

下載
來源:php中文網(wǎng)
本文內(nèi)容由網(wǎng)友自發(fā)貢獻,版權(quán)歸原作者所有,本站不承擔(dān)相應(yīng)法律責(zé)任。如您發(fā)現(xiàn)有涉嫌抄襲侵權(quán)的內(nèi)容,請聯(lián)系admin@php.cn
最新問題
開源免費商場系統(tǒng)廣告
最新下載
更多>
網(wǎng)站特效
網(wǎng)站源碼
網(wǎng)站素材
前端模板
關(guān)于我們 免責(zé)申明 意見反饋 講師合作 廣告合作 最新更新
php中文網(wǎng):公益在線php培訓(xùn),幫助PHP學(xué)習(xí)者快速成長!
關(guān)注服務(wù)號 技術(shù)交流群
PHP中文網(wǎng)訂閱號
每天精選資源文章推送
PHP中文網(wǎng)APP
隨時隨地碎片化學(xué)習(xí)
PHP中文網(wǎng)抖音號
發(fā)現(xiàn)有趣的

Copyright 2014-2025 http://m.miracleart.cn/ All Rights Reserved | php.cn | 湘ICP備2023035733號