程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> PHP編程 >> 關於PHP編程 >> 【譯】PHP的變量實現(給PHP開發者的PHP源碼-第三部分),開發者源碼

【譯】PHP的變量實現(給PHP開發者的PHP源碼-第三部分),開發者源碼

編輯:關於PHP編程

【譯】PHP的變量實現(給PHP開發者的PHP源碼-第三部分),開發者源碼


文章來自:http://www.aintnot.com/2016/02/12/phps-source-code-for-php-developers-part3-variables-ch

原文:http://blog.ircmaxell.com/2012/03/phps-source-code-for-php-developers_21.html

在"給PHP開發者的PHP源碼"系列的第三篇文章,我們打算擴展上一篇文章來幫助理解PHP內部是怎麼工作的。在第一篇文章,我們介紹了如何查看PHP的源碼,它的代碼結構是怎樣的以及一些介紹給PHP開發者的C指針基礎。第二篇文章介紹了函數。這一次,我們打算深入PHP最有用的結構之一:變量。

進入ZVAL

在PHP的核心代碼中,變量被稱為ZVAL。這個結構之所以那麼重要是有原因的,不僅僅是因為PHP使用弱類型而C使用強類型。那麼ZVAL是怎麼解決這個問題的呢?要回答這個問題,我們需要認真的查看ZVAL類型的定義。要查看這個定義,讓我們嘗試在lxr頁面的定義搜索框裡搜索zval。乍一眼看去,我們似乎找不到任何有用的東西。但是有一行typedef在zend.h文件(typedef在C裡面是一種定義新的數據類型的方式)。這個也許就是我們要找的東西,再繼續查看。原來,這看起來是不相干的。這裡並沒有任何有用的東西。但為了確認一些,我們來點擊_zval_struct這一行。

1 struct _zval_struct {
2 /* Variable information */
3 zvalue_value value; /* value */
4 zend_uint refcount__gc;
5 zend_uchar type; /* active type */
6 zend_uchar is_ref__gc;
7 };

然後我們就得到PHP的基礎,zval。看起來很簡單,對嗎?是的,沒錯,但這裡還有一些很有意義的神奇的東西。注意,這是一個結構或結構體。基本上,這可以看作PHP裡面的類,這些類只有公共的屬性。這裡,我們有四個屬性:value,refcount__gc,type以及is_ref__gc。讓我們來一一查看這些屬性(省略它們的順序)。

Value

我們第一個談論的元素是value變量,它的類型是zvalue_value。我不認識你,但我也從來沒有聽說過zvalue_value。那麼讓我們嘗試弄懂它是什麼。跟網站的其他部分一樣,你可以點擊某個類型查看它的定義。一旦你點擊了,你會看到它的定義跟下面的是一樣的:

typedef union _zvalue_value {
    long lval; /* long value */
    double dval; /* double value */
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht; /* hash table value */
    zend_object_value obj;
} zvalue_value;

現在,這裡有一些黑科技。看到那個union的定義嗎?那意味著這不是真正的結構體,而是一個單獨的類型。但是有多個類型的變量在裡面。如果這裡面有多種類型的話,那它怎麼能作為單一的類型呢?我很高興你問了這個問題。要理解這個問題,我們需要先回想我們在第一篇文章談論的C語言中的類型。

在C裡面,變量只是一行內存地址的標簽。也可以說類型只是標識哪一塊內存將被使用的方式。在C裡面沒有使用任何東西將4個字節的字符串和整型值分隔開。它們都只是一整塊的內存。編譯器會嘗試通過"標識"內存段作為變量來解析它,然後將這些變量轉換為特定的類型,但這並不是總是成功(順便說一句,當一個變量“重寫”它得到的內存段,那將會產生段錯誤)。

那麼,據我們所知,union是單獨的類型,它根據怎麼被訪問而使用不同的方式解釋。這可以讓我們定義一個值來支持多種類型。有一點要注意的是,所有類型的數據都必須使用同一塊內存來存儲。這個例子,在64位的編譯器,long和double都會占用64個位來保存。字符串結構體會占用96位(64位存儲字符指針,32位保存整型長度)。hash_table會占用64位,還有zend_object_value會占用96位(32位用來存儲元素,剩下的64位來存儲指針)。而整一個union會占用最大元素的內存大小,因此在這裡就是96位。

現在,如果再看清楚這個聯合體(union),我們可以看到只有5種PHP數據類型在這裡(long == int,double == float,str == string,hashtable == array,zend_object_value == object)。那麼剩下的數據類型去了哪裡呢?原來,這個結構體已經足夠來存儲剩余的數據類型。BOOL使用long(int)來存儲,NULL不占用數據段,RESOURCE也使用long來存儲。

TYPE

