深刻PHP內核(一)——弱類型變量原理探究

PHP做爲一門簡單而強大的語言,可以提供不少Web適用的語言特性,而從本期《問底》開始,王帥將從實踐出發,帶你弄清PHP內核中一些經常使用的部分,好比這裏的「弱類型變量原理」。

PHP是一門簡單而強大的語言,提供了不少Web適用的語言特性,其中就包括了變量弱類型,在弱類型機制下,你可以給一個變量賦任意類型的值。

PHP的執行是經過Zend Engine(下面簡稱ZE),ZE是使用C編寫,在底層實現了一套弱類型機制。ZE的內存管理使用寫時拷貝、引用計數等優化策略,減小再變量賦值時候的內存拷貝。php

下面不光帶你探索PHP弱類型的原理,也會在寫PHP擴展角度,介紹如何操做PHP的變量。
數據庫

1. PHP的變量類型

PHP的變量類型有8種:數組

  • 標準類型:布爾boolen,整型integer,浮點float,字符string
  • 複雜類型:數組array,對象object
  • 特殊類型:資源resource  

PHP不會嚴格檢驗變量類型,變量能夠不顯示的聲明其類型,而在運行期間直接賦值。也能夠將變量自由的轉換類型。以下例,沒有實現聲明的狀況下,$i能夠賦任意類型的值。安全

  1. <? php  $i = 1;   //int $i = 'show me the money';  //string $i = 0.02;  // float $i = array(1, 2, 3);  // array $i = new Exception('test', 123); // object $i = fopen('/tmp/aaa.txt', 'a') // resource ?>   

若是你對弱類型原理理解不深入,在變量比較時候,會出現「超出預期」的驚喜。數據結構

  1. <? PHP $str1 = null;  $str2 = false;  echo $str1==$str2 ? '相等' : '不相等';  $str3 = '';  $str4 = 0;  echo $str3==$str4 ? '相等' : '不相等';  $str5 = 0;  $str6 = '0';  echo $str5==$str6 ? '相等' : '不相等';  ?>   

以上三個結果所有是相等,由於在變量比較的時候,PHP內部作了變量轉換。若是但願值和類型同時判斷,請使用三個=(如,$a===0)來判斷。也許你會以爲司空見慣,也許你會以爲很神奇,那麼請跟我一塊兒深刻PHP內核,探索PHP變量原理。函數

2. 變量的存儲及標準類型介紹

PHP的全部變量,都是以結構體zval來實現,在Zend/zend.h中咱們能看到zval的定義:優化

  1. typedef union _zvalue_value {     long lval;                 /* long value */     double dval;               /* double value */     struct {                            char *val;         int len;               /* this will always be set for strings */     } str;                     /* string (always has length) */     HashTable *ht;             /* an array */     zend_object_value obj;     /* stores an object store handle, and handlers */  } zvalue_value;   
屬性名 含義 默認值
refcount__gc 表示引用計數 1
is_ref__gc 表示是否爲引用 0
value 存儲變量的值  
type 變量具體的類型  

其中refcount__gc和is_ref__gc表示變量是不是一個引用。type字段標識變量的類型,type的值能夠是:IS_NULL,IS_BOOL,IS_LONG,IS_FLOAT,IS_STRING,IS_ARRAY,IS_OBJECT,IS_RESOURCE。PHP根據type的類型,來選擇如何存儲到zvalue_value。

zvalue_value可以實現變量弱類型的核心,定義以下:this

  1. typedef union _zvalue_value {     long lval;                 /* long value */     double dval;               /* double value */     struct {                            char *val;         int len;               /* this will always be set for strings */     } str;                     /* string (always has length) */     HashTable *ht;             /* an array */     zend_object_value obj;     /* stores an object store handle, and handlers */  } zvalue_value;   

布爾型,zval.type=IS_BOOL,會讀取zval.value.lval字段,值爲1/0。若是是字符串,zval.type=IS_STRING,會讀取zval.value.str,這是一個結構體,存儲了字符串指針和長度。spa

