首页 > 代码库 > 深入理解php内核 编写扩展 I:介绍PHP和Zend
深入理解php内核 编写扩展 I:介绍PHP和Zend
内容:
编写扩展I - PHP和Zend起步
原文:http://devzone.zend.com/public/view/tag/Extension
Part I: Introduction to PHP and Zend
http://devzone.zend.com/article/1021-Extension-Writing-Part-I-Introduction-to-PHP-and-Zend
编写扩展_II - 参数、数组和ZVALs
编写扩展_II - 参数、数组和ZVALs[继续]
原文:http://devzone.zend.com/article/1022-Extension-Writing-Part-II-Parameters-Arrays-and-ZVALs
Part II: Parameters,Arrays, and ZVALs
原文:http://devzone.zend.com/article/1023-Extension-Writing-Part-II-Parameters-Arrays-and-ZVALs-continued
Part II: Parameters,Arrays, and ZVALs [continued]
编写扩展_III - 资源
Part III: Resources
http://devzone.zend.com/article/1024-Extension-Writing-Part-III-Resources
编写扩展 I:介绍PHP和Zend
介绍
扩展是什么?
生存周期
内存分配
建立构建环境
Hello World
构建你的扩展
初始设置(INI)
全局数值
初始设置(INI)作为全局数值
核对(代码)完整性
下一步是什么?
1.1介绍
既然您正在阅读本教程,那么您或许对编写PHP语言的扩展感兴趣。如果不是...呃,或许你并不知道这一兴趣,那么我们结束的时候你就会发现它。
本教程假定您基本熟悉PHP语言及其解释器实现所用的语言:C.
让我们从指明为什么你想要编写PHP扩展开始。
限于PHP语言本身的抽象程度,它不能直接访问某些库或特定于操作系统的调用。
你想要通过某些不平常的方法定制PHP的行为。
你有一些现成的PHP代码,但是你知道它可以(运行)更快、(占用空间)更小,而且消耗更少的内存。
你有一些不错的代码出售,买家可以使用它,但重要的是不能看到源代码。
这些都是非常正当的理由,但是,在创建扩展之前,你需要首先明白zend和php以及扩展分别是什么?
1.2 Zend 和PHP
Zend 指的是语言引擎,PHP 指的是我们从外面看到的一套完整的系统。这听起来有点糊涂,但其实并不复杂(见图3-1 PHP 内部结构图)。为了实现一个 WEB 脚本的解释器,你需要完成以下三个部分的工作:
1、 解释器部分,负责对输入代码的分析、翻译和执行;
2、 功能性部分,负责具体实现语言的各种功能(比如它的函数等等);
3、 接口部分,负责同 WEB 服务器的会话等功能。
Zend包括了第一部分的全部和第二部分的局部,PHP 包括了第二部分的局部和第三部分的全部。他们合起来称之为 PHP 包。Zend 构成了语言的核心,同时也包含了一些最基本的 PHP 预定义函数的实现。PHP 则包含了所有创造出语言本身各种显著特性的模块。
1.3 扩展是什么?
如果你用过PHP,那么你肯定用到过扩展。除了少数例外,每个用户空间的函数都被组织在不同的扩展中。这些函数中的很多够成了standard扩展-总数超过400。PHP本身带有86个扩展(原文写就之时-译注),平均每个含有大约30个函数。数学操作方面大约有2500个函数。似乎这还不够, PECL仓库另外提供了超过100个扩展,而且互联网上可以找到更多。
“除了扩展中的函数,还有什么?”我听到了你的疑问。 “扩展的里面是什么?PHP的‘核心’是什么?”
PHP的核心由两部分组成:
1)Zend引擎:最底层是Zend引擎(ZE)。ZE把人类易读的脚本解析成机器可读的符号,然后在进程空间内执行这些符号。ZE也处理内存管理、变量作用域及调度程序调用。
2)PHP内核:另一部分是PHP内核,它绑定了SAPI层(Server Application Programming Interface,通常涉及主机环境,如Apache,IIS,CLI,CGI等),并处理与它的通信。它同时对safe_mode和open_basedir的检测提供一致的控制层,就像流层将fopen()、fread()和fwrite()等用户空间的函数与文件和网络I/O联系起来一样。
1.4 生存周期
当给定的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)函数,并最终关闭自己的内核子系统。
这个过程乍听起来很让人气馁,但是一旦你深入一个运转的扩展,你会逐渐开始了解它。
无论通过多进程(通常编译为apache的模块来处理单进程(CLI/CGI模式)还是PHP请求),多线程模式
SAPI运行PHP都经过下面几个阶段:
1、模块初始化阶段(Module init) :
即调用每个拓展源码中的的PHP_MINIT_FUNCTION中的方法初始化模块,进行一些模块所需变量的申请,内存分配等。
2、请求初始化阶段(Request init) :
即接受到客户端的请求后调用每个拓展的PHP_RINIT_FUNCTION中的方法,初始化PHP脚本的执行环境。
3、执行PHP脚本
4、请求结束(Request Shutdown) :
这时候调用每个拓展的PHP_RSHUTDOWN_FUNCTION方法清理请求现场,并且ZE开始回收变量和内存。
5、关闭模块(Module shutdown) :
Web服务器退出或者命令行脚本执行完毕退出会调用拓展源码中的PHP_MSHUTDOWN_FUNCTION 方法
详细的内容在后面的博文 深入理解php内核:php生命周期:http://blog.csdn.NET/hguisu/article/details/7377520
1.4 内存分配
资源管理仍然是一个极为关键的问题,尤其是对服务器软件而言。资源里最具宝贵的则非内存莫属了,内存管理也必须极端小心。内存管理在 Zend 中已经被部分抽象,而且你也应该坚持使用这些抽象,原因显而易见:由于得以抽象,Zend 就可以完全控制内存的分配。Zend 可以确定一块内存是否在使用,也可以自动释放未使用和失去引用的内存块,因此就可以避免内存泄漏。
为了避免内存泄漏,ZE使用附加的标志来执行自己内部的内存管理器以标识持久性。持久分配的内存意味着比单次请求更持久。对比之下,对于在请求期间的非持久分配,不论是否调用释放(内存)函数,都将在请求尾期被释放。例如,用户空间的变量被分配为非持久的,因为请求结束后它们就没用了。
然而,理论上,扩展可以依赖ZE在页面请求结束时自动释放非持久内存,但是不推荐这样做。因为分配的内存将在很长时间保持为未回收状态,与之相关联的资源可能得不到适当的关闭,并且吃饭不擦嘴是坏习惯。稍后你会发现,事实上确保所有分配的数据都被正确清理很容易。
让我们简单地比较传统的内存分配函数(只应当在外部库中使用)和PHP/ZE的持久的以及非持久的内存非配函数。
传统的 |
非持久的 |
持久的 |
malloc(count) |
emalloc(count) 用于替代malloc()。 |
pemalloc(count, 1)* |
strdup(str) |
estrdup(str) |
pestrdup(str, 1) |
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()执行附加检测以防整数溢出。
1.5建立构建环境
既然你已经了解了一些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并安装于你的开发服务器(或者工作站)中,你就可以把你的第一个扩展加入其中了。
1.5 Hello World
什么程序设计的介绍可以完全忽略必需的Hello World程序?此例中,你将制作的扩展导出一个简单函数,它返回一个含有“Hello World”的字符串。用PHP的话你或许这样做:
- <?php
- function hello_world() {
- return ‘Hello World‘;
- }
现在你将把它转入PHP扩展。首先,我们在你的PHP源码树的目录ext/中创建一个名为hello的目录,并且chdir进入该目录。事实上,这个目录可以置于PHP源码树之中或之外的任何地方,但是我希望你把它放在这儿,以例示一个在以后的文章中出现的与此无关的概念。你需要在这儿创建3个文件:包含hello_world函数的源码文件,包含引用的头文件,PHP用它们加载你的扩展,以及phpize用来准备编译你的扩展的配置文件。
其实这个三个文件都使用php自带的工具ext_skel生成。ext_skel工具在源码包目录下。然后使用phpize建立php扩展
步骤:
生成模块名为hello_module : $ ./ext_skel --extname=hello_module
执行phpize:$
/usr/local/php/bin/phpize
Confugre:
:$ ./configure --enable-hello_module
--with-apxs=/usr/local/apache/bin/apxs --with-php- config=/usr/local/php/bin/php-config
编译 :$ make
phpize用来准备编译你的扩展的配置文件:config.m4
- 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
包含引用的头文件:php_hello.h
- #ifndef PHP_HELLO_H
- #define PHP_HELLO_H 1
- #definePHP_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
包含hello_world函数的源码文件 :hello.c
- #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
- hello_module,
- 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("HelloWorld", 1);
- }
在上面的示例扩展中,你所看到的代码大多是黏合剂,作为将扩展引入PHP的协议语言并且在其间建立会话用于通信。只有最后四行才是你所认为“实际做事的代码”,它们负责与用户空间的脚本交互这一层次。这些代码看起来确实非常像之前看到的PHP代码,而且一看就懂:
声明一个名为hello_world的函数
让该函数返回字符串:“Hello World” , 那个1是怎么回事儿?
回忆一下,ZE包含一个复杂的内存管理层,它可以确保分配的资源在脚本退出时被释放。然而,在内存管理领域,两次释放同一块内存是绝对禁止的(big no-no)。这种被称为二次释放(double freeing)的做法,是引起段错误的一个常见因素,原因是它使调用程序试图访问不再拥有的内存。类似地,你不应该让ZE去释放一个静态字符串缓冲区(如我们的示例扩展中的“Hello World”),因为它存在于程序空间,而不是被任何进程(process)拥有的数据块。RETURN_STRING()可以假定传入其中的任何字符串都需要被复制以便稍后可被安全地释放;但是由于内部的函数给字符串动态地分配内存、填充并返回并不罕见,第二参数RETURN_STRING()允许我们指定是否需要拷贝字符串的副本。要进一步说明这个概念,下面的代码片段与上面的对应版本等效:
- PHP_FUNCTION(hello_world)
- {
- char *str;
- str = estrdup("HelloWorld");
- RETURN_STRING(str, 0);
- }
在这个版本中,你手工为最终将被传回调用脚本的字符串“Hello World”分配内存,然后把这快内存“给予”RETURN_STRING(),用第二参数0指出它不需要制作自己的副本,可以拥有我们的。
1.6构建你的扩展
本练习的最后一步是将你的扩展构建为可动态加载的模块。如果你已经正确地拷贝了上面的代码,只需要在ext/hello/中运行3个命令:
$ phpize
$ ./configure--enable-hello_module
$ 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(),
布尔值值用:RETURN_BOOL(),
NULL 值用:RETURN_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返回)。当我们深入地讲解变量时,会在第二部分看到它们。
1.7初始设置(INI)
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的头文件:
- #ifdef HAVE_CONFIG_H
- #include"config.h"
- #endif
- #include "php.h"
- #include"php_ini.h"
- #include"php_hello.h"
最后,你可修改函数hello_world让它使用INI的值:
PHP_FUNCTION(hello_world)
{
RETURN_STRING(INI_STR("hello.greeting"),1);
}
注意,你将要拷贝从INI_STR()返回的值。这是因为,在进入PHP变量堆栈之前(as far as the PHP variable stack is concerned),它都是个静态字符串。实际上,如果你试图修改这个返回的字符串,PHP执行环境会变得不稳定,甚至崩溃。
本节中的第一处改变引入了两个你非常熟悉的函数:MINIT和MSHUTDOWN。正如稍早提到的,这些方法在SAPI初始启动和最终关闭期间被各自调用。它们不会在请求期间和请求之间被调用。本例中它们用来将你的扩展中定义的条目向php.ini注册。本系列后面的教程中,你也将看到如何使用MINIT和MSHUTDOWN函数注册资源、对象和流处理器。
函数hello_world()中使用INI_STR()取得hello.greeting条目的当前字符串值。也存在其他类似函数用于取得其他类型的值,长整型、双精度浮点型和布尔型,如下面表格中所示;同时也提供另外的ORIG版本,它们提供在php.ini文件中的INI(的原始)设定(在被 .htaccess或ini_set()指令改变之前)(原文:provides the valueof 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()-触发一个方法回调。这使得当设定改变时,扩展可以执行更精确的控制,或是根据新的设定触发一个相关的行为。
1.8全局数值
扩展经常需要在一个特定的请求中由始至终跟踪一个值,而且要把它与可能同时发生的其他请求分开。在非多线程的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_entryhello_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 voidphp_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将导致在非多线程的平台产生段错误。
1.9初始设置(INI)作为全局数值
回想一下,一个用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去改变哪个全局变量,我们的扩展的全局(作用域)的结构是什么样子,以及持有这些变量的全局作用域的名字是什么。
1.10核对(代码)完整性
迄今为止,我们的三个文件应该类似于下面的列表。(为了可读性,一些项目被移动和重新组织了。)
config.m4
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
php_hello.h
#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
#definePHP_HELLO_WORLD_VERSION "1.0"
#definePHP_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_entryhello_module_entry;
#define phpext_hello_ptr&hello_module_entry
#endif
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)
static function_entryhello_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_entryhello_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 voidphp_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("HelloWorld", 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();
}
下一步是什么?
本教程探究了一个简单PHP扩展的结构,包括导出函数、返回值、声明初始设置(INI)以及在(客户端)请求期间跟踪其内部状态。
下一部分,我们将探究PHP变量的内部结构,以及在脚本环境中如何存储、跟踪和维护它们。在函数被调用时,我们将使用zend_parse_parameters接收来自于程序的参数,以及探究如何返回更加复杂的结果,包括数组、对象和本教程提到的资源等类型。
深入理解php内核 编写扩展 I:介绍PHP和Zend