因為這個value聯合體並沒有控制它是怎麼被訪問的,我們需要其他方式來記錄變量的類型。這裡,我們可以通過數據類型來得出如何訪問value的信息。它使用type這個字節來處理這個問題(zend_uchar是一個無符號的字符,或者內存中的一個字節)。它從zend類型常量保留這些信息。這真的是一種魔法,是需要使用zval.type = IS_LONG來定義整型數據。因此這個字段和value字段就足夠讓我們知道PHP變量的類型和值。

IS_REF

這個字段標識變量是否為引用。那就是說,如果你執行了在變量裡執行了$foo = &$bar。如果它是0,那麼變量就不是一個引用,如果它是1,那麼變量就是一個引用。它並沒有做太多的事情。那麼,在我們結束_zval_struct之前,再看一看它的第四個成員。

REFCOUNT

這個變量是指向PHP變量容器的指針的計數器。也就是說,如果refcount是1,那就表示有一個PHP變量使用這個容器。如果refcount是2,那就表示有兩個PHP變量指向同一個變量容器。單獨的refcount變量並沒有太多有用的信息,但如果它與is_ref一起使用,就構成了垃圾回收器和寫時復制的基礎。它允許我們使用同一個zval容器來保存一個或多個PHP變量。refcount的語義解釋超出這篇文章的范圍,如果你想繼續深入,我推薦你查看這篇文檔。

這就是ZVAL的所有內容。

它是怎麼工作的?

在PHP內部,zval使用跟其他C變量一樣,作為內存段或者一個指向內存段的指針(或者指向指針的指針,等等),傳遞到函數。一旦我們有了變量,我們就想訪問它裡面的數據。那我們要怎麼做到呢?我們使用定義在zend_operators.h文件裡面的宏來跟zval一起使用,使得訪問數據更簡單。有一點很重要的是,每一個宏都有多個拷貝。不同的是它們的前綴。例如,要得出zval的類型,有Z_TYPE(zval)宏,這個宏返回一個整型數據來表示zval參數。但這裡還有一個Z_TYPE(zval_p)宏,它跟Z_TYPE(zval)做的事情是一樣的,但它返回的是指向zval的指針。事實上,除了參數的屬性不一樣之外,這兩個函數是一樣的,實際上,我們可以使用Z_TYPE(*zval_p),但_P和_PP讓事情更簡單。

我們可以使用VAL這一類宏來獲取zval的值。可以調用Z_LVAL(zval)來得到整型值(比如整型數據和資源數據)。調用Z_DVAL(zval)來得到浮點值。還有很多其他的,到這裡到此為止。要注意的關鍵是,為了在C裡面獲取zval的值,你需要使用宏(或應該)。因此,當我們看見有函數使用它們時,我們就知道它是從zval裡面提取它的值。

那麼,類型呢?

到現在為止,我們知識談論了類型和zval的值。我們都知道,PHP幫我們做了類型判斷。因此,如果我們喜歡,我們可以將一個字符串當作一個整型值。我們把這一步叫做convert_to_type。要轉換一個zval為string值,就調用convert_to_string函數。它會改變我們傳遞給函數的ZVAL的類型。因此,如果你看到有函數在調用這些函數,你就知道它是在轉換參數的數據類型。

Zend_Parse_Paramenters

上一篇文章中,介紹了zend_parse_paramenters這個函數。既然我們知道PHP變量在C裡面是怎麼表示的,那我們就來深入看看。

ZEND_API int zend_parse_parameters(int num_args TSRMLS_DC, const char *type_spec, ...)
{
    va_list va;
    int retval;

    RETURN_IF_ZERO_ARGS(num_args, type_spec, 0);

    va_start(va, type_spec);
    retval = zend_parse_va_args(num_args, type_spec, &va, 0 TSRMLS_CC);
    va_end(va);

    return retval;
}

現在,從表面上看,這看起來很迷惑。重點要理解的是,va_list類型只是一個使用'...'的可變參數列表。因此,它跟PHP中的func_get_args()函數的構造差不多。有了這個東西,我們可以看到zend_parse_parameters函數馬上調用zend_parse_va_args函數。我們繼續往下看看這個函數...

這個函數看起來很有趣。第一眼看去,它好像做了很多事情。但仔細看看。首先,我們可以看到一個for循環。這個for循環主要遍歷從zend_parse_parameters傳遞過來的type_spec字符串。在循環裡面我們可以看到它只是計算期望接收到的參數數量。它是如何做到這些的研究就留給讀者。

