程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> PHP編程 >> 關於PHP編程 >> 深入剖析php執行原理(4):函數的調用,深入剖析php

深入剖析php執行原理(4):函數的調用,深入剖析php

編輯:關於PHP編程

深入剖析php執行原理(4):函數的調用,深入剖析php


本章開始研究php中函數的調用和執行,先來看函數調用語句是如何被編譯的。

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

源碼依然取自php5.3.29。

函數調用

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

<?php
function foo($arg1)
{
    print($arg1);
}

$bar = 'hello php';
foo($bar);

在函數編譯一章裡已經分析過,函數foo最終會編譯生成對應的zend_function,存放於函數表(CG(function_table))中。

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

我們從簡單的來入手,弄清楚調用語句的編譯過程及產出,對於復雜的一些調用,下文也爭取都能談到一些。

1、語法推導

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

綠色的節點表示最後對應到php代碼中的字面。紅色的部分是語法推導過程中最重要的幾步,特別是function_call。

我們從語法分析文件zend_language_parser.y中挑出相關的:

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);}
;

function_call_parameter_list:
        non_empty_function_call_parameter_list    { $$ = $1; }
    |    /* empty */                        { Z_LVAL($$.u.constant) = 0; }
;


non_empty_function_call_parameter_list:
        expr_without_variable    { Z_LVAL($$.u.constant) = 1;  zend_do_pass_param(&$1, ZEND_SEND_VAL, Z_LVAL($$.u.constant) TSRMLS_CC); }
    |    variable                { Z_LVAL($$.u.constant) = 1;  zend_do_pass_param(&$1, ZEND_SEND_VAR, Z_LVAL($$.u.constant) TSRMLS_CC); }
    |    '&' w_variable          { Z_LVAL($$.u.constant) = 1;  zend_do_pass_param(&$2, ZEND_SEND_REF, Z_LVAL($$.u.constant) TSRMLS_CC); }
    |    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); }
    |    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); }
    |    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); }
;

其結構並不復雜:

1)function_call這條推導,代表了一個完整的函數調用。

2)namespace_name是指經過命名空間修飾過之後的函數名,由於我們的例子中,函數foo並沒有處於任何一個命名空間裡,所以namespace_name其實就是foo。如果我們的函數定義在命名空間中,則namespace_name是一個類似“全路徑”的fullname。

namespace MyProject
{
function foo($arg1)
{
    print($arg1);
}
}

namespace
{
$bar = 'hello php';
MyProject\foo($bar);// 以類似“全路徑”的fullname來調用函數,則namespace_name為MyProject\foo 
}

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

4)從這些推導產生式裡,我們還能看出編譯時的所運用的一些關鍵處理:

zend_do_begin_function_call-->zend_do_pass_param-->zend_do_end_function_call

開始 解析參數 結束

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

上面提到語法樹我們僅僅畫了一部分,准確講,沒有將namespace以及function_call_parameter_list以下的推導過程進一步畫出來。原因一是namespace的推導比較簡單。第二,由於function_call_parameter_list-->variable這步會回到variable上,而variable經過若干步一直到產生變量$bar的推導比較復雜,也不是本文的重點,所以這裡就不一進步探究了。

2、開始編譯

看下function_call的推導式,一開始,zend vm會執行zend_do_begin_function_call做一些函數調用的准備。

2.1、 zend_do_begin_function_call

代碼注解如下:

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

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

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

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

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

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

// 壓入CG(function_call_stack)
zend_stack_push(&CG(function_call_stack), (void *) &function, sizeof(zend_function *));
zend_do_extended_fcall_begin(TSRMLS_C);
return 0;

有幾點需要理解的:

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

<?php
namespace MyProject;

function foo($arg1)
{
    print($arg1);
}

$bar = 'hello php';
foo($bar); // zend_resolve_non_class_name會將foo處理成MyProject\foo namespace\foo($bar); // 在進入zend_do_begin_function_call之前,函數名已經被擴展成\MyProject\foo,再經過zend_resolve_non_class_name,將\MyProject\foo處理成MyProject\foo \MyProject\foo($bar); // zend_resolve_non_class_name會將\MyProject\foo處理成MyProject\foo

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

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

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

