Zend API:深入PHP内核

  1. 摘要
  2. 概述
  3. 可扩展性
    1. 内建模块
    2. Zend 引擎
  4. 源码布局
    1. 扩展规范
  5. 自动构建系统
  6. 开始创建扩展
    1. 模块的编译
  7. 故障处理
  8. 关于模块代码的讨论
    1. 模块结构
    2. 声明导出函数
    3. Zend 函数块
    4. 包含头文件
    5. 实现get_module() 函数
    6. 实现导出函数
    7. 小结
  9. 接收参数
    1. 取得参数数量
    2. 取回参数
  10. 参考API

摘要

知者不言,言者不知。 ――老子《道德经》五十六章

有时候,单纯依靠 PHP “本身”是不行的。尽管普通用户很少遇到这种情况,但一些专业性的应用则经常需要将 PHP 的性能发挥到极致(这里的性能是指速度或功能)。由于受到 PHP 语言本身的限制,同时还可能不得不把庞大的库文件包含到每个脚本当中。因此,某些新功能并不是总能被顺利实现,所以我们必须另外寻找一些方法来克服 PHP 的这些缺点。

了解到了这一点,我们就应该接触一下 PHP 的心脏并探究一下它的内核-可以编译成 PHP 并让之工作的 C 代码的时候了。

概述

“扩展 PHP”说起来容易做起来难。PHP 现在已经发展成了一个具有数兆字节源代码的非常成熟的系统。要想深入这样的一个系统,有很多东西需要学习和考虑。在写这一章节的时候,我们最终决定采用“边学边做”的方式。这也许并不是最科学和专业的方式,但却应该是最有趣和最有效的一种方式。在下面的小节里,你首先会非常快速的学习到如何写一个虽然很基础但却能立即运行的扩展,然后将会学习到有关 Zend API 的高级功能。另外一个选择就是将其作为一个整体,一次性的讲述所有的这些操作、设计、技巧和诀窍等,并且可以让我们在实际动手前就可以得到一副完整的愿景。这看起来似乎是一个更好的方法,也没有死角,但它却枯燥无味、费时费力,很容易让人感到气馁。这就是我们为什么要采用非常直接的讲法的原因。
注意,尽管这一章会尽可能多讲述一些关于 PHP 内部工作机制的知识,但要想真的给出一份在任何时间任何情况下的PHP 扩展指南,那简直是不可能的。PHP 是如此庞大和复杂,以致于只有你亲自动手实践一下才有可能真正理解它的内部工作机制,因此我们强烈推荐你随时参考它的源代码来进行工作。

Zend 是什么? PHP 又是什么?

Zend 指的是语言引擎,PHP 指的是我们从外面看到的一套完整的系统。这听起来有点糊涂,但其实并不复杂(见图3-1 PHP 内部结构图)。为了实现一个 WEB 脚本的解释器,你需要完成以下三个部分的工作:

  1. 解释器部分:负责对输入代码的分析、翻译和执行;
  2. 功能性部分:负责具体实现语言的各种功能(比如它的函数等等);
  3. 接口部分:负责同 WEB 服务器的会话等功能。
    4.
    enter image description here
    $$ PHP 内部结构图$$

Zend 包括了第一部分的全部和第二部分的局部,PHP 包括了第二部分的局部和第三部分的全部。他们合起来称之为 PHP 包。Zend 构成了语言的核心,同时也包含了一些最基本的 PHP 预定义函数的实现。PHP 则包含了所有创造出语言本身各种显著特性的模块。

可扩展性

正如上图(图3-1 PHP 内部结构图)所示,PHP 主要以三种方式来进行扩展:外部模块,内建模块和 Zend 引擎。下面我们将分别讨论这些方式:

####外部模块
外部模块可以在脚本运行时使用 dl() 函数载入。这个函数从磁盘载入一个共享对象并将它的功能与调用该函数的脚本进行绑定并使之生效。脚本终止后,这个外部模块将在内存中被丢弃。这种方式有利有弊,如下表所示:

优点 缺点
外部模块不需要重新对 PHP 进行编译。 共享对象在每次脚本调用时都需要对其进行加载,速度较慢。
PHP通过“外包”方式来让自身的体积保持很小。 附加的外部模块文件会让磁盘变得比较散乱。
每个想使用该模块功能的脚本都必须使用dl() 函数手动加载,或者在 php.ini 文件当中添加一些扩展标签(这并不总是一个恰当的解决方案)

