Keep smile !
分类: C/C++
2013-07-15 14:32:50
如果你用过PHP,那么你已经使用过扩展了。除了少数的几个例外,每一个用户空间的PHP函数都被归类到一个个的扩展中。这其中很大一部分(总共超过400个)的函数是“标准扩展”的一部分。PHP的源码包附带了大约86个扩展,每一个扩展包含平均30个左右的函数。不难算出,这足有2500个函数。如果这还不够的话,PECL仓库中还提供了超过100个扩展,在因特网上还散布着更多。
“所有的函数都包含在了扩展中,那么剩下的还有什么?”你肯定会这么问,”扩展到底用来扩展什么?PHP的核心又是什么?“
PHP的核心由两部分组成。处在最底层的是Zend引擎(ZE)。ZE负责解析人类可读的PHP脚本,生成机器可读的符号,然后在一个进程空间里执行这些符号。ZE还负责内存管理、变量的作用域和执行函数调用。核心的另外一部分是PHP层。PHP绑定到SAPI(Server Application Porgramming Interface, 通常用来指代宿主环境—Apache, IIS, CLI, CGI等)层上,并且负责与它之间的通信。另外,PHP层还为 safe_mode 和 open_basedir 检查提供了一致的控制层,另外还有通过中类似 fopen(), fread() 和 fwrite() 等用户空间函数关联到文件和网络I/O上的流接口。
当一个SAPI启动的时候,例如/usr/local/apache/bin/apachectl start,作为响应,PHP开始初始化它的核心子系统。在初始化的末尾,PHP载入每个扩展的代码并调用它们的模块初始化过程(MINIT)。这使得每个扩展得以初始化内部变量,申请资源、注册资源句柄并且向ZE注册它提供的函数,因此当脚本调用这些函数的时候,ZE知道需要执行哪一部分代码。
接下来,PHP等待SAPI的页面处理请求。在SAPI是CGI或者CLI的情况下,这个过程会立即发生并且只发生一次。当SAPI是Apache, IIS 或者其他功能完整的WEB服务器的时候,这个过程在每次用户请求页面的时候发生而且可以发生任意多次(可能是并发的)。无论请求如何发生,PHP通过ZE来设置好脚本执行的环境,然后调用每个扩展的请求初始化过程(RINIT)。RINIT使得扩展有机会设置特定的环境变量、申请面向请求的资源或者执行其他的任务,例如审计。一个典型的RINIT函数的例子就是会话扩展(Session),如果
session.auto_start 选项开启的话,RINIT会自动触发用户空间的 session_start() 函数并且预先设置好$_SESSION 变量。
一旦请求初始化完成,ZE接管控制权并把PHP脚本翻译为符号,并且最终转换为可以单步执行的操作码(OPCode)。当某条OPCode需要调用扩展中的函数时,ZE准备好函数的参数并且临时把控制权转交给该函数直到该函数执行完成。
当脚本执行结束,PHP调用每个扩展的请求结束函数(RSHUTDOWN)来执行一些最后的清理工作(例如把Session变量保存到磁盘)。接下来,ZE执行清理过程(被称为垃圾回收),这个过程相当于对请求中的每个变量执行 unset()。
一旦清理过程结束,PHP再次等待SAPI的下一个请求或者是结束信号。在CGI和CLI的情况下,已经没有”下一次“请求发生了,所以SAPI在处理完请求后立即结束。在结束过程中,PHP再次遍历每个扩展并调用它们的模块结束处理函数(MSHUTDOWN),最终结束它自己的核心子系统。
这个过程在开始时也许看起来很乏味,但随着你对扩展研究的深入,你为发现它逐渐变得有意义。
为了避免那些编写糟糕的扩展泄漏内存,ZE有一套内部的内存管理方案,每个内存申请函数使用一个额外的标记来指示内存是否是永久申请。永久申请表示申请的内存在请求结束后仍然有效。相反的,一块非永久申请的内存,无论是否主动调用释放函数,都会在请求结束时被释放。用户空间的变量就是非永久性内存申请的好例子,因为它们在请求结束后就毫无用处了。
理论上,扩展可以依赖ZE在每个页面请求结束的时候自动释放非永久性申请的内存,不过这不是推荐的做法。这样的话,申请的内存会持续更久的时间,与之相关的资源也不能及时的释放,况且只污染不治理也不是什么好习惯。在后面你会发现,保证所有申请的数据被合理的清理是很简单的事情。
让我们拿传统的内存申请函数(那些只能在外部库中使用的)与PHP/ZE内置的(永久和非永久)内存申请函数做下比较:
Traditional | Non-Persistent | Persistent |
---|---|---|
malloc(count) calloc(count, num) |
emalloc(count) ecalloc(count, num) |
pemalloc(count, 1)* pecalloc(count, num, 1) |
strdup(str) strndup(str, len) |
estrdup(str) estrndup(str, len) |
pestrdup(str, 1) 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) |
* The pemalloc() family include a ‘persistent’ flag which allows them to behave like their non-persistent counterparts. For example: emalloc(1234) is the same as pemalloc(1234, 0) ** safe_emalloc() and (in PHP 5) safe_pemalloc() perform an additional check to avoid integer overflows |
现在,你已经了解了一些PHP和Zend引擎工作背后的理论,我真心的希望你能够深入下去并且开发一些东西。在那之前,你需要安装一些必须的开发工具并设置一个适合你的开发环境。
首先,你需要PHP本身以及用来编译PHP的编译工具。如果你对编译PHP不是很了解的话,我推荐你读一下。(Windows下的开发环境将在后面的文章中介绍)。虽然那些用来发行的二进制包很方便,但它们往往故意去掉了两个在开发过程中很顺手的 ./configure 选项。其中一个是 –enable-debug,开启这个选项会在编译的PHP中加入额外的信息用来在程序出错的时候使你利用gdb更快的找到出错原因。另外一个选项依赖你的PHP版本,在PHP4.3中这个选项是 –enable-experimental-zts,在PHP5及以后的版本中是 –enable-maintaner-zts。这个选项会使PHP认为自己运行在多线程的环境中,这样你便可以发现那些在非多线程环境下毫无问题而在多线程环境下使你的扩展不稳定的编程错误。当你使用这些额外的选项把PHP编译并安装到你的服务器(或工作站)的时候,你就可以开始开发第一个扩展了。
介绍编程不涉及那个必须的Hello World程序肯定是不完整的。在下面,你将会完成一个仅导出一个函数的PHP扩展,这个函数返回一个包含”Hello World”的字符串。用PHP的话来说,它做如下的事情:
好了,下面我们把它变成一个PHP扩展。首先,在PHP的源码树中的 ext/ 目录下面创建一个名为 hello 的文件夹,然后进入到刚刚创建的目录。事实上,这个目录在不在PHP的源码树中无关紧要,我把它放在这里是为了演示在后面的文章中的一个概念。在这个目录里,需要创建三个文件:一个包含 hello_world 函数源代码的文件,一个头文件,其中包含着那些被PHP用来加载扩展的引用,还有一个配置文件,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 #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
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 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代码很类似,它的含义一目了然:
1. 声明一个名为 hello_world 的函数
2. 这个函数返回一个 “Hello World” 字符串
3. 呃。。。1? 这个 1 是个啥?
你一定还记得ZE内置一个精巧的内存管理层用来确保在脚本退出的时候那些申请的资源能够正确的释放。在内存管理的世界里,同一块内存被释放两次是个大忌。这叫做“重复释放”,它导致程序访问一块它不再拥有的内存区域,是造成段错误的常见原因。同样的,你也不希望ZE释放一块在程序空间而不是被某个进程拥有的数据块中的静态字符缓冲(例如我们例子中的 “Hello World”)。RETURN_STRING() 可以复制所有传给它的字符串便于后面安全的释放,但是很多内部函数通常会自己申请一块内存,动态的填充并返回它。因此 RETURN_STRING()允许我们指定是否需要将传入的字符串复制一份。为了更好的演示这个过程,可以参考下面的代码,它与上面相应的代码完成同样的功能:
PHP_FUNCTION(hello_world) { char *str; str = estrdup("Hello World"); RETURN_STRING(str, 0); }
在这段代码中,我们手工的为 “Hello World” 字符串申请了一段内存,然后把它传给 RETURN_STRING(),第二个参数中的 0 指示 RETURN_STRING() 不需要再做一个私有的拷贝,它可以使用我们提供的。
最后一个步骤就是把你的扩展编译为一个可动态载入的模块。如果你已经正确的完成了上面的代码,那么只需要在 ext/hello 目录中执行三个命令就可以了:
$ phpize $ ./configure --enable-hello $ make
执行完这些命令之后,你会在 ext/hello/modules/ 目录下面得到一个 hello.so 文件。把它复制到你的扩展目录(默认是 /usr/local/lib/php/extensions/,你可以通过检查 php.ini 来得到它),然后在 php.ini 中添加 extension=hello.so 指令使它在启动的时候被加载,对于 CGI/CLI 来说,这意味着PHP的下次执行;对于那些是 WEB 服务器的 SAPI,这意味这服务器的下次重启。让我们在命令行中先试验一把:
$ php -r 'echo hello_world();'
如果不出意外的话,你会看到这段脚本输出一个 “Hello World” 字符串,扩展中的 hello_world 函数返回一个字符串,然后 echo 命令将它输出出来。
其他的标量也可以通过类似的方式返回,RETRUN_LONG() 返回一个整型值,RETURN_DOUBLE() 返回浮点值,RETURN_BOOL() 返回布尔值,RETURN_NULL() (毫无意外地)返回 NULL 值。我们将在 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,来确保所有的源文件的被重新编译。再次说明,对于目前的改动来说这并不是必须的,但这么做会更保险而且不至于混乱。当模块编译完成,你需要将它再次复制到扩展目录中替换掉旧的版本。
好了,现在你可以再次调用PHP解释器,编写一些简单的脚本来测试我们刚才加入的函数。事实上,你可以马上去测试它,我将在这里等待你回来 :-)
完成了?很好!如果你用 var_dump 代替 echo 来观察这些函数的返回值,你可能发现 hello_bool() 函数的返回值是 true。这就是 1 在 RETURN_BOOL() 中表示的值。就像在 PHP 脚本中类似,一个值为0的整型表示 FALSE,其他的整型值表示 TRUE。扩展的作者们通常用 1 代表 TRUE,这是个值得你遵守的惯例。为了增加可读性,你还可以使用 RETURN_TRUE 和 RETURN_FALSE 这两个宏,下面是重写后的 hello_bool() 函数,这次我们使用 RETURN_TRUE:
PHP_FUNCTION(hello_bool) { RETURN_TRUE; }
要注意,这两个宏没有结尾的括号,它们通过这种形式来区别于其他的 RETURN_*() 宏,所以不要用错它们。
你可能已经注意到,在上面的例子中我们没有通过传递 0 或 1 来指定是否需要复制传入的值,这是因为对于这种简单的标量值不需要额外申请或者释放内存(除了用来保存变量的容器本身——我们将在第二部分涉及这些内容)。
除了上面的那些,还有另外的三种返回值类型:资源(例如 mysql_connect(), fosockopen(),ftp_connect()等函数的返回值),数组(也叫做 HASH 表)以及对象(例如 new 关键字的返回值)。在第二部分我们深入了解变量的时候,会涉及这些内容。
Zend引擎提供了两种管理 INI 值的方式,现在我们将了解比较简单的方式,在后面的部分,当你有机会操作全局变量的时候,我们将涉及另一种更深入更复杂的方式。
我们假设你需要定义一个 php.ini 变量,用来存放在 hello_world() 函数中 say hello 的对象。你需要在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 引入对 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); }
我们注意到你通知 RETURN_STRING() 复制 INI_STR() 的返回值,因为它涉及到PHP的变量栈,是一个静态的字符串。事实上,如果你试图修改了这个返回值,PHP的运行时环境会变得不稳定甚至崩溃。
第一部分的修改中,引入了两个你可能要非常熟悉的函数:MINIT 和 MSHUTDOWN。就像我们上面所说的,这两个函数分别在 SAPI 层启动和终止时调用。在每次请求到来的时候,它们不会再被调用。在上面的例子中,通过它们来注册我们扩展中定义的 php.ini 条目。在后面的文章中,你还会学到如何通过 MINIT 和 MSHUTDOWN 来注册资源、对象以及流处理器(Stream Handler)。
在 hello_world() 函数中,我们使用 INI_STR() 来获取 hello.greeting 的当前值。除此之外,还有其他的一些函数来获取长整型、浮点型和布尔型值。在下表中列出了这些函数,以及它们相应的ORIG函数用来获取 INI 设置中原始值(在被 .htaccess 和 ini_set() 修改前的值)的函数:
Current Value | Original Value | Type |
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() 的第一个参数是包含 INI 条目名称的字符串。为了避免命名冲突,你应该遵守和函数名相同的惯例——使用扩展名作为命名的前缀,例如 hello.greeting。另外,用点号分隔扩展名和INI条目名也是个好习惯。
第二个参数是初始值,无论你的 INI 条目是数值还是字符串值,都需要传递一个相应的字符串值。这归因于 .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 中被改变,而不能通过ini_set() 改变。PHP_INI_SYSTEM 指示该值只能在 php.ini 文件中修改。
我们暂时将跳过第四个参数的说明,通过它可以指定一个在INI设置改变时被调用的回调函数,这就允许扩展更精确的控制一个设置何时被改变或者触发一个相应的动作。
扩展经常需要在某个特定请求过程中跟踪一个值,这需要保持一个不依赖于其他请求的值。(Frequently, an extension will need to track a value through a particular request, keeping that value independent from other requests which may be occurring at the same time.)这对于单线程的 SAPI 来说很简单:只需要在源文件中声明一个全局变量然后访问它就可以了。麻烦的是,当 PHP 运行在多线程的环境下(例如 Apache2 和 IIS),它需要为每个独立的线程保存一个全局的值。PHP通过使用 TSRM(Thread Safe Resource Management) 抽象层极大的简化了这个过程,TSRM有时候也被称为 ZTS (Zend Thread Safety)。实际上,在此之前你已经在不知不觉中使用过部分的TSRM了。
首先,像全局变量一样,创建一个线程安全的全局变量也需要声明。在本例中,我们将声明一个初始值为 0 的整型值。每次 hello_long() 函数调用的时候,该值会自增并返回。在 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()) 来创建包含一个整型变量的结构。然后你根据是否是多线程环境定义一个 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() 只在一个新的进程或者线程启动的时候调用;但每个进程可以处理多于一个的请求,如果在这个函数里面初始化 count 的话,只对第一次页面请求有效,对于该进程接下来收到的请求,count将会继承上次请求后的值,这样 counter就不是每次都从0开始计数了。为了在每次请求开始的时候初始化 counter,我们需要实现 RINIT 函数,正如我们上面所说的,该函数在每次页面请求开始之前被调用。我们在此时涉及 php_hello_init_globals() 函数不仅因为你马上将要用到它,而且,在单线程的环境下,如果给 ZEND_INIT_MODULE_GLOBALS() 的 init 函数传递 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_ENG_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() 中根据 ini 设置确定递增还是递减:
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() 提供的值转换为合适的布尔值。这样,在脚本中就可以直接访问了。STD_PHP_INI_ENTRY 的最后三个参数告诉PHP 更改哪个全局变量,扩展的全局变量的结构以及包含它们的全局容器的名字。
至此,这三个文件应该是下面这个样子(为了可读性,有些代码被移动或者聚合在一起了):
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 #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
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_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(); }
在本教程中,我们探索了一个简单的 PHP 扩展,涉及到了函数、返回值、声明 INI 设置以及如何在请求过程中保存内部的状态(全局变量)。
在下一篇教程中,我们将探索 PHP 变量的内部结构和存储方式,以及它们在脚本中如何访问和维护。我们还将通过使用 zend_parse_parameters 来接收函数调用是传递过来的参数,然后学习如何返回更加复杂的返回值,比如数组、对象和资源。