http://devzone.zend.com/node/view/id/1021
简介
文中所用的教程需要您对PHP和PHP的解释器(用C语言开发)有初步的了解.首先来确认为什么我们需要写出一个新的PHP扩展.
1. 由于不同语言之间的差别,导致这些以这些语言开发的共享库不能够由PHP直接调用,但我们确实很需要这些共享库所已有的功能
2. 某些情况下,需要使PHP自身具有一些非常规的特性
3.我们已经获得了一些php代码,但是我们想让它运行的更快,更精致并且耗费的资源更少
4.你已经开发除了很重要的代码需要出售,但只想让购买方拥有可执行逻辑而不是源代码。
或许还有其它更多的原因,但是在能够创造出一个扩展之前,我们首先需要了解什么是PHP扩展。
什么是扩展
只要你在使用PHP,你就已经在使用php扩展了。除了少数情况外,所有PHP中所有用户可以调用的方法都被分到不同的php扩展中实现的。这些方法中的大部分都是php标准扩展的一部分。PHP源码自带了85个扩展,平均每个扩展包含30个方法。对于数学计算,大约有2500个方法。但是这仍然是不够的,PECL库提供了额外的超过100个以上的扩展.
“如果所有的方法都是由扩展实现,那扩展隶属于什么,PHP的核心(core)又是什么?",有人会这么问.
PHP的core由两个独立的部分构成。处于最底层的是Zend引擎, 它负责把程序员写的程序转变为机器可以识别的标识符,然后运行这些符号。此外,它还完成内存管理、变量作用域和指定方法调用等工作。另外的一部分就是PHP Core了。它负责处理于SAPI(Server Application Programming Interface, 通常指的是运行环境-Apache,IIS,CLI,CGI等)层的交互和绑定. 此外也提供了对一些如safe_mode和open_basedir的 检查,以及用户空间使用fopen(),fread()和fwrite()方法与文件系统和网络I/O相关的流的处理。
当SAPI启动时,例如通过 /usr/local/apache/bin/apachectl start这样的方式,PHP就会初始化它的core子系统。当这一步骤完成后,它就开始逐个加载所有的扩展的代码并调用每一个扩展的
Module Initialization 方法。该方法给了扩展初始化内部变量、分配资源、注册资源句柄和向ZE注册方法的机会,保证当php脚本调用某个方法时,ZE可以知道
需要执行什么样的代码。
然后,PHP等待SAPI层获得一个请求以便执行它。在CGI或者CLI模式下,PHP可以立即获得请求而且仅仅发生一次。在Apache、IIS或者其它完整的Web Server的SAPI模式下,由于请求是由客户端发起,因此这种等待-执行的方式可以重复任意多次,而且多种情况下是并发的执行。然而不论在何种模式下,PHP调用ZE建立起执行请求所需要的运行环境,然后调用每一个扩展的Request Initialization (RINIT
)方法。这个方法给扩展提供了建立特定环境变量、分配特定于请求的资源或者执行其它动作的机会。RINIT方法的一个主要的sample就是session扩展的使用。当session.auto_start设置为enable时,RINIT会自动出发用户空间的session_start()方法并生成$_SESSION变量。
当这种特定于请求的初始化完成后,ZE就接管了程序控制权。翻译php代码到机器可识别的符号,再对这些符号进行编码以便执行。当这些编码需要调用扩展定义的方法时,ZE将绑定方法名和参数,并暂时放弃控制权直到方法调用结束。
当脚本执行完成时,PHP会调用扩展的Request Shutdown (RSHUTDOWN
)方法,使扩展有机会能够清理在此次调用中使用的资源。然后,ZE将执行清理进程(也就是垃圾收集),该进程会非常有效的对这次调用使用的每一个变量使用unset()方法。
当上述几步都完成后,PHP就等待SAPI传递给它下一个请求或者一个停止的信号。在CGI和CLI模式下是不存在“下一个”的概念的,因此SAPI会立即执行shutdown。在shutdown过程中,PHP会调用每一个扩展的Module Shutdown (MSHUTDOWN
)方法,然后最终关闭它自己的core子系统。
Memory Allocation
为了避免内存泄露,ZE提供了自己的内存管理方式:永久分配(persistent allocation )和非永久分配(non-persistent allocation)。永久分配指的是所分配的内存的声明周期长于单个请求的声明周期,非永久分配指的是当单个请求结束时,不论free方法是否被调用,内存也就随之被回收。如用户自定义的变量就属于非永久分配,因为在请求完成时这些变量也就不在有任何使用的价值。
尽管在理论上,扩展可以依赖ZE在请求被完成时自动释放掉非永久变量所占有的内存,但在实际操作中并不建议这么做。所分配的内存会长时间的处于未回收状态,和这些内存关联的已经打开的资源也可能不能正确的关闭,并且这么做简直就像拉完大便(make a mess)后而不进行清理。 我们即将看到,事实上在扩展里是很容易做到所有分配的数据被合适的清理掉。
比较一下传统的内存分配和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) |
例如:
emalloc(1234) 与 pemalloc(1234,1)是完全相同的。
**safe_emalloc()和(PHP5中)safe_pemalloc()会做额外检查来防止整数溢出。
建立扩展的开发环境
进行PHP的扩展开发有两种方式。第一种是从PHP源码进行,第二种是从PHP提供的独立的工具进行。第二种比较简单,但缺点是只能做成扩展的形式。第一种可以把新做的扩展直接编译到PHP的最后的二进制包中,缺点是少了灵活性,当需要进行调整时就不得不重新编译整个PHP源码。因此我们采用第2种方式进行,这种方式也是绝大多数情况下所使用的方式。
Hello World
让我们从最经典的“Hello World”开始。我们需要完成的是这么一个扩展:它包含了一个方法,当从php脚本调用该方法时可以输出一个字符串“Hello World.” 在php脚本层面上,我们可以使用如下的实现方式:







































