综上所述,外部模块非常适合开发第三方产品,较少使用的附加的小功能或者仅仅是调试等这些用途。为了迅速开发一些附加功能,外部模块是最佳方式。但对于一些经常使用的、实现较大的,代码较为复杂的应用,那就有些得不偿失了。
第三方可能会考虑在 php.ini 文件中使用扩展标签来创建一个新的外部模块。这些外部模块完全同主PHP 包分离,这一点非常适合应用于一些商业环境。商业性的发行商可以仅发送这些外部模块而不必再额外创建那些并不允许绑定这些商业模块的PHP 二进制代码。

内建模块

内建模块被直接编译进 PHP 并存在于每一个 PHP 处理请求当中。它们的功能在脚本开始运行时立即生效。和外部模块一样,内建模块也有一下利弊:

优点 缺点
无需专门手动载入,功能即时生效。 修改内建模块时需要重新编译PHP。
无需额外的磁盘文件,所有功能均内置在 PHP 二进制代码当中。 PHP 二进制文件会变大并且会消耗更多的内存

Zend 引擎

当然,你也能直接在 Zend 引擎里面进行扩展。如果你需要在语言特性方面做些改动或者是需要在语言核心内置一些特别的功能,那么这就是一种很好的方式。但一般情况下应该尽力避免对 Zend 引擎的修改。这里面的改动会导致和其他代码的不兼容,而且几乎没有人会适应打过特殊补丁的 Zend 引擎。况且这些改动与主 PHP 源代码是不可分割的,因此就有可能在下一次的官方的源代码更新中被覆盖掉。因此,这种方式通常被认为是“不良的习惯”。由于使用极其稀少,本章将不再对此进行赘述。

源码布局

在我们开始讨论具体编码这个话题前,你应该让自己熟悉一下 PHP 的源代码树以便可以迅速地对各个源文件进行定位。这也是编写和调试 PHP 扩展所必须具备的一种能力。
下表列出了一些主要目录的内容:

除此之外,你也应该注意一下这些文件所包含的一些文件。举例来说,哪些文件与 Zend 执行器有关,哪些文件又为 PHP 初始化工作提供了支持等等。在阅读完这些文件之后,你还可以花点时间再围绕PHP包来看一些文件,了解一下这些文件和模块之间的依赖性――它们之间是如何依赖于别的文件又是如何为其他文件提供支持的。同时这也可以帮助你适应一下 PHP 创作者们代码的风格。要想扩展 PHP,你应该尽快适应这种风格。

扩展规范

Zend 是用一些特定的规范构建的。为了避免破坏这些规范,你应该遵循以下的几个规则:

1 . 宏
几乎对于每一项重要的任务,Zend 都预先提供了极为方便的宏。在下面章节的图表里将会描述到大部分基本函数、结构和宏。这些宏定义大多可以在 Zend.h 和 Zend_API.h 中找到。我们建议您在学习完本节之后仔细看一下这些文件。(当然你也可以现在就阅读这些文件,但你可能不会留下太多的印象。)

2 . 内存管理
资源管理仍然是一个极为关键的问题,尤其是对服务器软件而言。资源里最具宝贵的则非内存莫属了,内存管理也必须极端小心。内存管理在 Zend 中已经被部分抽象,而且你也应该坚持使用这些抽象,原因显而易见:由于得以抽象,Zend 就可以完全控制内存的分配。Zend 可以确定一块内存是否在使用,也可以自动释放未使用和失去引用的内存块,因此就可以避免内存泄漏。下表列出了一些常用函数:

函数 描述
emalloc() 用于替代 malloc()。
efree() 用于替代 free()。。
estrdup() 用于替代 strdup()
estrndup() 用于替代strndup()。速度要快于 estrdup() 而且是二进制安全的。如果你在复制之前预先知道这个字符串的长度那就推荐你使用这个函数。
ecalloc() 用于替代 calloc()。
erealloc() 用于替代 realloc()。

