一、函數的定義 用戶函數的定義從function 關鍵字開始,如下 function foo($var) { echo $var; } 1、詞法分析 在Zend/zend_language_scanner.l中我們找到如下所示的代碼: <ST_IN_SCRIPTING>"function" { re
一、函數的定義
用戶函數的定義從function 關鍵字開始,如下
function foo($var) { echo $var; }
1、詞法分析
在Zend/zend_language_scanner.l中我們找到如下所示的代碼:
<ST_IN_SCRIPTING>"function" { return T_FUNCTION; }
它所表示的含義是function將會生成T_FUNCTION標記。在獲取這個標記後,我們開始語法分析。
2、語法分析
在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關鍵字,是否引用,函數名
T_FUNCTION標記只是用來定位函數的聲明,表示這是一個函數,而更多的工作是與這個函數相關的東西,包括參數,返回值。
3、生成中間代碼
語法解析後,我們看到所執行編譯函數為zend_do_begin_function_declaration。在Zend/zend_complie.c文件找到其實現如下:
void zend_do_begin_function_declaration(znode *function_token, znode *function_name, int is_method, int return_reference, znode *fn_flags_znode TSRMLS_DC) /* {{{ */ { ...//省略 function_token->u.op_array = CG(active_op_array); lcname = zend_str_tolower_dup(name, name_len); orig_interactive = CG(interactive); CG(interactive) = 0; init_op_array(&op_array, ZEND_USER_FUNCTION, INITIAL_OP_ARRAY_SIZE TSRMLS_CC); CG(interactive) = orig_interactive; ...//省略 if (is_method) { ...//省略,類方法 在後面的章節介紹 !ǶGH } else { zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC); opline->opcode = ZEND_DECLARE_FUNCTION; opline->op1.op_type = IS_CONST; build_runtime_defined_function_key(&opline->op1.u.constant, lcname, name_len TSRMLS_CC); 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; zend_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)); } } /* }}} */
生成的代碼為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是同一個函數名,校驗函數名是否重覆,這個過程是在哪進行的呢?
4、執行中間代碼
在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
二、函數的參數
函數的定義只是一個將函數名註冊到函數列表的過程。
1、用戶自定義函數的參數
我們知道對於函數的參數檢查是通過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;
2、內部函數的參數
以常見的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()在解析參數的同時戶儘可能的轉換參數類型,這樣就可以確保我們總是能得到所期望的類型的變數
3、函數的返回值
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返回的原因之一。