From PHPBB用户手册 (重定向自编写PHP扩展) Jump to: navigation, search 本文由hshq_cn翻译并于2007-12-13发表于ChinaUnix, 原文由Sara Golemon 发表在Zend Devzone Contents [隐藏] 1 介绍 2 扩展是什么? 3 生存周期 4 内存分配 5 建立构建环境 6 Hello W
(重定向自编写PHP扩展)
Jump to: navigation, search
本文由hshq_cn翻译并于2007-12-13发表于ChinaUnix, 原文由Sara Golemon 发表在Zend Devzone
Contents[隐藏]
|
既然您正在阅读本教程,那么您或许对编写PHP语言的扩展感兴趣。如果不是...呃,或许你并不知道这一兴趣,那么我们结束的时候你就会发现它。
本教程假定您基本熟悉PHP语言及其解释器实现所用的语言:C.
让我们从指明为什么你想要编写PHP扩展开始。
这些都是非常正当的理由,但是,在创建扩展之前,你需要首先明白扩展是什么?
如果你用过PHP,那么你肯定用到过扩展。除了少数例外,每个用户空间的函数都被组织在不同的扩展中。这些函数中的很多够成了standard扩展-总数超过400。PHP本身带有86个扩展(原文写就之时-译注),平均每个含有大约30个函数。数学操作方面大约有2500个函数。似乎这还不够, PECL仓库另外提供了超过100个扩展,而且互联网上可以找到更多。
“除了扩展中的函数,还有什么?”我听到了你的疑问。 “扩展的里面是什么?PHP的‘核心’是什么?”
PHP的核心由两部分组成。最底层是Zend引擎(ZE)。ZE把人类易读的脚本解析成机器可读的符号,然後在进程空间内执行这些符号。ZE也处理内存管理、变量作用域及调度程序调用。另一部分是PHP内核,它绑定了SAPI层(Server Application Programming Interface,通常涉及主机环境,如Apache,IIS,CLI,CGI等),并处理与它的通信。它同时对safe_mode和open_basedir的检测提供一致的控制层,就像流层将fopen()、fread()和fwrite()等用户空间的函数与文件和网络I/O联系起来一样。
当给定的SAPI启动时,例如在对/usr/local/apache/bin/apachectl start的响应中,PHP由初始化其内核子系统开始。在接近启动例程的末尾,它加载每个扩展的代码并调用其模块初始化例程(MINIT)。这使得每个扩展可以初始化内部变量、分配资源、注册资源处理器,以及向ZE注册自己的函数,以便于脚本调用这其中的函数时候ZE知道执行哪些代码。
接下来,PHP等待SAPI层请求要处理的页面。对于CGI或CLI等SAPI,这将立刻发生且只发生一次。对于Apache、IIS或其他成熟的web 服务器SAPI,每次远程用户请求页面时都将发生,因此重复很多次,也可能并发。不管请求如何产生,PHP开始于要求ZE建立脚本的运行环境,然後调用每个扩展的请求初始化 (RINIT)函数。RINIT使得扩展有机会设定特定的环境变量,根据请求分配资源,或者执行其他任务,如审核。 session扩展中有个RINIT作用的典型示例,如果启用了session.auto_start选项,RINIT将自动触发用户空间的session_start()函数以及预组装$_SESSION变量。
一旦请求被初始化了,ZE开始接管控制权,将PHP脚本翻译成符号,最终形成操作码并逐步运行之。如任一操作码需要调用扩展的函数,ZE将会把参数绑定到该函数,并且临时交出控制权直到函数运行结束。
脚本运行结束後,PHP调用每个扩展的请求关闭(RSHUTDOWN)函数以执行最後的清理工作(如将session变量存入磁盘)。接下来,ZE执行清理过程(垃圾收集)-有效地对之前的请求期间用到的每个变量执行unset()。
一旦完成,PHP继续等待SAPI的其他文档请求或者是关闭信号。对于CGI和CLI等SAPI,没有“下一个请求”,所以SAPI立刻开始关闭。关闭期间,PHP再次遍历每个扩展,调用其模块关闭(MSHUTDOWN)函数,并最终关闭自己的内核子系统。
这个过程乍听起来很让人气馁,但是一旦你深入一个运转的扩展,你会逐渐开始了解它。
为了避免写的不好的扩展丢失内存,ZE使用附加的标志来执行自己内部的内存管理器以标识持久性。持久分配的内存意味着比单次请求更持久。对比之下,对于在请求期间的非持久分配,不论是否调用释放(内存)函数,都将在请求尾期被释放。例如,用户空间的变量被分配为非持久的,因为请求结束後它们就没用了。
然而,理论上,扩展可以依赖ZE在页面请求结束时自动释放非持久内存,但是不推荐这样做。因为分配的内存将在很长时间保持为未回收状态,与之相关联的资源可能得不到适当的关闭,并且吃饭不擦嘴是坏习惯。稍後你会发现,事实上确保所有分配的数据都被正确清理很容易。
让我们简单地比较传统的内存分配函数(只应当在外部库中使用)和PHP/ZE的持久的以及非持久的内存非配函数。
统的 | 非持久的 | 持久的 |
malloc(count) | emalloc(count) | pemalloc(count, 1)* |
calloc(count, num) | ecalloc(count, num) | pecalloc(count, num, 1) |
strdup(str) | estrdup(str) | pestrdup(str, 1) |
strndup(str, len) | estrndup(str, len) | pemalloc() & memcpy() |
free(ptr) | efree(ptr) | pefree(ptr, 1) |
realloc(ptr, newsize) | erealloc(ptr, newsize) | perealloc(ptr, newsize, 1) |
malloc(count * num + extr)** | safe_emalloc(count, num, extr) | safe_pemalloc(count, num, extr) |
* pemalloc()族包含一个‘持久’标志以允许它们实现对应的非持久函数的功能。 例如:emalloc(1234)与pemalloc(1234, 0)相同。 ** safe_emalloc()和(PHP 5中的)safe_pemalloc()执行附加检测以防整数溢出。
既然你已经了解了一些PHP和Zend引擎的内部运行理论,我打赌你希望继续深入并开始构建一些东西。在此之前,你需要收集一些必需的构建工具并设定适合于你的目的的环境。
首先,你需要PHP本身及其所需要的构建工具集。如果你不熟悉从源码构建PHP,我建议你看看http://www.php.net/install.unix. (为Windows开发PHP扩展将在以後的文章中提到). 然而,使用PHP的二进制分发包有些冒险,这些版本倾向于忽略./configure的两个重要选项,它们在开发过程中很便利。第一个--enable-debug。这个选项将把附加的符号信息编译进PHP的执行文件,以便如果发生段错误,你能从中得到一个内核转储文件,使用gdb追踪并发现什么地方以及为什么会发生段错误。另一个选项依赖于你的PHP版本。在PHP 4.3中该选项名为--enable-experimental-zts,在PHP 5及以後的版本中为--enable-maintainer-zts。这个选项使PHP以为自己执行于多线程环境,并且使你能捕获通常的程序错误,然而它们在非多线程环境中是无害的,却使你的扩展不可安全用于多线程环境。一旦你已经使用这些额外的选项编译了PHP并安装于你的开发服务器(或者工作站)中,你就可以把你的第一个扩展加入其中了。
什么程序设计的介绍可以完全忽略必需的Hello World程序?此例中,你将制作的扩展导出一个简单函数,它返回一个含有“Hello World”的字符串。用PHP的话你或许这样做:
<?php function hello_world() { return 'Hello World'; } ?>
现在你将把它转入PHP扩展。首先,我们在你的PHP源码树的目录ext/中创建一个名为hello的目录,并且chdir进入该目录。事实上,这个目录可以置于PHP源码树之中或之外的任何地方,但是我希望你把它放在这儿,以例示一个在以後的文章中出现的与此无关的概念。你需要在这儿创建3个文件:包含hello_world函数的源码文件,包含引用的头文件,PHP用它们加载你的扩展,以及phpize用来准备编译你的扩展的配置文件。
PHP_ARG_ENABLE(hello, whether to enable Hello World support, [ --enable-hello Enable Hello World support]) if test "$PHP_HELLO" = "yes"; then AC_DEFINE(HAVE_HELLO, 1, [Whether you have Hello World]) PHP_NEW_EXTENSION(hello, hello.c, $ext_shared) fi
#ifndef PHP_HELLO_H #define PHP_HELLO_H 1 #define PHP_HELLO_WORLD_VERSION "1.0" #define PHP_HELLO_WORLD_EXTNAME "hello" PHP_FUNCTION(hello_world); extern zend_module_entry hello_module_entry; #define phpext_hello_ptr &hello_module_entry #endif
#ifdef HAVE_CONFIG_H #include "config.h" #endif #include "php.h" #include "php_hello.h" static function_entry hello_functions[] = { PHP_FE(hello_world, NULL) {NULL, NULL, NULL} }; zend_module_entry hello_module_entry = { #if ZEND_MODULE_API_NO >= 20010901 STANDARD_MODULE_HEADER, #endif PHP_HELLO_WORLD_EXTNAME, hello_functions, NULL, NULL, NULL, NULL, NULL, #if ZEND_MODULE_API_NO >= 20010901 PHP_HELLO_WORLD_VERSION, #endif STANDARD_MODULE_PROPERTIES }; #ifdef COMPILE_DL_HELLO ZEND_GET_MODULE(hello) #endif PHP_FUNCTION(hello_world) { RETURN_STRING("Hello World", 1); }
在上面的示例扩展中,你所看到的代码大多是黏合剂,作为将扩展引入PHP的协议语言并且在其间建立会话用于通信。只有最後四行才是你所认为“实际做事的代码”,它们负责与用户空间的脚本交互这一层次。这些代码看起来确实非常像之前看到的PHP代码,而且一看就懂:
回忆一下,ZE包含一个复杂的内存管理层,它可以确保分配的资源在脚本退出时被释放。然而,在内存管理领域,两次释放同一块内存是绝对禁止的(big no-no)。这种被称为二次释放(double freeing)的做法,是引起段错误的一个常见因素,原因是它使调用程序试图访问不再拥有的内存。类似地,你不应该让ZE去释放一个静态字符串缓冲区(如我们的示例扩展中的“Hello World”),因为它存在于程序空间,而不是被任何进程(process)拥有的数据块。RETURN_STRING()可以假定传入其中的任何字符串都需要被复制以便稍後可被安全地释放;但是由于内部的函数给字符串动态地分配内存、填充并返回并不罕见,第二参数RETURN_STRING()允许我们指定是否需要拷贝字符串的副本。要进一步说明这个概念,下面的代码片段与上面的对应版本等效:
PHP_FUNCTION(hello_world) { char *str; str = estrdup("Hello World"); RETURN_STRING(str, 0); }
在这个版本中,你手工为最终将被传回调用脚本的字符串“Hello World”分配内存,然後把这快内存“给予”RETURN_STRING(),用第二参数0指出它不需要制作自己的副本,可以拥有我们的。
本练习的最後一步是将你的扩展构建为可动态加载的模块。如果你已经正确地拷贝了上面的代码,只需要在ext/hello/中运行3个命令:
$ phpize $ ./configure --enable-hello $ make
每个命令都运行後,可在目录ext/hello/modules/中看到文件hello.so。现在,你可像其他扩展一样把它拷贝到你的扩展目录(默认是/usr/local/lib/php/extensions/,检查你的php.ini以确认),把extension=hello.so加入你的php.ini以使PHP启动时加载它。 对于CGI/CLI,下次运行PHP就会生效;对于web服务器SAPI,如Apache,需要重新启动web服务器。我们现在从命令行尝试下:
$ php -r 'echo hello_world();'
如果一切正常,你会看到这个脚本输出的Hello World,因为你的已加载的扩展中的函数hello_world()返回这个字符串,而且echo命令原样输出传给它的内容(本例中是函数的结果)。
可以同样的方式返回其他标量,整数值用RETURN_LONG(),浮点值用 RETURN_DOUBLE(),true/false值用RETURN_BOOL(),RETURN_NULL()?你猜对了,是NULL。我们看下它们各自在实例中的应用,通过在文件hello.c中的function_entry结构中添加对应的几行PHP_FE(),并且在文件结尾添加一些PHP_FUNCTION()。
static function_entry hello_functions[] = { PHP_FE(hello_world, NULL) PHP_FE(hello_long, NULL) PHP_FE(hello_double, NULL) PHP_FE(hello_bool, NULL) PHP_FE(hello_null, NULL) {NULL, NULL, NULL} }; PHP_FUNCTION(hello_long) { RETURN_LONG(42); } PHP_FUNCTION(hello_double) { RETURN_DOUBLE(3.1415926535); } PHP_FUNCTION(hello_bool) { RETURN_BOOL(1); } PHP_FUNCTION(hello_null) { RETURN_NULL(); }
你也需要在头文件php_hello.h中函数hello_world()的原型声明旁边加入这些函数的原型声明,以便构建进程正确进行:
PHP_FUNCTION(hello_world); PHP_FUNCTION(hello_long); PHP_FUNCTION(hello_double); PHP_FUNCTION(hello_bool); PHP_FUNCTION(hello_null);
由于你没有改变文件config.m4,这次跳过phpize和./configure步骤直接跳到make在技术上是安全的。然而,此时我要你再次做完全部构建步骤,以确保构建良好。另外,你应该调用make clean all而不是简单地在最後一步make,确保所有源文件被重建。重复一遍,迄今为止,根据你所做得改变的类型这些(步骤)不是必需的,但是安全比混淆要好。一旦模块构建好了,再次把它拷贝到你的扩展目录,替换旧版本。
此时你可以再次调用PHP解释器, 简单地传入脚本测试刚加入的函数。事实上,为什么不现在就做呢?我会在这儿等待...
完成了?好的。如果用了var_dump()而不是echo查看每个函数的输出,你或许注意到了hello_bool()返回true。那就是RETURN_BOOL()中的值1表现的结果。和在PHP脚本中一样,整数值0等于FALSE,而其他整数等于TRUE。仅仅是作为约定,扩展作者通常用1,鼓励你也这么做,但是不要感觉被它限制了。出于另外的可读性目的,也可用宏RETURN_TRUE和RETURN_FALSE;再来一个hello_bool(),这次使用RETURN_TRUE:
PHP_FUNCTION(hello_bool) { RETURN_TRUE; }
注意这儿没用括号。那样的话,与其他的宏RETURN_*()相比,RETURN_TRUE和RETURN_FALSE的样式有区别(are aberrations),所以确信不要被它误导了(to get caught by this one)。
大概你注意到了,上面的每个范例中,我们都没传入0或1以表明是否进行拷贝。这是因为,对于类似这些简单的小型标量,不需要分配或释放额外的内存(除了变量容器自身-我们将在第二部分作更深入的考查。)
还有其他的三种返回类型:资源(就像mysql_connect(),fsockopen()和ftp_connect()返回的值的名字一样,但是不限于此),数组(也被称为HASH)和对象(由关键字new返回)。当我们深入地讲解变量时,会在第二部分看到它们。
Zend引擎提供了两种管理INI值的途径。现在我们来看简单一些的,然後当你处理全局数据时再探究更完善但也更复杂的方式。
假设你要在php.ini中为你的扩展定义一个值,hello.greeting,它保存将在hello_world()函数中用到的问候字符串。你需要向hello.c和php_hello.h中增加一些代码,同时对hello_module_entry结构作一些关键性的改变。先在文件php_hello.h中靠近用户空间函数的原型声明处增加如下原型:
PHP_MINIT_FUNCTION(hello); PHP_MSHUTDOWN_FUNCTION(hello); PHP_FUNCTION(hello_world); PHP_FUNCTION(hello_long); PHP_FUNCTION(hello_double); PHP_FUNCTION(hello_bool); PHP_FUNCTION(hello_null);
现在进入文件hello.c,去掉当前版本的hello_module_entry,用下面的列表替换它:
zend_module_entry hello_module_entry = { #if ZEND_MODULE_API_NO >= 20010901 STANDARD_MODULE_HEADER, #endif PHP_HELLO_WORLD_EXTNAME, hello_functions, PHP_MINIT(hello), PHP_MSHUTDOWN(hello), NULL, NULL, NULL, #if ZEND_MODULE_API_NO >= 20010901 PHP_HELLO_WORLD_VERSION, #endif STANDARD_MODULE_PROPERTIES }; PHP_INI_BEGIN() PHP_INI_ENTRY("hello.greeting", "Hello World", PHP_INI_ALL, NULL) PHP_INI_END() PHP_MINIT_FUNCTION(hello) { REGISTER_INI_ENTRIES(); return SUCCESS; } PHP_MSHUTDOWN_FUNCTION(hello) { UNREGISTER_INI_ENTRIES(); return SUCCESS; }
现在,你只需要在文件hello.c顶部的那些#include旁边增加一个#include,这样可以获得正确的支持INI的头文件:
<pre class="brush:php;toolbar:false">#ifdef HAVE_CONFIG_H #include "config.h" #endif #include "php.h" #include "php_ini.h" #include "php_hello.h" 最後,你可修改函数hello_world让它使用INI的值: <pre class="brush:php;toolbar:false">PHP_FUNCTION(hello_world) { RETURN_STRING(INI_STR("hello.greeting"), 1); }
本节中的第一处改变引入了两个你非常熟悉的函数:MINIT和MSHUTDOWN。正如稍早提到的,这些方法在SAPI初始启动和最终关闭期间被各自调用。它们不会在请求期间和请求之间被调用。本例中它们用来将你的扩展中定义的条目向php.ini注册。本系列後面的教程中,你也将看到如何使用MINIT和MSHUTDOWN函数注册资源、对象和流处理器。
函数hello_world()中使用INI_STR()取得hello.greeting条目的当前字符串值。也存在其他类似函数用于取得其他类型的值,长整型、双精度浮点型和布尔型,如下面表格中所示;同时也提供另外的ORIG版本,它们提供在php.ini文件中的INI(的原始)设定(在被 .htaccess或ini_set()指令改变之前)(原文:provides the value of the referenced INI setting as it was set in php.ini-译注)。
当前值 | 原始值 | 类型 |
INI_STR(name) | INI_ORIG_STR(name) | char * (NULL terminated) |
INI_INT(name) | INI_ORIG_INT(name) | signed long |
INI_FLT(name) | INI_ORIG_FLT(name) | signed double |
INI_BOOL(name) | INI_ORIG_BOOL(name) | zend_bool |
传入PHP_INI_ENTRY()的第一个参数含有在php.ini文件中用到的名字字符串。为了避免命名空间冲突,你应该使用同函数一样的约定,即是,将你的扩展的名字作为所有值的前缀,就像你对hello.greeting做的一样。仅仅是作为约定,一个句点被用来分隔扩展的名字和更具说明性的初始设定名字。
第二个参数是初始值(默认值?-译注),而且,不管它是不是数字值,都要使用char*类型的字符串。这主要是依据如下事实:.ini文件中的原值就是文本-连同其他的一切作为一个文本文件存储。你在後面的脚本中所用到的INI_INT()、INI_FLT()或INI_BOOL()会进行类型转换。
传入的第三个值是访问模式修饰符。这是个位掩码字段,它决定该INI值在何时和何处是可修改的。对于其中的一些,如register_globals,它只是不允许在脚本中用ini_set()改变该值,因为这个设定只在请求启动期间(在脚本能够运行之前)有意义。其他的,如allow_url_fopen,是管理(员才可进行的)设定,你不会希望共享主机环境的用户去修改它,不论是通过ini_set()还是.htaccess的指令。该参数的典型值可能是PHP_INI_ALL,表明该值可在任何地方被修改。然後还有PHP_INI_SYSTEM|PHP_INI_PERDIR,表明该设定可在php.ini文件中修改,或者通过.htaccess文件中的Apache指令修改,但是不能用ini_set()修改。或者,也可用PHP_INI_SYSTEM,表示该值只能在php.ini文件中修改,而不是任何其他地方。
我们现在忽略第四个参数,只是提一下,它允许在初始设定发生改变时-例如使用ini_set()-触发一个方法回调。这使得当设定改变时,扩展可以执行更精确的控制,或是根据新的设定触发一个相关的行为。
扩展经常需要在一个特定的请求中由始至终跟踪一个值,而且要把它与可能同时发生的其他请求分开。在非多线程的SAPI中很简单:只是在源文件中声明一个全局变量并在需要时访问它。问题是,由于PHP被设计为可在多线程web服务器(如Apache 2和IIS)中运行,它需要保持各线程使用的全局数值的独立。通过使用TSRM (Thread Safe Resource Management,线程安全的资源管理器) 抽象层-有时称为ZTS(Zend Thread Safety,Zend线程安全),PHP将其极大地简化了。实际上,此时你已经用到了部分TSRM,只是没有意识到。(不要探寻的太辛苦;随着本系列的进行,你将到处看到它的身影。)
如同任意的全局作用域,创建一个线程安全的作用域的第一步是声明它。鉴于本例的目标,你将会声明一个值为0的long型全局数值。每次hello_long()被调用,都将该值增1并返回。在php_hello.h文件中的#define PHP_HELLO_H语句後面加入下面的代码段:
#ifdef ZTS #include "TSRM.h" #endif ZEND_BEGIN_MODULE_GLOBALS(hello) long counter; ZEND_END_MODULE_GLOBALS(hello) #ifdef ZTS #define HELLO_G(v) TSRMG(hello_globals_id, zend_hello_globals *, v) #else #define HELLO_G(v) (hello_globals.v) #endif
这次也会使用RINIT方法,所以你需要在头文件中声明它的原型:
PHP_MINIT_FUNCTION(hello); PHP_MSHUTDOWN_FUNCTION(hello); PHP_RINIT_FUNCTION(hello);
现在我们回到文件hello.c中并紧接着包含代码块後面加入下面的代码:
#ifdef HAVE_CONFIG_H #include "config.h" #endif #include "php.h" #include "php_ini.h" #include "php_hello.h" ZEND_DECLARE_MODULE_GLOBALS(hello) 改变hello_module_entry,加入PHP_RINIT(hello): zend_module_entry hello_module_entry = { #if ZEND_MODULE_API_NO >= 20010901 STANDARD_MODULE_HEADER, #endif PHP_HELLO_WORLD_EXTNAME, hello_functions, PHP_MINIT(hello), PHP_MSHUTDOWN(hello), PHP_RINIT(hello), NULL, NULL, #if ZEND_MODULE_API_NO >= 20010901 PHP_HELLO_WORLD_VERSION, #endif STANDARD_MODULE_PROPERTIES };
而且修改你的MINIT函数,附带着另外两个函数,它们在请求启动时执行初始化:
static void php_hello_init_globals(zend_hello_globals *hello_globals) { } PHP_RINIT_FUNCTION(hello) { HELLO_G(counter) = 0; return SUCCESS; } PHP_MINIT_FUNCTION(hello) { ZEND_INIT_MODULE_GLOBALS(hello, php_hello_init_globals, NULL); REGISTER_INI_ENTRIES(); return SUCCESS; }
最後,你可修改hello_long()函数使用这个值:
PHP_FUNCTION(hello_long) { HELLO_G(counter)++; RETURN_LONG(HELLO_G(counter)); }
在你加入php_hello.h的代码中,你用到了两个宏-ZEND_BEGIN_MODULE_GLOBALS()和ZEND_END_MODULE_GLOBALS()-用来创建一个名为zend_hello_globals的结构,它包含一个long型的变量。然後有条件地将HELLO_G()定义为从线程池中取得数值,或者从全局作用域中得到-如果你编译的目标是非多线程环境。
在hello.c中,你用ZEND_DECLARE_MODULE_GLOBALS()宏来例示zend_hello_globals结构,或者是真的全局(如果此次构建是非线程安全的),或者是本线程的资源池的一个成员。作为扩展作者,我们不需要担心它们的区别,因为Zend引擎为我们打理好这个事情。最後,你在 MINIT中用ZEND_INIT_MODULE_GLOBALS()分配一个线程安全的资源id-现在还不用考虑它是什么。
你可能已经注意到了,php_hello_init_globals()实际上什么也没做,却得多声明个RINIT将变量counter初始化为0。为什么?
关键在于这两个函数何时被调用。php_hello_init_globals()只在开始一个新的进程或线程时被调用;然而, 每个进程都能处理多个请求,所以用这个函数将变量counter初始化为0将只在第一个页面请求时运行。向同一进程发出的後续页面请求将仍会得到以前存储在这儿的counter变量的值,因此不会从0开始计数。要为每个页面请求把counter变量初始化为0,我们实现RINIT函数, 正如之前看到的,它在每个页面请求之前被调用。此时我们包含php_hello_init_globals()函数是因为稍後你将会用到它,而且在ZEND_INIT_MODULE_GLOBALS()中为这个初始化函数传入NULL将导致在非多线程的平台产生段错误。
回想一下,一个用PHP_INI_ENTRY()声明的php.ini值会作为字符串被解析,并按需用INI_INT()、INI_FLT()和INI_BOOL()转为其他格式。对于某些设定,那么做使得在脚本的执行过程中,当读取这些值时反复做大量不需要的重复工作。幸运的是,可以让ZE将INI值存储为特定的数据类型,并只在它的值被改变时执行类型转换。我们通过声明另一个INI值来尝试下,这次是个布尔值,用来指示变量counter是递增还是递减。开始吧,先把php_hello.h中的MODULE_GLOBALS块改成下面的代码:
ZEND_BEGIN_MODULE_GLOBALS(hello) long counter; zend_bool direction; ZEND_END_MODULE_GLOBALS(hello)
接下来,修改PHP_INI_BEGIN()块,声明INI值,像这样:
PHP_INI_BEGIN() PHP_INI_ENTRY("hello.greeting", "Hello World", PHP_INI_ALL, NULL) STD_PHP_INI_ENTRY("hello.direction", "1", PHP_INI_ALL, OnUpdateBool, direction, zend_hello_globals, hello_globals) PHP_INI_END()
现在用下面的代码初始化init_globals方法中的设定:
static void php_hello_init_globals(zend_hello_globals *hello_globals) { hello_globals->direction = 1; }
并且最後,在hello_long()中应用这个初始设定来决定是递增还是递减:
PHP_FUNCTION(hello_long) { if (HELLO_G(direction)) { HELLO_G(counter)++; } else { HELLO_G(counter)--; } RETURN_LONG(HELLO_G(counter)); }
就是这些。在INI_ENTRY部分指定的OnUpdateBool方法会自动地把php.ini、.htaccess或者脚本中通过ini_set()提供的值转换为适当的TRUE/FALSE值,这样你就可以在脚本中直接访问它们。STD_PHP_INI_ENTRY的最後三个参数告诉PHP去改变哪个全局变量,我们的扩展的全局(作用域)的结构是什么样子,以及持有这些变量的全局作用域的名字是什么。
迄今为止,我们的三个文件应该类似于下面的列表。(为了可读性,一些项目被移动和重新组织了。)
PHP_ARG_ENABLE(hello, whether to enable Hello World support, [ --enable-hello Enable Hello World support]) if test "$PHP_HELLO" = "yes"; then AC_DEFINE(HAVE_HELLO, 1, [Whether you have Hello World]) PHP_NEW_EXTENSION(hello, hello.c, $ext_shared) fi
#ifndef PHP_HELLO_H #define PHP_HELLO_H 1 #ifdef ZTS #include "TSRM.h" #endif ZEND_BEGIN_MODULE_GLOBALS(hello) long counter; zend_bool direction; ZEND_END_MODULE_GLOBALS(hello) #ifdef ZTS #define HELLO_G(v) TSRMG(hello_globals_id, zend_hello_globals *, v) #else #define HELLO_G(v) (hello_globals.v) #endif #define PHP_HELLO_WORLD_VERSION "1.0" #define PHP_HELLO_WORLD_EXTNAME "hello" PHP_MINIT_FUNCTION(hello); PHP_MSHUTDOWN_FUNCTION(hello); PHP_RINIT_FUNCTION(hello); PHP_FUNCTION(hello_world); PHP_FUNCTION(hello_long); PHP_FUNCTION(hello_double); PHP_FUNCTION(hello_bool); PHP_FUNCTION(hello_null); extern zend_module_entry hello_module_entry; #define phpext_hello_ptr &hello_module_entry #endif
#ifdef HAVE_CONFIG_H #include "config.h" #endif #include "php.h" #include "php_ini.h" #include "php_hello.h" ZEND_DECLARE_MODULE_GLOBALS(hello) static function_entry hello_functions[] = { PHP_FE(hello_world, NULL) PHP_FE(hello_long, NULL) PHP_FE(hello_double, NULL) PHP_FE(hello_bool, NULL) PHP_FE(hello_null, NULL) {NULL, NULL, NULL} }; zend_module_entry hello_module_entry = { #if ZEND_MODULE_API_NO >= 20010901 STANDARD_MODULE_HEADER, #endif PHP_HELLO_WORLD_EXTNAME, hello_functions, PHP_MINIT(hello), PHP_MSHUTDOWN(hello), PHP_RINIT(hello), NULL, NULL, #if ZEND_MODULE_API_NO >= 20010901 PHP_HELLO_WORLD_VERSION, #endif STANDARD_MODULE_PROPERTIES }; #ifdef COMPILE_DL_HELLO ZEND_GET_MODULE(hello) #endif PHP_INI_BEGIN() PHP_INI_ENTRY("hello.greeting", "Hello World", PHP_INI_ALL, NULL) STD_PHP_INI_ENTRY("hello.direction", "1", PHP_INI_ALL, OnUpdateBool, direction, zend_hello_globals, hello_globals) PHP_INI_END() static void php_hello_init_globals(zend_hello_globals *hello_globals) { hello_globals->direction = 1; } PHP_RINIT_FUNCTION(hello) { HELLO_G(counter) = 0; return SUCCESS; } PHP_MINIT_FUNCTION(hello) { ZEND_INIT_MODULE_GLOBALS(hello, php_hello_init_globals, NULL); REGISTER_INI_ENTRIES(); return SUCCESS; } PHP_MSHUTDOWN_FUNCTION(hello) { UNREGISTER_INI_ENTRIES(); return SUCCESS; } PHP_FUNCTION(hello_world) { RETURN_STRING("Hello World", 1); } PHP_FUNCTION(hello_long) { if (HELLO_G(direction)) { HELLO_G(counter)++; } else { HELLO_G(counter)--; } RETURN_LONG(HELLO_G(counter)); } PHP_FUNCTION(hello_double) { RETURN_DOUBLE(3.1415926535); } PHP_FUNCTION(hello_bool) { RETURN_BOOL(1); } PHP_FUNCTION(hello_null) { RETURN_NULL(); }
与用户空间的代码不同,内部函数的参数实际上并不是在函数头部声明的,而是将参数列表的地址传入每个函数-不论是否传入了参数-而且,函数可以让Zend引擎将它们转为便于使用的东西。
我们通过定义新函数hello_greetme()来看一下,它将接收一个参数然後把它与一些问候的文本一起输出。和以前一样,我们将在三个地方增加代码:
在php_hello.h中,靠近其他的函数原型声明处:
PHP_FUNCTION(hello_greetme);
在hello.c中,hello_functions结构的底部:
<pre class="brush:php;toolbar:false"> PHP_FE(hello_bool, NULL) PHP_FE(hello_null, NULL) PHP_FE(hello_greetme, NULL) {NULL, NULL, NULL} };
以及hello.c底部靠近其他函数的後面:
PHP_FUNCTION(hello_greetme) { char *name; int name_len; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &name, &name_len) == FAILURE) { RETURN_NULL(); } php_printf("Hello %s\n", name); RETURN_TRUE; }
大多数zend_parse_parameters()块看起来是总是一样的。ZEND_NUM_ARGS()告诉Zend引擎要取得的参数的信息,TSRMLS_CC用来确保线程安全,返回值将被检查是SUCCESS还是FAILURE。通常情况下,zend_parse_parameters()将返回SUCCESS;然而,如果调用脚本试图传入太多或太少的参数,或者传入的参数不能被转为适当的类型,Zend会自动输出一条错误信息并优雅地将控制权还给调用脚本。
本例指定s表明此函数期望只传入一个参数,而且该参数应该被转为string数据类型并装入通过地址传入的char*变量(也就是通过name)。
注意,还有一个int变量通过地址被传入zend_parse_parameters()。这使Zend引擎提供字符串的字节长度,如此二进制安全的函数不再需要依赖strlen(name)确定字符串的长度。实际上使用strlen(name)甚至得不到正确的结果,因为name可能在字符串结束之前包含一个或多个NULL字符。
一旦你的函数确切地得到了name参数,接下来要做的就是把它作为正式问候语的一部分输出。注意,用的是php_printf()而不是更熟悉的printf()。使用这个函数是有重要的理由的。首先,它允许字符串通过PHP的缓冲机制的处理,该机制除了可以缓冲数据,还可执行额外的处理,比如gzip压缩。其次,虽然stdout是极佳的输出目标,使用CLI或CGI时,多数SAPI期望通过特定的pipe或socket传来输出。所以,试图只是通过printf()写入stdout可能导致数据丢失、次序颠倒或者被破坏,因为它绕过了预处理。
最後,函数通过返回TRUE将控制权还给调用程序。你可以没有显式地返回值(默认是NULL)而是让控制到达你的函数的结尾,但这是坏习惯。函数如果不传回任何有意义的结果,应该返回TRUE以说明:“完成任务,一切正常”。
PHP字符串实际可能包含NULL值,所以,输出含有NULL的二进制安全的字符串以及後跟NULL的多个字符的方法是,使用下面的代码块替换php_printf()指令:
<pre class="brush:php;toolbar:false">php_printf("Hello "); PHPWRITE(name, name_len); php_printf("\n");
这段代码使用php_printf()处理确信没有NULL的字符串,但使用另外的宏-PHPWRITE-处理用户提供的字符串。这个宏接受zend_parse_parameters()提供的长度(name_len)参数以便可以打印name的完整内容,不论它是否含有NULL。
zend_parse_parameters()也会处理可选参数。下一个例子中,你将创建一个函数,它期望一个long(PHP的整数类型)、一个double(浮点)和一个可选的Boolean值。这个函数在用户空间的声明可能像这样:
function hello_add($a, $b, $return_long = false) { $sum = (int)$a + (float)$b; if ($return_long) { return intval($sum); } else { return floatval($sum); }}
在C语言中,这个函数类似下面的代码(不要忘记在php_hello.h和hello.c的hello_functions[]中加入相关条目以启用它):
PHP_FUNCTION(hello_add) { long a; double b; zend_bool return_long = 0; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ld|b", &a, &b, &return_long) == FAILURE) { RETURN_NULL(); } if (return_long) { RETURN_LONG(a + b); } else { RETURN_DOUBLE(a + b); } }
这次你的数据类型字符串读起来像:“我要一个long(l),一个double(d)”。下一个管道字符表示其馀的参数是可选的。如果函数调用时没有传入可选参数,那么zend_parse_parameters()将不会改变传给它的对应变量。最後的b当然是用于Boolean。数据类型字符串後面的是a、b和return_long,它们按地址传递,这样zend_parse_parameters()可以将值装入它们。
警告:在32位平台中经常不加区分地使用int和long,但是,当你的代码在64位硬件上编译时,在本该使用一个的地方使用另一个是很危险的。所以记住要把long用于整型,把int用于字符串的长度。
表 1显示不同的类型和对应的字母代码,以及可用于zend_parse_parameters()的C类型:
表 1:类型和用在zend_parse_parameters()中的字母代码
类型 | 代码 | 变量类型 |
Boolean | b | zend_bool |
Long | l | long |
Double | d | double |
String | s | char*, int |
Resource | r | zval* |
Array | a | zval* |
Object | o | zval* |
zval | z | zval* |
你可能立刻注意到表 1中的最後四个类型都是zval*。待会儿你将看到,PHP中实际使用zval数据类型存储所有的用户空间变量。三种“复杂”数据类型,资源、数组和对象,当它们的数据类型代码被用于zend_parse_parameters()时,Zend引擎会进行类型检查,但是因为在C中没有与它们对应的数据类型,所以不会执行类型转换。
一般而言,zval和PHP用户空间变量是要你费脑筋(wrap your head around)的最困难的概念。它们也将是至关重要的。首先我们考查zval的结构:
struct { union { long lval; double dval; struct { char *val; int len; } str; HashTable *ht; zend_object_value obj; } value; zend_uint refcount; zend_uchar type; zend_uchar is_ref; } zval;
如你所见,通常每个zval具有三个基本的元素:type、is_ref和refcount。is_ref和refcount将在本教程的稍候讲解;现在让我们关注type。
到如今你应该已经熟悉了PHP的八种数据类型。它们是表1种列出的七种,再加上NULL-虽然实际的字面意义是什么也没有(或许这就是原因),是特殊(unto its own)的类型。给定一个具体的zval,可用三个便利的宏中的一个测试它的类型:Z_TYPE(zval)、Z_TYPE_P(zval*)或Z_TYPE_PP(zval**)。三者之间仅有的功能上的区别在于传入的变量所期望的间接的级别。其他的宏也遵从相同的关于_P和_PP的使用约定,例如你将要看到的宏*VAL。
type的值决定zval的value联合的哪个部分被设置。下面的代码片断演示了一个缩微版本的var_dump():
PHP_FUNCTION(hello_dump) { zval *uservar; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &uservar) == FAILURE) { RETURN_NULL(); } switch (Z_TYPE_P(uservar)) { case IS_NULL: php_printf("NULL\n"); break; case IS_BOOL: php_printf("Boolean: %s\n", Z_LVAL_P(uservar) ? "TRUE" : "FALSE"); break; case IS_LONG: php_printf("Long: %ld\n", Z_LVAL_P(uservar)); break; case IS_DOUBLE: php_printf("Double: %f\n", Z_DVAL_P(uservar)); break; case IS_STRING: php_printf("String: "); PHPWRITE(Z_STRVAL_P(uservar), Z_STRLEN_P(uservar)); php_printf("\n"); break; case IS_RESOURCE: php_printf("Resource\n"); break; case IS_ARRAY: php_printf("Array\n"); break; case IS_OBJECT: php_printf("Object\n"); break; default: php_printf("Unknown\n"); } RETURN_TRUE; }
如你所见,数据类型Boolean和long共享同样的内部元素。如同本系列第一部分中用的RETURN_BOOL(),FALSE用0表示,TRUE用1表示。
当使用zend_parse_parameters()请求一个特定的数据类型时,例如string,Zend引擎检查输入变量的数据类型。如果匹配,Zend只是通过将其传入zval的对应部分来得到正确的数据类型。如果是不同的类型,Zend使用通常的类型转换规则将其转为适当的和/或可能的类型。
修改前面实现的hello_greetme()函数,将它分成小一些的(功能)片断:
PHP_FUNCTION(hello_greetme) { zval *zname; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &zname) == FAILURE) { RETURN_NULL(); } convert_to_string(zname); php_printf("Hello "); PHPWRITE(Z_STRVAL_P(zname), Z_STRLEN_P(zname)); php_printf("\n"); RETURN_TRUE; }
这次,zend_parse_parameters()只是获取一个PHP变量(zval),忽略其类型,接着显式地将该变量转为字符串(类似于$zname = (string)$zname;),然後使用zname结构的字符串值调用php_printf()。正如你所猜测的那样,存在其它可用于bool、long和double的convert_to_*()函数。
至今为止,你用到的zval已由Zend引擎分配空间,也通过同样的途径释放。然而有时候需要创建你自己的zval。考虑下面的代码段:
{ zval *temp; ALLOC_INIT_ZVAL(temp); Z_TYPE_P(temp) = IS_LONG; Z_LVAL_P(temp) = 1234; zval_ptr_dtor(&temp); }
ALLOC_INIT_ZVAL(),如名所示,为zval*分配内存并把它初始化为一个新变量。那样之後,可用Z_*_P()设置该变量的类型和值。zval_ptr_dtor()处理繁重的清理变量内存工作。
那两个Z_*_P()调用实际可以被归为一条件但的语句: ZVAL_LONG(temp, 1234);
对于其他类型也存在相似的宏,并且遵循和本系列第一部分中出现的RETURN_*()相同的语法规则。实际上宏RETURN_*()只是对RETVAL_*()薄薄的一层包装,再深入则是ZVAL_*()。下面的五个版本都是相同的:
RETURN_LONG(42); RETVAL_LONG(42); return; ZVAL_LONG(return_value, 42); return; Z_TYPE_P(return_value) = IS_LONG; Z_LVAL_P(return_value) = 42; return; return_value->type = IS_LONG; return_value->value.lval = 42; return;
如果你很敏锐,你会思考如何定义它们才能实现在类似hello_long()函数中的使用方式。“return_value从哪儿来?为什么它不用ALLOC_INIT_ZVAL()分配内存?”,你可能会疑惑。
在日常的扩展开发中,你可能不知道return_value实际是在每个PHP_FUNCTION()原型定义中定义的函数参数。Zend引擎给它分配内存并将其初始化为NULL,这样即使你的函数没有显式地设置它,返回值仍然是可用的。当你的内部函数执行结束,该值被返回到调用程序,或者被释放-如果调用程序被写为忽略返回值。
因为你之前用过PHP,你已经承认了数组作为运载其他变量的变量。这种方式在内部实现上使用了众所周知的HashTable。要创建将被返回PHP的数组,最简单的方法涉及使用表2中列举的函数:
表 2:zval数组创建函数
PHP语法 | C语法(arr是zval*) | 意义 |
$arr = array(); | array_init(arr); | 初始化一个新数组 |
$arr[] = NULL; | add_next_index_null(arr); | 向数字索引的数组增加指定类型的值 |
$arr[] = 42; | add_next_index_long(arr, 42); | |
$arr[] = true; | add_next_index_bool(arr, 1); | |
$arr[] = 3.14; | add_next_index_double(arr, 3.14); | |
$arr[] = 'foo'; | add_next_index_string(arr, "foo", 1); | |
$arr[] = $myvar; | add_next_index_zval(arr, myvar); | |
$arr[0] = NULL; | add_index_null(arr, 0); | 向数组中指定的数字索引增加指定类型的值 |
$arr[1] = 42; | add_index_long(arr, 1, 42); | |
$arr[2] = true; | add_index_bool(arr, 2, 1); | |
$arr[3] = 3.14; | add_index_double(arr, 3, 3.14); | |
$arr[4] = 'foo'; | add_index_string(arr, 4, "foo", 1); | |
$arr[5] = $myvar; | add_index_zval(arr, 5, myvar); | |
$arr['abc'] = NULL; | add_assoc_null(arr, "abc"); | 向关联索引的数组增加指定类型的值 |
$arr['def'] = 711; | add_assoc_long(arr, "def", 711); | |
$arr['ghi'] = true; | add_assoc_bool(arr, "ghi", 1); | |
$arr['jkl'] = 1.44; | add_assoc_double(arr, "jkl", 1.44); | |
$arr['mno'] = 'baz'; | add_assoc_string(arr, "mno", "baz", 1); | |
$arr['pqr'] = $myvar; | add_assoc_zval(arr, "pqr", myvar); |
同RETURN_STRING()宏一样,add_*_string()函数的最後一个参数接受1或0来指明字符串内容是否被拷贝。它们各自都有形如add_*_stringl()的对应版本。l表示会显式提供字符串长度(而不是让Zend引擎调用strval()来得到这个值,该函数不是二进制安全的)。
使用二进制安全的形式很简单,只需要在(表示)复制的参数前面指定长度,像这样:
add_assoc_stringl(arr, "someStringVar", "baz", 3, 1);
使用add_assoc_*()函数,数组的关键字假定不包含NULL-add_assoc_*() 函数自身对于关键字不是二进制安全的。不可使用带有NULL的关键字(实际上对象的受保护的和私有的属性已经使用了这种技术),可是如果必须这样做,当我们稍候使用zend_hash_*()函数时,你将立刻知道怎样实现。
要实践学到的东西,创建下面的函数,它返回一个数组到调用程序。确定向php_hello.h和hello_functions[]中增加条目以使该函数得到适当地声明。
PHP_FUNCTION(hello_array) { char *mystr; zval *mysubarray; array_init(return_value); add_index_long(return_value, 42, 123); add_next_index_string(return_value, "I should now be found at index 43", 1); add_next_index_stringl(return_value, "I'm at 44!", 10, 1); mystr = estrdup("Forty Five"); add_next_index_string(return_value, mystr, 0); add_assoc_double(return_value, "pi", 3.1415926535); ALLOC_INIT_ZVAL(mysubarray); array_init(mysubarray); add_next_index_string(mysubarray, "hello", 1); add_assoc_zval(return_value, "subarray", mysubarray); }
构建扩展并查看var_dump(hello_array());的结果:
array(6) { [42]=> int(123) [43]=> string(33) "I should now be found at index 43" [44]=> string(10) "I'm at 44!" [45]=> string(10) "Forty Five" ["pi"]=> float(3.1415926535) ["subarray"]=> array(1) { [0]=> string(5) "hello" } }
从数组中取回值意味着使用ZENDAPI的zend_hash族函数直接从HashTable中把它们作为zval**抽取出来。我们以接受一个数组为参数的简单函数开始:
function hello_array_strings($arr) { if (!is_array($arr)) return NULL; printf("The array passed contains %d elements\n", count($arr)); foreach($arr as $data) { if (is_string($data)) echo "$data\n"; }}
或者,在C中:
PHP_FUNCTION(hello_array_strings) { zval *arr, **data; HashTable *arr_hash; HashPosition pointer; int array_count; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "a", &arr) == FAILURE) { RETURN_NULL(); } arr_hash = Z_ARRVAL_P(arr); array_count = zend_hash_num_elements(arr_hash); php_printf("The array passed contains %d elements\n", array_count); for(zend_hash_internal_pointer_reset_ex(arr_hash, &pointer); zend_hash_get_current_data_ex(arr_hash, (void**) &data, &pointer) == SUCCESS; zend_hash_move_forward_ex(arr_hash, &pointer)) { if (Z_TYPE_PP(data) == IS_STRING) { PHPWRITE(Z_STRVAL_PP(data), Z_STRLEN_PP(data)); php_printf("\n"); } } RETURN_TRUE; }
为了保持函数的简短,只输出了字符串类型的数组元素。你可能会奇怪,为什么不用之前在hello_greetme()函数中用过的convert_to_string()?我们来看看那样做怎么样;用下面的代码替换上面的for循环:
for(zend_hash_internal_pointer_reset_ex(arr_hash, &pointer); zend_hash_get_current_data_ex(arr_hash, (void**) &data, &pointer) == SUCCESS; zend_hash_move_forward_ex(arr_hash, &pointer)) { convert_to_string_ex(data); PHPWRITE(Z_STRVAL_PP(data), Z_STRLEN_PP(data)); php_printf("\n"); }
现在重新编译扩展并运行下面的用户空间代码:
<pre class="brush:php;toolbar:false"><?php $a = array('foo',123); var_dump($a); hello_array_strings($a); var_dump($a); ?>
注意,原始数组被改变了!记住,convert_to_*()函数具有与调用set_type()相同的效果。由于处理的数组与传入的是同一个,此处改变它的类型将改变原始变量。要避免则需要首先制作一份zval的副本。为此,再次将for循环改成下面的代码:
for(zend_hash_internal_pointer_reset_ex(arr_hash, &pointer); zend_hash_get_current_data_ex(arr_hash, (void**) &data, &pointer) == SUCCESS; zend_hash_move_forward_ex(arr_hash, &pointer)) { zval temp; temp = **data; zval_copy_ctor(&temp); convert_to_string(&temp); PHPWRITE(Z_STRVAL(temp), Z_STRLEN(temp)); php_printf("\n"); zval_dtor(&temp); }
这次更明显的-temp = **data-只是拷贝了原zval的数据,但由于zval可能含有类似char*字符串或HashTable*数组等额外已分配资源,这些相关的资源需要用zval_copy_ctor()进行复制。之後就是普通的转换、打印,以及最终用zval_dtor()去除这个副本用到的资源。
如果你感到奇怪:为什么首次引入convert_to_string()时(参见hello_greetme()的第二个版本,功能被划分为小片断-译注)没做zval_copy_ctor()?那是因为向函数传入变量会自动地从原始变量分离出zval,拷贝一个副本。这始终只作用于zval的表层(on the base),所以,任何次级资源(例如数组元素和对象属性)在使用前仍然需要进行分离。
既然已经看过了数组的值,我们稍微扩充下此次练习,也来看看(数组的)关键字:
for(zend_hash_internal_pointer_reset_ex(arr_hash, &pointer); zend_hash_get_current_data_ex(arr_hash, (void**) &data, &pointer) == SUCCESS; zend_hash_move_forward_ex(arr_hash, &pointer)) { zval temp; char *key; int key_len; long index; if (zend_hash_get_current_key_ex(arr_hash, &key, &key_len, &index, 0, &pointer) == HASH_KEY_IS_STRING) { PHPWRITE(key, key_len); } else { php_printf("%ld", index); } php_printf(" => "); temp = **data; zval_copy_ctor(&temp); convert_to_string(&temp); PHPWRITE(Z_STRVAL(temp), Z_STRLEN(temp)); php_printf("\n"); zval_dtor(&temp); }
记住数组可以具有数字索引、关联字符串关键字或兼有二者。调用zend_hash_get_current_key_ex()使得既可以取得数组当前位置的索引(原文是type-译注),也可以根据返回值确定它的类型,可能为HASH_KEY_IS_STRING、 HASH_KEY_IS_LONG或HASH_KEY_NON_EXISTANT。由于zend_hash_get_current_data_ex()能够返回zval**,你可以确定它不会返回HASH_KEY_NON_EXISTANT,所以只需要检测IS_STRING和IS_LONG的可能性。
遍历HashTable还有其他方法。Zend引擎针对这个任务展露了三个非常相似的函数:zend_hash_apply()、zend_hash_apply_with_argument()和zend_hash_apply_with_arguments()。第一种形式仅仅遍历HashTable,第二种形式允许传入单个void*参数,第三种形式通过vararg列表允许数量不限的参数。hello_array_walk()展示了它们各自的行为:
static int php_hello_array_walk(zval **element TSRMLS_DC) { zval temp; temp = **element; zval_copy_ctor(&temp); convert_to_string(&temp); PHPWRITE(Z_STRVAL(temp), Z_STRLEN(temp)); php_printf("\n"); zval_dtor(&temp); return ZEND_HASH_APPLY_KEEP; } static int php_hello_array_walk_arg(zval **element, char *greeting TSRMLS_DC) { php_printf("%s", greeting); php_hello_array_walk(element TSRMLS_CC); return ZEND_HASH_APPLY_KEEP; } static int php_hello_array_walk_args(zval **element, int num_args, va_list args, zend_hash_key *hash_key) { char *prefix = va_arg(args, char*); char *suffix = va_arg(args, char*); TSRMLS_FETCH(); php_printf("%s", prefix); php_hello_array_walk(element TSRMLS_CC); php_printf("%s\n", suffix); return ZEND_HASH_APPLY_KEEP; } PHP_FUNCTION(hello_array_walk) { zval *zarray; int print_newline = 1; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "a", &zarray) == FAILURE) { RETURN_NULL(); } zend_hash_apply(Z_ARRVAL_P(zarray), (apply_func_t)php_hello_array_walk TSRMLS_CC); zend_hash_apply_with_argument(Z_ARRVAL_P(zarray), (apply_func_arg_t)php_hello_array_walk_arg, "Hello " TSRMLS_CC); zend_hash_apply_with_arguments(Z_ARRVAL_P(zarray), (apply_func_args_t)php_hello_array_walk_args, 2, "Hello ", "Welcome to my extension!"); RETURN_TRUE; }
上述代码大多明白易懂,你应该对相关函数的用法足够熟悉了。传入hello_array_walk()的数组被遍历了三次,一次不带参数,一次带单个参数,第三次带两个参数。这次的设计中,walk_arg()和walk_args()函数依赖于不带参的walk()函数处理转换和打印zval的工作,因为这项工作在三者中是通用的。
如同多数用到zend_hash_apply()的地方,在这段代码中,walk()(原文是“apply()”-译注)函数返回ZEND_HASH_APPLY_KEEP。这告诉zend_hash_apply()函数离开HashTable中的(当前)元素,继续处理下一个。这儿也可以返回其他值:ZEND_HASH_APPLY_REMOVE-如名所示,删除当前元素并继续应用到下一个;ZEND_HASH_APPLY_STOP-在当前元素中止数组的遍历并退出zend_hash_apply()函数。
其中不太熟悉的部件大概是TSRMLS_FETCH()。回想第一部分,TSRMLS_*宏是TSRM层的一部分,用于避免各线程的作用域被其他的侵入。因为zend_hash_apply()的多线程版本用了vararg列表,tsrm_ls标记没有传入walk()函数。为了在回调php_hello_array_walk()时找回并使用它,你的函数调用TSRMLS_FETCH()从资源池中找到正确的线程。(注意:该方法比直接传参慢很多,所以非必须不要使用。)
用foreach的形式遍历数组是常见的任务,但是常常需要通过数字索引或关联关键字查找数组中的特定值。下一个函数返回由第一个参数指定的数组的一个值,该值基于第二个参数指定的偏移量或关键字得到。
PHP_FUNCTION(hello_array_value) { zval *zarray, *zoffset, **zvalue; long index = 0; char *key = NULL; int key_len = 0; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "az", &zarray, &zoffset) == FAILURE) { RETURN_NULL(); } switch (Z_TYPE_P(zoffset)) { case IS_NULL: index = 0; break; case IS_DOUBLE: index = (long)Z_DVAL_P(zoffset); break; case IS_BOOL: case IS_LONG: case IS_RESOURCE: index = Z_LVAL_P(zoffset); break; case IS_STRING: key = Z_STRVAL_P(zoffset); key_len = Z_STRLEN_P(zoffset); break; case IS_ARRAY: key = "Array"; key_len = sizeof("Array") - 1; break; case IS_OBJECT: key = "Object"; key_len = sizeof("Object") - 1; break; default: key = "Unknown"; key_len = sizeof("Unknown") - 1; } if (key && zend_hash_find(Z_ARRVAL_P(zarray), key, key_len + 1, (void**)&zvalue) == FAILURE) { RETURN_NULL(); } else if (!key && zend_hash_index_find(Z_ARRVAL_P(zarray), index, (void**)&zvalue) == FAILURE) { RETURN_NULL(); } *return_value = **zvalue; zval_copy_ctor(return_value); }
该函数开始于switch块,它用和Zend引擎相同的方式处理类型转换。NULL视为0,Boolean据值视为0或1,double转化为long(也进行截断),resource转化为它的数字值。对 resource类型的处理是PHP 3的遗留,那时候资源确实只是在查找中用的数字,而不是特殊的类型(unto themselves)。
数组和对象只不过视为字符串字面量“Array”或“Object”,因没有什么转换具有实在的意义。最後插入缺省条件极小心地处理其他情形,以防PHP的未来版本可能引入其他数据类型而使该扩展产生编译问题。
如果函数查找的是关联关键字,那么key只会被设置为非NULL,所以可用它来确定查找是基于关联还是索引。如果因为关键字不存在使选定的查找失败了,函数因此返回NULL表明失败。否则找到的zval被复制到return_value。
如果以前用过$GLOBALS数组,你应该知道在PHP脚本的全局作用域声明和使用的每个变量也都存在于这个数组中。回想下,数组在内部是用HashTable表示的,想到个问题:“是否存在特别的地方可以找到GLOBALS数组?”答案是“存在”,就是EG(symbol_table)-Executor Globals结构,它的类型是HashTable(不是HashTable*,留心,只是HashTable)。
已经知道了如何查找数组中关联于关键字的元素,现在又知道了哪儿可以找到全局符号表,应该可以在扩展的代码中查找变量了:
PHP_FUNCTION(hello_get_global_var) { char *varname; int varname_len; zval **varvalue; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &varname, &varname_len) == FAILURE) { RETURN_NULL(); } if (zend_hash_find(&EG(symbol_table), varname, varname_len + 1, (void**)&varvalue) == FAILURE) { php_error_docref(NULL TSRMLS_CC, E_NOTICE, "Undefined variable: %s", varname); RETURN_NULL(); } *return_value = **varvalue; zval_copy_ctor(return_value); }
现在这些对你来说应该非常熟悉了。这个函数接受一个字符串参数,用它从全局作用域找到一个变量并且返回其副本。
这儿有个新内容php_error_docref()。你会发现该函数或是它的近亲遍布PHP源码树的各个角落。第一个参数是个可选的文档引用(缺省是用当前的函数)。其次是到处都出现的TSRMLS_CC,後面跟着关于错误的严重级别,最後是printf()样式的描述错误信息的格式字符串及相关的参数。让你的函数在失败情形下总是提供一些有意义的错误是很重要的。实际上,现在是个很好的机会,回头向hello_array_value()加入一条错误语句。本教程结尾的核对(代码)完整性一节也将包含它们(指错误语句-译注)。
除了全局符号表,Zend引擎也维持一个到局部符号表的引用。由于内部函数没有自己的符号表(为什么需要这个呢?),局部符号表实际上引用了调用当前内部函数的用户函数的局部作用域。看一个简单的函数,它设置了局部作用域的变量:
PHP_FUNCTION(hello_set_local_var) { zval *newvar; char *varname; int varname_len; zval *value; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sz", &varname, &varname_len, &value) == FAILURE) { RETURN_NULL(); } ALLOC_INIT_ZVAL(newvar); *newvar = *value; zval_copy_ctor(newvar); zend_hash_add(EG(active_symbol_table), varname, varname_len + 1, &newvar, sizeof(zval*), NULL); RETURN_TRUE; }
这儿绝没有什么新东西。继续前进,构建迄今得到的(代码),针对它运行一些测试脚本。确信得到了期望的结果,确实得到了。