深入理解php内核 编写扩展_III- 资源
Part III: Resources
原文:http://devzone.zend.com/article/1024-Extension-Writing-Part-III-Resources
编写扩展_III- 资源
介绍
资源
初始化资源
接收资源作为函数参数
销毁资源
强制销毁资源
持久资源
查找现存的持久资源
核对(代码)完整性
总结
3.1介绍
迄今为止,你已经处理了一些熟悉的概念,而且很容易就可以在在用户空间找到它们的对应物。在本教程中,你将深入一个更陌生的数据类型的内部运行机制,该类型对用户空间完全隐藏了内部细节,但是它的行为最终会让你有种似曾相识的感觉。
3.2 资源
PHP的zval可以描绘广泛的内部数据类型,然而有一种数据类型是它不能充分描绘的-脚本中的指针(pointer)。当指针引用的是不透明的typedef时,像值一样表示指针更加困难。由于没有有效的方式描绘这些复合结构,因此也没有办法对它们使用传统的操作符。要解决这个问题,只需要通过一个(本质上)任意的标识符(label)引用指针,这(种方式)被称为资源。
要使资源的标识符对Zend引擎有意义,必须先向PHP注册其底层的数据类型。你将从在php_hello.h中定义一个简单的数据结构开始。你可以把它放在几乎任何地方,但是,为了本次练习,把它放在#define语句后面,PHP_MINIT_FUNCTION声明前面。你也要定义一个常量,作为资源的名字,例如当调用var_dump()时会显示。
typedef struct _php_hello_person { |
现在打开hello.c,在ZEND_DECLARE_MODULE_GLOBALS语句前面增加一个真正的全局整型数:
int le_hello_person; |
在PHP扩展中,只有很少的几处需要声明真正的全局变量的地方,目录入口标识符(List entryidentifiers)(le_*)是其中之一。这些值只是由一个查找表用来把资源类型和它们的文本名字以及析构方法相关联,它们不需要是线程安全的。你的扩展将在MINIT步骤为其导出的每个资源生成一个唯一编号。现在把它加入你的扩展,在PHP_MINIT_FUNCTION(hello)的顶部放入下面的代码:
le_hello_person = zend_register_list_destructors_ex(NULL, NULL, PHP_HELLO_PERSON_RES_NAME, module_number); |
3.3初始化资源
既然已经注册了资源,你需要用它做些事情。把下面的函数连同hello_functions结构中的匹配项加入hello.c,对应的原型声明放入php_hello.h:
PHP_FUNCTION(hello_person_new) |
在分配内存和复制数据以前,该函数对要传入资源的数据进行了一些健壮性检查:是否提供了名字?这个人的年龄是否超出了人类的寿命范围?当然,延缓衰老的研究可能使年龄的数据类型(及其健壮性检查限制)在某天产生类似千年虫的问题,但是假定没人会超过255岁在任何时候都应当是安全的。
一旦函数对入口条件感到满意,所要做的就是分配一些内存并放入它的数据。最后把新注册的资源装入return_value。这个函数(指ZEND_REGISTER_RESOURCE-译注)不需要了解数据结构的内部情况;它只需要知道数据的指针地址和相关联的资源类型。
3.4接收资源作为函数参数
从本系列之前的教程开始,你已经知道了如何使用zend_parse_parameters() 接收资源参数。现在是时候用它从给定的资源中获取数据了。把下一个函数加入扩展:
PHP_FUNCTION(hello_person_greet) |
此处功能上的重要部分应该很容易理解。ZEND_FETCH_RESOURCE()需要一个变量来放入指针值。它也需要了解变量的内部类型,以及从哪儿得到资源标识符。
该函数调用中的-1表明利用&zperson标识资源,是种可选方式。如果这儿提供了-1以外的数字值,Zend引擎将尝试使用该编号标识资源,而不是使用zval*参数的数据。如果传入的资源与最后一个参数指定的资源类型不匹配,将会利用倒数第二个参数给出的资源名产生一个错误消息。
skin资源的方式不止一个。实际上下面的四个代码块具有同样的效果:
ZEND_FETCH_RESOURCE(person, php_hello_person *, &zperson, -1,\ PHP_HELLO_PERSON_RES_NAME, le_person_name); |
对于不在PHP_FUNCTION()中的情形,最后一对的形式非常有用,因此没有为return_value赋值;或者当出现资源类型不匹配这种非常合理的(原因),并且不想只返回FALSE时。
无论选择哪种方法从参数中获取你的资源数据,结果都是一样的。现在你有一个熟悉的C结构,可以使用同其他C程序完全一样的方式访问它。此时该结构仍然“属于”资源变量,所以你的函数不应该释放指针或是在退出前改变引用计数。那么资源如何被销毁呢?
3.5销毁资源
PHP中大多数创建资源的函数都有对应的函数用于释放资源。例如,mysql_connect()有mysql_close(),mysql_query()有mysql_free_result(),fopen()有fclose(),诸如此类。或许经验告诉你如果只是unset()含有资源值的变量,那么,不论它们附有什么样的真实资源,也都会被释放/关闭。例如:
该代码片段的第一行打开一个用于写入的文件-foo.txt,并且把流资源赋给变量$fp。当第二行清除$fp时,PHP自动关闭文件-即使fclose()从未被调用。这是怎样做到的呢?
奥秘就在你在MINIT函数中调用的zend_register_resource()中。你传入的两个NULL参数对应清除(或dtor)函数。第一个用于普通资源,第二个用于持久资源。我们现在先关注普通资源,稍后再回到持久资源,但是它们在常规语义上是相同的。像下面一样修改行zend_register_resource:
le_hello_person = zend_register_list_destructors_ex(php_hello_person_dtor, NULL, PHP_HELLO_PERSON_RES_NAME, module_number); |
并且在紧邻MINIT方法的上面创建新函数:
static void php_hello_person_dtor(zend_rsrc_list_entry *rsrc TSRMLS_DC) |
如你所见,只是释放了曾分配并关联于资源的缓冲。当持有你的资源引用的最后一个用户变量超出作用域,该函数将被自动调用以使你的扩展能够释放内存、从远程主机断开或执行其他的最终清理。
3.6强制销毁资源
如果对资源的dtor函数的调用依赖于所有指向它的变量超出作用域,那么类似fclose()或mysql_free_result()这样的函数是怎样在资源的引用仍然存在的情况下完成任务的?回答之前,我希望你实验下面的代码:
<?php |
两次调用var_dump()都能看到资源编码的数值,由此知道资源的引用仍然存在;但是第二次调用var_dump()表明其类型是“unknown”。这是因为Zend引擎在内存中维护的资源查找表不再包含匹配那个编码的文件句柄-所以任何利用那个编码执行 ZEND_FETCH_RESOURCE()的尝试都将失败。
如同很多其他基于资源的函数,fclose()通过使用zend_list_delete()实现这个(目的)。或许明显,或许不明显,该函数从特定的资源表中删除一项。其最简单的应用可能是:
PHP_FUNCTION(hello_person_delete) |
当然,这个函数将销毁*任意*资源类型,不管它是我们的person资源、文件句柄、MySQL连接或其他的什么。为了避免给其他的扩展造成潜在的问题以及使得用户空间代码难于调试,首先检验资源类型是好习惯。这很容易做到,利用ZEND_FETCH_RESOURCE()取得资源放入哑变量。继续前进,把它加入你的函数,介于zend_parse_parameters()调用和zend_list_delete()调用之间。
3.7持久资源
如果你用过mysql_pconnect()、popen()或任何其他持久的资源类型,那么你将知道,资源可以长期驻留,不只是在所有引用它的变量超出作用域之后,甚至是在一个请求结束了并且新的请求产生之后。这些资源称为持久资源,因为它们贯通SAPI的整个生命周期持续存在,除非特意销毁。
标准资源和持久资源的两个关键区别是注册时dtor函数的安排,以及使用pemalloc()分配数据而不是emalloc()。
让我们为person资源创建一个能保持持久性的版本。先向MINIT增加另一个zend_register_resource()代码行。不要忘记紧接着le_hello_person定义le_hello_person_persist变量:
PHP_MINIT_FUNCTION(hello) |
基本语法是一样的,但这次你在zend_register_resource()的第二个参数指定析构函数而非第一个。二者真正的区别是dtor何时被调用。传入第一个参数的dtor函数在活动请求关闭时被调用,而传入第二个参数的dtor函数直到模块在最终关闭被卸载时才被调用。
由于引用了一个新的资源dtor函数,你将需要定义它。把这个看似熟悉的方法加入hello.c中MINIT上面的某处就行:
static void php_hello_person_persist_dtor(zend_rsrc_list_entry *rsrc TSRMLS_DC) |
现在你需要一种方法例示持久版本的person资源。既有的约定是创建一个名字中以“p”作前缀的新函数。向你的扩展中加入该方法:
PHP_FUNCTION(hello_person_pnew) |
如你所见,该函数与hello_person_new()仅有微小的不同。在实践中,你将看到这类成对的用户空间函数被典型地实现为围绕共同核心的封装函数。看看源代码中其他的资源创建函数对是如何避免这种重复的。
既然你的扩展将创建两类资源,因此需要能处理两种类型。幸运的是,ZEND_FETCH_RESOURCE有个姐妹函数胜任这个任务。用下面的代码替换hello_person_greet()中当前对ZEND_FETCH_RESOURCE的调用:
ZEND_FETCH_RESOURCE2(person, php_hello_person*, &zperson, -1, \ PHP_HELLO_PERSON_RES_NAME , le_hello_person, le_hello_person_persist); |
这将用合适的数据加载你的person变量,无论是否传入持久资源。
这两个FETCH宏调用允许你指定很多资源类型,但是也有罕见的需要多于两个的情况。为防万一,这是利用基本函数写的最后一条语句:
person = (php_hello_person*) zend_fetch_resource(&zperson TSRMLS_CC, -1, \ PHP_HELLO_PERSON_RES_NAME, NULL, 2, le_hello_person, le_hello_person_persist); |
这儿要注意两件重要的事情。首先,你能看到FETCH_RESOURCE宏自动尝试校验资源。展开来说,此种情况下宏ZEND_VERIFY_RESOURCE只是转换为:
if (!person) { |
当然,你不会总是仅仅因为不能取到一个资源就要你的扩展函数退出,所以你能使用真实的zend_fetch_resource()函数尝试取得资源类型,然后应用自己的逻辑处理将返回的NULL值。
3.8查找现存的持久资源
持久资源实际上只是相当于你重用它的能力。为了重用它,你需要安全的地方存储它。Zend引擎通过EG(persistent_list)执行器全局作用域(executor global)实现该(目的),它是个包含list_entry结构的HashTable,通常被引擎用于内部。依照下面修改hello_person_pnew():
PHP_FUNCTION(hello_person_pnew) |
这个版本的hello_person_pnew()首先在EG(persistent_list)全局作用域中检测已经存在的php_hello_person结构,如果可用就用它而不是浪费时间和资源重新分配。如果还不存在,函数分配一个新的结构装入新数据,并且把该结构加入持久列表中。不论哪种方式,函数都在请求中给你留下一个注册为资源的新的结构。
用于存储指针的持久列表总是位于当前进程或线程中,因此可能同时查找同样数据的两个请求没有任何联系。如果一个进程故意关闭一个持久资源,PHP将处理它,从持久列表中删除那个资源的引用,以使未来的调用不会使用已释放的数据。
3.8核对(代码)完整性
再一次,到本教程结束时你的扩展文件应该为:
config.m4
PHP_ARG_ENABLE(hello, [whether to enable Hello World support], |
php_hello.h
#ifndef PHP_HELLO_H |
hello.c
#ifdef HAVE_CONFIG_H |
总结
在本教程-编写扩展系列的第三部分中,你学习了把任意的、有时不透明的数据放入PHP用户空间变量的几个简单的必需步骤。在后面的步骤中,你将通过连接第三方库来组合这些技术,以创建在PHP中常看到的粘合库。
在第四部分,我们将关注对象-从PHP 4中可用的简单的带函数的数组,到PHP 5可用的更复杂的重载的OOP结构。