emalloc(), estrdup(), estrndup(), ecalloc(), 和 erealloc() 用于申请内部的内存,efree() 则用来释放这些前面这些函数申请的内存。e*() 函数所用到的内存仅对当前本地的处理请求有效,并且会在脚本执行完毕,处理请求终止时被释放。
Zend 还有一个线程安全资源管理器,这可以为多线程WEB 服务器提供更好的本地支持。不过这需要你为所有的全局变量申请一个局部结构来支持并发线程。但是因为在写本章内容时Zend 的线程安全模式仍未完成,因此我们无法过多地涉及这个话题。

3 . 目录与文件函数

下列目录与文件函数应该在 Zend 模块内使用。它们的表现和对应的 C 语言版本完全一致,只是在线程级提供了虚拟目录的支持。

Zend函数 对应的C函数
V_GETCWD) getcwd()
V_FOPEN() fopen()
V_OPEN) open()
V_CHDIR() chdir()
V_GETWD() getwd()
V_CHDIR_FILE() 将当前的工作目录切换到一个以文件名为参数的该文件所在的目录。
V_STAT() stat()
V_LSTAT() lstat()

4 . 字符串处理
在 Zend 引擎中,与处理诸如整数、布尔值等这些无需为其保存的值而额外申请内存的简单类型不同,如果你想从一个函数返回一个字符串,或往符号表新建一个字符串变量,或做其他类似的事情,那你就必须确认是否已经使用上面的 e*() 等函数为这些字符串申请内存。(你可能对此没有多大的感觉。无所谓,现在你只需在脑子里有点印象即可,我们稍后就会再次回到这个话题)

5 . 复杂类型
像数组和对象等这些复杂类型需要另外不同的处理。它们被储存在哈希表中,Zend 提供了一些简单的 API 来操作这些类型。

自动构建系统

