0%

PHP5 扩展开发(1)- 生命周期与PHP变量

PHP的源码在结构上非常清晰。下面是PHP源码的目录结构。

1
2
3
4
5
6
7
8
9
10
11
根目录: 
/ 这个目录包含的东西比较多,主要包含一些说明文件以及设计方案。
build/ 顾名思义,这里主要放置一些和源码编译相关的一些文件,比如开始构建之前的buildconf脚本等文件,还有一些检查环境的脚本等。
ext/ 官方扩展目录,包括了绝大多数PHP的函数的定义和实现,如array系列,pdo系列,spl系列等函数的实现,都在这个目录中。个人写的扩展在测试时也可以放到这个目录,方便测试和调试。
main/ 这里存放的就是PHP最为核心的文件了,主要实现PHP的基本设施,这里和Zend引擎不一样,Zend引擎主要实现语言最核心的语言运行环境。
Zend/ Zend引擎的实现目录,比如脚本的词法语法解析,opcode的执行以及扩展机制的实现等等。
pear/ “PHP 扩展与应用仓库”,包含PEAR的核心文件。
sapi/ 包含了各种服务器抽象层的代码,例如apache的mod_php,cgi,fastcgi以及fpm等等接口。
TSRM/ PHP的线程安全是构建在TSRM库之上的,PHP实现中常见的*G宏通常是对TSRM的封装,TSRM(Thread Safe Resource Manager)线程安全资源管理器。
tests/ PHP的测试脚本集合,包含PHP各项功能的测试文件
win32/ 这个目录主要包括Windows平台相关的一些实现,比如sokcet的实现在Windows下和*Nix平台就不太一样,同时也包括了Windows下编译PHP相关的脚本。

PHP 声明周期

PHP开始执行以后会经过两个主要的阶段:处理请求之前的开始阶段和请求之后的结束阶段。

开始阶段有两个过程:第一个过程是模块初始化阶段(MINIT), 在整个 SAPI 生命周期内(例如 Apache 启动以后的整个生命周期内或者命令行程序整个执行过程中), 该过程只进行一次。

第二个过程是模块激活阶段(RINIT),该过程发生在请求阶段, 例如通过 URL 请求某个页面,则在每次请求之前都会进行模块激活(RINIT 请求开始)。

例如PHP注册了一些扩展模块,则在MINIT阶段会回调所有模块的 MINIT 函数。 模块在这个阶段可以进行一些初始化工作,例如注册常量,定义模块使用的类等等。 模块在实现时可以通过如下宏来实现这些回调函数:

1
2
3
4
5
PHP_MINIT_FUNCTION(myphpextension)
{
// 注册常量或者类等初始化操作
return SUCCESS;
}

请求到达之后 PHP 初始化执行脚本的基本环境,例如创建一个执行环境,包括保存PHP运行过程中变量名称和值内容的符号表, 以及当前所有的函数以及类等信息的符号表。

然后 PHP 会调用所有模块的RINIT函数, 在这个阶段各个模块也可以执行一些相关的操作:

1
2
3
4
5
6
PHP_RINIT_FUNCTION(myphpextension)
{
// 例如记录请求开始时间
// 随后在请求结束的时候记录结束时间。这样我们就能够记录下处理请求所花费的时间了
return SUCCESS;
}

请求处理完后就进入了结束阶段,一般脚本执行到末尾或者通过调用 exit()die() 函数, PHP都将进入结束阶段。和开始阶段对应,结束阶段也分为两个环节:

一个在请求结束后停用模块 (RSHUTDOWN,对应RINIT), 一个在 SAPI 生命周期结束(Web服务器退出或者命令行脚本执行完毕退出)时关闭模块 (MSHUTDOWN,对应 MINIT)。

变量类型

PHP在内核中是通过zval这个结构体来存储变量的,它的定义在 Zend/zend.h 文件里,简短精炼,只有四个成员组成

1
2
3
4
5
6
7
8
struct _zval_struct {
/* Variable information */
zvalue_value value; /* value */
zend_uint refcount__gc;
zend_uchar type; /* active type */
zend_uchar is_ref__gc;
};
typedef struct _zval_struct zval;

其中 zend_uint 与 zend_uchar 是在Zend/zend_types.h中定义的:

1
2
typedef unsigned char zend_uchar;
typedef unsigned int zend_uint;

保存变量值的 value 则是 zvalue_value 类型(PHP5),它是一个 union,同样定义在了 Zend/zend.h 文件里:

1
2
3
4
5
6
7
8
9
10
typedef union _zvalue_value {
long lval; /* long value */
double dval; /* double value */
struct {
char *val;
int len;
} str;
HashTable *ht; /* hash table value */
zend_object_value obj;
} zvalue_value;

在以上实现的基础上,PHP5 语言得以实现了8种数据类型,这些数据类型在内核中的分别对应于特定的常量,它们分别是:

1
2
3
4
5
6
7
8
IS_NULL
IS_BOOL
IS_LONG singed long 类型 所以PHP语言中的整形都是带符号的
IS_DOUBLE
IS_STRING zval中保存了指向这块内存的指针,同时记录了字符串的实际长度
IS_ARRAY C语言的数组只能承载一种类型的数据,PHP数组借助HashTable可以承载任意类型的数据 每个元素都有两个部分组成,索引与值
IS_OBJECT
IS_RESOURCE

zval结构体中的 type 成员值便是上面IS_*常量之一

PHP源码中通过三个宏来验证类型:

1
2
3
#define Z_TYPE(zval)        (zval).type
#define Z_TYPE_P(zval_p) Z_TYPE(*zval_p)
#define Z_TYPE_PP(zval_pp) Z_TYPE(**zval_pp)

