扩展是什么
如果你用过
“所有的函数都包含在了扩展中,那么剩下的还有什么?”你肯定会这么问,”扩展到底用来扩展什么?
safe_mode 和 open_basedir 检查提供了一致的控制层,另外还有通过中类似 fopen(), fread() 和 fwrite() 等用户空间函数关联到文件和网络I/O上的流接口。
生命周期
当一个SAPI启动的时候,例如/usr/local/apache/bin/apachectl start,作为响应,
接下来,
session.auto_start 选项开启的话,RINIT会自动触发用户空间的 session_start() 函数并且预先设置好$_SESSION 变量。
一旦请求初始化完成,ZE接管控制权并把
当脚本执行结束,
一旦清理过程结束,
这个过程在开始时也许看起来很乏味,但随着你对扩展研究的深入,你为发现它逐渐变得有意义。
内存申请
为了避免那些编写糟糕的扩展泄漏内存,ZE有一套内部的内存管理方案,每个内存申请函数使用一个额外的标记来指示内存是否是永久申请。永久申请表示申请的内存在请求结束后仍然有效。相反的,一块非永久申请的内存,无论是否主动调用释放函数,都会在请求结束时被释放。用户空间的变量就是非永久性内存申请的好例子,因为它们在请求结束后就毫无用处了。
理论上,扩展可以依赖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 safe_pemalloc() perform an additional check to avoid integer overflows |
设置开发环境
现在,你已经了解了一些
首先,你需要。(Windows下的开发环境将在后面的文章中介绍)。虽然那些用来发行的二进制包很方便,但它们往往故意去掉了两个在开发过程中很顺手的 ./configure 选项。其中一个是 –enable-debug,开启这个选项会在编译的
Hello World
介绍编程不涉及那个必须的Hello World程序肯定是不完整的。在下面,你将会完成一个仅导出一个函数的
好了,下面我们把它变成一个 hello_world 函数源代码的文件,一个头文件,其中包含着那些被
config.m4
#ifndef
hello.c
#ifdef HAVE_CONFIG_H #include "config.h" #endif #include "= 20010901 STANDARD_MODULE_HEADER, #endif = 20010901
在上面的例子中,大部分的代码都只是“胶水”——用来将扩展引入
1. 声明一个名为 hello_world 的函数
2. 这个函数返回一个 “Hello World” 字符串
3. 呃。。。1? 这个 1 是个啥?
你一定还记得ZE内置一个精巧的内存管理层用来确保在脚本退出的时候那些申请的资源能够正确的释放。在内存管理的世界里,同一块内存被释放两次是个大忌。这叫做“重复释放”,它导致程序访问一块它不再拥有的内存区域,是造成段错误的常见原因。同样的,你也不希望ZE释放一块在程序空间而不是被某个进程拥有的数据块中的静态字符缓冲(例如我们例子中的 “Hello World”)。RETURN_STRING() 可以复制所有传给它的字符串便于后面安全的释放,但是很多内部函数通常会自己申请一块内存,动态的填充并返回它。因此 RETURN_STRING()允许我们指定是否需要将传入的字符串复制一份。为了更好的演示这个过程,可以参考下面的代码,它与上面相应的代码完成同样的功能:
在这段代码中,我们手工的为 “Hello World” 字符串申请了一段内存,然后把它传给 RETURN_STRING(),第二个参数中的 0 指示 RETURN_STRING() 不需要再做一个私有的拷贝,它可以使用我们提供的。
编译你的扩展
最后一个步骤就是把你的扩展编译为一个可动态载入的模块。如果你已经正确的完成了上面的代码,那么只需要在 ext/hello 目录中执行三个命令就可以了:
$
执行完这些命令之后,你会在 ext/hello/modules/ 目录下面得到一个 hello.so 文件。把它复制到你的扩展目录(默认是 /usr/local/lib/
$
如果不出意外的话,你会看到这段脚本输出一个 “Hello World” 字符串,扩展中的 hello_world 函数返回一个字符串,然后 echo 命令将它输出出来。
其他的标量也可以通过类似的方式返回,RETRUN_LONG() 返回一个整型值,RETURN_DOUBLE() 返回浮点值,RETURN_BOOL() 返回布尔值,RETURN_NULL() (毫无意外地)返回 NULL 值。我们将在 function_entry 结构中添加新的 行并在文件结尾添加相应的 来浏览一遍这些函数。
static function_entry hello_functions[] = {
你还需要在 中,在 hello_world() 原型的下面添加以下函数的原型,以便能够顺利的编译:
因为你并没有修改 config.m4 文件,技术上讲你可以安全的跳过 make。但是,在此时我希望你能够执行完整的过程以确保能够正确的编译。另外,在最后一步你应该执行 make clean all,来确保所有的源文件的被重新编译。再次说明,对于目前的改动来说这并不是必须的,但这么做会更保险而且不至于混乱。当模块编译完成,你需要将它再次复制到扩展目录中替换掉旧的版本。
好了,现在你可以再次调用
完成了?很好!如果你用 var_dump 代替 echo 来观察这些函数的返回值,你可能发现 hello_bool() 函数的返回值是 true。这就是 1 在 RETURN_BOOL() 中表示的值。就像在 RETURN_TRUE 和 RETURN_FALSE 这两个宏,下面是重写后的 hello_bool() 函数,这次我们使用 RETURN_TRUE:
要注意,这两个宏没有结尾的括号,它们通过这种形式来区别于其他的 RETURN_*() 宏,所以不要用错它们。
你可能已经注意到,在上面的例子中我们没有通过传递 0 或 1 来指定是否需要复制传入的值,这是因为对于这种简单的标量值不需要额外申请或者释放内存(除了用来保存变量的容器本身——我们将在第二部分涉及这些内容)。
除了上面的那些,还有另外的三种返回值类型:资源(例如 mysql_connect(), fosockopen(),ftp_connect()等函数的返回值),数组(也叫做 HASH 表)以及对象(例如 new 关键字的返回值)。在第二部分我们深入了解变量的时候,会涉及这些内容。
INI 设置
Zend引擎提供了两种管理 INI 值的方式,现在我们将了解比较简单的方式,在后面的部分,当你有机会操作全局变量的时候,我们将涉及另一种更深入更复杂的方式。
我们假设你需要定义一个 hello_world() 函数中 say hello 的对象。你需要在hello.c 和 中添加一些内容并对 hello_module_entry 结构做些大修改。首先,在 的用户空间函数原型附近添加下面的原型:
然后在 hello.c 文件的开头部分找到当前的 hello_module_entry,用下面的代码替换它:
zend_module_entry hello_module_entry = { #if ZEND_MODULE_API_NO >= 20010901 STANDARD_MODULE_HEADER, #endif = 20010901
接下来,你需要在 hello.c 文件顶部添加一条 #include 引入对 INI 提供支持的头文件:
#ifdef HAVE_CONFIG_H #include "config.h" #endif #include "
最后,修改 hello_world() 函数,让它使用 INI 文件中的值:
我们注意到你通知 RETURN_STRING() 复制 INI_STR() 的返回值,因为它涉及到
第一部分的修改中,引入了两个你可能要非常熟悉的函数:MINIT 和 MSHUTDOWN。就像我们上面所说的,这两个函数分别在 SAPI 层启动和终止时调用。在每次请求到来的时候,它们不会再被调用。在上面的例子中,通过它们来注册我们扩展中定义的 条目。在后面的文章中,你还会学到如何通过 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 |
的第一个参数是包含 INI 条目名称的字符串。为了避免命名冲突,你应该遵守和函数名相同的惯例——使用扩展名作为命名的前缀,例如 hello.greeting。另外,用点号分隔扩展名和INI条目名也是个好习惯。
第二个参数是初始值,无论你的 INI 条目是数值还是字符串值,都需要传递一个相应的字符串值。这归因于 .ini 文件本身的文本性。当你在代码中使用 INI_INT(), INI_FLT(), INI_BOOL()的时候,它们会自动进行类型转换。
第三个参数指定访问模式修饰符。这是个用来确定该 INI 设置在何时何处能够被修改的掩码值。例如register_globals,在脚本中通过 ini_set() 修改它的值是毫无意义的,因为它在脚本执行之前的请求开始阶段起作用。另外,像allow_url_fopen,是管理类的设置,你不希望共享主机的用户能够通过 ini_set() 或者.htaccess 来改变它。这个参数的一个典型值是 ,指示该值可以在任意地方被改变。指示该值只能在 或者 .htaccess 中被改变,而不能通过ini_set() 改变。 指示该值只能在 文件中修改。
我们暂时将跳过第四个参数的说明,通过它可以指定一个在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 来说很简单:只需要在源文件中声明一个全局变量然后访问它就可以了。麻烦的是,当
首先,像全局变量一样,创建一个线程安全的全局变量也需要声明。在本例中,我们将声明一个初始值为 0 的整型值。每次 hello_long() 函数调用的时候,该值会自增并返回。在 中的 #define 下面添加如下代码:
#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 函数,因此你需要在头文件中声明它的原型:
然后,我们进入 hello.c,在包含文件指令的下面添加如下内容:
#ifdef HAVE_CONFIG_H #include "config.h" #endif #include "
修改 hello_module_entry 结构,添加 :
zend_module_entry hello_module_entry = { #if ZEND_MODULE_API_NO >= 20010901 STANDARD_MODULE_HEADER, #endif = 20010901
修改 MINIT 函数,并添加其他两个处理请求初始化的函数:
static void
最后,修改 hello_long() 函数使用这个全局变量:
在 中,你使用一对宏(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,我们暂时不需要关心这是什么。
你也许已经注意到,在 函数中并没有做什么实际的工作,相反,我们在 RINIT 中将 counter 初始化为 0, 这是为什么?
关键原因在于这两个函数调用的时刻。 只在一个新的进程或者线程启动的时候调用;但每个进程可以处理多于一个的请求,如果在这个函数里面初始化 count 的话,只对第一次页面请求有效,对于该进程接下来收到的请求,count将会继承上次请求后的值,这样 counter就不是每次都从0开始计数了。为了在每次请求开始的时候初始化 counter,我们需要实现 RINIT 函数,正如我们上面所说的,该函数在每次页面请求开始之前被调用。我们在此时涉及 函数不仅因为你马上将要用到它,而且,在单线程的环境下,如果给 ZEND_INIT_MODULE_GLOBALS() 的 init 函数传递 NULL 将会导致段错误。
INI设置作为全局值
不知你是否还记得,通过 声明的 值被作为字符串值来解析,然后在调用INI_INT(), INI_FLT(), INI_BOOL()的时候做必要的转换。某些设置在脚本的执行过程中经常需要一遍遍的被读取,这就导致了很多无谓的重复工作。幸运的是,你可以指示 ZE 将 INI 值存储为特定的数据类型,只有在值改变的时候才进行类型转换。下面,我们将声明另外一个布尔型的 INI 值,来指示 counter 应该递增还是递减。首先,修改 中的 MODULE_GLOBALS:
ZEND_BEGIN_MODULE_GLOBALS(hello) long counter; zend_bool direction; ZEND_ENG_MODULE_GLOBALS(hello)
然后,修改 添加 INI 值的声明:
然后,在 init_globals 中初始化设置:
static void direction = 1; }
最后,在 hello_long() 中根据 ini 设置确定递增还是递减:
好了,完成了。在 INI_ENTRY 中设置的 OnUpdateBool 方法会自动的将 , .htaccess 或者在脚本中通过 ini_set() 提供的值转换为合适的布尔值。这样,在脚本中就可以直接访问了。STD_ 的最后三个参数告诉
清理和检查
至此,这三个文件应该是下面这个样子(为了可读性,有些代码被移动或者聚合在一起了):
config.m4
#ifndef
hello.c
#ifdef HAVE_CONFIG_H #include "config.h" #endif #include "= 20010901 STANDARD_MODULE_HEADER, #endif = 20010901 direction = 1; }
下一步?
在本教程中,我们探索了一个简单的
在下一篇教程中,我们将探索 zend_parse_parameters 来接收函数调用是传递过来的参数,然后学习如何返回更加复杂的返回值,比如数组、对象和资源。