PHP 提供了一套非常灵活的自动构建系统(automatic build system),它把所有的模块均放在 Ext 子目录下。每个模块除自身的源代码外,还都有一个用来配置该扩展的 config.m4 文件(详情请参见http://www.gnu.org/software/m4/manual/m4.html)。
包括 .gitignore 在内的所有文件都是由位于 Ext 目录下的 ext_skel 脚本自动生成的,它的参数就是你想创建模块的名称。这个脚本会创建一个与模块名相同的目录,里面包含了与该模块对应的一些的文件。
下面是操作步骤:
编写ext/extsion.def文件

string extions_function(string str, int n )

运行配置文件

$ ./ext_skel --extname=my_module
Creating directory my_module
Creating basic files: config.m4 .cvsignore my_module.c php_my_module.h CREDITS EXPERIMENTAL tests/001.phpt my_module.php [done].
To use your new extension, you will have to execute the following steps:
1. $ cd ..
2. $ vi ext/my_module/config.m4
3. $ ./buildconf
4. $ ./configure --[with|enable]-my_module
5. $ make
6. $ ./php -f ext/my_module/my_module.php
7. $ vi ext/my_module/my_module.c
8. $ make
Repeat steps 3-6 until you are satisfied with ext/my_module/config.m4 and step 6 confirms that your module is compiled into PHP. Then, start writing code and repeat the last two steps as often as necessary.

这些指令就会生成前面所说的那些文件。为了能够在自动配置文件和构建程序中包含新增加的模块,你还需要再运行一次 buildconf命令。这个命令会通过搜索 Ext 目录和查找所有config.m4文件来重新生成configure 脚本。默认情况下的的 config.m4 文件如例 3-1 所示,看起来可能会稍嫌复杂:
例3.1 默认的 config.m4 文件

dnl $Id$
dnl config.m4 for extension feng
dnl Comments in this file start with the string 'dnl'.
dnl Remove where necessary. This file will not work
dnl without editing.
dnl If your extension references something external, use with:
dnl PHP_ARG_WITH(feng, for feng support,
dnl Make sure that the comment is aligned:
dnl [ --with-feng Include feng support])
dnl Otherwise use enable:
dnl PHP_ARG_ENABLE(feng, whether to enable feng support,
dnl Make sure that the comment is aligned:
dnl [ --enable-feng Enable feng support])
if test "$PHP_FENG" != "no"; then
dnl Write more examples of tests here...
dnl # --with-feng -> check with-path
dnl SEARCH_PATH="/usr/local /usr" # you might want to change this
dnl SEARCH_FOR="/include/feng.h" # you most likely want to change this
dnl if test -r $PHP_FENG/$SEARCH_FOR; then # path given as parameter
dnl FENG_DIR=$PHP_FENG
dnl else # search default path list
dnl AC_MSG_CHECKING([for feng files in default path])
dnl for i in $SEARCH_PATH ; do
dnl if test -r $i/$SEARCH_FOR; then
dnl FENG_DIR=$i
dnl AC_MSG_RESULT(found in $i)
dnl fi
dnl done
dnl fi
dnl
dnl if test -z "$FENG_DIR"; then
dnl AC_MSG_RESULT([not found])
dnl AC_MSG_ERROR([Please reinstall the feng distribution])
dnl fi
dnl # --with-feng -> add include path
dnl PHP_ADD_INCLUDE($FENG_DIR/include)
dnl # --with-feng -> check for lib and symbol presence
dnl LIBNAME=feng # you may want to change this
dnl LIBSYMBOL=feng # you most likely want to change this
dnl PHP_CHECK_LIBRARY($LIBNAME,$LIBSYMBOL,
dnl [
dnl PHP_ADD_LIBRARY_WITH_PATH($LIBNAME, $FENG_DIR/$PHP_LIBDIR, FENG_SHARED_LIBADD)
dnl AC_DEFINE(HAVE_FENGLIB,1,[ ])
dnl ],[
dnl AC_MSG_ERROR([wrong feng lib version or lib not found])
dnl ],[
dnl -L$FENG_DIR/$PHP_LIBDIR -lm
dnl ])
dnl
dnl PHP_SUBST(FENG_SHARED_LIBADD)
PHP_NEW_EXTENSION(feng, feng.c, $ext_shared,, -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1)
fi

如果你不太熟悉 M4 文件(现在毫无疑问是熟悉 M4 文件的大好时机),那么就可能会有点糊涂。但是别担心,其实非常简单。
注意:凡是带有 dnl 前缀的都是注释,注释是不被解析的。
config.m4 文件负责在配置时解析 configure 的命令行选项。这就是说它将检查所需的外部文件并且要做一些类似配置与安装的任务。
默认的配置文件将会在 configure 脚本中产生两个配置指令:--with-my_module--enable-my_module。当需要引用外部文件时使用第一个选项(就像用 –with-apache 指令来引用 Apache 的目录一样)。第二个选项可以让用户简单的决定是否要启用该扩展。不管你使用哪一个指令,你都应该注释掉另外一个。也就是说,如果你使用了--enable-my_module,那就应该去掉--with-my_module。反之亦然。
默认情况下,通过 ext_skel 创建的 config.m4 都能接受指令,并且会自动启用该扩展。启用该扩展是通过 PHP_EXTENSION 这个宏进行的。如果你要改变一下默认的情况,想让用户明确的使用 --enable-my_module--with-my_module 指令来把扩展包含在 PHP 二进制文件当中,那么将 if test "$PHP_MY_MODULE" != "no"改为if test "$PHP_MY_MODULE" == "yes"即可。

if test "$PHP_MY_MODULE" == "yes"; then dnl
Action.. PHP_EXTENSION(my_module, $ext_shared)
fi

这样就会导致在每次重新配置和编译 PHP 时都要求用户使用--enable-my_module 指令。
另外请注意在修改 config.m4 文件后需要重新运行 buildconf命令。

开始创建扩展

我们先来创建一个非常简单的扩展,这个扩展除了一个将其整形参数作为返回值的函数外几乎什么都没有。下面(“例3-2 一个简单的扩展”)就是这个样例的代码:
例3.2 一个简单的扩展

/* include standard header */
#include "php.h"
/* declaration of functions to be exported */
ZEND_FUNCTION(first_module);
/* compiled function list so Zend knows what‘s in this module */
zend_function_entry firstmod_functions[] =
{
ZEND_FE(first_module, NULL)
{NULL, NULL, NULL}
};
/* compiled module information */
zend_module_entry firstmod_module_entry =
{
STANDARD_MODULE_HEADER,
"First Module",
firstmod_functions,
NULL,
NULL,
NULL,
NULL,
NULL,
NO_VERSION_YET,
STANDARD_MODULE_PROPERTIES
};
/* implement standard "stub" routine to introduce ourselves to Zend */
#if COMPILE_DL_FIRST_MODULE
ZEND_GET_MODULE(firstmod)
#endif
/* implement function that is meant to be made available to PHP */
ZEND_FUNCTION(first_module)
{
long parameter;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l", &parameter) == FAILURE) {
return;
}
RETURN_LONG(parameter);
}