比如:

1
2
zval *foo
Z_TYPE_P(foo) == IS_NULL

除此之外每个类型都有特定的宏来提供操作,比如获取字符串的值、长度,获取数组的值等

创建PHP变量

PHP 提供一个创建 PHP 变量的宏:MAKE_STD_ZVAL(pzv)

这个宏会用内核的方式来申请一块内存并将其地址付给 pzv, 并初始化它的 refcount 和 is_ref 两个属性,更棒的是,它不但会自动的处理内存不足问题, 还会在内存中选个最优的位置来申请。

先看一下 PHP 的符号表,Zend/zend_globals.h,其中有两个成员:

1
2
3
4
5
6
struct _zend_executor_globals {
...
HashTable *active_symbol_table;
HashTable symbol_table; /* main symbol table */
...
};

symbol_table 代表着 PHP 的全局变量,如 $GLOBALS,
active_symbol_table 它代表的是处于当前作用域的变量符号表

他们都可以使用EG宏来访问

下面是个创建PHP变量的例子

1
2
3
<?php
$foo = 'bar';
?>

而在源码中,我们需要

1
2
3
4
5
6

zval *fooval;

MAKE_STD_ZVAL(fooval);
ZVAL_STRING(fooval, "bar", 1);
ZEND_SET_SYMBOL( EG(active_symbol_table) , "foo" , fooval);

首先,我们声明一个 zval 指针,并申请一块内存。然后通过 ZVAL_STRING 宏将值设置为 ‘bar’。

最后一行的作用就是将这个 zval 加入到当前的符号表里去,并将其 label 定义成 foo,这样用户就可以在代码里通过 $foo 来使用它了

除此之外,内核中提供一些宏来简化我们的操作,可以只用一步便设置好 zval 的类型和值,如:ZVAL_LONG、ZVAL_DOUBLE 等

编写第一个扩展

使用 ext_skel 生成器

使用PHP源码中的内置工具 ext_skel 可以生成一个扩展模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cd php-5.4.41/ext

./ext_skel --extname=hello

ncqdeMacBook-Pro:ext ncq$ cd hello/
ncqdeMacBook-Pro:hello ncq$ ll
total 56
-rw-r--r-- 1 ncq staff 6 10 24 22:15 CREDITS
-rw-r--r-- 1 ncq staff 0 10 24 22:15 EXPERIMENTAL
-rw-r--r-- 1 ncq staff 2002 10 24 22:15 config.m4
-rw-r--r-- 1 ncq staff 289 10 24 22:15 config.w32
-rw-r--r-- 1 ncq staff 5086 10 24 22:15 hello.c
-rw-r--r-- 1 ncq staff 499 10 24 22:15 hello.php
-rw-r--r-- 1 ncq staff 2837 10 24 22:15 php_hello.h
drwxr-xr-x 3 ncq staff 102 10 24 22:15 tests

我们看一下config.m4文件的重要部分,为了顺利编译生成扩展,我们需要去掉模板文件前面相关的 dnl 注释:

1
2
3
4
5
6
7
PHP_ARG_ENABLE(hello, whether to enable hello support,
[ --enable-hello Enable hello support])

if test "$PHP_HELLO" != "no"; then
PHP_SUBST(HELLO_SHARED_LIBADD)
PHP_NEW_EXTENSION(hello, hello.c, $ext_shared)
fi

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
2
3
4
5
6
7
8
9
10
11
/* 生成PHPINFO内容 */
PHP_MINFO_FUNCTION(hello)
{

}

/* 定义可供PHP调用的函数 */
PHP_FUNCTION(confirm_hello_compiled)
{

}

我们可以看一下 PHP_FUNCTION 和 ZEND_FUNCTION 会被展开成什么:

1
2
3
4
#define PHP_FUNCTION                ZEND_FUNCTION
#define ZEND_FUNCTION(name) ZEND_NAMED_FUNCTION(ZEND_FN(name))
#define ZEND_NAMED_FUNCTION(name) void name(INTERNAL_FUNCTION_PARAMETERS)
#define ZEND_FN(name) zif_##name

通过以上宏定义,我们可以看出,最终被展开成:

1
void zif_confirm_hello_compiled(INTERNAL_FUNCTION_PARAMETERS) {}

其中 zif 是 zend internal function,也就是Zend内部函数,是PHP语言调用的函数在C语言中函数名称的前缀。

INTERNAL_FUNCTION_PARAMETERS 是一个很长的参数表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
zend_module_entry hello_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
STANDARD_MODULE_HEADER,
#endif
"hello",
hello_functions,
PHP_MINIT(hello),
PHP_MSHUTDOWN(hello),
PHP_RINIT(hello), /* Replace with NULL if there's nothing to do at request start */
PHP_RSHUTDOWN(hello), /* Replace with NULL if there's nothing to do at request end */
PHP_MINFO(hello),
#if ZEND_MODULE_API_NO >= 20010901
PHP_HELLO_VERSION,
#endif
STANDARD_MODULE_PROPERTIES
};

zend_module_entry hello_module_entry 是联系C扩展与PHP语言的重要纽带,其中第二个参数是 zend_function_entry[] 类型,所以我们可以看到该定义:

1
2
3
4
const zend_function_entry hello_functions[] = {
PHP_FE(confirm_hello_compiled, NULL) /* For testing, remove later. */
PHP_FE_END /* Must be the last line in hello_functions[] */
};

PHP_FE/ZEND_FE() 宏函数是对我们 walu_hello 函数的一个声明,如果我们有多个函数可以直接写成多行,PHP_FE_END/ZEND_FE_END 是个固定的 { NULL, NULL, NULL, 0, 0 } 值,注意每个之间不需要加逗号。