深刻理解PHP內核(十二)函數-函數的定義、傳參及返回值

原文連接:http://www.orlion.ga/344/node

1、函數的定義數組

  用戶函數的定義從function 關鍵字開始,以下app

function foo($var) {    echo $var;
}

  一、詞法分析函數

  在Zend/zend_language_scanner.l中咱們找到以下所示的代碼:ui

<ST_IN_SCRIPTING>"function" {    return T_FUNCTION;
}

  它所表示的含義是function將會生成T_FUNCTION標記。在獲取這個標記後,咱們開始語法分析。this

  二、語法分析spa

  在Zend/zend_language_parser.y文件中找到函數的聲明過程標記以下:指針

複製代碼

function:
    T_FUNCTION { $$.u.opline_num = CG(zend_lineno); }
;
 
is_reference:        /* empty */ { $$.op_type = ZEND_RETURN_VAL; }    |   '&'         { $$.op_type = ZEND_RETURN_REF; }
;
 
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); }
;

複製代碼

    關注點在function is_reference T_STRING,表示function關鍵字,是否引用,函數名code

  T_FUNCTION標記只是用來定位函數的聲明,表示這是一個函數,而更多的工做是與這個函數相關的東西,包括參數,返回值。blog

  三、生成中間代碼

  語法解析後,咱們看到所執行編譯函數爲zend_do_begin_function_declaration。在Zend/zend_complie.c文件找到其實現以下:

複製代碼

 zend_do_begin_function_declaration(znode ** is_method,  return_reference, znode *fn_flags_znode TSRMLS_DC) 
    function_token->u.op_array ==== &=
 
    „!*opline =->opcode =->op1.op_type =&opline->->op2.op_type =->op2.u.constant.type =->op2.u.constant.value.str.val =->op2.u.constant.value.str.len =->op2.u.constant, ->extended_value =-
>->op1.u.constant.value.str.len, & **) &

複製代碼

  生成的代碼爲ZEND_DECLARE_FUNCTION,根據這個中間的代碼及操做數對應的op_type。咱們能夠找到中間代碼的執行函數爲ZEND_DECLARE_FUNCTION_SPEC_HANDLER。

    在生成中間代碼的時候,能夠看到已經統一了函數名所有爲小寫,表示函數的名稱不是區  分大小寫的。

  爲驗證這個實現,咱們看一段代碼

複製代碼

function T() {
    echo 1;
}
 
function t() {
    echo 2;
}

複製代碼

  執行代碼會報錯Fatal error: Cannot redeclare t() (previously declared in …)

  表示對於PHP來講T和t是同一個函數名,校驗函數名是否重複,這個過程是在哪進行的呢?

  四、執行中間代碼

  在Zend/zend_vm_execute.h文件中找到ZEND_DECLARE_FUNCTION中間代碼對應的執行函數:ZEND_DECLARE_FUNCTION_SPEC_HANDLER。此函數只調用了函數do_bind_function。其調用代碼爲:

do_bind_function(EX(opline), EG(function_table), 0);

  在這個函數中將EX(opline)所指向的函數添加到EG(function_table)中,並判斷是否已經存在相同名字的函數,若是存在則報錯,EG(function_table)用來存放執行過程當中所有的函數信息,至關於函數的註冊表。它的結構是一個HashTable,因此在do_bind_function函數中添加新的函數使用的是HashTable的操做函數zend_hash_add

 

2、函數的參數

  函數的定義只是一個將函數名註冊到函數列表的過程。

  一、用戶自定義函數的參數

  咱們知道對於函數的參數檢查是經過zend_do_receive_arg函數來實現的,在此函數中對於參數的關鍵代碼以下:

複製代碼

CG(active_op_array)->arg_info = erealloc(CG(active_op_array)->arg_info,        sizeof(zend_arg_info)*(CG(active_op_array)->num_args));
cur_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;

複製代碼

  整個參數的傳遞是經過給中間代碼的arg_info字段執行賦值操做完成。關鍵點是在arg_info字段,arg_info字段的結構以下:

複製代碼

typedef struct _zend_arg_info {    const char *name;   /*參數的名稱*/
    zend_uint name_len;     /*參數名稱的長度*/
    const char *class_name; /* 類名*/
     zend_uint class_name_len;   /*類名長度*/
    zend_bool array_type_hint;  /*數組類型提示*/
    zend_bool allow_null;   /*是否容許爲NULLͺ*/
    zend_bool pass_by_reference;    /*是否引用傳遞*/
    zend_bool return_reference; 
    int required_num_args;  
} zend_arg_info;

複製代碼

  參數的值傳遞和參數傳遞的區別是經過pass_by_reference參數在生成中間代碼時實現的。

  對於參數的個數,中間代碼中包含的arg_nums字段在每次執行**zend_do_receive_argxx時都會加1.以下代碼:

