怎样编写PHP扩展

什么是扩展?

         每个PHP程序员必接触过扩展,PECL库提供超过100多个扩展,比较常用的memcache,apc,mysqli等。在php.ini文件中,extension_dir指示就是扩展路径。

为什么要扩展?

  1.  PHP的设计理念,开源语言,方便各个社区自行开发丰富的功能,互不影响,而且与PHP内部无耦合。
  2. 有些功能脚本语言无法实现或者实现代价较高,比如常驻内存的应用,以及算法复杂度较高的已有C程序,又不想用PHP重写。
  3. 出于效率的考虑。扩展由于实际上执行的是C程序,因此提高效率。

扩展如何执行?

          PHP语言本身的结构分成两大部分:ZEND和PHP core。

PHP结构

         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,后文会有实例演示:

          ZEND内存管理函数

         左 侧是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。

 

运行扩展

  1.         在helloworld目录下,运行phpize,检查当前zend版本,PHP API版本,ZEND API版本信息。
  2.         随后生成config.m4以及config.w32(windows底下使用),以及configure程序。
  3.         打开config.m4,打开
  4.         PHP_ARG_ENABLE(helloworld, whether to enable helloworld support
  5.         [  --enable-helloworld           Enable helloworld support])
  6.         AC_DEFINE(HAVE_HELLOWORLDLIB,1,[Whether you have helloworld])
  7.         随后使用./configure –enable-helloworld
  8.         此时自动生成makefile,然后make,将在modules目录下存在helloworld.so。
  9.        mv helloworld.so到php的扩展路径下,重启apache
  10.        运行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扩展编写的步骤如下:

  1.    在php_helloworld.h中,增加:PHP_FUNCTION(confirm_helloworld_compiled);——声明
  2.    在helloworld.c中,增加:
  3.       zend_function_entry helloworld_functions[] = {增加一个PHP_FE(confirm_helloworld_compiled)}
  4.    增加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!<br>\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结构。感兴趣的同学可以直接参照:

        http://devzone.zend.com/article/1021

       到此,相信完全可以自己编写PHP扩展了。

 

后续可阅读
       在windows底下编写PHP扩展:http://www.yanbin.org/blog/extending-php-part-one.html

大家感兴趣的问题

       1.  扩展是否可以调用其他扩展?

       2. INI变量存放在哪里?zend空间,PHP空间到底是什么概念?多个页面请求修改INI变量是否会有问题?

       3. 是否可以访问其他扩展的INI变量?

       4. 没有zend之前是否可以编写扩展?

延伸话题

        将PHP反编译成C++/C代码,使得PHP的执行变成编译语言的执行过程。

posted @ 2012-10-29 14:42  arcwolf  阅读(389)  评论(0)    收藏  举报