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

来源:http://www.cnblogs.com/h-hq/archive/2016/02/24/5213746.html
-Advertisement-
Play Games

在PHP的核心代碼中,變數被稱為ZVAL。這個結構之所以那麼重要是有原因的,不僅僅是因為PHP使用弱類型而C使用強類型。那麼ZVAL是怎麼解決這個問題的呢?要回答這個問題,我們需要認真的查看ZVAL類型的定義。要查看這個定義,讓我們嘗試在lxr頁面的定義搜索框里搜索zval。乍一眼看去,我們似乎找不...


文章來自: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的博客(我們會在這個系列的文章來回跳轉)。在下一篇,他會談到數組的所有內容。


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

-Advertisement-
Play Games
更多相關文章
  • 具體來說cookie機制採用的是在客戶端保持狀態的方案,而session機制採用的是在伺服器端保持狀態的方案。同時我們也看到,由於採用伺服器端保持狀態的方案在客戶端也需要保存一個標識,所以session機制可能需要藉助於cookie機制來達到保存標識的目的,但實際上它還有其他選擇。 cookie機制
  • 用c#編寫window服務常見的幾個事件 protected int i = 0; public Service1() { InitializeComponent(); } //啟動服務時執行 protected override void OnStart(string[] args) { //使時
  • 本人文筆不好,大致說說自己的困惑 隨著2016的到來,一年一度的職業規劃即將而來,因為本人一直從事著C#工作,也接觸了一些前端知識。 但是由於微軟老大哥,技術更新太快,學習成本太高,但是眾所周知C#工資並不高,畢竟人活著要吃一口飯, ,因此產生換個技術的想法。 但是換什麼技術啊?真是糾結啊 比如手機
  • 安裝JDK 選擇安裝目錄 安裝過程中會出現兩次 安裝提示 。第一次是安裝 jdk ,第二次是安裝 jre 。建議兩個都安裝在同一個java文件夾中的不同文件夾中。(不能都安裝在java文件夾的根目錄下,jdk和jre安裝在同一文件夾會出錯) 如下圖所示 1:安裝jdk 隨意選擇目錄 只需把預設安裝目...
  • 半形全形的處理是字元串處理的常見問題,本文嘗試為大家提供一個思路。 一、概念 全形字元unicode編碼從65281~65374 (十六進位 0xFF01 ~ 0xFF5E)半形字元unicode編碼從33~126 (十六進位 0x21~ 0x7E)空格比較特殊,全形為 12288(0x3000),
  • 指針: 一、聲明 一個 int 類型的 指針 然後 賦值。 二、聲明中直接賦值。 三、空指針 四、懸空指針 野指針: 懸空指針本質上就是 聲明瞭一個 指針類型的變數【如:int *p】,並且沒有賦值。在沒有賦初值的情況下,利用這個指針進行修改【如:*p=100】。就相當於這個指針指向了一個未知的地址
  • 之前在[譯]更快的方式實現PHP數組去重討論了使用array_flip後再調用array_keys函數替換直接調用array_unique函數實現數組去重性能較好。由於原文沒有給出源碼分析和測試的結果,導致給讀者造成迷惑,在此說聲抱歉。為瞭解開讀者的疑惑,筆者承諾了會補上源碼的分析,現在此補上詳細的...
  • 原文:https://nikic.github.io/2012/03/28/Understanding-PHPs-internal-array-implementation.html 歡迎來到”給PHP開發者的PHP源碼”系列的第四部分,這一部分我們會談論PHP數組在內部是如何表示和在代碼庫里使用...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...