这段代码已经包含了一个完整的 PHP 模块。稍后我们会详细解释这段代码,现在让我们先讨论一下构建过程。(在我们讨论 API 函数前,这可以让心急的人先实验一下。)

模块的编译

模块的编译基本上有两种方法:

  1. 在 Ext 目录内使用“make” 机制,这种机制也可以编译出动态可加载模块。
  2. 手动编译源代码。

第一种方法明显受到人们的偏爱。自 PHP 4.0 以来,这也被标准化成了一个的复杂的构建过程。这种复杂性也导致了难于被理解这个缺点。在本章最后我们会更详细的讨论这些内容,但现在还是让我们使用默认的 make 文件吧。
第二种方法很适合那些(因为某种原因而)没有完整 PHP 源码树的或者是很喜欢敲键盘的人。虽然这些情况是比较罕见,但为了内容的完整性我们也会介绍一下这种方法。

####使用 make 进行编译
为了能够使用这种标准机制流程来编译这些代码,让我们把它所有的子目录都复制到 PHP 源码树的 Ext 目录下。然后运行 buildconf 命令,这将会创建一个新的包含了与我们的扩展相对应的选项的 configure 脚本。默认情况下,样例中的所有代码都是未激活的,因此你不用担心会破坏你的构建程序。在 buildconf 执行完毕后,再使用 configure --help命令就会显示出下面的附加模块:

--enable-array_experiments BOOK: Enables array experiments
--enable-call_userland BOOK: Enables userland module
--enable-cross_conversion BOOK: Enables cross-conversion module
--enable-first_module BOOK: Enables first module
--enable-infoprint BOOK: Enables infoprint module
--enable-reference_test BOOK: Enables reference test module
--enable-resource_test BOOK: Enables resource test module
--enable-variable_creation BOOK: Enables variable-creation module

前面样例(“例3-2 一个简单的扩展”)中的模块(first_module)可以使用 –enable-first_module 或 enable-first_module=yes 来激活。
手动编译
手动编译需要运行以下命令:

编译 cc -fpic -DCOMPILE_DL_FIRST_MODULE=1 -I/usr/local/include -I. -I.. -I../Zend -c -o

注意:
手动将模块静态编译和连接到 PHP 二进制代码的指令很长很长,因此我们在这里不作讨论。(手动输入那些指令是很低效的。)

故障处理

实际上,在对静态或动态模块进行编译时没有太多故障处理工作要做。唯一可能的问题就是编译器会警告说找不到某些定义或者类似的事情。如果出现这种情况,你应该确认一下所有的头文件都是可用的并且它们的路径都已经在编译命令中被指定。为了确保每个文件都能被正确地定位,你可以先提取一个干净的 PHP 源码树,然后在 Ext 目录使用自动构建工具来创建这些文件。用这种方法就可以确保一个安全的编译环境。假如这样也不行,那就只好试试手动编译了。

PHP 也可能会警告说在你的模块里面有一些未定义的函数。(如果你没有改动样例文件的话这种情况应该不会发生。)假如你在模块中拼错了一些你想访问的外部函数的名字,那么它们就会在符号表中显示为“未能连接的符号”。这样在 PHP 动态加载或连接时,它们就不会运行--在二进制文件中没有相应的符号。为了解决这个问题,你可以在你的模块文件中找一下错误的声明或外部引用。注意,这个问题仅仅发生在动态可加载模块身上。而在静态模块身上则不会发生,因为静态模块在编译时就会抛出这些错误。

关于模块代码的讨论

OK,现在你已经有了一个安全的构建环境,也可以把模块编译进 PHP 了。那么,现在就让我们开始详细讨论一下这里面究竟是如何工作的吧~

模块结构