CG(active_op_array)->num_args++;

  而且當前參數的索引爲ŒCG(active_op_array)->num_args-1.以下代碼:

cur_arg_info = &CG(active_op_array)->arg_info[CG(active_op_array)->num_args-1];

  以上的分析是針對函數定義時的參數設置,這些參數是固定的。而在實際編寫程序時可能咱們會用到可變參數。此時咱們會用到函數func_num_args和func_get_args。它們是之內部函數存在。因而在Zend\zend_builtin_functions.c文件中找到這兩個函數的實現。咱們首先來看func_num_args函數的實現,其代碼以下:

複製代碼

/* {{{ proto int func_num_args(void)
   Get the number of arguments that were passed to the function */ZEND_FUNCTION(func_num_args)
{
    zend_execute_data *ex = EG(current_execute_data)->prev_execute_data; 
    if (ex && ex->function_state.arguments) {
        RETURN_LONG((long)(zend_uintptr_t)*(ex->function_state.arguments));
    } else {
        zend_error(E_WARNING,"func_num_args():  Called from the global scope - no function context");
        RETURN_LONG(-1);
    }
}/* }}} */

複製代碼

  在存在ex->function_state.arguments的狀況下,及函數調用時,返回ex->function_state.arguments轉化後的值,不然顯示錯誤並返回-1。這裏最關鍵的一點是EG(current_execute_data)。這個變量存放的是當前執行程序或函數的數據,此時咱們須要取前一個執行程序的數據,爲何呢?由於這個函數的調用是在進入函數後執行的。函數的相關數據等都在以前執行過程當中,因而調用的是:

zend_execute_data *ex = EG(current_execute_data)->prev_execute_data;

 

  二、內部函數的參數

  以常見的count函數爲例,其參數處理部分的代碼以下:

複製代碼

/* {{{ proto int count(mixed var [, int mode])
   Count the number of elements in a variable (usually an array) */PHP_FUNCTION(count)
{
    zval *array;    long mode = COUNT_NORMAL; 
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z|l",         &array, &mode) == FAILURE) {        return;
    }
    ... //省略}

複製代碼

  這裏包括了兩個操做:一個是取參數的個數,一個是解析參數列表。

  (1)取參數的個數

  取參數的個數是經過ZEND_NUM_ARGS()宏來實現的,其定義以下:

#define ZEND_NUM_ARGS()     (ht)

  ht是在Zend/zend.h文件中定義的宏INTERNAL_FUNCTION_PARAMETERS中的ht,以下

#define INTERNAL_FUNCTION_PARAMETERS int ht, zval *return_value,zval **return_value_ptr, zval *this_ptr, int return_value_used TSRMLS_DC

  (2)解析參數列表

  PHP內部函數在解析參數時使用的是zend_parse_parameters。它能夠大大簡化參數的接收處理工做,雖然它在處理可變參數時還有點弱。

  其聲明以下:

ZEND_API int zend_parse_parameters(int num_args TSRMLS_DC, char *type_spec, 
...)
  • 第一個參數num_args代表表示想要接收的參數個數,咱們常用ZEND_NUM_ARGS()來表示對傳入的參數「有多少要多少」

  • 第二個參數應該是宏TSRMLS_CC。

  • 第三個參數type_spec是一個字符串,用來指定咱們所期待接收的各個參數的類型,有點相似於printf中指定輸出格式的那個格式化字符串。

  • 剩下的參數就是咱們用來接收PHP參數值的變量的指針。

  zend_parse_parameters()在解析參數的同時戶儘量的轉換參數類型,這樣就能夠確保咱們老是能獲得所指望的類型的變量

 

  三、函數的返回值

  PHP中函數都有返回值,沒return返回null

  (1)return語句

  從Zend/zend_language_parser.y文件中能夠確認其生成中間代碼調用的是zend_do_return函數。

複製代碼

void zend_do_return(znode *expr, int do_end_vparse TSRMLS_DC) /* {{{ */{
    zend_op *opline;    int start_op_number, end_op_number; if (do_end_vparse) {        if (CG(active_op_array)->return_reference                && !zend_is_function_or_method_call(expr)) {
            zend_do_end_variable_parse(expr, BP_VAR_W, 0 TSRMLS_CC);/* 處理返回引用 */
        } else {
            zend_do_end_variable_parse(expr, BP_VAR_R, 0 TSRMLS_CC);/* 處理常規變量返回 */
        }
    }
 
   ...// 省略,取其餘中間代碼操做 
    opline->opcode = ZEND_RETURN; 
    if (expr) {
        opline->op1 = *expr; 
        if (do_end_vparse && zend_is_function_or_method_call(expr)) {
            opline->extended_value = ZEND_RETURNS_FUNCTION;
        }
    } else {
        opline->op1.op_type = IS_CONST;
        INIT_ZVAL(opline->op1.u.constant);
    }
 
    SET_UNUSED(opline->op2);
}/* }}} */

