1、编写PHP扩展三步曲编写PHP扩展三步曲简介如果你正在阅读这份指南,你大概已经有兴趣为php编写扩展了。如果没有兴趣.,唔,或许当你看完本文,你会发现这是一件你未曾尝试过的非常有趣的事情。这份指南假设你熟悉PHP语言和php解释器的编写语言:C。让我们来说说为什么要编写PHP扩展:1、由于抽象程度的问题,一些库或系统调用不能被PHP直接调用。2、在一些不常见的情况,你需要控制PHP的行为。3、你已经写好一些PHP代码,但你想让它运行得更快,体积更小,占有更少的内存。4、你打算销售一些独特的、聪明的代码,非常重要的是:它能够执行,但不能查看源代码。这些都是很常见的原因,但是为了创建扩展,你首先需
2、要理解什么是扩展。什么是扩展如果你使用过PHP,那你就已经使用过扩展。除了很少例外,PHP里的每个用户空间(userspace)函数,都来自一个或多 个扩展。这些函数中的很大部分属于“标准扩展”共有超过400个这样的函数。PHP源码包中有86个扩展,每个扩展平均有大约30个函数。照这样计 算,大约有2500个函数。如果你觉得这些还不够,在PECL库中,还提供了超过100个其他扩展,而且在互联网上还能找到更多。“所有的函数都来自扩展,那还剩下什么?”。我听见你这样问。“扩展到底是什么?PHP的核心又是什么呢?”PHP 的核心由两大部分组成。在低层次是the Zend引擎(ZE)。ZE的任务是:将
3、人们可读的脚本转化成机器可读的符号,并在进程空间中执行这些符号。ZE还处理内存管理、变量作用域、分发函数调 用。另外一大部分是PHP。PHP处理通讯,和跟SAPI层(服务器端应用编程接口,通常指以下主机环境Apache,IIS,CLI,CGI等)的 绑定。它也为safe_mode和open_basedir检查提供一个统一的控制层,比如,通过fopen、fread、fwrite进行文件和网络操 作的字节流层。生命周期当SAPI启动时,例如响应操作 /usr/local/apache/bin/apachectl start,PHP开始初始化它的核心子系统。在启动过程的后期,它装载每个扩展,并调用扩
4、展的模块初始化函数(MINIT)。这就给了每个扩展初始化内 部变量、分配资源、注册资源处理器、向ZE注册函数的机会,这样,如果脚本调用这些函数,ZE就知道怎么去执行。接下来,PHP等待SAPI层的 页面处理请求。在用作CGI或CLI时,这会立即发生并只执行一次。当运行在Apache、IIS或其他完整的web服务器SAPI下时,它在用户请求页 面时触发,重复执行,并有可能并发执行。不用关心请求是怎样进来的,PHP向ZE请求设置一个脚本运行的环境,然后调用每个扩展的请求初始化函数 (RINIT)。RINIT给扩展一个设置特定环境变量、分配特定资源、执行其他任务例如审核的机会。一个RINIT的简单例
5、子是sessions扩展, 如果session.auto_start选项被打开,RINIT会自动触发用户空间函数session_start(),构造$_SESSION变量。一旦请求被初始化了,ZE将PHP脚本转换成符号,进而转换成可执行的操作码去执行。一旦操作码调用一个扩展函数,ZE就将参数打包传递给扩展函数,临时交出控制权,直至执行完成。当脚本执行完毕,PHP调用每个扩展的请求结束函数(RSHUTDOWN),以执行一些清除任务(例如将session变量保存到磁盘上)。接下来,ZE运行一个清除进程(垃圾收集),高效地对前一个请求中用到的每个变量执行unset()操作。一 旦完成,PHP等待SA
6、PI请求另一个文档或发送一个shutdown信号。在CGI和CLI的SAPI中,没有下一次请求,因此SAPI立即执行 shutdown。在shutdown时,PHP会调用扩展的模块Shutdown函数(MSHUTDOWN),最终关闭核心子系统。整个过程在刚开始时听起来让人退却,但是一旦你深入一个正常运行的扩展中,这些过程就会变得显而易见。内存分配在编写扩展时,为了避免内存泄漏,ZE通过一个表明持久的附加标志提供了自身内部的内存管理。“持久分配”的内存可以在单个页面请 求结束后继续存在。与此相对的“非持久分配”,内存被分配后,在请求结束时,无论是否调用释放函数都将被释放。例如,用户空间的变量,都
7、应该使用非持久分 配,因为在请求结束后,它们就没有意义了。在理论上,扩展可以依靠ZE在每个页面请求结束时自动释放非持久分配的内存,但这样做是不推荐的。内存 分配将保持未回收状态比较长一段时间,内存关联的资源很少能正确释放,弄得一团糟却不清除它,这是不好的习惯。你逐渐会发现,实际上很容易保证所有分配得 数据都被正确释放。让我们简要比较一下传统的内存分配函数和PHP/ZE的持久、非持久内存分配:传统malloc(count) calloc(count, num)strdup(str) strndup(str, len)free(ptr)realloc(ptr, newsize)malloc(cou
8、nt *num + extr)非持久emalloc(count) ecalloc(count, num)estrdup(str) estrndup(str, len)efree(ptr)erealloc(ptr, newsize)safe_emalloc(count, num, extr)持久pemalloc(count, 1) pecalloc(count, num, 1)pestrdup(str, 1) pemalloc() & memcpy()pefree(ptr, 1)perealloc(ptr, newsize, 1)safe_pemalloc(count, num, extr)配置
9、编辑环境现在你已经接触到了一些隐藏在PHP和Zend引擎下的理论,我打赌你已经想要开始构建它们了。然而,在你能构建之前,你需要收集一些必要的工具,并配置环境。首先,你需要PHP,设置构建工具需要PHP。如果你对build PHP源代码还不熟悉,你建议你看看 (为Windows开发PHP扩展将在后面的文档中讲述)。跟使用PHP的二进制分发包相比,源代码版本有两点非常重要。/configure选项在开发 过程中非常顺手。首先是-enable-debug。这个选项将使用附加的符号信息编译PHP,这样,如果一个错误发生,你能收集到core dump,这样就可以使用gdb来进行跟踪分析错误为什么会发生。
10、另一个选项根据版本的不同有不同的名称。在PHP 4.3中,名为-enabled-experimental-zts,在PHP5和后续版本中,叫做-enable-maintainer-zts。 这个选项可以让PHP知道它运行在多线程的环境中,这样你能捕获到在单线程的环境中没有问题的编程错误。这样你的扩展就能在多线程的环境中安全使用。一旦 你用这些特定的选项编译完PHP并安装好它,你就能开始你的第一个扩展了。Hello World没有必不可少的“Hello World”应用,哪一个入门教程是完整的?在这个实例中,你将编写一个扩展,导出一个返回“Hello World”字符串的函数。在PHP代码中,你
11、可能会这样写:现在你将它转换成一个扩展。首先,让我们在PHP源代码目录树下的ext目录中创建一个名为hello的目录,并进入这个目录。事实上,这个目录也可以在PHP目录树外存在,但是我希望你把它放在这里,在后面的文档中会介绍一些不相关的概念。你需要创建3个文件:一个源文件,包含hello_world函 数;一个头文件,包含PHP装载这个扩展的引用;一个配置文件,它会被phpize用来准备扩展的编译环境。config.m4PHP_ARG_ENABLE(hello, whether to enable Hello World support, -enable-hello Enable Hello
12、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)fiphp_hello.h#ifndef PHP_HELLO_H#define PHP_HELLO_H 1#define PHP_HELLO_WORLD_VERSION 1.0#define PHP_HELLO_WORLD_EXTNAME helloPHP_FUNCTION(hello_world);ext
13、ern zend_module_entry hello_module_entry;#define phpext_hello_ptr &hello_module_entry#endifhello.c#ifdef HAVE_CONFIG_H#include config.h#endif#include php.h#include php_hello.hstatic function_entry hello_functions = PHP_FE(hello_world, NULL) NULL, NULL, NULL;zend_module_entry hello_module_entry = #if
14、 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_HELLOZEND_GET_MODULE(hello)#endifPHP_FUNCTION(hello_world)
15、 RETURN_STRING(Hello World, 1);你在上面的扩展示例中看到的大多数代码都是胶水代码将扩展声明给PHP,并建立他们之间的通信环境。只有最后4行代码很象之前我们写的PHP代码:1、声明函数名为hello_world2、函数将字符串“Hello World”作为返回值3、.嗯?关于1,那到底是什么?回忆一下,ZE包含了一个内存管理层,以保证分配的资源在脚本退出的时候被释放。在内存管理中,一个大问题是两次释放同一个内存块。这种行为叫做 double freeing,就象访问不再属于自己的内存,这是一个引起错误的常见原因。同样的,你不想ZE释放一个静态的字符串缓冲(例如我们扩
16、展示例里的 Hello World),因为它存在在代码空间,而并不是属于任务进程的数据块。RETURN_STRING()能假定任何字符串都需要拷贝传递,这样就能被安全释放;但是因为在函数内部为字符串分配内存,动态填充,然后返回它,这并不罕见,RETURN_STRING()允许我们指定是否有必要生成一个字符串的拷贝。为了演示这种概念,下面的代码片断等同于在刚才你看到的代码:PHP_FUNCTINO(hello_world) char *str; str = estrdup(Hello World); RETURN_STRING(str, 0);在这个版本中,你为字符串“Hello World”手
17、动分配了内存,最终会被传回调用脚本,然后把内存交给RETURN_STRING。使用第二个参数值0表明它不需要创建自己的拷贝。构建你的扩展练习的最后一步将把扩展构建成动态加载的模块。如果你正确拷贝了上面的代码,只需要在ext/hello/目录下执行3个命令:phpize./configure -enable-hellomake在运行了这些命令后,ext/hello/modules/目录下会生成hello.so文件。现在你就可以象其他扩展一样,把它拷贝到你的扩展目录(缺省为/usr/local/lib/php/extensions,检查php.ini进行确认),并在php.ini中增加一行exte
18、nsion=hello.so,以在PHP启动的时候载入它。对于CGI或CLI的SAPI,这意味着下次运行PHP的时候会生效;对于Web服务器SAPI,例如 Apache,在下次Web服务器重启的时候生效。现在让我们通过命令行来试试:php -r echo hello_world();如果所有的步骤都正确,你应该能看到脚本输出的Hello World了,因为扩展中的hello_world函数返回了这个字符串,echo命令就将它显示出来。返回其它类型的值也类似这种形式,使用RETURN_LONG()返回整型,RETURN_DOUBLE()返回浮点型,RETURN_BOOL()返回布 尔型,RETU
19、RN_NULL()返回空值。让我们来看看hello.c中在function_entry结构加入的PHP_PE行,以及在文件尾部加入 的PHP_FUNCTION()行。static function_entry hello_functions = PHP_PE(hello_world, NULL) PHP_PE(hello_long, NULL) PHP_PE(hello_double, NULL) PHP_PE(hello_bool, NULL) PHP_PE(hello_null, NULL) NULL, NULL, NULL;PHP_FUNCTION(hello_long)PHP_FUNC
20、TION(hello_double)PHP_FUNCTION(hell_bool)PHP_FUNCTION(hello_null)你也需要在头文件php_hello.h加入函数的原型声明,这样才能正确编译: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是安全的。然而,为了得到正确
21、的 build,我建议你还是执行整个三个步骤。另外,你应该调用make clean all而不是简单的make,来保证所有的源文件都被重新build。一旦模块被构建出来,你要再次把它拷贝到你的扩展目录,替换掉老版本。到这一步,你可以再次调用PHP解释器,用简单的脚本测试你新增的函数。实际上,你为什么不现在就这么做呢?我等着你.完成了?非常好。如果你曾用var_dump代替echo查看每个函数的输出,你可能已经注意到hello_bool()返回true。那就是传递给 RETURN_BOOL()的值1得到的。就像在PHP脚本中,整数0等同FALSE,其它整数等同TRUE。扩展的编写者按照惯例通常使
22、用1来返回 TRUE,我也建议你这么做,只要不拘泥与此就行了。为了获得更好的可读性,可以使用RETURN_TRUE和RETURN_FALSE宏。我们使用 RETURN_TRUE重新实现hello_bool():PHP_FUNCTION(hello_bool)注意,使用RETURN_TRUE和RETURN_FALSE的时候,没有圆括号,跟其它的RETURN_*()宏不一样。因此,注意不要犯这样的错误!你可能已经注意到,上面的每个代码我们都没有传递0或1来指出值是否需要拷贝。这是因为对于这些简单的标量来说,没有附加的内存需要分配和释放。还有3个附加的返回类型:RESOURCE(用来返回mysql_
23、connect(), fsockopen(), ftp_connect()),ARRAY(返回HASH),OBJECT(通过关键字new来返回)。当我们深入了解这些变量时,我们将在第二章中进行 讨论。INI设置Zend引擎提供了两种管理INI值的方式。现在我们先看看简单一些的方式,完整阐释它;对于更复杂一些的方式,在我们有机会获取全局变量时再讨论它。假定你想为你的扩展在php.ini中定义一个值hello.greeting,这样在你的hello_world()函数中可以获取到这个值用来输出。你 需要在hello.c和php_hello.h中对hello_module_entry结构做出一些改动
24、。在php_hello.h靠近函数原型声明的地方加入新的原型:PHP_MINIT_FUNCTION(hell);PHP_MSHUTDONW_FUNCTION(hello);PHP_FUNCTION(hello_world);.现在,用下面的代码替换hello.c中的hello_module_entryzend_module_entry hello_module_entry = #if ZEND_MODULE_API_NO = 20010901 STANDARD_MODULE_HEADER,#endif PHP_HELLO_WORLD_EXTNAME, hello_functions, PHP_
25、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;P
26、HP_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.gree
27、ting), 1);注意,你拷贝了INI_STR()的返回值。这是因为它一个静态变量。事实上,如果你尝试修改这个返回值,PHP扩展环境将变得不稳定,甚至崩溃。在本节里,第一个变化是引入了两个方法MINIT和MSHUTDOWN,你将对他们非常熟悉。正如以前提及的,它们会由SAPI在启动初始化和关闭时分别调用。它们不会在请求间和请求中被调用。在这个例子中,你用它来注册在扩展中定义的php.ini入口(entries),在后续的讨论中,你将看到如何使 用MINIT和MSHUTDOWN函数注册resource,object和stream handlers。在你的hello_world()函数中,你使用
28、INI_STR()获取hello.greeting的当前字符串值。获取long、double、boolean型值的函数,连同获取默认值的函数,如下表所示。当前值:INI_STR(name) INI_INT(name) INI_FLT(name) INI_BOOL(name)默认值:INI_ORIG_STR(name) INI_ORIG_INT(name) INI_ORIG_FLT(name) INI_ORIG_BOOL(name)类 型:char *(NULL terminated) signed long signed double zend_bool传 递给PHP_INI_ENTRY()的
29、第一个参数是在php.ini中定义的entry的名称字符串。为了避免命名冲突,你应使用与函数相同的习惯:对所 有值加上以扩展名命名的前缀,例如hello.greeting。作为一个习惯,使用句点将后面的ini设置名称的描述部分分隔开。第二个参数是初始值,无论它是不是一个数字型值,都传递一个char *的字符串。这样做主要的原因是ini文件本身是文本化的。你可以使用INI_INT(),INI_FLT(),INI_BOOL()在自己的脚本里进行类型转换。第 三个参数是访问模式修饰符。这是一个掩码字段,用来决定这个INI值何时、何处可以被修改。例如register_globals,并不能简单的在脚本
30、中 使用ini_set()来进行修改,因为这些设置只在请求startup时脚本运行之前被使用。其它的设置,例如allow_url_fopen,在 共享的主机环境中,即使是通过ini_set()或是通过.htaccess,你也并不想让用户随意改变它。这个参数的典型值是PHP_INI_ALL, 表明它的值可以在任何地方修改。PHP_INI_SYSTEM|PHP_INI_PERDIR表示可以在php.ini文件中修改,也可以通过.htaccess文件修改,但不能通过ini_set()修改。PHP_INI_SYSTEM表示只能在php.ini中进行修改。第四个参数允许在ini设置改变时(例如ini_s
31、et()),触发一个回调函数,我们现在略过不讲。当设置发生变化时,这允许扩展执行更精确的控制,或者根据新值触发相关动作。全局数值扩展经常需要跟踪一个值,保持该值与其它的请求无关,即使是这些请求是发生在同一时刻。在非多线程的SAPI中可能很简单:只需要 声明一个全局变量,在需要是访问它就行了。可能存在的问题是,PHP被设计成可以运行在多线程的Web服务器中(如Apache2和IIS),这需要保持 全局数值的线程安全。PHP采用TSRM(Thread Safe Resource Management)抽象层进行了极大地简化,有时也被称为ZTS(Zend Thread Safety)。实际上,你已经使用过TSRM,只是不知道而已。(不要艰难地试图找出它,很快你就会发现,它隐藏在每个地方。)创建一个线程安全地全局变量的第一步是,用global声明一个变量。在下面的例子中,声明了一个long型的全局变量。每次
copyright@ 2008-2022 冰豆网网站版权所有
经营许可证编号:鄂ICP备2022015515号-1