所有的 PHP 模块通常都包含以下几个部分:

  1. 包含头文件(引入所需要的宏、API定义等);
  2. 声明导出函数(用于 Zend 函数块的声明);
  3. 声明 Zend 函数块;
  4. 声明 Zend 模块;
  5. 实现 get_module() 函数;
  6. 实现导出函数。

声明导出函数

为了声明导出函数(也就是让其成为可以被 PHP 脚本直接调用的原生函数),Zend 提供了一个宏来帮助完成这样一个声明。代码如下:

PHP_FUNCTION(feng_input)

其中PHP_FUNCTION定义在main/php.h的347行

/* PHP-named Zend macro wrappers */
#define PHP_FUNCTION ZEND_FUNCTION

ZEND_FUNCTION 声明了一个使用 Zend 内部 API 来编译的新的C 函数。这个 C 函数是 void 类型,以 INTERNAL_FUNCTIONPARAMETERS (这是另一个宏)为参数,而且函数名字以 zif 为前缀。把上面这句声明展开可以得到这样的代码:

接着再把 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 参数”详细介绍了这些参数)

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;

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 所定义的函数有关。

包含头文件

模块所必须包含的头文件仅有一个 php.h,它位于 main 目录下。这个文件包含了构建模块时所必需的各种宏和API 定义。

小提示:
专门为模块创建一个含有其特有信息的头文件是一个很好的习惯。这个头文件应该包含 php.h 和所有导出函数的定义。如果你是使用 ext_skel 来创建模块的话,那么你可能已经有了这个文件,因为这个文件会被 ext_skel 自动生成。

实现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", &parameter) == FAILURE) {
return;
}
RETURN_LONG(parameter) ;
}

这个函数是用宏 ZEND_FUNCTION 来声明的,和前面我们讨论的 Zend 函数块中的 ZEND_FE 声明相对应。在函数的声明之后,我们的代码便开始检查和接收这个函数的参数。在将参数进行转换后将其值返回。(参数的接收和处理我们马上会在下一节中讲到)。

小结

一切基本上就这样了 ―― 我们在实现一个模块时不会再遇到其他方面的事了。内建模块也基本上同动态模块差不多。因此,有了前面几节我们所掌握的信息,再在你遇到 PHP 源代码的时候你就有能力去搞定这些小麻烦。
在下面的几个小节里,我们将会学习到如何利用 PHP 内核来创建一个更为强大的扩展!

接收参数

对于扩展来说,最重要的一件事就是如何接收和处理那些通过函数参数传递而来的数据。大多数扩展都是用来处理某些特定的输入数据(或者是根据参数来决定进行某些特定的动作),而函数的参数则是 PHP 代码层和 C 代码层之间交换数据的唯一途径。当然,你也可以通过事先定义好的全局变量来交换数据(这个我们稍后会谈到),不过这种习惯可不太好,我们应该尽量避免。

在 PHP 中并不需要做任何显式的函数声明,这也就是我们为什么说 PHP 的调用语法是动态的而且 PHP 从不会检查任何错误的原因。调用语法是否正确完全是留给用户自己的工作。也就是说,在调用一个函数时完全有可能这次用一个参数而下次用 4 个参数,而且两种情况在语法上都是正确的。

取得参数数量

因为 PHP 不但没法根据函数的显式声明来对调用进行语法检查,而且它还支持可变参数,所以我们就不得不在所调用函数的内部来获取参数个数。这个工作可以交给宏 ZEND_NUM_ARGS 来完成。在(PHP4)以前,这个宏(在 PHP3 中应该指的是宏 ARG_COUNT,因为 ZEND_NUM_ARGS 宏是直到 PHP 4.0 才出现的,并且其定义一直未变。PHP4 及以后虽也有 ARG_COUNT 宏定义,但却仅仅是为兼容性而保留的,并不推荐使用,译者注)是利用所调用的 C 函数中的变量 ht(就是定义在宏 INTERNAL_FUNCTION_PARAMETERS 里面的那个,HashTable * 类型)来获取参数个数的,而现在变量 ht 就只包含函数的参数个数了(int 类型)。与此同时还定义了一个哑宏:ZEND_NUM_ARGS(直接等于 ht,见 Zend.h)。尽量地采用 ZEND_NUM_ARGS 是个好习惯,因为这样可以保证在函数调用接口上的兼容性。