C語言中,用"\0"做爲字符串結束符。也就是說一個字符串"Hello\0World"在C語言中,用printf來輸出的話,只能輸出hello,由於"\0"會認爲字符已經結束。PHP中是經過結構體的_zval_value.str.len來控制字符串長度,相關函數不會遇到"\0"結束。因此PHP的字符串是二進制安全的。.net

若是是NULL,只須要zval.type=IS_NULL,不須要讀取值。

經過對zval的封裝,PHP實現了弱類型,對於ZE來講,經過zval能夠存取任何類型。

3. 高級類型Array和Object數組Array

數組是PHP語言中很是強大的一個數據結構,分爲索引數組和關聯數組,zval.type=IS_ARRAY。在關聯數組中每一個key能夠存儲任意類型的數據。PHP的數組是用Hash Table實現的,數組的值存在zval.value.ht中。 

後面會專門講到PHP哈希表的實現。

對象類型的zval.type=IS_OBJECT,值存在zval.value.obj中。

4. 特殊類型——資源類型(Resource)介紹

資源類型是個很特殊的類型,zval.type=IS_RESOURCE,在PHP中有一些很難用常規類型描述的數據結構,好比文件句柄,對於C語言來講是一個指針,不過PHP中沒有指針的概念,也不能用常規類型來約束,所以PHP經過資源類型概念,把C語言中相似文件指針的變量,用zval結構來封裝。資源類型值是一個整數,ZE會根據這個值去資源的哈希表中獲取。  

資源類型的定義:

  1. typedefstruct_zend_rsrc_list_entry {     void *ptr;     int type;     int refcount;  }zend_rsrc_list_entry;   

其中,ptr是一個指向資源的最終實現的指針,例如一個文件句柄,或者一個數據庫鏈接結構。type是一個類型標記,用於區分不一樣的資源類型。refcount用於資源的引用計數。

內核中,資源類型是經過函數ZEND_FETCH_RESOURCE獲取的。

  1. ZEND_FETCH_RESOURCE(con, type, zval *, default, resource_name, resource_type);   

5. 變量類型的轉換

按照如今咱們對PHP語言的瞭解,變量的類型依賴於zval.type字段指示,變量的內容按照zval.type存儲到zval.value。當PHP中須要變量的時候,只須要兩個步驟:把zval.value的值或指針改變,再改變zval.type的類型。不過對於PHP的一些高級變量Array/Object/Resource,變量轉換要進行更多操做。

變量轉換原理分爲3種:

5.1 標準類型相互轉換

比較簡單,按照上述的步驟轉化便可。

5.2 標準類型與資源類型轉換

資源類型能夠理解爲是int,比較方便轉換標準類型。轉換後資源會被close或回收。

  1. <? php $var = fopen('/tmp/aaa.txt''a'); // 資源 #1 $var = (int) $var; var_dump($var);  // 輸出1 ?>  

5.3 標準類型與複雜類型轉換

Array轉換整型int/浮點型float會返回元素個數;轉換bool返回Array中是否有元素;轉換成string返回'Array',並拋出warning。
詳細內容取決於經驗,請閱讀PHP手冊: http://php.net/manual/en/language.types.type-juggling.php

5.4 複雜類型相互轉換

array和object能夠互轉。若是其它任何類型的值被轉換成對象,將會建立一個內置類stdClass的實例。

在咱們寫PHP擴展的時候,PHP內核提供了一組函數用於類型轉換:  

void convert_to_long(zval* pzval)
void convert_to_double(zval* pzval)
void convert_to_long_base(zval* pzval, int base)
void convert_to_null(zval* pzval)
void convert_to_boolean(zval* pzval)
void convert_to_array(zval* pzval)
void convert_to_object(zval* pzval)
void convert_object_to_type(zval* pzval, convert_func_t converter)

PHP內核提供的一組宏來方便的訪問zval,用於更細粒度的獲取zval的值:

內核訪問zval容器的API
訪問變量
Z_LVAL(zval) (zval).value.lval
Z_DVAL(zval) (zval).value.dval
Z_STRVAL(zval) (zval).value.str.val
Z_STRLEN(zval) (zval).value.str.len
Z_ARRVAL(zval) (zval). value.ht
Z_TYPE(zval) (zval).type
Z_LVAL_P(zval) (*zval).value.lval
Z_DVAL_P(zval) (*zval).value.dval
Z_STRVAL_P(zval_p) (*zval).value.str.val
Z_STRLEN_P(zval_p) (*zval).value.str.len
Z_ARRVAL_P(zval_p) (*zval). value.ht
Z_OBJ_HT_P(zval_p) (*zval).value.obj.handlers
Z_LVAL_PP(zval_pp) (**zval).value.lval
Z_DVAL_PP(zval_pp) (**zval).value.dval
Z_STRVAL_PP(zval_pp) (**zval).value.str.val
Z_STRLEN_PP(zval_pp) (**zval).value.str.len
Z_ARRVAL_PP(zval_pp) (**zval). value.ht

6. 變量的符號表與做用域

PHP的變量符號表與zval值的映射,是經過HashTable(哈希表,又叫作散列表,下面簡稱HT),HashTable在ZE中普遍使用,包括常量、變量、函數等語言特性都是HT來組織,在PHP的數組類型也是經過HashTable來實現。
舉個例子:

  1. <? php $var = 'Hello World'; ?>   

$var的變量名會存儲在變量符號表中,表明$var的類型和值的zval結構存儲在哈希表中。內核經過變量符號表與zval地址的哈希映射,來實現PHP變量的存取。

爲何要提做用域呢?由於函數內部變量保護。按照做用域PHP的變量分爲全局變量和局部變量,每種做用域PHP都會維護一個符號表的HashTable。當在PHP中建立一個函數或類的時候,ZE會建立一個新的符號表,代表函數或類中的變量是局部變量,這樣就實現了局部變量的保護--外部沒法訪問函數內部的變量。當建立一個PHP變量的時候,ZE會分配一個zval,並設置相應type和初始值,把這個變量加入當前做用域的符號表,這樣用戶才能使用這個變量。
內核中使用ZEND_SET_SYMBOL來設置變量:

  1. ZEND_SET_SYMBOL( EG(active_symbol_table), "foo", foo);  