如同我们看到的那样,上述的大部分代码是胶水-----对PHP说明这个扩展并建立起扩展与PHP之间的通信。只有最后4行看起来像是调用方法时真正进行处理的逻辑。的确,从这个层次来看这几行代码和我们前面提到的PHP代码看起来很像,直观上也非常容易理解:
1. 声明一个名称为hello_world的方法
2. 这个方法返回一个值为"hello World"的字符串
3. 但是, 1 是什么意思?
前面我们讲过,ZE提供了自己的内存管理机制,保证当php脚本执行结束时所有的资源都能够被合适的释放掉。对内存管理层面而言,对同一块内存进行两次释放是禁忌之事。这种称为double freeing,也通常是导致 segmentation faults的原因。因为它使程序去访问已经不属于程序自己的内存空间。所以,我们不想允许ZE去释放一个静态字符串所对应的内存空间,因为这个字符串位于整个程序空间,并不属于进程的数据块中。【静态数据成员不能被free掉,因为进程要求这些静态数据在进程的声明周期内都存在】。RETURN_STRING方法可以假设传给它的字符串都需要被复制一份以便以后能够安全的被释放掉,但在方法内部给字符串分配内存,复制然后返回是很常见的操作。RETURN_STRING()允许我们指定当调用该方法时是否需要字符串的copy. 上面的4行代码与下面的代码是等价的:








建立扩展
有了上述三个文件,最后一部就是bulid这三个文件以创建一个可动态加载的扩展了。只需要在hello目录下执行三个命令就OK了。



extension=hello.so
, 以触发当php启动运行时对该扩展的自动加载。对于APACHE,IIS之类Web Server,需要重新启动Web Server,对于CGI或者CLI则不用重启。现在,运行

使用类似的方式也可以获得标量的返回结果。如
RETURN_LONG()
返回整形, RETURN_DOUBLE()
返回浮点数,RETURN_BOOL()返回true/false,
RETURN_NULL()
返回NULL等。实现如下:




























同时,我们需要在头文件php_hello.h中添加上述方法的声明,以便编译能够正确完成。

















然后继续使用 make 即可完成新版本的编译了,很简单。