PHP中文手册《Zend API:深入 PHP 内核》
1. 摘要
2. 概述
3. 可扩展性
4. 源码布局
5. 自动构建系统
6. 开始创建扩展
7. 使用扩展
8. 故障处理
9. 关于模块代码的讨论
10. 接收参数
11. 创建变量
12. 使用拷贝构造函数复制变量内容
13. 返回函数值
14. 信息输出
15. 启动函数与关闭函数
16. 调用用户函数
17. 支持初始化文件(php.ini)
18. 何去何从
19. 参考:关于配置文件的一些宏
20. API宏
Zend API:深入PHP 内核(一)摘要
知者不言,言者不知。
――老子《道德经》五十六章
有时候,单纯依靠PHP“本身”是不行的。尽管普通用户很少遇到这种情况,但一些专业性的应用则经常需要将PHP的性能发挥到极致(这里的性能是指速度或功能)。由于受到PHP语言本身的限制,同时还可能不得不把庞大的库文件包含到每个脚本当中。因此,某些新功能并不是总能被顺利实现,所以我们必须另外寻找一些方法来克服PHP的这些缺点。
了解到了这一点,我们就应该接触一下PHP的心脏并探究一下它的内核-可以编译成PHP并让之工作的C代码-的时候了。
Zend API:深入PHP 内核(二)概述
“扩展PHP”说起来容易做起来难。PHP现在已经发展成了一个具有数兆字节源代码的非常成熟的系统。要想深入这样的一个系统,有很多东西需要学习和考虑。在写这一章节的时候,我们最终决定采用“边学边做”的方式。这也许并不是最科学和专业的方式,但却应该是最有趣和最有效的一种方式。在下面的小节里,你首先会非常快速的学习到如何写一个虽然很基础但却能立即运行的扩展,然后将会学习到有关Zend API 的高级功能。另外一个选择就是将其作为一个整体,一次性的讲述所有的这些操作、设计、技巧和诀窍等,并且可以让我们在实际动手前就可以得到一副完整的愿景。这看起来似乎是一个更好的方法,也没有死角,但它却枯燥无味、费时费力,很容易让人感到气馁。这就是我们为什么要采用非常直接的讲法的原因。
注意,尽管这一章会尽可能多讲述一些关于PHP内部工作机制的知识,但要想真的给出一份在任何时间任何情况下的PHP扩展指南,那简直是不可能的。PHP是如此庞大和复杂,以致于只有你亲自动手实践一下才有可能真正理解它的内部工作机制,因此我们强烈推荐你随时参考它的源代码来进行工作。
Zend 是什么?PHP又是什么?
Zend 指的是语言引擎,PHP指的是我们从外面看到的一套完整的系统。这听起来有点糊涂,但其实并不复杂(见图3-1 PHP 内部结构图)。为了实现一个WEB脚本的解释器,你需要完成以下三个部分的工作:
1.解释器部分:负责对输入代码的分析、翻译和执行;
2.功能性部分:负责具体实现语言的各种功能(比如它的函数等等);
3.接口部分:负责同WEB服务器的会话等功能。
Zend 包括了第一部分的全部和第二部分的局部,PHP包括了第二部分的局部和第三部分的全部。他们合起来称之为PHP包。Zend构成了语言的核心,同时也包含了一些最基本的PHP预定义函数的实现。PHP则包含了所有创造出语言本身各种显著特性的模块。
图3-1 PHP内部结构图
下面将要讨论PHP允许在哪里扩展以及如何扩展。
Zend API:深入 PHP 内核 (三)可扩展性
外部模块
外部模块可以在脚本运行时使用dl()函数载入。这个函数从磁盘载入一个共享对象并将它的功能与调用该函数的脚本进行绑定并使之生效。脚本终止后,这个外部模块将在内存中被丢弃。这种方式有利有弊:
优点:
外部模块不需要重新对 PHP 进行编译。
PHP 通过“外包”方式来让自身的体积保持很小。
缺点:
共享对象在每次脚本调用时都需要对其进行加载,速度较慢。
附加的外部模块文件会让磁盘变得比较散乱。
使用:dl()函数手动加载 或者php.ini文件当中添加扩展。
综上所述,外部模块非常适合开发第三方产品,较少使用的附加的小功能或者仅仅是调试等这些用途。为了迅速开发一些附加功能,外部模块是最佳方式。但对于一些经常使用的、实现较大的,代码较为复杂的应用,那就有些得不偿失了。
内建模块
内建模块被直接编译进 PHP 并存在于每一个PHP处理请求当中。它们的功能在脚本开始运行时立即生效。和外部 模块一样,内建模块也有一下利弊:
优点:
无需专门手动载入,功能即时生效。
无需额外的磁盘文件,所有功能均内置在PHP二进制代码当中。
缺点:
修改内建模块时需要重新编译 PHP。
PHP 二进制文件会变大并且会消耗更多的内存。
Zend API:深入 PHP 内核 (四)源码布局
在我们开始讨论具体编码这个话题前,你应该让自己熟悉一下 PHP 的源代码树以便可以迅速地对各个源文件进行定位。这也是编写和调试 PHP 扩展所必须具备的一种能力。
目录 | 内容 |
php-src | 包含了PHP主源文件和主头文件;在这里你可以找到所有的 PHP API 定义、宏等内容。(重要). 其他的一些东西你也可以在这里找到。 |
ext | 这里是存放动态和内建模块的仓库;默认情况下,这些就是被集成于主源码树中的“官方” PHP 模块。自 PHP 4.0开始,这些PHP标准扩展都可以编译为动态可载入的模块。(至少这些是可以的)。 |
main | 这个目录包含主要的 PHP 宏和定义。 (重要) |
pear | 这个目录就是“PHP 扩展与应用仓库”的目录。包含了PEAR 的核心文件。 |
sapi | 包含了不同服务器抽象层的代码。 |
TSRM | Zend 和 PHP的 “线程安全资源管理器” (TSRM) 目录。 |
Zend | 包含了Zend 引擎文件;在这里你可以找到所有的 Zend API 定义与宏等。(重要) |
当然,讨论PHP包里面全部每一个文件无疑是超出了本章的范围,但你还是应该仔细看一
必看文件:
main/php.h: 位于 PHP 主目录。这个文件包含了绝大部分 PHP 宏及 API 定义。
Zend/zend.h: 位于 Zend 主目录。这个文件包含了绝大部分 Zend 宏及 API 定义。
Zend/zend_API.h: 也位于 Zend 主目录,包含了 Zend API 的定义。
除此之外,你也应该注意一下这些文件所包含的一些文件。举例来说,哪些文件与 Zend 执行器有关,哪些文件又为 PHP 初始化工作提供了支持等等。在阅读完这些文件之后,你还可以花点时间再围绕 PHP 包来看一些文件,了解一下这些 文件和模块之间的依赖性――它们之间是如何依赖于别的文件又是如何为其他文件提供支持的。同时这也可以帮助你适应一 下 PHP 创作者们代码的风格。要想扩展 PHP,你应该尽快适应这种风格。
扩展规范
Zend 是用一些特定的规范构建的。为了避免破坏这些规范,你应该遵循以下的几个规则:
宏
几乎对于每一项重要的任务,Zend 都预先提供了极为方便的宏。在下面章节的图表里将会描述到大部分基本函数、结构和宏。这些宏定义大多可以在 Zend.h 和 Zend_API.h 中找到。我们建议您在学习完本节之后仔细看一下这些文件。(当 然你也可以现在就阅读这些文件,但你可能不会留下太多的印象。)
内存管理
资源管理仍然是一个极为关键的问题,尤其是对服务器软件而言。资源里最具宝贵的则非内存莫属了,内存管理也必须极端小心。内存管理在 Zend 中已经被部分抽象,而且你也应该坚持使用这些抽象,原因显而易见:由于得以抽象,Zend 就可以完全控制内存的分配。Zend 可以确定一块内存是否在使用,也可以自动释放未使用和失去引用的内存块,因此就可 以避免内存泄漏。下表列出了一些常用函数:
函数: 描述
函数 | 描述 |
emalloc() | 用于替代 malloc()。内存分配函数 |
efree() | 用于替代 free()。释放已分配的块 |
estrdup() | 用于替代 strdup()。将串拷贝到新建的位置处 |
estrndup() | 用于替代strndup()。速度要快于 estrdup() 而且是二进制安全的。如果你在复制之前预先知道这个字符串的长度那就推荐你使用这个函数。 |
ecalloc() | 用于替代 calloc()。分配主存储器 |
erealloc() | 用于替代 realloc()。重新分配主存 |
emalloc(), estrdup(), estrndup(), ecalloc(), 和 erealloc() 用于申请内部的内存,efree() 则用来释放这些前面这些函数 申请的内存。e*() 函数所用到的内存仅对当前本地的处理请求有效,并且会在脚本执行完毕,处理请求终止时被释放。
目录与文件函数
下列目录与文件函数应该在 Zend模块内使用。它们的表现和对应的 C 语言版本完全一致, 只是在线程级提供了虚拟目录的支持。
Zend 函数 | 对应的 C 函数 |
V_GETCWD() | getcwd() 取当前工作目录 |
V_FOPEN() | fopen() 打开一个流 |
V_OPEN() | open() 打开一个文件用于读或写 |
V_CHDIR() | chdir() 改变工作目录 |
V_GETWD() | getwd() 取当前工作目录 建议使用getcwd() |
V_CHDIR_FILE() | 将当前的工作目录切换到一个以文件名为参数的该文件所在的目录。 |
V_STAT() | stat() 读取打开文件信息 (取得文件状态) |
V_LSTAT() | stat()类似,当文件为符号连接时, lstat()会返回该link 本身的状态。 |
字符串处理
在 Zend 引擎中,与处理诸如整数、布尔值等这些无需为其保存的值而额外申请内存的简单类型不同,如果你想从一个函数返回一个字符串,或往符号表新建一个字符串变量,或做其他类似的事情,那你就必须确认是否已经使用上面的 e*() 等函数为这些字符串申请内存。(你可能对此没有多大的感觉。无所谓,现在你只需在脑子里有点印象即可,我们稍后就会再次回到这个话题)
复杂类型
像数组和对象等这些复杂类型需要另外不同的处理。它们被出存在哈希表中,Zend 提供了一些简单的 API 来操作这些类型 。
Zend API:深入 PHP 内核 (五)自动构建系统
PHP 提供了一套非常灵活的自动构建系统(automatic build system),它把所有的模块均放在 Ext 子目录下。每个模块除自身的源代码外,还都有一个用来配置该扩展的 config.m4 文件(详情请参见http://www.gnu.org/software/m4/manual/m4.html)。
包括 .cvsignore 在内的所有文件都是由位于 Ext 目录下的 ext_skel 脚本自动生成的,
它的参数就是你想创建模块的名称。这个脚本会创建一个与模块名相同的目录,里面包含了与该模块对应的一些的文件。
下面是操作步骤:
./ext_skel –extname=my_module
Creating directory my_module
Creating basic files: config.m4 .cvsignoremy_module.c php_my_module.h CREDITS EXPERIMENTAL tests/001.php
t my_module.php [done].
To use your new extension, you will have toexecute 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
(注:以上操作貌似是跟随php源码编译成内建模块。)
(外建模快步骤:)
1. vim config.m4
2. vim my_module.c my_module.h
3. /usr/local/php/bin/phpize[--clean]
4. ./configure --with-php-config=/usr/local/php/bin/php-config
5. make && make install
6. vim php.ini extension = "my_module.so"
7./usr/local/php/sbin/php-fpm restart
8. php -f my_module.php
这些指令就会生成前面所说的那些文件。为了能够在自动配置文件和构建程序中包含新增加的模块,你还需要再运行一次 buildconf 命令。这个命令会通过搜索 Ext 目录和查找所有 config.m4 文件来重新生成 configure 脚本。默认情况下的的 config.m4 文件如例 3-1 所示,看起来可能会稍嫌复杂:
dnl $Id: build.xml,v 1.1 2005/08/21 16:27:06 goba Exp $
dnl config.m4 for extension my_module
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(my_module, for my_module support, dnl Make surethat the comment is aligned:
dnl [ --with-my_module Include my_module support])
dnl Otherwise use enable:
dnl PHP_ARG_ENABLE(my_module, whether to enable my_module support,
dnl Make sure that the comment is aligned:
dnl [ --enable-my_module Enable my_module support])
if test $PHP_MY_MODULE != “no”; then
dnl Write more examples of tests here...
dnl # –with-my_module -> check with-path
dnl SEARCH_PATH = /usr/local /usr # you might want to change this
dnl SEARCH_FOR=/include/my_module.h you most likely want to changethis
dnl if test -r $PHP_MY_MODULE/; then # path given as parameter
dnl MY_MODULE_DIR=$PHP_MY_MODULE
dnl else # search default path list
dnl AC_MSG_CHECKING([for my_module files in default path])
dnl for i in $SEARCH_PATH ; do
dnl if test -r $i/$SEARCH_FOR; then
dnl MY_MODULE_DIR=$i
AC_MSG_RESULT(found in $i)
dnl fi
dnl done
dnl fi
dnl
dnl if test -z "$MY_MODULE_DIR"; then
dnl AC_MSG_RESULT([not found])
dnl AC_MSG_ERROR([Please reinstall the my_module distribution])
dnl fi
dnl # –with-my_module -> add include path
dnl PHP_ADD_INCLUDE($MY_MODULE_DIR/include)
dnl # –with-my_module -> chech for lib and symbolpresence
dnl LIBNAME=my_module # you may want to change this
dnl LIBSYMBOL=my_module # you most likely want to change this
dnl PHP_CHECK_LIBRARY($LIBNAME,$LIBSYMBOL,
dnl [ dnl PHP_ADD_LIBRARY_WITH_PATH($LIBNAME, $MY_MODULE_DIR/lib,MY_MODULE_SHARED_LIBADD)
dnl AC_DEFINE(HAVE_MY_MODULELIB,1,[ ])
dnl ],[
dnl AC_MSG_ERROR([wrong my_module lib version or lib not found])
dnl ],[
dnl -L$MY_MODULE_DIR/lib -lm -ldl
dnl ])
dnl
dnl PHP_SUBST(MY_MODULE_SHARED_LIBADD)
PHP_NEW_EXTENSION(my_module, my_module.c, $ext_shared)
fi
文件解析:
1 dnl 前缀的都是注释
2 config.m4 文件负责在配置时解析 configure 的命令行选项。这就是说它将检查所需的外部文件并且要 做一些类似配置与安装的任务。
3 默认的配置文件将会在 configure脚本中产生两个配置指令:–with-my_module 和 –enable-my_module。当需要引用外部文件时使用第一个选项(就像用 –with-apache 指令来引用Apache 的目录一样)。第二个选项可以让用户简单的决定是否要启用该扩展。不管你使用哪一个指令, 你都应该注释掉另外一个。也就是说,如果你使用了–enable-my_module, 那就应该去掉–with-my_module。反之亦然。(注:第二个选项'可能'是用于内建模块)
4 默认情况下,通过 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 指令。
5 另外请注意在修改config.m4文件后需要重新运行buildconf命令(外建模快使用phpize重新编译)
6 说明:
PHP_REQUIRE_CXX 用于指定这个扩展用到了C++;
PHP_ADD_INCLUDE 指定PHP扩展模块用到的头文件目录;
PHP_CHECK_LIBRARY 指定PHP扩展模块PHP_ADD_LIBRARY_WITH_PATH定义以及库连接错误信息等;
PHP_ADD_LIBRARY(stdc++,"",EXTRA_LDFLAGS)用于将标准C++库链接进入扩展
PHP_SUBST(EXTERN_NAME_SHARED_LIBADD) 用于说明这个扩展编译成动态链接库的形式;
PHP_NEW_EXTENSION 用于指定有哪些源文件应该被编译,文件和文件之间用空格隔开;
ext_skel默认生成的模块框架是针对C的,我们要使用C++进行PHP扩展,那除以上的 PHP_REQUIRE_CXX,PHP_ADD_LIBRARY两个宏必需外,还要把my_module .c改名成 my_module.cpp。
(注:使用c++开发需要额外到一些操作:)
将引用C 开发的文件时使用extern“C”{}包起来
并使用BEGIN_EXTERN_C() END_EXTERN_C()将ZEND_GET_MODULE(my_module)函数包起来
编译安装
/usr/local/php/bin/phpize
./configure--with-php-config=/usr/local/php/bin/php-config
make&& make install
vim php.ini
extension = "my_module.so"
重启
/usr/local/php/sbin/php-fpm restart
测试
php -r 'var_dump(my_module("xxx"));'
或者 php my_module.php
Zend API:深入PHP内核 (六)开始创建扩展
我们先来创建一个非常简单的扩展,这个扩展除了一个将其整形参数作为返回值的函数外几乎什么都没有。下面(“例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", ¶meter) == 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_experimentsBOOK: 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 <your_object_file> <your_c_file> |
连接 | cc -shared -L/usr/local/lib -rdynamic -o <your_module_file> <your_object_file(s)> |
编译命令只是简单的让编译器产生一些中间代码(不要忽略了-fpic 参数),然后又定义了COMPILE_DL 常量来通知代码这是要编译为一个动态可加载的模块(通常用来测试,我们稍后会讨论它)。这些选项后面是一些编译这些源代码所必须包含的库文件目录。
注意:本例中所有include 的路径都是都是Ext 目录的相对路径。如果您是在其他目录编译的这些源文件,那么还要相应的修改路径名。编译所需要的目录有 PHP 目录,Zend 目录和模块所在的目录(如果有必要的话)。
连接命令也是一个非常简单的把模块连接成一个动态模块的命令。
你可以在编译指令中加入优化选项,尽管这些已经在样例中忽略了(不过你还是可以从前面讨论的 make 模版文件中发现一些)。
注意,手动将模块静态编译和连接到PHP 二进制代码的指令很长很长,因此我们在这里不作讨论。(手动输入那些指令是很低效的。)
Zend API:深入 PHP 内核 (七)使用扩展
根据你所选择的不同的构建过程,你要么把扩展编译进一个新的PHP的二进制文件,然后再连接到Web服务器(或以CGI模式运行),要么将其编译成一个.so(共享库)文件。如果你将上面的样例文件first_module.c编译成了一个共享库,那么编译后的文件应该是first_module.so。要想使用它,你就必须把他复制到一个PHP能访问到的地方。如果仅仅是为了测试的话,简单起见,你可以把它复制到你的htdocs目录下,然后用“例3.3 first_module.so的一个测试文件”中的代码来进行一下测试。如果你将其直接编译编译进PHP二进制文件的话,那就不用调用dl()函数了,因为这个模块的函数在脚本一开始运行就生效了。
警告:
为了安全起见,你不应该将你的动态模块放入一个公共目录。即使是一个简单的测试你可以那么做,那也应该把它放进产品环境中的一个隔离的目录。
例3.3 first_module.so的一个测试文件
<?php
// remove next comment if necessary
// dl("first_module.so");
$param = 2
$return = first_module($param);
print("We sent '$param' and got'$return'");
?>
调用这个测试文件,结果应该输出为:We sent ’2′ and got ’2′。
若有需要,你可以调用dl()函数来载入一个动态可加载模块。这个函数负责寻找指定的共享库并进行加载使其函数在PHP中生效。这个样例模块仅输出了一个函数first_module(),这个函数仅接受一个参数,并将其转换为整数作为函数的结果返回。
如果你已经进行到了这一步,那么,恭喜你,你已经成功创建了你的第一个PHP扩展!
Zend API:深入 PHP 内核 (八)故障处理
实际上,在对静态或动态模块进行编译时没有太多故障处理工作要做。唯一可能的问题就是编译器会警告说找不到某些定义或者类似的事情。如果出现这种情况,你应该确认一下所有的头文件都是可用的并且它们的路径都已经在编译命令中被指定。为了确保每个文件都能被正确地定位,你可以先提取一个干净的 PHP源码树,然后在 Ext 目录使用自动构建工具来创建这些文件。用这种方法就可以确保一个安全的编译环境。假如这样也不行,那就只好试试手动编译了。
PHP 也可能会警告说在你的模块里面有一些未定义的函数。(如果你没有改动样例文件的话这种情况应该不会发生。) 假如你在模块中拼错了一些你想访问的外部函数的名字, 那么它们就会在符号表中显示为“未能连接的符号”。这样在 PHP动态加载或连接时,它们就不会运行—在二进制文件中没有相应的符号。为了解决这个问题,你可以在你的模块文件中找一下错误的声明或外部引用。注意,这个问题仅仅发生在动态可加载模块身上。而在静态模块身上则不会发生,因为静态模块在编译时就会抛出这些错误。