查看_zend_executor_globals結構

  1. Zend/zend_globals.h  
  2.  struct _zend_executor_globals {          //略        HashTable symbol_table;//全局變量的符號表        HashTable *active_symbol_table;//局部變量的符號表        //略  };   

在寫PHP擴展時候,能夠經過EG宏來訪問PHP的變量符號表。EG(symbol_table)訪問全局做用域的變量符號表,EG(active_symbol_table)訪問當前做用域的變量符號表,局部變量存儲的是指針,在對HashTable進行操做的時候傳遞給相應函數。

爲了更好的理解變量的哈希表與做用域,舉個簡單的例子:

  1. <? php $temp = 'global'function test() {     $temp = 'active'; } test(); var_dump($temp); ?>   

建立函數外的變量$temp,會把這個它加入全局符號表,同時在全局符號表的HashTable中,分配一個字符類型的zval,值爲‘global‘。建立函數test內部變量$temp,會把它加入屬於函數test的符號表,分配字符型zval,值爲’active' 。

7. PHP擴展中變量操做

建立PHP變量

咱們能夠在擴展中調用函數MAKE_STD_ZVAL(pzv)來建立一個PHP可調用的變量,MAKE_STD_ZVAL應用到的宏有:

  1. #define     MAKE_STD_ZVAL(zv)               ALLOC_ZVAL(zv);INIT_PZVAL(zv)   #define     ALLOC_ZVAL(z)                   ZEND_FAST_ALLOC(z, zval, ZVAL_CACHE_LIST)   #define     ZEND_FAST_ALLOC(p, type, fc_type)       (p) = (type *) emalloc(sizeof(type))   #define     INIT_PZVAL(z)                       (z)->refcount__gc = 1;(z)->is_ref__gc = 0;   

MAKE_STD_ZVAL(foo)展開後獲得:

  1. (foo) = (zval *) emalloc(sizeof(zval));   (foo)->refcount__gc = 1;   (foo)->is_ref__gc = 0;   

能夠看出,MAKE_STD_ZVAL作了三件事:分配內存、初始化zval結構中的refcount、is_ref。 

內核中提供一些宏來簡化咱們的操做,能夠只用一步便設置好zval的類型和值。

API Macros for Accessing zval 
實現方法
ZVAL_NULL(pvz) Z_TYPE_P(pzv) = IS_NULL
ZVAL_BOOL(pvz) Z_TYPE_P(pzv) = IS_BOOL;
Z_BVAL_P(pzv) = b ? 1 : 0;
ZVAL_TRUE(pvz) ZVAL_BOOL(pzv, 1);
ZVAL_FALSE(pvz) ZVAL_BOOL(pzv, 0);
ZVAL_LONG(pvz, l)(l 是值) Z_TYPE_P(pzv) = IS_LONG;Z_LVAL_P(pzv) = l;
ZVAL_DOUBLE(pvz, d) Z_TYPE_P(pzv) = IS_DOUBLE;Z_LVAL_P(pzv) = d;
ZVAL_STRINGL(pvz, str, len, dup) Z_TYPE_P(pzv) = IS_STRING;Z_STRLEN_P(pzv) = len;
if (dup) {
    {Z_STRVAL_P(pzv) =estrndup(str, len + 1);} 
}else {
    {Z_STRVAL_P(pzv) = str;}
}
ZVAL_STRING(pvz, str, len) ZVAL_STRINGL(pzv, str,strlen(str), dup);
ZVAL_RESOURCE(pvz, res) Z_TYPE_P(pzv) = IS_RESOURCE;Z_RESVAL_P(pzv) = res; 


ZVAL_STRINGL(pzv,str,len,dup)中的dup參數

先闡述一下ZVAL_STRINGL(pzv,str,len,dup); str和len兩個參數很好理解,由於咱們知道內核中保存了字符串的地址和它的長度,後面的dup的意思其實很簡單,它指明瞭該字符串是否須要被複制。值爲 1 將先申請一塊新內存並賦值該字符串,而後把新內存的地址複製給pzv,爲 0 時則是直接把str的地址賦值給zval。

ZVAL_STRINGL與ZVAL_STRING的區別

若是你想在某一位置截取該字符串或已經知道了這個字符串的長度,那麼可使用宏 ZVAL_STRINGL(zval, string, length, duplicate) ,它顯式的指定字符串長度,而不是使用strlen()。這個宏該字符串長度做爲參數。但它是二進制安全的,並且速度也比ZVAL_STRING快,由於少了個strlen。

ZVAL_RESOURCE約等於ZVAL_LONG

在章節4中咱們說過,PHP中的資源類型的值是一個整數,因此ZVAL_RESOURCE和ZVAL_LONG的工做差很少,只不過它會把zval的類型設置爲 IS_RESOURCE。

8. 總結

PHP的弱類型是經過ZE的zval容器轉換完成,經過哈希表來存儲變量名和zval數據,在運行效率方面有必定犧牲。另外由於變量類型的隱性轉換,在開發過程當中對變量類型檢測力度不夠,可能會致使問題出現。 

不過PHP的弱類型、數組、內存託管、擴展等語言特性,很是適合Web開發場景,開發效率很高,可以加快產品迭代週期。在海量服務中,一般瓶頸存在於數據訪問層,而不是語言自己。在實際使用PHP不只擔任邏輯層和展示層的任務,咱們甚至用PHP開發的UDPServer/TCPServer做爲數據和cache的中間層。