深入理解PHP內核(六)函數的定義、傳參及返回值

来源:http://www.cnblogs.com/orlion/archive/2016/02/16/5193400.html
-Advertisement-
Play Games

一、函數的定義 用戶函數的定義從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返回的原因之一。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 需要調用win32api,winform、wpf通用 [DllImport("user32.dll")] public static extern bool GetCursorPos(out POINT lpPoint); [StructLayout(LayoutKind.Sequential)]
  • 在本文中,我們將通過一個簡單的處理來記錄在我們的網站中的錯誤和異常。我們這樣操作,每當遇到程式錯誤時,將使用者導航到一個單獨的頁面,同時錯誤將被記錄到伺服器上的一個文本文件,每當錯誤發生時,我們將以日誌的形式每天記錄。 首先,我先寫一個靜態方法用於將錯誤信息記錄到文本文件,這裡是將錯誤信息記錄到服務
  • 註釋 •#語句 •<# 語句 #> 變數 •命名法則 $ 做為首碼 •使用字母、數字、下劃線均可 •查看所有變數 Get-ChildItem variable:或Get-Varialbe •獲取作用域變數Get-Variable –Scope [Local|Global] •實例化對象 $dt=Ne
  • <MediaElement x:Name="myGif" MediaEnded="myGif_MediaEnded" UnloadedBehavior="Manual" Source="file://C:\waiting.GIF" LoadedBehavior="Play" Stretch="Non
  • ThinkPHP3.2.3 Page.class.php文件源碼 <?php // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST TH
  • I/O類使用 由於在IO操作中,需要使用的數據源有很多,作為一個IO技術的初學者,從讀寫文件開始學習IO技術是一個比較好的選擇。因為文件是一種常見的數據源,而且讀寫文件也是程式員進行IO編程的一個基本能力。本章IO類的使用就從讀寫文件開始。 1 文件操作 文件(File)是 最常見的數據源之一,在程
  • Java學習筆記 類型1.1:類的相關 Tags: 編程,Java 類之間關係 代理例子查看《Java編程思想》中的P131。簡單來說,代理就是組合之後,對組合進來的類的方法進行封裝(不是重寫,而是在函數體內調用組合進來類的對應的"同名"方法。)。 類訪問許可權 |訪問修飾符| 同類 | 同包 |不同
  • 所有的外部輸入參數都應該檢查合法性。 未正確處理輸入數據將可能導致sql註入等漏洞。 框架提供系列函數來取$_REQUEST中的值 requestInt requestString requestFloat requestBool ps:註意$_REQUEST中變數類型可能會是數組 如請求為 ?i[
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...