编写PHP扩展一:PHP与Zend介绍
译:我在江湖丢了
原文地址:http://devzone.zend.com/303/extension-writing-part-i-introduction-to-php-and-zend/
博客地址:http://lg.uuhonghe.com/index/view?id=3
简介
何为扩展
生命周期
Hello World
创建自己的扩展
ini 配置
全局变量
设置 ini 为全局变量
完整性检查
然后哩?
简介
如果你正阅读本教程,那你可能对PHP语言的扩展编写颇感兴趣。如果不是。。。也许你读完之后会发现你对这个之前不知道的东西产生了兴趣。
本文假设读者基本了解PHP语言和用C写的PHP解释器。
让我们先确认一下你为何想写一个PHP扩展:
1、由于语言内核的抽象深度使你有一些库和系统调用无法使用PHP直接完成。
2、你希望PHP以一些不寻常的方法实现自身行为。
3、你已经写了一堆PHP代码,但你知道它可以跑得更快。
4、你有个实现了特别机智点子的代码想卖,但更重要的是你要卖的代码要能跑但不能在源码里看到。
这些都是非常正当的理由了,但要创建一个扩展,首先你得理解什么是扩展。
何为扩展?
如果你写过PHP,那你一定用过扩展了。只需要小小的几个扩展,一切PHP里的用户空间功能都在这个或那个扩展的函数组里了。大量这些函数都在标准扩展的一部分——标准扩展总共的400多个。PHP源码里捆绑了86个扩展,平均每个有大概30个函数。掐指一算,总约2500个函数。如果这还不够,PECL仓库里还提供了100个以上的附加扩展,更多的可以再网上别的地方找到。
“这些函数都在扩展里,那还有什么?”你会问,“它们扩展了什么?PHP的核心是什么?”。
PHP的内核由两部分组成。在最底层你能找到Zend引擎(简称ZE)。ZE把人能识别的脚本解析为机器识别的符号,并且在进程空间里运行这些符号。ZE同时处理内存管理,变量域和函数调用。这种区分方式的另一部门是PHP内核。PHP内核处理通信、连接和SAPI层(Server Application Programming Interface, 通常也用于指主机环境,如Apache, IIS, CLI, CGI 等),还提供了控制层对 safe_mode 和 open_basedir 的统一检测,还有和用于文件和网络I/O的用户空间函数fopen(), fread()和fwrite()相关的串流层。
生命周期
当一个SAPI启动,例如在 /usr/local/apache/bin/apachectl start 的响应中,PHP以初始化它的内核子系统开始。在这个启动全程将结束之际,会加载每个扩展的内核并调用他们的模块初始化例程(MINIT)。
这给每个扩展一个机会去初始化内部变量,分配资源,注册资源句柄,并且使用ZE注册它的函数,以便当脚本调这些函数时,ZE知道运行哪段代码。
接下来,PHP等待SAPI层来请求一个页面去处理。在CGI或者CLI SAPI的情况下,这会直接发生并且只发生一次。在Apache, IIS 或者其他一些成熟的 web 服务器SAPI中,这是在远程用户请求时发生,并且或发生多次,可能伴随着并发。无论请求如何到达,PHP以通知ZE建立一个供脚本运行的环境为开始,然后调用每个扩展的请求初始化(RINI)函数。RINI给扩展一个机会去建立自己特定的环境变量,分配请求特定的资源,或执行其他任务如审计。RINI函数行为的一个主要例子是在sessions 扩展里,如果session.auto_start项是开启的, RINI将会自动触发用户空间session_start()函数并且预设置$_SESSION变量。
一旦请求被初始化了,ZE通过翻译PHP脚本为tokens,最后转为opcodes来接管,opcodes能够单步调试和执行。
当其中一个opcodes包含的一个扩展方法被调用,ZE会捆绑上该方法的参数,并且临时性地交出控制直接方法完成。
在脚本运行完成之后,PHP调用每个扩展的请求关闭(RSHUTDOWN)函数来做最后的清理工作(例如保证会话变量到磁盘)。接着,ZE运行一个清理进程(被称为垃圾回收),该进程能有效在请求前期中使用的每个变量上执行unset()操作.
操作完成后,PHP等待SAPI请求另一个文档或者信号关闭。在CGI和CLI SAPI情况下,是没有“下一个请求”的,因此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) |
* pemalloc()
家族包含一个
"persistent" 标志来让他们与他们的未持续性部分相对应。例如:
safe_emalloc(1234)
和 safe_pemalloc(1234,
0)
一样。
** safe_emalloc()
和 safe_pemalloc()
(在
PHP5中) 加了个检查避免整型溢出。
构建一个开发环境
现在我们已经学习了PHP和Zend引擎工作方式背后的一些理论,我猜你想运足功力开始开发了。然后在你开始前,必须得收集一些必备的工作来搭建一个满足你需要的开发环境。
首先,你需要PHP本身,因为这一系列开发工具都离不开PHP。如果你还对使用源码搭建PHP不熟悉,推荐你看下这篇文章先:http://www.php.net/install.unix.
(使用Windows开发PHP扩展的文章稍后给出)。虽然用你的linux发行版里的二进制包安全是非常诱人的,但是这些会遗漏两项在开发过程中十分方便的./configure选项。第一个就是 --enable-debug
。 这个选项令PHP在编译时加入可执行的符号信息,以便当发生一个段错误时,你能从内核存储中得到它并且使用gdb跟踪段错误发生的地方和原因。另外一个选项取决于你开发使用的PHP的版本。在PHP4.3中这选项名为--enable-experimental-zts
,在PHP5或更高的版本中为--enable-maintainer-zts
。该先项让PHP在多线程环境中思考自己的操作,让你捕获一些常见在非线程环境下是无害而导致您的扩展在多线程环境下用不了的异常。只要你在编译PHP时使用了这些额外的选项安装在你的开发服务器(或者工作站)上,你可以开始编写你的第一个扩展了。
Hello World
任何没有完成Hello World应用的程序介绍都是不完整的。因此,下面将制作一个只有一个返回字符串"Hello World"的函数的扩展。在PHP代码里你可能这样写:
<?php function hello_word(){ return "Hello World"; } ?>
现在我们使用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并且建立一个对话让它们通信的。只有最后4行代码可以称之后"real code",用于执行用户空间层脚本能交互的任务。确实这一层的代码看上去很像我们之前看的PHP代码也容易看懂:
1、声明一个方法 hello_world
2、让这个方法返回一个字符串:"Hello World"
3、。。呃。。。1?这个1几个意思?
回忆一下,ZE包含了一套复杂的内存管理层,能确保分配的资源在脚本退出时被释放。然而在内存管理的掌控下,释放同样的块两次是非常大的禁忌。这种行为通常称为"double freeing",是一个常见的段错误的原因,涉及到一个正在调用的程序访问一个不再属于它的内存块。同样的,你不希望允许ZE去释放一个存活于程序空间并且其数据块被其他进程占用的静态字符串buffer(例如我们示例中的"Hello World")。 RETURN_STRING()
能够假设任何一个传递给它的字符串都被复制过以便可以安全的释放;但是因为在内核函数里分配内存给一个字符串不太常见,所以动态的填充它,然后返回, RETURN_STRING()
允许用户指定是不是必须复制字符串。为了进一步说明这个概念,下面这个代码片段作用和上面相应部分一样:
PHP_FUNCTION(hello_world) { char *str; str = estrup("hello World"); RETURN_STRING(str, 0); }
在此版本中,我们手动地分配内存给"Hello World"这个串,并且最终传回调用脚本,然后把内存传给 RETURN_STRING()
, 第二个参数值为0表示不需要复制一份,可以直接使用传递过来的。
编译你的扩展
本练习的最后一步就是将你的扩展编译为一个动态可加载模块。如果你把上面的示例原封不动的抄下来,那么只需要在ext/hello/目录下运行下面三步命令就行:
phpize ./configure --enable-hello (译者注:如果编译PHP的时候使用了 --prefix 参数,此处要加上 --with-php-config 选项, 如笔者编译PHP时使用的是 ./configure --prefix=/use/local/phpdev/ 此处命令应使用 ./configure --enable-hello --with-php-config=/usr/local/phpdev/bin/php-config) make
运行完上述三个命令之后,你应该在ext/hello/modules/下找到一个 hello.so 文件。(译者注:如果在make时报错: error: unknown type name "function_entry" ,可以把 "function_entry" 改为 "zend_function_entry",参见:https://bugs.php.net/bug.php?id=61479
)。现在,就像其他PHP扩展一样,你可以把你的扩展拷贝到扩展目录(默认为,/usr/local/lib/php/extensions/,可以通过php.ini确认)然后在php.ini里加上extension=hello.so一行可以在以触发它在程序启动时被加载到了。对于CGI/CLI SAPIs 来说,启动就指下一次运行PHP;而对我web server SAPIs如Apache来说,启动指下次web server重启。让我们试下运行下面命令:
$ php -r "echo hello_world();"
如果一切顺利,你现在应该能看到这段代码输出"Hello World"了,因为你的扩展里的"hello_world()"返回了一个字符串"Hello World",而echo命令会原封不动地显示传递给他的参数(此处即为该函数的返回值)。
其他标量也可用类似的方式返回,使用RETURN_LONG()
返回整型, RETURN_DOUBLE()
返回浮点数, RETURN_BOOL()
返回true/false值,RETURN_NULL()
你猜到了,返回 NULL
值。让我们来逐行分析一下在hello.c中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而不是简单的执行make,来确保所有源文件重建。再次声明,现在的改动上述这些步骤是非必须的,但是会更安全更清晰。模块建好后,再拷贝到你的扩展目录下,替换旧版本。
这个时候你可以再次调用你的PHP解析器,运行一段简单的脚本来试试你刚刚添加的方法。事实上,为何不现在不试呢?我等着呢。。。
试完了?很好。如果你使用 var_dump()
而不是 echo
来查看每个函数的返回值你也许会发现"hello_bool()"返回true。这就是值1在"RETURN_BOOL()"所表示的。就像在PHP脚本里,整型值0
等于 FALSE
, 但是其他所有的整形值都等于 TRUE
。扩展作者经常使用"1"作这一系列的约定,我们希望你也这样,但是也不必拘泥于此。为了更具可读性, RETURN_TRUE
和 RETURN_FALSE
两个宏也可使用;现在再次修改"hello_bool()",这次使用"RETURN_TRUE":
PHP_FUNCTION(hello_bool){ RETURN_TRUE; }
注意这里没有使用括号哦。 RETURN_TRUE
和RETURN_FALSE
与其他宏RETURN_*()
格式的变体,所以这里注意别被捕获。
你也许注意到上面这些代码样品我们都没有传0和1什来表示这些值是否需要被拷贝。这是因为没有额外的内存(变量容器之外——我们将在第2部分深入)需要被分配或释放,因为这些标量都很简单很小。
还有另外三种返回类型: RESOURCE
(例如"mysql_connect()","fsockopen()"和"ftp_connect()"的返回值), ARRAY
(也称为HASH
),还有 OBJECT
(通过关键词new返回)。我们将会在第二章更深入变量学习的时候了解这一系列。
INI Settings
Zend 引擎提供了两个方式处理 INI
变量。
我们先看一下较简单的一种,而更全面、更复杂的方式,等以后你有机会接触全局变量再说。
现在我们想在php.ini中定义一个变量,"hello.greeting", 用来处理你在"hello_function()"函数里用来打招呼的变量。你需要在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
上加一个 #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); }
注意,你复制了来自 INI_STR()
. 的返回值。因为这涉及到PHP变量栈都关心的问题,这是一个静态变量。实际上,如果你试图去修改这个方法返回的值,PHP运行环境会变得不稳定甚至可能崩溃。
首次修改的部分包含了两个你需要熟悉的方法:: MINIT
, 和 MSHUTDOWN
。正如上面提到的,这两个方法分别是在SAPI层初始启动和最后关闭时调用的。他们不在请求之间也不在请求之中调用。在这个示例中,你用他们注册在你扩展中定义的php.ini选项。在本文的后面,你将学习如何使用"MINIT"和"MSHUTDOWN"方法来注册资源、对象和流句柄。
在你的方法hellow_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()
的第一个参数是一个包含了php.ini的选项的名称的字符串。为避免命名空间碰撞,你必须使用和你方法相同的约定;那就是在所有值前加上你扩展的名字为前缀,比如刚才的"hello.greeting",作一个惯例的约定,一度曾将扩展名和ini设置名中更具描述性的部分和分开。
第二个参数是初始值,并且通常是作为char*串不管是不是数字。这主要是因为实际上.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文件中设置,也能通过Apache指令在.htaccess文件中修改,但是不允许通过 ini_set()
修改。再者有PHP_INI_SYSTEM
,意思是该值只能在php.ini文件中修改,其他地方都不行。
我们就此跳过第四个参数,只点出该值用来传入一个回调方法便以ini配置无论在何时被修改时调用,比如当使用 ini_set()
修改时。这能令扩展更精确的控制设置被修改,或者通过修改一个新的配置来触发相应的行为。
全局变量
通常,扩展需要在特定的请求里跟踪变量的值,使之独立于并发请求。在无线程的SAPI中这可能比较简单:只需要在源文件中声明一个全局变量,在需要时调用。然而麻烦在于PHP被设计运行于线程级的web服务器(如Apache 2 和 IIS),这就需要保证一个线程中的全局变量与其他线程中的分离。PHP通过使用TSRM(Thread Safe Resource Management)抽象层,大大地简化了这一操作,有时被称为ZTS(Zend Thread Safety)。实际 上,到现在你已经使用了一部分TSRM了,虽然你对其不甚了解。(不要急着去搜索,通过这一系列的进展,你会发现它无处不在。)
创建线程安全全局变量的第一步,跟创建其他全局变量一样,声明它。为了实现这个例子,你将声明一个以 long
型开始,值为 0
的全局变量。每次hello_long()
方法被调用时,我们增值该变量变返回。下面这段php_hello.h中的代码放在#define
PHP_HELLO_H
模块后面:
#ifdef ZTS #include "TSRM.h" #endif ZEND_BEGIN_MODULE_GLOBALS(hello) login counter; ZEND_END_MODULE_GLOBALS(hello) #ifdef ZTS #define HELLO_G(v) TSRM(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中添加下面这段代码到include 模块后:
#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 };