摘要:文章來自原文在給開發者的源碼系列的第三篇文章,我們打算擴展上一篇文章來幫助理解內部是怎么工作的。進入在的核心代碼中,變量被稱為。要轉換一個為值,就調用函數。有了這個東西,我們可以看到函數馬上調用函數。
文章來自:http://www.hoohack.me/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這一行。
struct _zval_struct { /* Variable information */ zvalue_value value; /* value */ zend_uint refcount__gc; zend_uchar type; /* active type */ zend_uchar is_ref__gc; };
然后我們就得到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的博客(我們會在這個系列的文章來回跳轉)。在下一篇,他會談到數組的所有內容。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/21342.html
摘要:為了防止你錯過了之前的文章,以下是鏈接第一部分給開發者的源碼源碼結構第二部分理解內部函數的定義第三部分的變量實現所有的東西都是哈希表基本上,里面的所有東西都是哈希表。哈希后的結果可以被作為正常的數組的鍵值又名為內存塊。表示哈希表的容量。 文章來自:http://www.hoohack.me/2016/02/15/understanding-phps-internal-array-im...
摘要:另一個說明我叫它做宏。你可以為函數定義寫一個宏事實上,就是這么做的,但我們會在后面的文章中深入了解這個。我想說的是,宏允許在預處理編譯時使用更簡單的代碼。或者說頭文件定義了在文件中可以被其他文件看到的函數,包括預處理宏。 文章來自:http://www.hoohack.me/2016/02/04/phps-source-code-for-php-developers-ch 原文:ht...
摘要:文章來自原文歡迎來到給開發者的源碼系列的第二部分。是在內部代表任意一個變量的定義。這種情況下函數會拋出警告,而此函數馬上返回會返回給的用戶層代碼。原因是,是少數通過而不是擴展定義的函數。下一部分下一部分會再次發表在。 文章來自:http://www.hoohack.me/2016/02/10/understanding-phps-internal-function-definitio...
摘要:獨立的擴展可以獨立于源碼之外進行分發。定義一個新擴展我們給示例擴展命名為。對于一個獨立擴展來說,你只需要做一些宏調用即可。通過以上的步驟,你已經有了一個獨立的擴展了。 本文翻譯自 PHP 源碼中的 README.SELF-CONTAINED-EXTENSIONS。文中標記了 注 的內容均為自己添加。內容有點老,也挺啰嗦,沒講什么深入的內容,但是可以作為入門學習參考。 獨立的 PHP 擴...
摘要:原文地址通過加載環境變量并且能夠自動的通過和自動調用這是一個版本為什么是你不能在代碼中存儲任何的敏感賬號數據存儲在環境中存儲配置是的一項規則在部署中可能變化的所有的內容諸如數據庫認證或者第三方服務的認證應該從代碼中剝離出來也就是環境變量的 原文地址:PHP dotenv 通過 .env 加載環境變量并且能夠自動的通過 getenv(), $_ENV 和 $_SERVER 自動調用. 這...
閱讀 2348·2021-11-15 11:37
閱讀 2625·2021-09-23 11:21
閱讀 2951·2021-09-07 10:11
閱讀 3164·2019-08-30 15:53
閱讀 2826·2019-08-29 15:13
閱讀 1606·2019-08-26 13:57
閱讀 1098·2019-08-26 12:23
閱讀 2438·2019-08-26 11:51