複製代碼

  生成中間代碼爲ZEND_RETURN。第一個操做數的類型在返回值爲可用的表達式時,其類型爲表達式的操做類型,不然類型爲IS_CONST。這在後續計算執行中間代碼函數時有用到。根據操做數的不一樣,ZEND_RETURN中間代碼會執行ZEND_RETURN_SPEC_CONST_HANDLER,ZEND_RETURN_SPEC_TMP_HANDLER或ZEND_RETURN_SPEC_TMP_HANDLER。這三個函數的執行流程基本相似,包括對一些錯誤的處理。這裏咱們以ZEND_RETURN_SPEC_CONST_HANDLER爲例說明函數返回值的執行過程:

複製代碼

static int ZEND_FASTCALL  
ZEND_RETURN_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    zend_op *opline = EX(opline);
    zval *retval_ptr;
    zval **retval_ptr_ptr; 
 
    if (EG(active_op_array)->return_reference == ZEND_RETURN_REF) { 
        //  ǓǔŷsÁ\ɁƶMļ@ɗÁĻļ
        if (IS_CONST == IS_CONST || IS_CONST == IS_TMP_VAR) {   
            /* Not supposed to happen, but we'll allow it */
            zend_error(E_NOTICE, "Only variable references \
                should be returned by reference");
            goto return_by_value;
        }
 
        retval_ptr_ptr = NULL;  //  ǓǔŔ
 
        if (IS_CONST == IS_VAR && !retval_ptr_ptr) {
            zend_error_noreturn(E_ERROR, "Cannot return string offsets by reference");        } if (IS_CONST == IS_VAR && !Z_ISREF_PP(retval_ptr_ptr)) {            if (opline->extended_value == ZEND_RETURNS_FUNCTION &&
                EX_T(opline->op1.u.var).var.fcall_returned_reference) {
            } else if (EX_T(opline->op1.u.var).var.ptr_ptr ==
                    &EX_T(opline->op1.u.var).var.ptr) {                if (IS_CONST == IS_VAR && !0) {                      /* undo the effect of get_zval_ptr_ptr() */
                    PZVAL_LOCK(*retval_ptr_ptr);
                }
                zend_error(E_NOTICE, "Only variable references \
                 should be returned by reference");
                goto return_by_value;
            }
        } 
        if (EG(return_value_ptr_ptr)) { //  Ǔǔŷs
            SEPARATE_ZVAL_TO_MAKE_IS_REF(retval_ptr_ptr);   //  is_ref__gcőęŒ1
            Z_ADDREF_PP(retval_ptr_ptr);    //  refcount__gcŒď×1 
            (*EG(return_value_ptr_ptr)) = (*retval_ptr_ptr);
        }
    } else {
return_by_value:
 
        retval_ptr = &opline->op1.u.constant; 
        if (!EG(return_value_ptr_ptr)) {            if (IS_CONST == IS_TMP_VAR) {
 
            }
        } else if (!0) { /* Not a temp var */
            if (IS_CONST == IS_CONST ||
                EG(active_op_array)->return_reference == ZEND_RETURN_REF ||
                (PZVAL_IS_REF(retval_ptr) && Z_REFCOUNT_P(retval_ptr) > 0)) {
                zval *ret;
 
                ALLOC_ZVAL(ret);
                INIT_PZVAL_COPY(ret, retval_ptr);   //  Ł™ͿʍǓǔŔ                 zval_copy_ctor(ret);                *EG(return_value_ptr_ptr) = ret;
            } else {                *EG(return_value_ptr_ptr) = retval_ptr; //  ħ6ɶŔ                Z_ADDREF_P(retval_ptr);
            }
        } else {
            zval *ret;
 
            ALLOC_ZVAL(ret);
            INIT_PZVAL_COPY(ret, retval_ptr);    //  Ł™ͿʍǓǔŔ 
            *EG(return_value_ptr_ptr) = ret;    
        }
    } 
    return zend_leave_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU);   //  Ǔǔĉˆșʒ
}

複製代碼

  函數的返回值在程序執行時存儲在*EG(return_value_ptr_ptr)。ZEND內核對值返回和引用返回做了區別,而且在此基礎上對常量,臨時變量和其餘類型的變量在返回時做了不一樣的處理。在return執行完以後,ZEND內核經過調用zend_leave_helper_SPEC函數,清除函數內部使用的變量等。這也是ZEND內核自動給函數加上NULL返回的緣由之一。

相關文章
相關標籤/搜索