什么是扩展?
每个PHP程序员必接触过扩展,PECL库提供超过100多个扩展,比较常用的memcache,apc,mysqli等。在php.ini文件中,extension_dir指示就是扩展路径。
为什么要扩展?
PHP的设计理念,开源语言,方便各个社区自行开发丰富的功能,互不影响,而且与PHP内部无耦合。
有些功能脚本语言无法实现或者实现代价较高,比如常驻内存的应用,以及算法复杂度较高的已有C程序,又不想用PHP重写。
出于效率的考虑。扩展由于实际上执行的是C程序,因此提高效率。
扩展如何执行?
PHP语言本身的结构分成两大部分:ZEND和PHP core。
zend可以比喻成车的引擎,负责php代码的解释和执行。PHP是车的框架,扩展是轮子,必须要依靠相应的扩展才能实现真正的功能。
Apache启动后(apachectl start),将初始化PHP core,然后加载每个扩展代码,并且调用每个扩展的MINIT例程,使得每个 扩展可以初始化内部变量,分配资源,注册资源处理器,向Zend注册自己的函数,以便于PHP代码中调用这些代码的时候可以知道这些扩展代码的位置。之 后,等待到达SAPI(在本文中,指apache)的请求,到达后,PHP要求zend初始化执行环境,PHP调用每个扩展的RINIT函数,使得扩展设 立自己的特定的环境变量,根据请求分配资源,执行其他任务。随后控制权转到Zend。Zend将代码解释执行,如果中间遇到扩展函数,Zend将变量绑定 给扩展,然后控制权转给扩展函数执行。扩展执行完毕以后,PHP调用每个扩展的RSHUTDOWN函数进行清理工作,Zend进行垃圾回收。对扩展期间的 变量进行unset。如果apache关闭,则调用每个扩展的模块关闭函数MSHUTDOWN,最后关闭core。
通 过执行过程,知道了扩展中的几个特殊函数:MINIT,MSHUTDOWN,RINIT,RSHUTDOWN。也知道了Zend既然负责执行PHP代码, 有它自己的内存管理,所以在扩展中一定要注意,应使用zend 或者PHP的API进行内存分配或者字符串拷贝等操作。
以下是Zend的内存分配API,后文会有实例演示:
左 侧是C的内存管理函数,中间和右侧是Zend的API,非持久性资源指页面请求结束后即会释放的资源,持久性资源则是无论页面请求是否结束都一直存在的资 源。必须使用zend的内存管理API,原因是zend依靠给变量打上自己的标记来表示是否需要在页面请求结束后释放,如果使用C的API分配,会导致 zend提前释放,导致crush。
怎样写扩展?
在php的源码下,存在一个ext/ext_skel脚本,该脚本负责生成扩展的框架代码。
假设在linux系统,web server为apache,当前路径下存在php源码
./php-5.2.12/ext/ext_skel –extname helloworld –skel=./php-5.2.12/ext/skeleton/
将在当前路径下创建helloworld目录。
helloworld目录下,存在3个文件:
php_helloworld.h helloworld.c helloworld.php
helloworld.php是测试扩展是否可用的PHP代码,与C程序一样,扩展函数的声明在php_helloworld.h,扩展函数的实现helloworld.c中,所以实质上,扩展是C程序,里面使用了大量Zend的宏和API。
运行扩展
在helloworld目录下,运行phpize,检查当前zend版本,PHP API版本,ZEND API版本信息。
随后生成config.m4以及config.w32(windows底下使用),以及configure程序。
打开config.m4,打开
PHP_ARG_ENABLE(helloworld, whether to enable helloworld support
[ --enable-helloworld Enable helloworld support])
AC_DEFINE(HAVE_HELLOWORLDLIB,1,[Whether you have helloworld])
随后使用./configure –enable-helloworld
此时自动生成makefile,然后make,将在modules目录下存在helloworld.so。
mv helloworld.so到php的扩展路径下,重启apache
运行php helloworld.php,就可以看见:configurations字样,表示扩展已经成功。
扩展语法——数据类型
表 1:类型和用在zend_parse_parameters()中的字母代码
类型
代码
变量类型
Boolean
b
zend_bool
Long
l
long
Double
d
double
String
s
char*, int
Resource
r
zval*
Array
a
zval*
Object
o
zval*
zval
z
zval*
还有NULL,array object都表达成zend内部数据结构zval
扩展语法——基本语句
打开helloworld.c:
#include “php.h” ——每个扩展都必须包含的头文件,里面包含了zend的数据结构以及API定义
#include “php_ini.h” ——如果需要使用php.ini定义的变量,就需要包括
zend_function_entry helloworld_functions[] = {….}——这里声明自己的扩展函数,其中必须以PHP_FE(函数名),最后以{NULL, NULL, NULL}结束,向zend注册扩展函数。实际上PHP_FE宏将自动生成一个这样的函数声明:
void zif_函数名 (INTERNAL_FUNCTION_PARAMETERS),其中INTERNAL_FUNCTION_PARAMETERS是固定的,是zend执 行需要的信息,包括参数的个数,zval * return_value,以及返回的结果变量指针。
zend_module_entry helloworld_module_entry = {…}——这里声明上文提到的扩展的特殊函数,MINIT,MSHUTDONW,RINIT,RSHUTDOWN,注意第三项必须是上面的 helloworld_functions,即zend_function_entry
进入扩展函数本身的实现来看看:
PHP_FUNCTION(confirm_helloworld_compiled) ——每个扩展函数必须以PHP_FUNCTION宏包裹,括号里面是函数名。
char *arg = NULL;
int arg_len, len;
char *strg;
回忆前面提到的扩展类型,其中有一个string类型,对应到扩展里面是char* 和int,含义为如果要在扩展中声明一个字符串,就得声明两个变量,一个是char *指针,初始化必须是空,另外就是int len,即字符串的长度。
来看看,如何从用户的PHP代码中给扩展传递参数,我们知道,PHP是弱类型语言,没有参数的类型概念,但C有,因此需要使用zend的一个非常常用的API:zend_parse_parameters
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, “s”, &arg, &arg_len) == FAILURE) {…}——将用户参数转换成相应的zend类型,并将参数的值放入前面声明的char *变量中,看起来很困惑,char *是空的,不要担心,zend分配了内存保存,并且在arg_len中保存了字符串的长度。如果参数的类型和期待的类型不一致,将导致FAILURE返回 值。
len = spprintf(&strg, 0, “Congratulations! You have successfully modified ext/%.78s/config.m4. Module %.78s is now compiled into PHP.”, “helloworld”, arg);——使用了PHP的api spprintf,内部实现其实在zend中会创建缓冲区,建议包括printf这些流处理函数都使用php的api。
RETURN_STRINGL(strg, len, 0);——返回值也不能使用C的return strg语法,必须使用Zend规定的宏返回。
结合前面介绍的扩展基本数据类型,RETURN系列包括RETURN_LONG,RETURN_STRING,RETURN_BOOL,RETURN_NULL,RETURN_DOUBLE。
到这里,其实已经阐述完毕PHP扩展编写的基本要素。总结PHP扩展编写的步骤如下:
在php_helloworld.h中,增加:PHP_FUNCTION(confirm_helloworld_compiled);——声明
在helloworld.c中,增加:
zend_function_entry helloworld_functions[] = {增加一个PHP_FE(confirm_helloworld_compiled)}
增加confirm_helloworld_compiled的实现:PHP_FUNCTION(confirm_helloworld_compiled){}
并且已经介绍了最基本的如何获取参数,如何返回值。
扩展语法——MINIT,MSHUTDOWN,RINIT,RSHUTDOWN
我 们回到前面提过的几个特殊函数,前面说过了这些函数的运行时机。下面展示这些函数的应用场合。可能我们会有这样的应用,只需要在apache启动的时候初 始化一个值,然后所有页面请求共用该变量,或者需要一个页面请求内可见的变量,在一个页面请求内反复调用时该变量的值可以保持。前者需要使用 MINIT,MSHUTDOWN,后者则需要RINIT和RSHUTDOWN。
打开helloworld.c,发现框架代码有这样的代码:
/* {{{ PHP_INI
*/
/* PHP_INI_BEGIN()……PHP_INI_END()*/
只是被注释了!如果我们需要一个所有页面请求都共用的变量,需要将注释去掉。
PHP_INI_BEGIN()
PHP_INI_ENTRY(“helloworld.greeting”, “haha!jean”, PHP_INI_ALL,
NULL)——告诉扩展注册一个php.ini中可以有的变量helloworld.greeting,初始值为haha!jean,如果php.ini
存在该变量,则取php.ini中的值,PHP_INI_ALL表示任何时候PHP代码中可以通过ini_set来改变该变量值。
PHP_INI_END()
生成的扩展框架代码中,天然有PHP_MINIT_FUNCTION(helloworld),不过里面只是RETURN
SUCCESS而已。我们加上REGISTER_INI_ENTRIES();,同样的,在
PHP_MSHUTDOWN_FUNCTION(helloworld),加上相反的操作,注销变量
UNREGISTER_INI_ENTRIES();
新增加一个函数PHP_FUNCTION(hello_world),请按照前面总结的PHP扩展编写步骤增加相应的声明。
该函数中,只有RETURN_STRING(INI_STR(“helloworld.greeting”), 1)。
INI_STR表示获取greeting变量的值,注意如果是整数,要使用INI_LONG,类似的还有INI_DOUBLE,INI_BOOL。第二个
参数1是一个很重要的概念,表示是否需要拷贝一份,并且将拷贝返回。因为INI变量是不存在于zend空间的,因此需要zend拷贝并返回。
至此,我们修改了helloworld.c文件,请按照前面运行扩展的步骤重新编译一次。并且在helloworld.php中,使用$str2 = hello_world(),就可以得到$str2为”haha!jean!”
再来演示RINIT和RSHUTDOWN这对函数。
要声明一个页面请求内可重复利用的变量,需要在php_helloworld.h中,加上:
ZEND_BEGIN_MODULE_GLOBALS(helloworld)——声明了扩展中页面请求内可共用的变量
long counter;
zend_bool direction;——注意要使用zend提供的扩展数据类型
ZEND_END_MODULE_GLOBALS(helloworld)
回到helloworld.c中,
ZEND_DECLARE_MODULE_GLOBALS(helloworld)——声明存在页面请求内共用变量
在前面的PHP_INI_BEGIN()包裹中,加上:
STD_PHP_INI_ENTRY(“helloworld.direction”, “1″, PHP_INI_ALL,
OnUpdateBool, direction, zend_helloworld_globals,
helloworld_globals)——与前面的PHP_INI_ENTRY略有不同,指定了写时转换类型函数OnUpdateBool,以及这些变
量的数据结构zend_helloworld_globals,以及变量名。
为了使用RINIT,必须要先增加一个:
static void php_helloworld_init_globals(zend_helloworld_globals *helloworld_globals){
helloworld_globals->direction = 0;
}
这是MINIT中必须要调用一个初始化函数,该函数可以为空(如果没有特殊值需要赋值),但是一定要定义。
在MINIT函数中,最前面增加一行:
ZEND_INIT_MODULE_GLOBALS(helloworld, php_helloworld_init_globals, NULL); ——对页面请求内的全局变量初始化。
在RINIT函数中,可以使用:
HELLOWORLD_G(counter)=0;——引用注册的long counter变量,必须要加上宏HELLWORLD是大写扩展名。
在实际使用的函数中,比如定义一个函数PHP_FUNCTION(hello_long)
可以HELLOWORLD_G(counter)++,在PHP代码中反复运行hello_long,可以发现该值是不断累加的,而不是一个扩展函数内的局部变量。
至此,已经演示了MINIT,MSHUTDOWN,RINIT,RSHUTDOWN函数的使用方法,扩展的基本写法已经掌握了。
扩展语法——与C库联合编译
前面提到扩展一个很重要的用途,就是使得PHP代码可以直接调用C函数,而且写的方式和调用PHP函数一样简单。如何做到呢?
假设我们有一个很简单的C程序,即:
void hello(char *s)
{
if( s== NULL )
return;
strcpy(s, “hello!world!
\n”);
}
简单的功能,需要传递一个已经分配好空间的字符串,然后将hello!world的内容拷贝到字符串中。
将该函数编译成C静态库,生成libhello.a文件。
我们在helloworld.c中写一个简单的PHP_FUNCTION(hello_c)函数,注意按照前面步骤加上该有的声明:
char * buf = NULL;
int buf_len = 256;
buf = emalloc(buf_len);——用到了ZEND内存管理API
if( buf == NULL ){ RETURN_FALSE;}
hello(buf);
RETURN_STRING(buf,0);——不用为1,因为不需要再拷贝一次
在自动生成的makefile中,加上-L路径/ -lhello即可
重新编译,再运行,可以得到hello!world!字样
至此,可以知道扩展是为了让zend调用C函数,因此类似是C函数的wrapper,扩展的编写其实是轻量级的。
扩展高级语法
刚
才数据类型里面提到了,array, object,resource这3中变量,即可以直接将php的array传递给C函数。这部分有大量的zend
api为你从PHP接到的array,object以及resource转换成zend能识别的zval结构。感兴趣的同学可以直接参照:
到此,相信完全可以自己编写PHP扩展了。
大家感兴趣的问题
1. 扩展是否可以调用其他扩展?
2. INI变量存放在哪里?zend空间,PHP空间到底是什么概念?多个页面请求修改INI变量是否会有问题?
3. 是否可以访问其他扩展的INI变量?
4. 没有zend之前是否可以编写扩展?
延伸话题
将PHP反编译成C++/C代码,使得PHP的执行变成编译语言的执行过程。