下面的代码展示了如何检查传入函数的参数个数的正确性:

if(ZEND_NUM_ARGS() != 2)
{
WRONG_PARAM_COUNT;
}

如果没有为该函数传入两个参数,那么就会退出该函数并且发出一个错误消息。在这段代码中我们使用了一个工具宏:WRONG_PARAM_COUNT,它主要用来抛出一个类似

“Warning: Wrong parameter count for firstmodule() in /home/www/htdocs/firstmod.php on line 5”

这样的错误信息。
这个宏会主要负责抛出一个默认的错误信息,然后便返回调用者。我们可以在 zend_API.h 中找到它的定义:

ZEND_API void wrong_param_count(void);
#define WRONG_PARAM_COUNT { wrong_param_count(); return; }

正如您所见,它调用了一个内部函数 wrong_param_count() ,这个函数会输出一个警告信息。至于如何抛出一个自定义的错误信息,可以参见后面的“打印信息”一节。

取回参数

对传入的参数进行解析是一件很常见同时也是颇为乏味的事情,而且同时你还得做好标准化的错误检查和发送错误消息等琐事。不过从 PHP 4.1.0 开始,我们就可以用一个新的参数解析 API 来搞定这些事情。这个 API 可以大大简化参数的接收处理工作,尽管它在处理可变参数时还有点弱。但既然绝大部分函数都没有可变参数,那么使用这个 API 也就理所应当地成为了我们处理函数参数时的标准方法。
这个用于参数解析的函数的原型大致如下:

int zend_parse_parameters(int num_args TSRMLS_DC, char *type_spec, ...);

第一个参数 num_args 表明了我们想要接收的参数个数,我们经常使用 ZEND_NUM_ARGS() 来表示对传入的参数“有多少要多少”。第二参数应该总是宏 TSRMLS_CC 。第三个参数 type_spec 是一个字符串,用来指定我们所期待接收的各个参数的类型,有点类似于 printf 中指定输出格式的那个格式化字符串。剩下的参数就是我们用来接收 PHP 参数值的变量的指针。
zend_parse_parameters() 在解析参数的同时会尽可能地转换参数类型,这样就可以确保我们总是能得到所期望的类型的变量。任何一种标量类型都可以转换为另外一种标量类型,但是不能在标量类型与复杂类型(比如数组、对象和资源等)之间进行转换。
如果成功地解析和接收到了参数并且在转换期间也没出现错误,那么这个函数就会返回 SUCCESS,否则返回 FAILURE。如果这个函数不能接收到所预期的参数个数或者不能成功转换参数类型时就会抛出一些类似下面这样的错误信息:

Warning - ini_get_all() requires at most 1 parameter, 2 given

Warning - wddx_deserialize() expects parameter 1 to be string, array given

当然,每个错误信息都会带有错误发生时所在的文件名和行数的。
下面这份清单完整地列举出了我们可以指定接收的参数类型:

下面的一些字符在类型说明字符串(就是那个 char *type_spec)中具有特别的含义:

当然啦,熟悉这个函数的最好的方法就是举个例子来说明。下面我们就来看一个例子:

/* 取得一个长整数,一个字符串和它的长度,再取得一个 zval 值。 */
long l;
char *s;
int s_len;
zval *param;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "lsz", &l, &s, &s_len, &param) == FAILURE) {
return;
}
/* 取得一个由 my_ce 所指定的类的一个对象,另外再取得一个可选的双精度的浮点数。 */
zval *obj;
double d = 0.5;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "O|d", &obj, my_ce, &d) == FAILURE) {
return;
}
/* 取得一个对象或空值,再取得一个数组。如果传递进来一个空对象,则 obj 将被设置为 NULL。*/
zval *obj;
zval *arr;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "O!a", &obj, &arr) == FAILURE) {
return;
}
/* 取得一个分离过的数组。*/
zval *arr;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "a/", &arr) == FAILURE) {
return;
}
/* 仅取得前 3 个参数(这对可变参数的函数很有用)。*/
zval *z;
zend_bool b;
zval *r;
if (zend_parse_parameters(3, "zbr!", &z, &b, &r) == FAILURE) {
return;
}

参考API

script>