全部翻译内容pdf文档下载地址: http://download.csdn.net/detail/lgg201/5107012
本书目前在github上由laruence(http://www.laruence.com)和walu(http://www.walu.cc)两位大牛组织翻译. 该翻译项目地址为: https://github.com/walu/phpbook
原书名: <Extending and Embedding PHP>
原作者: Sara Golemon
译者: goosman.lei(雷果国)
译者Email: [email protected]
译者Blog: http://blog.csdn.net/lgg201
变量的里里外外
每种编程语言共有的一个特性是存储和取回信息; 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 double数据类型.浮点数并不是以精确的精度存储的;而是用一个公式表示值的小数部分的有限精度(译注:浮点数被表示为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; } }
是的, 简单, 正确. 对比前面<?php echo $value; ?>的例子, 不难猜想这种编码会使得代码不好管理. 幸运的是, 在脚本执行输出变量的行为时, 无论是扩展, 还是嵌入式环境, 引擎都使用了非常相似的里程. 使用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的生命周期