繼續往下看,我麼可以看到有一些合理的檢查(檢查參數是否都正確地傳遞),還有錯誤檢查,檢查是否傳遞了足夠數量的參數。接下來進入一個我們感興趣的循環。這個循環真正解析那些參數。在循環裡面,我們可以看到有三個if語句。第一個處理可選參數的標識符。第二個處理var-args(參數的數量)。第三個if語句正是我們感興趣的。可以看到,這裡調用了zend_parse_arg()函數。讓我們再深入看看這個函數...

繼續往下看,我們可以看到這裡有一些非常有趣的事情。這個函數再調用另一個函數(zend_parse_arg_impl),然後得到一些錯誤信息。這在PHP裡面是一種很常見的模式,將函數的錯誤處理工作提取到父函數。這樣代碼實現和錯誤處理就分開了,而且可以最大化地重用。你可以繼續深入研究那個函數,非常容易理解。但我們現在仔細看看zend_parse_arg_impl()...

現在,我們真正到了PHP內部函數解析參數的步驟。讓我們看看第一個switch語句的分支,這個分支用來解析整型參數。接下來的應該很容易理解。那麼,我們從分支的第一行開始吧:

long *p = va_arg(*va, long *);

如果你記得我們之前說的,va_args是C語言處理變量參數的方式。所以這裡是定義一個整型指針(long在C裡面是整型)。總之,它從va_arg函數裡面得到指針。這說明,它得到傳遞給zend_parse_parameters函數的參數的指針。所以這就是我們會用分支結束後的值賦值的指針結果。接下來,我們可以看到進入一個根據傳遞進來的變量(zval)類型的分支。我們先看看IS_STRING分支(這一步會在傳遞整型值到字符串變量時執行)。

case IS_STRING:
{
    double d;
    int type;

    if ((type = is_numeric_string(Z_STRVAL_PP(arg), Z_STRLEN_PP(arg), p, &d, -1)) == 0) {
        return "long";
    } else if (type == IS_DOUBLE) {
        if (c == 'L') {
            if (d > LONG_MAX) {
                *p = LONG_MAX;
                break;
            } else if (d < LONG_MIN) {
                *p = LONG_MIN;
                break;
            }
        }

        *p = zend_dval_to_lval(d);
    }
}
break;

現在,這個做的事情並沒有看起來的那麼多。所有的事情都歸結與is_numeric_string函數。總的來說,該函數檢查字符串是否只包含整數字符,如果不是的話就返回0。如果是的話,它將該字符串解析到變量裡(整型或浮點型,p或d),然後返回數據類型。所以我們可以看到,如果字符串不是純數字,他返回“long”字符串。這個字符串用來包裝錯誤處理函數。否則,如果字符串表示double(浮點型),它先檢查這個浮點數作為整型數來存儲的話是否太大,然後它使用zend_dval_to_lval函數來幫助解析浮點數到整型數。這就是我們所知道的。我們已經解析了我們的字符串參數。現在繼續看看其他分支:

case IS_DOUBLE:
    if (c == 'L') {
        if (Z_DVAL_PP(arg) > LONG_MAX) {
            *p = LONG_MAX;
            break;
        } else if (Z_DVAL_PP(arg) < LONG_MIN) {
        *p = LONG_MIN;
        break;
    }
}
case IS_NULL:
case IS_LONG:
case IS_BOOL:
convert_to_long_ex(arg);
*p = Z_LVAL_PP(arg);
break;

這裡,我們可以看到解析浮點數的操作,這一步跟解析字符串裡的浮點數相似(巧合?)。有一個很重要的事情要注意的是,如果參數的標識不是大寫'L',它會跟其他類型變量一樣的處理方式(這個case語句沒有break)。現在,我們還有一個有趣的函數,convert_to_long_ex()。這跟我們之前說到的convert_to_type()函數集合是一類的,該函數轉換參數為特定的類型。唯一的不同是,如果參數不是引用的話(因為這個函數在改變數據類型),這個函數就將變量的值及其引用分離(拷貝)了。( The only difference is that it separates (copies) the passed in variable if it's not a reference (since it's changing the type). )這就是寫時復制的作用。因此,當我們傳遞一個浮點數到到一個非引用的整型變量,該函數會把它當作整型來處理,但我們仍然可以得到浮點型數據。

case IS_ARRAY:
case IS_OBJECT:
case IS_RESOURCE:
default:
return "long";

最後,我們還有另外三個case分支。我們可以看到,如果你傳遞一個數組、對象、資源或者其他不知道的類型到整型變量中,你會得到錯誤。

剩下的部分我們留給讀者。閱讀zend_parse_arg_impl函數對更好地理解額PHP類型判斷系統真的很有用。一部分一部分地讀,然後盡量追蹤在C裡面的各種參數的狀態和類型。

下一部分

下一部分會在Nikic的博客(我們會在這個系列的文章來回跳轉)。在下一篇,他會談到數組的所有內容。

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