変数の入出力
すべてのプログラミング言語に共通する機能の 1 つは、情報の保存と取得です。多くの言語では、使用前にすべての変数を定義する必要があり、その型情報は固定です。これにより、プログラマーは使用時に変数を作成でき、必要に応じて変数の型を自動的に変換することもできるので、これについては知っておく必要があります。この章では、この情報が PHP の親言語である C でどのようにエンコードされるかを説明します (C の型は厳密です)
もちろん、データを追跡することは作業の半分にすぎません。これらすべての情報に加えて、各変数にはラベルとコンテナーも必要です。ユーザー空間の観点からは、変数名とスコープの概念として考えることができます。
データ型
php のデータストレージユニットは zval です。 Zend Value とも呼ばれる、Zend/zend.h で定義された 4 つのメンバーのみを持つ構造体であり、その形式は次のとおりです:
typedef struct _zval_struct { zval_value value; zend_uint refcount; zend_uchar type; zend_uchar is_ref; } zval;
これらのメンバーのほとんどの基本的なストレージ タイプを直感的に推測できます: refcount of符号なし整数、符号なし文字の型、および is_ref 値メンバーは、実際には 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 はさまざまな種類のデータを格納できます。
zend は現在、次の表に示す 8 つのデータ型を定義しています。
目的
IS_DOUBLE | point データ型の場合は、ホスト システムの符号付き double データ型を使用します。浮動小数点数は正確な精度で格納されず、制限された精度で値の小数部分を表すために式が使用されます。 ---- BSD ライブラリ関数マニュアルより: float(3)) この表記法により、コンピューターは幅広い値 (正または負) を格納できます: 2.225*10^ は 8 バイト (-308) で表現できます。 ) から 1.798*10^(308) に変換されます。残念ながら、その数値は 2 進数の分数のように正確に格納されるわけではありません。ただし、10 進数の 0.8 の場合、値は 0.1 になります。を2進数に変換すると0.1100110011…という無限ループになり、10進数に戻すと捨てられた2進数のビットは保存できないため復元できません。同様に1/3を変換すると0.333333と考えることができます。 10 進数では非常に似ていますが、3 * 0.333333 は 1.0 に等しくないため、コンピュータで浮動小数点数を扱う場合、この不正確さがよく混乱を引き起こします (これらの範囲制限は通常、32 ビット プラットフォームに基づいています。システム スコープは異なる場合があります) |
IS_STRING | |
PHP 文字列に割り当てられるメモリの総量は常に最小化されることに注意してください。最後のバイトには終端の NULL 文字が含まれるため、バイナリ セーフティを考慮しない関数は文字列ポインタを直接渡すことができます | 。|
IS_ARRAY | 配列は、他の変数を整理することだけを機能とする特殊な目的の変数です。C の配列概念とは異なり、php 配列は単一タイプのデータのベクトルではありません (zval arrayofzvals[]; など)。 ) 実際、PHP の配列はデータ バケットの複雑なコレクションであり、その内部には HashTable 要素 (バケット) の 2 つの対応する情報が含まれています。PHP 配列のアプリケーション シナリオでは、ラベルは次のとおりです。連想配列のキーまたは値。データはキーが指す変数 (zval) です。拡張機能の開発者にとって、同等のオブジェクト指向コードを php4 と php5 の間で構築することは大きな課題です。 Zend Engine 1 (php4) と Zend Engine 2 (php5) では、内部オブジェクト モデルに非常に大きな変更が加えられています。たとえば、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; } } 是的, 简单, 正确. 对比前面eb4bd62954e83d68558dfe15e6d196b2的例子, 不难猜想这种编码会使得代码不好管理. 幸运的是, 在脚本执行输出变量的行为时, 无论是扩展, 还是嵌入式环境, 引擎都使用了非常相似的里程. 使用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)! |