PHP的源码在结构上非常清晰。下面是PHP源码的目录结构。
1 | 根目录: |
PHP 声明周期
PHP开始执行以后会经过两个主要的阶段:处理请求之前的开始阶段和请求之后的结束阶段。
开始阶段有两个过程:第一个过程是模块初始化阶段(MINIT
), 在整个 SAPI 生命周期内(例如 Apache 启动以后的整个生命周期内或者命令行程序整个执行过程中), 该过程只进行一次。
第二个过程是模块激活阶段(RINIT
),该过程发生在请求阶段, 例如通过 URL 请求某个页面,则在每次请求之前都会进行模块激活(RINIT 请求开始)。
例如PHP注册了一些扩展模块,则在MINIT阶段会回调所有模块的 MINIT
函数。 模块在这个阶段可以进行一些初始化工作,例如注册常量,定义模块使用的类等等。 模块在实现时可以通过如下宏来实现这些回调函数:
1 | PHP_MINIT_FUNCTION(myphpextension) |
请求到达之后 PHP 初始化执行脚本的基本环境,例如创建一个执行环境,包括保存PHP运行过程中变量名称和值内容的符号表, 以及当前所有的函数以及类等信息的符号表。
然后 PHP 会调用所有模块的RINIT函数, 在这个阶段各个模块也可以执行一些相关的操作:
1 | PHP_RINIT_FUNCTION(myphpextension) |
请求处理完后就进入了结束阶段,一般脚本执行到末尾或者通过调用 exit()
或 die()
函数, PHP都将进入结束阶段。和开始阶段对应,结束阶段也分为两个环节:
一个在请求结束后停用模块 (RSHUTDOWN,对应RINIT), 一个在 SAPI 生命周期结束(Web服务器退出或者命令行脚本执行完毕退出)时关闭模块 (MSHUTDOWN,对应 MINIT)。
变量类型
PHP在内核中是通过zval这个结构体来存储变量的,它的定义在 Zend/zend.h 文件里,简短精炼,只有四个成员组成
1 | struct _zval_struct { |
其中 zend_uint 与 zend_uchar 是在Zend/zend_types.h中定义的:
1 | typedef unsigned char zend_uchar; |
保存变量值的 value 则是 zvalue_value 类型(PHP5),它是一个 union,同样定义在了 Zend/zend.h 文件里:
1 | typedef union _zvalue_value { |
在以上实现的基础上,PHP5 语言得以实现了8种数据类型,这些数据类型在内核中的分别对应于特定的常量,它们分别是:
1 | IS_NULL |
zval结构体中的 type 成员值便是上面IS_*常量之一
PHP源码中通过三个宏来验证类型:
1 | #define Z_TYPE(zval) (zval).type |
比如:
1 | zval *foo |
除此之外每个类型都有特定的宏来提供操作,比如获取字符串的值、长度,获取数组的值等
创建PHP变量
PHP 提供一个创建 PHP 变量的宏:MAKE_STD_ZVAL(pzv)
。
这个宏会用内核的方式来申请一块内存并将其地址付给 pzv, 并初始化它的 refcount 和 is_ref 两个属性,更棒的是,它不但会自动的处理内存不足问题, 还会在内存中选个最优的位置来申请。
先看一下 PHP 的符号表,Zend/zend_globals.h,其中有两个成员:
1 | struct _zend_executor_globals { |
symbol_table 代表着 PHP 的全局变量,如 $GLOBALS,
active_symbol_table 它代表的是处于当前作用域的变量符号表
他们都可以使用EG宏来访问
下面是个创建PHP变量的例子
1 |
|
而在源码中,我们需要
1 |
|
首先,我们声明一个 zval 指针,并申请一块内存。然后通过 ZVAL_STRING 宏将值设置为 ‘bar’。
最后一行的作用就是将这个 zval 加入到当前的符号表里去,并将其 label 定义成 foo,这样用户就可以在代码里通过 $foo 来使用它了
除此之外,内核中提供一些宏来简化我们的操作,可以只用一步便设置好 zval 的类型和值,如:ZVAL_LONG、ZVAL_DOUBLE 等
编写第一个扩展
使用 ext_skel
生成器
使用PHP源码中的内置工具 ext_skel
可以生成一个扩展模板
1 | cd php-5.4.41/ext |
我们看一下config.m4文件的重要部分,为了顺利编译生成扩展,我们需要去掉模板文件前面相关的 dnl 注释:
1 | PHP_ARG_ENABLE(hello, whether to enable hello support, |
PHP中约定 --enable-extname
一般用于直接可编译的扩展,--with-extname
大多需要依赖第三方的 lib
如果我们在编译PHP的时候显式运行 ./configure --enable-hello
,那么终端环境便会自动将 $PHP_HELLO
变量设置为 yes,而 PHP_SUBST
函数只不过是 PHP 官方对 autoconf 里的 AC_SUBST 函数的一层封装。 最后重要的一点是,PHP_NEW_EXTENSION
函数声明了这个扩展的名称、需要的源文件名、此扩展的编译形式。如果我们的扩展使用了多个文件,便可以将这多个文件名罗列在函数的参数里。
$ext_shared 参数用来声明这个扩展不是一个静态模块,而是在php运行时动态加载的。
1 | PHP_NEW_EXTENSION(sample, sample.c sample2.c sample3.c, $ext_shared) |
编写第一个函数
我们看一下自动生成的 hello.c 文件
PHP_FUNCTION() 可以写成 ZEND_FUNCTION(),ZEND_FUNCTION() 更前卫、标准一些,两者是完全相同的。
1 | /* 生成PHPINFO内容 */ |
我们可以看一下 PHP_FUNCTION 和 ZEND_FUNCTION 会被展开成什么:
1 | #define PHP_FUNCTION ZEND_FUNCTION |
通过以上宏定义,我们可以看出,最终被展开成:
1 | void zif_confirm_hello_compiled(INTERNAL_FUNCTION_PARAMETERS) {} |
其中 zif 是 zend internal function,也就是Zend内部函数,是PHP语言调用的函数在C语言中函数名称的前缀。
INTERNAL_FUNCTION_PARAMETERS 是一个很长的参数表。
1 | zend_module_entry hello_module_entry = { |
zend_module_entry hello_module_entry 是联系C扩展与PHP语言的重要纽带,其中第二个参数是 zend_function_entry[] 类型,所以我们可以看到该定义:
1 | const zend_function_entry hello_functions[] = { |
PHP_FE/ZEND_FE() 宏函数是对我们 walu_hello 函数的一个声明,如果我们有多个函数可以直接写成多行,PHP_FE_END/ZEND_FE_END 是个固定的 { NULL, NULL, NULL, 0, 0 } 值,注意每个之间不需要加逗号。