變數的里里外外
每種程式語言共有的一個特性是儲存和取回資訊; php也不例外. 雖然許多語言要求所有的變數都要在使用之前被定義, 並且它們的類型資訊是固定的, 然而php允許程式設計師在使用的時候創建變數, 並且可以儲存任意類型語言能夠表達的資訊. 並且還可以在需要的時候自動的轉換變數類型.
因為你已經使用過用戶空間的php , 因此你應該知道這個概念是"弱類型". 本章, 你將看到這些資訊在php的父語言----c(C的類型是嚴格的)中是怎樣編碼的.
當然, 數據的編碼只是一半工作. 為了保持對所有這些信息片的跟踪, 每個變量還需要一個標籤和一個容器. 從用戶空間角度來看, 你可以把它們看做是變量名和作用域的概念.
資料型別
php中的資料儲存單位是zval, 也稱為Zend Value. 它是一個只有4個成員的結構體, 在Zend/zend.h中定義, 格式如下:
typedef struct _zval_struct { zval_value value; zend_uint refcount; zend_uchar type; zend_uchar is_ref; } zval;
我們可以直覺猜想到這些成員中多數的基礎儲存類型: unsigned integer的refcount, unsigned character的type和is_ref. 而value成員實際上是一個定義為union的結構, 在php5中, 它定義如下:
typedef union _zvalue_value { long lval; double dval; struct { char *val; int len; } str; HashTable *ht; zend_object_value obj; } zvalue_value;
union允許Zend使用一個單一的, 統一的結構來將許多不同類型的數據存儲到一個php變量中.
zend當前定義了下表列出的8種數據類型:
類型值 | 目的 |
IS_NULL | 這個類型自動的賦值給未初始化的變數,直到它第一次被使用.也可以在用戶空間使用內建的NULL常數進行顯式的賦值.這個變數類型提供了一種特殊的"沒有資料"的類型,它和布林的FALSE以及整型的0有所不同. |
IS_BOOL | 布爾變數可以有兩種可能狀態中的一種, TRUE/FALSE.用戶空間控制結構if/while/ternary/for等中間的條件表達式在評估時都會隱式的轉換為布林類型. |
IS_DOUBLE | 浮點資料型態,使用主機系統的signed doubleble資料類型.浮點數並資料類型不是以精確的精確度儲存的;而是用一個公式表示值的小數部分的有限精度(譯註:浮點數被表示為3部分:符號,尾數--小數部分,指數.浮點數的值 =符號 *尾數* 2 ^指數----來自BSD Library Functions Manual: float(3)).這種計數法允許計算機儲存很大範圍的值(正數或負數):用8位元組就可以表示2.225*10^ (-308)到1.798*10^(308)範圍內的數字.不幸的是它評估的數字實際的十進制並不能總是像二進制分數一樣乾淨的存儲.例如,十進製表達式0.5轉換為二進制的精確值為0.1,然而十進制的0.8轉換為二進制則是無限循環的0.1100110011...,當它轉換回十進制時,因為無法存儲被丟棄的二進制位將無法恢復.類似的可以想一下將1/3轉換為十進制的0.333333,兩個值非常相近,但是它不精確,因為3 * 0.333333並不等於1.0.這個不精確常常會在計算機上處理浮點數時讓人迷惑.(這些範圍限制通常是基於32位平台的;不同的系統範圍可能不同) |
IS_STRING | php中最常見的數據類型是字符串,它的存儲方式符合有經驗的C程序員的預期.分配一塊足夠大去保存字符串中所有的位元組/字元的記憶體,並將指向該字串的指標保存在宿主zval中. 值得注意的是php字串的長度總是顯式的在zval結構中指出.這就允許字串包含NULL位元組而不被截斷.關於php字串的這一方面,我們往後稱為"二進位安全"因為這樣做使得它可以安全的包含任意類型的二進位資料. 需要注意的是為一個php字串分配的記憶體總量總是最小化的:長度加1.最後的一個位元組存放終止的NULL字元,因此不關心二進位安全的函數可以直接傳遞字串指標. |
IS_ARRAY | 數組是一種特殊目的的變數,它唯一的功能就是組織其他變數.不像C中的數組概念, php的數組並不是單一類型資料的向量(比如zval arrayofzvals[]; ).實際上, php的陣列是一個複雜的資料桶集合,它的內部是一個HashTable.每個HashTable元素(桶)包含兩個對應的資訊片:標籤和資料.在php陣列的應用場景中,標籤就是關聯數組的key或數值下表,資料就是key指向的變數(zval) |
IS_OBJECT | 物件擁有陣列的多元素資料儲存,此外還增加了方法,存取修飾符,作用域常數,特殊的事件處理器.作為一個擴展開發者,構建在php4和php5中等價的面向對象代碼是一個很大的挑戰,因為在Zend引擎1(php4)和Zend引擎2(php5)之間,內部的物件模型有非常大的變更. |
IS_RESOURCE | 有一些資料類型並不能簡單的映射到用戶空間.比如, stdio的FILE指標或libmysqlclient的連接句柄,它們不能被簡單的映射為標量值的數組,那樣做它們就失去了意義.為了保護用戶空間腳本編寫者不去處理這些問題, php提供了一個泛華的資源數據類型.資源類型的實現細節我們將在第9章"資源數據類型"中涉及,現在我們只需要知道有這麼東西就好了. |
上表中的IS_*常量被存储在zval结构的type元素中, 用来确定在测试变量的值时应该查看value元素中的哪个部分.
最明显的检查一个数据的类型的方法如下代码:
void describe_zval(zval *foo) { if (foo->type == IS_NULL) { php_printf("The variable is NULL"); } else { php_printf("The variable is of type %d", foo->type); } }
显而易见, 但是是错的.
好吧, 没有错, 但确实不是首选做法. Zend头文件包含了很多的zval访问宏, 它们是作者期望在测试zval数据时使用的方式. 这样做主要的原因是避免在引擎的api变更后产生不兼容问题, 不过从另一方面来看这样做还会使得代码更加易读. 下面是相同功能的代码段, 这一次使用了Z_TYPE_P()宏:
void describe_zval(zval *foo) { if (Z_TYPE_P(foo) == IS_NULL) { php_printf("The variable is NULL"); } else { php_printf("The variable is of type %d", Z_TYPE_P(foo)); } }
这个宏的_P后缀标识传递的参数应该是一级间访的指针. 还有另外两个宏Z_TYPE()和Z_TYPE_PP(), 它们期望的参数类型是zval(非指针)和zval **(两级间访指针).
注意
在这个例子中使用了一个特殊的输出函数php_printf(), 它被用于展示数据片. 这个函数语法上等同于stdio的printf函数; 不过它对webserver sapi有特殊的处理, 使用php的输出缓冲机制提升性能. 你将在第5章"你的第一个扩展"中更多的了解这个函数以及它的同族PHPWRITE().
数据值
和类型一样, zval的值也可以用3个一组的宏检查. 这些宏总是以Z_开始, 可选的以_P或_PP结尾, 具体依赖于它们的间访层级.
对于简单的标量类型, boolean, long, double, 宏简写为: BVAL, LVAL, DVAL.
void display_values(zval boolzv, zval *longpzv, zval **doubleppzv) { if (Z_TYPE(boolzv) == IS_BOOL) { php_printf("The value of the boolean is: %s\n", Z_BVAL(boolzv) ? "true" : "false"); } if (Z_TYPE_P(longpzv) == IS_LONG) { php_printf("The value of the long is: %ld\n", Z_LVAL_P(longpzv)); } if (Z_TYPE_PP(doubleppzv) == IS_DOUBLE) { php_printf("The value of the double is: %f\n", Z_DVAL_PP(doubleppzv)); } }
由于字符串变量包含两个成员, 因此它有一对宏分别表示char *(STRVAL)和int(STRLEN)成员:
void display_string(zval *zstr) { if (Z_TYPE_P(zstr) != IS_STRING) { php_printf("The wrong datatype was passed!\n"); return; } PHPWRITE(Z_STRVAL_P(zstr), Z_STRLEN_P(zstr)); }
数组数据类型内部以HashTable *存储, 可以使用: Z_ARRVAL(zv), Z_ARRVAL_P(pzv), Z_ARRVAL_PP(ppzv)访问. 在阅读旧的php内核和pecl模块的代码时, 你可能会碰到HASH_OF()宏, 它期望一个zval *参数. 这个宏等价于Z_ARRVAL_P()宏, 不过, 这个用法已经废弃, 在新的代码中应该不再被使用.
对象的内部表示结构比较复杂, 它有较多的访问宏: OBJ_HANDLE返回处理标识, OBJ_HT返回处理器表, OBJCE用于类定义, OBJPROP用于属性的HahsTable, OBJ_HANDLER用于维护OBJ_HT表中的一个特殊处理器方法. 现在不要被这么多的对象访问宏吓到, 在第10章"php4对象"和第11章"php5对象"中它们的细节都会介绍.
在一个zval中, 资源数据类型被存储为一个简单的整型, 它可以通过RESVAL这一组宏来访问. 这个整型将被传递给zend_fetch_resource()函数在已注册资源列表中查找资源对象. 我们将在第9章深入讨论资源数据类型.
数据的创建
现在你知道了怎样从一个zval中取出数据, 是时候创建一些自己的数据了. 虽然zval可以作为一个直接变量定义在函数的顶部, 这使得变量的数据存储在本地, 为了让它离开这个函数到达用户空间就需要对其进行拷贝.
因为你大多数时候都是希望自己创建的zval到达用户空间, 因此你就需要分配一个块内存给它, 并且将它赋值给一个zval *指针. 与之前的"显而易见"的方案一样, 使用malloc(sizeof(zval))并不是正确的答案. 取而代之的是你要用另外一个Zend宏: MAKE_STD_ZVAL(pzv). 这个宏将会以一种优化的方式在其他zval附近为其分配内存, 自动的处理超出内存错误(下一章将会解释), 并初始化新zval的refcount和is_ref属性.
除了MAKE_STD_ZVAL(), 你可能还经常会碰到其他的zval *创建宏, 比如ALLOC_INIT_ZVAL(). 这个宏和MAKE_STD_ZVAL唯一的区别是它会将zval *的数据类型初始化为IS_NULL.
一旦数据存储空间可用, 就可以向你的新zval中填充一些信息了. 在阅读了前面的数据存储部分后, 你可能准备使用Z_TYPE_P()和Z_SOMEVAL_P()宏去设置你的新变量. 我们来看看这个"显而易见"的方案是否正确?
同样, "显而易见"的并不正确!
Zend暴露了另外一组宏用来设置zval *的值. 下面就是这些新的宏和它们展开后你已经熟悉的格式:
ZVAL_NULL(pvz); Z_TYPE_P(pzv) = IS_NULL;
虽然这些宏相比使用更加直接的版本并没有节省什么, 但它的出现体现了完整性.
ZVAL_BOOL(pzv, b); Z_TYPE_P(pzv) = IS_BOOL; Z_BVAL_P(pzv) = b ? 1 : 0; ZVAL_TRUE(pzv); ZVAL_BOOL(pzv, 1); ZVAL_FALSE(pzv); ZVAL_BOOL(pzv, 0);
注意, 任何非0值提供给ZVAL_BOOL()都将产生一个真值. 当在内部代码中硬编码时, 使用1表示真值被认为是较好的实践. 宏ZVAL_TRUE()和ZVAL_FALSE()提供用来方便编码, 有时也会提升代码的可读性.
ZVAL_LONG(pzv, l); Z_TYPE_P(pzv) = IS_LONG; Z_LVAL_P(pzv) = l; ZVAL_DOUBLE(pzv, d); Z_TYPE_P(pzv) = IS_DOUBLE; Z_DVAL_P(pzv) = d;
基础的标量宏和它们自己一样简单. 设置zval的类型, 并给它赋一个数值.
ZVAL_STRINGL(pzv,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(pzv, str, dup); ZVAL _STRINGL(pzv, str, strlen(str), dup);
这里, zval的创建就开始变得有趣了. 字符串就像数组, 对象, 资源一样, 需要分配额外的内存用于它们的数据存储. 在下一章你将继续探索内存管理的陷阱; 现在, 只需要注意, 当dup的值为1时, 将分配新的内存并拷贝字符串内容, 当dup的值为0时, 只是简单的将zval指向已经存在的字符串数据.
ZVAL_RESOURCE(pzv, res); Z_TYPE_P(pzv) = IS_RESOURCE; Z_RESVAL_P(pzv) = res;
回顾前面, 资源在zval中只是存储了一个简单的整型, 它用于在Zend管理的资源表中查找. 因此ZVAL_RESOURCE()宏就很像ZVAL_LONG()宏, 但是, 使用不同的类型.
数据类型/值/创建回顾练习
static void eae_001_zval_dump_real(zval *z, int level) { HashTable *ht; int ret; char *key; uint index; zval **pData; switch ( Z_TYPE_P(z) ) { case IS_NULL: php_printf("%*stype = null, refcount = %d%s\n", level * 4, "", Z_REFCOUNT_P(z), Z_ISREF_P(z) ? ", is_ref " : ""); break; case IS_BOOL: php_printf("%*stype = bool, refcount = %d%s, value = %s\n", level * 4, "", Z_REFCOUNT_P(z), Z_ISREF_P(z) ? ", is_ref " : "", Z_BVAL_P(z) ? "true" : "false"); break; case IS_LONG: php_printf("%*stype = long, refcount = %d%s, value = %ld\n", level * 4, "", Z_REFCOUNT_P(z), Z_ISREF_P(z) ? ", is_ref " : "", Z_LVAL_P(z)); break; case IS_STRING: php_printf("%*stype = string, refcount = %d%s, value = \"%s\", len = %d\n", level * 4, "", Z_REFCOUNT_P(z), Z_ISREF_P(z) ? ", is_ref " : "", Z_STRVAL_P(z), Z_STRLEN_P(z)); break; case IS_DOUBLE: php_printf("%*stype = double, refcount = %d%s, value = %0.6f\n", level * 4, "", Z_REFCOUNT_P(z), Z_ISREF_P(z) ? ", is_ref " : "", Z_DVAL_P(z)); break; case IS_RESOURCE: php_printf("%*stype = resource, refcount = %d%s, resource_id = %d\n", level * 4, "", Z_REFCOUNT_P(z), Z_ISREF_P(z) ? ", is_ref " : "", Z_RESVAL_P(z)); break; case IS_ARRAY: ht = Z_ARRVAL_P(z); zend_hash_internal_pointer_reset(ht); php_printf("%*stype = array, refcount = %d%s, value = %s\n", level * 4, "", Z_REFCOUNT_P(z), Z_ISREF_P(z) ? ", is_ref " : "", HASH_KEY_NON_EXISTANT != zend_hash_has_more_elements(ht) ? "" : "empty"); while ( HASH_KEY_NON_EXISTANT != (ret = zend_hash_get_current_key(ht, &key, &index, 0)) ) { if ( HASH_KEY_IS_STRING == ret ) { php_printf("%*skey is string \"%s\"", (level + 1) * 4, "", key); } else if ( HASH_KEY_IS_LONG == ret ) { php_printf("%*skey is long %d", (level + 1) * 4, "", index); } ret = zend_hash_get_current_data(ht, &pData); eae_001_zval_dump_real(*pData, level + 1); zend_hash_move_forward(ht); } zend_hash_internal_pointer_end(Z_ARRVAL_P(z)); break; case IS_OBJECT: php_printf("%*stype = object, refcount = %d%s\n", level * 4, "", Z_REFCOUNT_P(z), Z_ISREF_P(z) ? ", is_ref " : ""); break; default: php_printf("%*sunknown type, refcount = %d%s\n", level * 4, "", Z_REFCOUNT_P(z), Z_ISREF_P(z) ? ", is_ref " : ""); break; } } PHP_FUNCTION(eae_001_zval_dump) { zval *z; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &z) == FAILURE) { return; } eae_001_zval_dump_real(z, 0); RETURN_NULL(); } PHP_FUNCTION(eae_001_zval_make) { zval *z; MAKE_STD_ZVAL(z); ZVAL_NULL(z); eae_001_zval_dump_real(z, 0); ZVAL_TRUE(z); eae_001_zval_dump_real(z, 0); ZVAL_FALSE(z); eae_001_zval_dump_real(z, 0); ZVAL_LONG(z, 100); eae_001_zval_dump_real(z, 0); ZVAL_DOUBLE(z, 100.0); eae_001_zval_dump_real(z, 0); ZVAL_STRING(z, "100", 0); eae_001_zval_dump_real(z, 0); }
数据存储
你已经在用户空间一侧使用过php了, 因此你应该已经比较熟悉数组了. 我们可以将任意数量的php变量(zval)放入到一个容器(array)中, 并可以为它们指派数字或字符串格式的名字(标签----key)
如果不出意外, php脚本中的每个变量都应该可以在一个数组中找到. 当你创建变量时, 为它赋一个值, Zend把这个值放到被称为符号表的一个内部数组中.
有一个符号表定义了全局作用域, 它在请求启动后, 扩展的RINIT方法被调用之前初始化, 接着在脚本执行完成后, 后续的RSHUTDOWN方法被执行之前销毁.
当一个用户空间的函数或对象方法被调用时, 则分配一个新的符号表用于函数或方法的生命周期, 它被定义为激活的符号表. 如果当前脚本的执行不在函数或方法中, 则全局符号表被认为是激活的.
我们来看看globals结构的实现(在Zend/zend_globals.h中定义), 你会看到下面的两个元素定义:
struct _zend_execution_globals { ... HashTable symbol_table; HashTable *active_symbol_table; ... };
symbol_table, 使用EG(symbol_table)访问, 它永远都是全局变量作用域, 和用户空间的$GLOBALS变量相似, 用于对应于php脚本的全局作用域. 实际上, $GLOBALS变量的内部就是对EG(symbol_table)上的一层包装.
另外一个元素active_symbol_table, 它的访问方法类似: EG(active_symbol_table), 表示此刻激活的变量作用域.
这里有一个需要注意的关键点, EG(symbol_table), 它不像你在php和zend api下工作时将遇到的几乎所有其他HashTable, 它是一个直接变量. 几乎所有的函数在HashTable上操作时都期望一个间访的HashTable *作为参数. 因此, 你在使用时需要在EG(symbol_table)前加取地址符(&).
考虑下面的代码块, 它们的功能是等价的
/* php实现 */ <?php $foo = 'bar'; ?> /* C实现 */ { zval *fooval; MAKE_STD_ZVAL(fooval); ZVAL_STRING(fooval, "bar", 1); ZEND_SET_SYMBOL(EG(active_symbol_table), "foo", fooval); }
首先, 使用MAKE_STD_ZVAL()分配一个新的zval, 它的值被初始化为字符串"bar". 接着是一个新的宏调用, 它的作用是将fooval这个zval增加到当前激活的符号表中, 设置的变量名为"foo". 因为此刻并没有用户空间函数被激活, 因此EG(active_symbol_table) == &EG(symbol_table), 最终的含义就是这个变量被存储到了全局作用域中.
数据取回
为了从用户空间取回一个变量, 你需要在符号表的存储中查找. 下面的代码段展示了使用zend_hash_find()函数达成这个目的:
{ zval **fooval; if (zend_hash_find(EG(active_symbol_table), "foo", sizeof("foo"), (void**)&fooval) == SUCCESS) { php_printf("Got the value of $foo!"); } else { php_printf("$foo is not defined."); } }
这个例子中有一点看起来有点奇怪. 为什么要把fooval定义为两级间访指针呢? 为什么sizeof()用于确定"foo"的长度呢? 为什么是&fooval? 哪一个被评估为zval ***, 转换为void **?如果你问了你自己所有上面3个问题, 请拍拍自己的后背.
首先, 要知道HashTable并不仅用于用户空间变量, 这一点很有价值. HashTable结构用途很广, 它被用在整个引擎中, 甚至它还能完美的存储非指针数据. HashTable的桶是定长的, 因此, 为了存储任意大小的数据, HashTable将分配一块内存用来放置被存储的数据. 对于变量而言, 被存储的是一个zval *, 因此HashTable的存储机制分配了一块足够保存一个指针的内存. HashTable的桶使用这个新的指针保存zval *的值, 因此在HashTable中被保存的是zval **. HashTable完全可以漂亮的存储一个完整的zval, 那为什么还要这样存储zval *呢? 具体原因我们将在下一章讨论.
在尝试取回数据的时候, HashTable仅知道有一个指针指向某个数据. 为了将指针弹出到调用函数的本地存储中, 调用函数自然就要取本地指针(变量)的地址, 结果就是一个未知类型的两级间访的指针变量(比如void **). 要知道你的未知类型在这里是zval *, 你可以看到把这种类型传递给zend_hash_find()时, 编译器会发现不同, 它知道是三级间访而不是两级. 这就是我们在前面加一个强制类型转换的目的, 用来抑制编译器的警告.
在前面的例子中使用sizeof()的原因是为了在"foo"常量用作变量的标签时包含它的终止NULL字节. 这里使用4的效果是等价的; 不过这比较危险, 因为对标签名的修改会影响它的长度, 现在这样做在标签名变更时比较容易查找需要修改的地方. (strlen("foo") + 1)也可以解决这个问题, 但是, 有些编译器并没有优化这一步, 结果产生的二进制文件最终执行时可能得到的是一个毫无意义的字符串长度, 拿它去循环可不是那么好玩的!
如果zend_hash_find()定位到了你要查找的项, 它就会将所请求数据第一次被增加到HashTable中时时分配的桶的指针地址弹出到所提供的指针(zend_hash_find()第4个参数)中, 同时返回一个SUCCESS整型常量. 如果zend_hash_find()不能定位到数据, 它就不会修改指针(zend_hash_find()第四个参数)而是返回整型常量FAILURE.
站在用户空间的角度看, 变量存储到符号表所返回的SUCCESS或FAILURE实际上就是变量是否已经设置(isset).
类型转换
现在你可以从符号表抓取变量, 那可能你就想对它们做些什么. 一种直接的事倍功半的方法是检查变量的类型, 并依赖类型执行特殊的动作. 就像下面代码中简单的switch语句就可以工作.
void display_zval(zval *value) { switch (Z_TYPE_P(value)) { case IS_NULL: /* NULLs are echoed as nothing */ break; case IS_BOOL: if (Z_BVAL_P(value)) { php_printf("1"); } break; case IS_LONG: php_printf("%ld", Z_LVAL_P(value)); break; case IS_DOUBLE: php_printf("%f", Z_DVAL_P(value)); break; case IS_STRING: PHPWRITE(Z_STRVAL_P(value), Z_STRLEN_P(value)); break; case IS_RESOURCE: php_printf("Resource #%ld", Z_RESVAL_P(value)); break; case IS_ARRAY: php_printf("Array"); break; case IS_OBJECT: php_printf("Object"); break; default: /* Should never happen in practice, * but it's dangerous to make assumptions */ php_printf("Unknown"); break; } }
是的, 简单, 正确. 对比前面的例子, 不难猜想这种编码会使得代码不好管理. 幸运的是, 在脚本执行输出变量的行为时, 无论是扩展, 还是嵌入式环境, 引擎都使用了非常相似的里程. 使用Zend暴露的convert_to_*()函数族可以让这个例子变得很简单:
void display_zval(zval *value) { convert_to_string(value); PHPWRITE(Z_STRVAL_P(value), Z_STRLEN_P(value)); }
你可能会猜到, 有很多这样的函数用于转换到大多数数据类型. 值得注意的是convert_to_resource(), 它没有意义, 因为资源类型的定义旧是不能映射到真实用户空间表示的值.
如果你担心convert_to_string()调用对传递给函数的zval的值的修改不可逆, 那说明你很棒. 在真正的代码段中, 这是典型的坏主意, 当然, 引擎在输出变量时并不是这样做的. 下一章你将会看到安全的使用转换函数的方法, 它会安全的修改值的内容, 而不会破坏它已有的内容.
小结
本章中你看到了php变量的内部表示. 你学习了区别类型, 设置和取回值, 将变量增加到符号表中以及将它们取回. 下一章你将在这些知识的基础之上, 学习怎样拷贝一个zval, 怎样在不需要的时候销毁它们, 最重要的而是, 怎样避免在不需要的时候产生拷贝.
你还将看到Zend的单请求内存管理层的一角, 了解了持久化和非持久化分配. 在下一章的结尾, 你旧有实力可以去创建一个工作的扩展并在上面用自己的代码做实验了.
以上就是 [翻译][php扩展开发和嵌入式]第2章-变量的里里外外的内容,更多相关内容请关注PHP中文网(www.php.cn)!