<?php
namespace MyProject\sub;

function foo($arg1)
{
    print($arg1);
}

namespace MyProject;
$bar = 'hello php';
sub\foo($bar);      // 以sub\foo調用函數,並不算shortname,因為存在\

注意上述例子,我們以sub\foo來調用函數。zend_resolve_non_class_name會將函數名處理成MyProject\sub\foo。不過is_compound是在zend_resolve_non_class_name之前算的,由於sub\foo存在"\",所以is_compound為"\foo",!is_compound是false,因而不能進入zend_do_begin_dynamic_function_call。

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

<?php
$bar = 'hello php';

// 先調用
foo($bar);

// 後定義
function foo($arg1)
{
    print($arg1);
}

4,在zend_do_begin_function_call的最後,我們將函數壓入CG(function_call_stack)。這是一個棧,因為在後續對傳參的編譯,我們仍然需要用到函數,所以這裡將其壓亞入棧中,方便後面獲取使用。之所以用棧,是因為調用函數傳遞的參數,可能是另一次函數調用。為了確保參數總是能找到對應的函數,所以用棧。

<?php
function foo($arg1)
{
    print($arg1);
}

$bar = 'hello php';
foo(strlen($bar));   // 首先foo入棧,然後分析參數strlen($bar),發現依然是個函數,於是strlen入棧,再分析參數$bar,此時彈出對應的函數正好為strlen。

2.2、 zend_do_begin_dynamic_function_call

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

一是,在命名空間中,以shortname調用函數;

二是,在調用函數時,尚未定義函數。

其實還有第三種情況會走到zend_do_begin_dynamic_function_call,就是當我們調用函數的時候,函數名並非直接寫成字面,而是通過變量等形式來間接確定。這種情況下,zend vm會直接執行zend_do_begin_dynamic_function_call。

舉例1:

<?php
function foo($arg1)
{
    print($arg1);
}

$bar = 'hello php';
$func = 'foo';
$func($bar);          // 我們以變量$func作為函數名,試圖調用函數foo,$func類型是IS_CV

此時, $func($bar) 對應function_call語法推導式的最後一條:

function_call:
... | 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);}

推導式中的variable_without_objects對應的就是變量 $func 。$func其實是一個compiled_variable,並且在op_array->vars數組中索引為1,索引為0的是在它之前定義的變量 $bar 。

舉例2:

function foo($arg1)
{
    print($arg1);
}

$bar = 'hello php';
$func = 'foo';
$ref_func = 'func';
$$ref_func($bar); // 以可變變量的形式來調用函數,$$ref_func類型是IS_VAR

該例是以可變變量來調用函數,和例1一樣, $$ref_func($bar)也是對應function_call語法推導式的最後一條,所以不會走進zend_do_begin_function_call,而是直接進入zend_do_begin_dynamic_function_call。不同的點在於 $$ref_func 節點類型不再是compiled_variable,而是普通的variable,標識為IS_VAR。

下面的圖畫出了5種case,第1種不經過zend_do_begin_dynamic_function_call,而後4種會調用zend_do_begin_dynamic_function_call處理,注意最後2種不經過zend_do_begin_function_call:

具體看下zend_do_begin_dynamic_function_call的代碼:

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

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

    // 將NULL壓入CG(function_call_stack)
    zend_stack_push(&CG(function_call_stack), (void *) &ptr, sizeof(zend_function *));
    zend_do_extended_fcall_begin(TSRMLS_C);
}

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

以ns_call = 1為例:

<?php
namespace MyProject;

function foo($arg1)
{
    print($arg1);
}

$bar = 'hello php';
foo($bar);

 生成的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)獲取當前函數,注意可能拿出的是NULL zend_stack_top(&CG(function_call_stack), (void **) &function_ptr_ptr); function_ptr = *function_ptr_ptr; // 調用的地方以引用傳參,但是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)中獲取當前參數對應的函數。注意,可能拿到的只是一個NULL。因為php的語法允許我們先函數調用,再接著對函數進行定義。如前文所述,這種情況下zend_do_begin_function_call中會向CG(function_call_stack)中壓入NULL,同時會產生DO_FCALL_BY_NAME指令。

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

