深入php内核八(代码模块讨论)
OK,现在你已经有了一个安全的构建环境,也可以把模块编译进 PHP 了。那么,现在就让我们开始详细讨论一下这里面究竟是如何工作的吧~
模块结构
所有的 PHP 模块通常都包含以下几个部分:
包含头文件(引入所需要的宏、API定义等);
声明导出函数(用于 Zend 函数块的声明);
声明 Zend 函数块;
声明 Zend 模块;
实现 get_module() 函数;
实现导出函数。
包含头文件
模块所必须包含的头文件仅有一个 php.h,它位于 main 目录下。这个文件包含了构建模块时所必需的各种宏和API 定义。
小提示: 专门为模块创建一个含有其特有信息的头文件是一个很好的习惯。这个头文件应该包含 php.h 和所有导出函数的定义。如果你是使用 ext_skel 来创建模块的话,那么你可能已经有了这个文件,因为这个文件会被 ext_skel 自动生成。
声明导出函数
为了声明导出函数(也就是让其成为可以被 PHP 脚本直接调用的原生函数),Zend 提供了一个宏来帮助完成这样一个声明。代码如下:
ZEND_FUNCTION ( my_function );
ZEND_FUNCTION 声明了一个使用 Zend 内部 API 来编译的新的C 函数。这个 C 函数是 void 类型,以 INTERNAL_FUNCTION_PARAMETERS (这是另一个宏)为参数,而且函数名字以 zif_ 为前缀。把上面这句声明展开可以得到这样的代码:
void zif_my_function ( INTERNAL_FUNCTION_PARAMETERS );
接着再把 INTERNAL_FUNCTION_PARAMETERS 展开就会得到这样一个结果:
void zif_my_function( int ht
, zval * return_value
, zval * this_ptr
, int return_value_used
, zend_executor_globals * executor_globals
);
在解释器(interpreter)和执行器(executor)被分离出PHP 包后,这里面(指的是解释器和执行器)原有的一些 API 定义及宏也渐渐演变成了一套新的 API 系统:Zend API。如今的 Zend API 已经承担了很多原来(指的是分离之前)本属于 PHP API的职责,大量的 PHP API被以别名的方式简化为对应的Zend API。我们推荐您应该尽可能地使用 Zend API,PHP API 只是因为兼容性原因才被保留下来。举例来说, zval 和 pval其实是同一类型,只不过 zval 定义在 Zend 部分,而 pval 定义在 PHP 部分(实际上 pval 根本就是 zval 的一个别名)。但由于 INTERNAL_FUNCTION_PARAMETERS 是一个 Zend 宏,因此我们在上面的声明中使用了 zval 。在编写代码时,你也应该总是使用 zval 以遵循新的 Zend API 规范。
这个声明中的参数列表非常重要,你应该牢记于心。(表 3.1 “PHP 调用函数的 Zend 参数”详细介绍了这些参数)
表3.1 PHP 调用函数的 Zend 参数
参数 | 说明 |
ht | 这个参数包含了Zend 参数的个数。但你不应该直接访问这个值,而是应该通过 ZEND_NUM_ARGS() 宏来获取参数的个数。 |
return_value | 这个参数用来保存函数向 PHP 返回的值。访问这个变量的最佳方式也是用一系列的宏。后面我们会有详细说明。 |
this_ptr | 根据这个参数你可以访问该函数所在的对象(换句话说,此时这个函数应该是一个类的“方法”)。推荐使用函数 getThis() 来得到这个值。 |
return_value_used | 这个值主要用来标识函数的返回值是否为脚本所使用。0 表示脚本不使用其返回值,而 1 则相反。通常用于检验函数是否被正确调用以及速度优化方面,这是因为返回一个值是一种代价很昂贵的操作(可以在 array.c 里面看一下是如何利用这一特性的)。 |
executor_globals | 这个变量指向 Zend Engine 的全局设置,在创建新变量时这个这个值会很有用。我们也可以函数中使用宏 TSRMLS_FETCH() 来引用这个值。 |
声明 Zend 函数块
现在你已经声明了导出函数,除此之外你还必须得将其引入 Zend 。这些函数的引入是通过一个包含有 N 个zend_function_entry 结构的数组来完成的。数组的每一项都对应于一个外部可见的函数,每一项都包含了某个函数在 PHP 中出现的名字以及在 C 代码中所定义的名字。zend_function_entry 的内部定义如“例3.4 zend_function_entry 的内部声明”所示:
例3.4 zend_function_entry 的内部声明
typedef struct _zend_function_entry {
char *fname;
void (*handler)(INTERNAL_FUNCTION_PARAMETERS);
unsigned char *func_arg_types;
} zend_function_entry;
| 说明 |
fname | 指定在 PHP 里所见到的函数名(比如:fopen、mysql_connect 或者是我们样例中的 first_module)。 |
handler | 指向对应 C 函数的句柄。样例可以参考前面使用宏INTERNAL_FUNCTION_PARAMETERS 的函数声明。 |
func_arg_types | 用来标识一些参数是否要强制性地按引用方式进行传递。通常应将其设定为 NULL。 |
对于上面的例子,我们可以这样来声明:
zend_function_entry firstmod_functions[] =
{
ZEND_FE(first_module, NULL)
{NULL, NULL, NULL}
};
你可能已经看到了,这个结构的最后一项是 {NULL, NULL, NULL} 。事实上,这个结构的最后一项也必须始终是 {NULL, NULL, NULL} ,因为 Zend Engine 需要靠它来确认这些导出函数的列表是否列举完毕。
注意:
你不应该使用一个预定义的宏来代替列表的结尾部分(即{NULL, NULL, NULL}),因为编译器会尽量寻找一个名为 “NULL” 的函数的指针来代替 NULL !
宏 ZEND_FE(“Zend Function Entry”的简写)将简单地展开为一个zend_function_entry 结构。不过需要注意,这些宏对函数采取了一种很特别的命名机制:把你的C函数前加上一个 zif_ 前缀。比方说,ZEND_FE(first_module) 其实是指向了一个名为 zif_first_module() 的 C 函数。如果你想把宏和一个手工编码的函数名混合使用时(这并不是一个好的习惯),请你务必注意这一点。
小提示: 如果出现了一些引用某个名为 zif_*() 函数的编译错误,那十有八九与 ZEND_FE 所定义的函数有关。
“表 3.2 可用来定义函数的宏”给出了一个可以用来定义一个函数的所有宏的列表:
表3.2 可用来定义函数的宏
宏 | 说明 |
ZEND_FE(name, arg_types) | 定义了一个zend_function_entry 内字段name为 “name” 的函数。arg_types 应该被设定为 NULL。这个声明需要有一个对应的 C 函数,该这个函数的名称将自动以 zif_ 为前缀。举例来说, ZEND_FE("first_module", NULL) 就引入了一个名为 first_module() 的 PHP 函数,并被关联到一个名为 zif_first_module() 的C函数。这个声明通常与 ZEND_FUNCTION 搭配使用。 |
ZEND_NAMED_FE(php_name, name, arg_types) | 定义了一个名为 php_name 的 PHP 函数,并且被关联到一个名为 name 的 C 函数。arg_types 应该被设定为 NULL。 如果你不想使用宏 ZEND_FE 自动创建带有 zif_ 前缀的函数名的话可以用这个来代替。通常与 ZEND_NAMED_FUNCTION搭配使用。 |
ZEND_FALIAS(name, alias, arg_types) | 为 name 创建一个名为 alias 的别名。arg_types 应该被设定为 NULL。这个声明不需要有一个对应的 C 函数,因为它仅仅是创建了一个用来代替 name 的别名而已。 |
PHP_FE(name, arg_types) | 以前的 PHP API,等同于 ZEND_FE 。仅为兼容性而保留,请尽量避免使用。 |
PHP_NAMED_FE(runtime_name, name, arg_types) | 以前的 PHP API,等同于ZEND_NAMED_FE 。仅为兼容性而保留,请尽量避免使用。 |
注意:你不能将 ZEND_FE 和 PHP_FUNCTION 混合使用,也不能将PHP_FE 和 ZEND_FUNCTION 混合使用。但是将 ZEND_FE + ZEND_FUNCTION 和 PHP_FE + PHP_FUNCTION 一起混合使用是没有任何问题的。当然我们并不推荐这样的混合使用,而是建议你全部使用 ZEND_* 系列的宏。
声明 Zend 模块
Zend 模块的信息被保存在一个名为zend_module_entry 的结构,它包含了所有需要向 Zend 提供的模块信息。你可以在“例 3.5 zend_module_entry 的内部声明”中看到这个 Zend 模块的内部定义:
例3.5 zend_module_entry 的内部声明
typedef struct _zend_module_entry zend_module_entry;
struct _zend_module_entry {
unsigned short size;
unsigned int zend_api;
unsigned char zend_debug;
unsigned char zts;
char *name;
zend_function_entry *functions;
int (*module_startup_func)(INIT_FUNC_ARGS);
int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);
int (*request_startup_func)(INIT_FUNC_ARGS);
int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS);
void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS);
char *version;
… // 其余的一些我们不感兴趣的信息
};
字段 | 说明 |
size, zend_api, zend_debug and zts | 通常用 "STANDARD_MODULE_HEADER" 来填充,它指定了模块的四个成员:标识整个模块结构大小的 size ,值为 ZEND_MODULE_API_NO 常量的 zend_api,标识是否为调试版本(使用 ZEND_DEBUG 进行编译)的 zend_debug,还有一个用来标识是否启用了 ZTS (Zend 线程安全,使用 ZTS 或USING_ZTS 进行编译)的 zts。 |
name | 模块名称 (像“File functions”、“Socket functions”、“Crypt”等等). 这个名字就是使用 phpinfo() 函数后在“Additional Modules”部分所显示的名称。 |
functions | Zend 函数块的指针, 这个我们在前面已经讨论过。 |
module_startup_func | 模块启动函数。这个函数仅在模块初始化时被调用,通常用于一些与整个模块相关初始化的工作(比如申请初始化的内存等等)。如果想表明模块函数调用失败或请求初始化失败请返回 FAILURE,否则请返回 SUCCESS。可以通过宏 ZEND_MINIT 来声明一个模块启动函数。如果不想使用,请将其设定为 NULL。 |
module_shutdown_func | 模块关闭函数。这个函数仅在模块卸载时被调用,通常用于一些与模块相关的反初始化的工作(比如释放已申请的内存等等)。这个函数和 module_startup_func() 相对应。如果想表明函数调用失败或请求初始化失败请返回 FAILURE,否则请返回 SUCCESS。可以通过宏ZEND_MSHUTDOWN 来声明一个模块关闭函数。如果不想使用,请将其设定为 NULL。 |
request_startup_func | 请求启动函数。这个函数在每次有页面的请求时被调用,通常用于与该请求相关的的初始化工作。如果想表明函数调用失败或请求初始化失败请返回 FAILURE,否则请返回 SUCCESS。注意: 如果该模块是在一个页面请求中被动态加载的,那么这个模块的请求启动函数将晚于模块启动函数的调用(其实这两个初始化事件是同时发生的)。可以使用宏 ZEND_RINIT 来声明一个请求启动函数,若不想使用,请将其设定为 NULL。 |
request_shutdown_func | 请求关闭函数。这个函数在每次页面请求处理完毕后被调用,正好与 request_startup_func() 相对应。如果想表明函数调用失败或请求初始化失败请返回 FAILURE,否则请返回 SUCCESS。注意: 当在页面请求作为动态模块加载时, 这个请求关闭函数先于模块关闭函数的调用(其实这两个反初始化事件是同时发生的)。可以使用宏 ZEND_RSHUTDOWN 来声明这个函数,若不想使用,请将其设定为 NULL 。 |
info_func | 模块信息函数。当脚本调用 phpinfo() 函数时,Zend 便会遍历所有已加载的模块,并调用它们的这个函数。每个模块都有机会输出自己的信息。通常情况下这个函数被用来显示一些环境变量或静态信息。可以使用宏 ZEND_MINFO 来声明这个函数,若不想使用,请将其设定为 NULL 。 |
version | 模块的版本号。如果你暂时还不想给某块设置一个版本号的话,你可以将其设定为 NO_VERSION_YET。但我们还是推荐您在此添加一个字符串作为其版本号。版本号通常是类似这样: "2.5-dev", "2.5RC1", "2.5" 或者 "2.5pl3" 等等。 |
Remaining structure elements | 这些字段通常是在模块内部使用的,通常使用宏STANDARD_MODULE_PROPERTIES 来填充。而且你也不应该将他们设定别的值。STANDARD_MODULE_PROPERTIES_EX 通常只会在你使用了全局启动函数(ZEND_GINIT)和全局关闭函数(ZEND_GSHUTDOWN)时才用到,一般情况请直接使用 STANDARD_MODULE_PROPERTIES 。 |
在我们的例子当中,这个结构被定义如下:
zend_module_entry firstmod_module_entry =
{
STANDARD_MODULE_HEADER,
"First Module",
firstmod_functions,
NULL, NULL, NULL, NULL, NULL,
NO_VERSION_YET,
STANDARD_MODULE_PROPERTIES,
};
这基本上是你可以设定最简单、最小的一组值。该模块名称为“First Module”,然后是所引用的函数列表,其后所有的启动和关闭函数都没有使用,均被设定为了 NULL。
作为参考,你可以在表 3.3 “所有可声明模块启动和关闭函数的宏”中找到所有的可设置启动与关闭函数的宏。这些宏暂时在我们的例子中还尚未用到,但稍后我们将会示范其用法。你应该使用这些宏来声明启动和关闭函数,因为它们都需要引入一些特殊的变量(INIT_FUNC_ARGS 和 SHUTDOWN_FUNC_ARGS),而这两个参数宏将在你使用下面这些预定义宏时被自动引入(其实就是图个方便)。如果你是手工声明的函数或是对函数的参数列表作了一些必要的修改,那么你就应该修改你的模块相应的源代码来保持兼容。
宏 | 描述 |
ZEND_MINIT(module) | 声明一个模块的启动函数。函数名被自动设定为zend_minit_<module> (比如:zend_minit_first_module)。通常与ZEND_MINIT_FUNCTION 搭配使用。 |
ZEND_MSHUTDOWN(module) | 声明一个模块的关闭函数。函数名被自动设定为zend_mshutdown_<module> (比如:zend_mshutdown_first_module)。通常与ZEND_MSHUTDOWN_FUNCTION搭配使用。 |
ZEND_RINIT(module) | 声明一个请求的启动函数。函数名被自动设定为zend_rinit_<module> (比如:zend_rinit_first_module)。通常与ZEND_RINIT_FUNCTION搭配使用。 |
ZEND_RSHUTDOWN(module) | 声明一个请求的关闭函数。函数名被自动设定为zend_rshutdown_<module> (比如:zend_rshutdown_first_module)。通常与ZEND_RSHUTDOWN_FUNCTION 搭配使用。 |
ZEND_MINFO(module) | 声明一个输出模块信息的函数,用于phpinfo()。函数名被自动设定为zend_info_<module> (比如:zend_info_first_module)。通常与ZEND_MINFO_FUNCTION搭配使用。 |
实现 get_module() 函数
这个函数只用于动态可加载模块。我们先来看一下如何通过宏ZEND_GET_MODULE 来创建这个函数:
#if COMPILE_DL_FIRSTMOD
ZEND_GET_MODULE(firstmod)
#endif
这个函数的实现被一条件编译语句所包围。这是很有必要的,因为 get_module() 函数仅仅在你的模块想要编译成动态模块时才会被调用。通过在编译命令行指定编译条件:COMPILE_DL_FIRSTMOD (也就是上面我们设置的那个预定义)的打开与否,你就可以决定是编译成一个动态模块还是编译成一个内建模块。如果想要编译成内建模块的话,那么这个 get_module() 将被移除。
get_module() 函数在模块加载时被 Zend 所调用,你也可以认为是被你 PHP 脚本中的 dl() 函数所调用。这个函数的作用就是把模块的信息信息块传递 Zend 并通知 Zend 获取这个模块的相关内容。
如果你没有在一个动态可加载模块中实现 get_module() 函数,那么当你在访问它的时候 Zend 就会向你抛出一个错误信息。
实现导出函数
导出函数的实现是我们构建扩展的最后一步。在我们的first_module 例子中,函数被实现如下:
ZEND_FUNCTION(first_module)
{
long parameter;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l", ¶meter) == FAILURE) {
return;
}
RETURN_LONG(parameter) ;
}
这个函数是用宏 ZEND_FUNCTION 来声明的,和前面我们讨论的 Zend 函数块中的 ZEND_FE 声明相对应。在函数的声明之后,我们的代码便开始检查和接收这个函数的参数。在将参数进行转换后将其值返回。(参数的接收和处理我们马上会在下一节中讲到)。
小结
一切基本上就这样了 ―― 我们在实现一个模块时不会再遇到其他方面的事了。内建模块也基本上同动态模块差不多。因此,有了前面几节我们所掌握的信息,再在你遇到 PHP 源代码的时候你就有能力去搞定这些小麻烦。
在下面的几个小节里,我们将会学习到如何利用 PHP 内核来创建一个更为强大的扩展!