expr_without_variable    { Z_LVAL($$.u.constant) = 1;  zend_do_pass_param(&$1, ZEND_SEND_VAL, Z_LVAL($$.u.constant) TSRMLS_CC); }
variable                 { Z_LVAL($$.u.constant) = 1;  zend_do_pass_param(&$1, ZEND_SEND_VAR, Z_LVAL($$.u.constant) TSRMLS_CC); }
'&' w_variable           { Z_LVAL($$.u.constant) = 1;  zend_do_pass_param(&$2, ZEND_SEND_REF, Z_LVAL($$.u.constant) TSRMLS_CC); }

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

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

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

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

// 如果用戶傳遞的參數,本身就是一次函數調用,則將op改成ZEND_SEND_VAR_NO_REF
if (op == ZEND_SEND_VAR && zend_is_function_or_method_call(param)) {
    /* Method call */
    op = ZEND_SEND_VAR_NO_REF;
    send_function = ZEND_ARG_SEND_FUNCTION;
}
// 如果用戶傳遞的參數,是一個表達式,並且結果會產生中間變量,則也將op改成ZEND_SEND_VAR_NO_REF
else if (op == ZEND_SEND_VAL && (param->op_type & (IS_VAR|IS_CV))) {
    op = ZEND_SEND_VAR_NO_REF;
}

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

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

3,op == ZEND_SEND_VAL對應的是一個表達式,如果該表達式產生了一個變量作為結果,則也需要將op改成ZEND_SEND_VAR_NO_REF。例如:

繼續來看zend_do_pass_param:

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

// 如果實際傳遞的參數是變量,調用zend_do_end_variable_parse處理鏈式調用
if (original_op == ZEND_SEND_VAR) {
    switch (op) {
        case ZEND_SEND_VAR_NO_REF:
            zend_do_end_variable_parse(param, BP_VAR_R, 0 TSRMLS_CC);
            break;
        case ZEND_SEND_VAR:
            if (function_ptr) {
                zend_do_end_variable_parse(param, BP_VAR_R, 0 TSRMLS_CC);
            } else {
                zend_do_end_variable_parse(param, BP_VAR_FUNC_ARG, offset TSRMLS_CC);
            }
            break;
        case ZEND_SEND_REF:
            zend_do_end_variable_parse(param, BP_VAR_W, 0 TSRMLS_CC);
            break;
    }
}

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

function foo(&$a)
{
    print($a);
}

foo($bar == 1);  // 拋錯 "Only variables can be passed by reference"

上面 $bar == 1 表達式的編譯結果,op_type為IS_TMP_VAR,可以看做一種臨時的中間結果,並非IS_VAR,IS_CV,因此無法編譯成功。看著邏輯有點繞,其實很好理解。因為我們傳遞引用,實際目的是希望能夠在函數中,對這個參數的值進行修改,需要參數是可寫的。然而 $bar == 1 產生的中間結果,我們無法做出修改,是只讀的。

來看zend_do_pass_param的最後一段:

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

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

// 設置opcode、op1、op2等
opline->opcode = op;
opline->op1 = *param;
opline->op2.u.opline_num = offset;
SET_UNUSED(opline->op2);

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

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

還是回到我們開始的例子,對於 foo($bar) ,則經過zend_do_pass_param之後,產生的SEND指令細節如下:

 

4、結束編譯

結束函數調用是通過zend_do_end_function_call來完成的。根據前文所述,zend_do_begin_function_call並不產生一條實際的調用指令,但它確定了最終函數調用走的是DO_FCALL還是DO_FCALL_BY_NAME,並且據此來生成ZEND_INIT_NS_FCALL_BY_NAME或ZEND_INIT_FCALL_BY_NAME指令。

實際的調用指令是放在zend_do_end_function_call中來生成的。

具體分析下zend_do_end_function_call:

zend_op *opline;

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

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

// 從CG(function_call_stack)彈出當前被調用的函數
zend_stack_del_top(&CG(function_call_stack));

// 傳參個數
opline->extended_value = Z_LVAL(argument_list->u.constant);

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

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

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

5、總結

用一張圖總結一下函數調用大致的編譯流程:

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

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved