前言
本文主要简单介绍(本人水平有限,也是边学习边写)一下PHP的生命周期,扩展的开发步骤,以及扩展开发一些常用的宏,最后小试牛刀,写一个函数。阅读本文前,您需要掌握基础的C/C++的知识。
我们先把php的源码克隆下来
git clone https://github.com/php/php-src.git
cd php-src
git checkout -b my_ext
进入ext目录,我们会看见两个文件ext_skel和ext_skel_win32.php,从文件名我们就可以看出一个是在*unix系统运行,另外一个是在windows环境下运行。这个文件是生成扩展目录的脚手架,用这个脚本生成的扩展目录里面包含了扩展所需要的符合php扩展开发规范的基本文件及文件夹。比如我们的扩展名字叫myExt
./ext_skel --extname=my_ext
我们在查看下ext目录,是不是多了一个myExt目录,进入这个目录,我们修改config.m4文件,找到第16和18行,把前面的注释符dnl去掉,现在我们返回根目录生成configure脚本并编译PHP
./buildconf
./configure --enable-debug --enable-my_ext
make
这样我们就简单的完成的PHP的编译。(如果报错,请确定已经安装了php的基础依赖库。比如re2c, bison, 实在解决不了,我们可以相互交流:)现在我们来测试下php是否已经载入刚刚生成的扩展。首先,用以下命令看下我们的扩展是否已经编译
可以看到,扩展已经能被php所列出,表示编译成功。但这并不能表示扩展已经成功载入,扩展文件在生成的时候,有一个以php为后缀的文件,这是为我们生成的测试文件,我们可以执行下这个文件看下输出
输出的信息我想大家都看的懂。扩展已经载入成功。如果你还不放心,我们可以采用php通用的运行测试的办法来测试下,扩展目录下的tests文件夹就是脚手架给我们生成放测试用例的,里面已经有了一个随扩展生成的测试用例(我们的扩展改变了,那我们也应该在这个目录写相应的测试用例),运行以下命令并查看输出
我们可以看到测试用例通过。(以上操作和php版本无关,同样可以运行在php5上)
准备工作已经完成,下面我们来介绍一下扩展的两个主要文件my_ext.c
和php_my_ext.h
,这两个文件符合php的扩展命名规范,源文件$extname.c
,头文件php_$extname.h
,扩展的入口就是这两个文件了。所以可想而知,我们只要修改这两个文件就行了。我们简单介绍下my_ext.c
文件。打开这个文件,把鼠标以每小时180公里的速度移动到最下面,我们会看到这么一个结构体(忽略第一个和最后一个,这是默认信息,主要看中间的)
zend_module_entry my_ext_module_entry = {
STANDARD_MODULE_HEADER,
"my_ext", /*扩展名称*/
my_ext_functions, /*扩展的函数数组*/
PHP_MINIT(my_ext), /*扩展初始化函数*/
PHP_MSHUTDOWN(my_ext), /*扩展关闭时调用函数(php-fpm关闭时)*/
PHP_RINIT(my_ext), /* Replace with NULL if there's nothing to do at request start */
PHP_RSHUTDOWN(my_ext), /* Replace with NULL if there's nothing to do at request end */
PHP_MINFO(my_ext), /*扩展信息,就是phpinfo将会输出的信息*/
PHP_MY_EXT_VERSION, /*扩展版本*/
STANDARD_MODULE_PROPERTIES
};
这里简单的介绍下php的生命周期,在单进程模式(cli)下,在如下图:
我们可以看到,进程启动和销毁时执行MODULE INIT
和MODULE SHUTDOWN
,他们在整个进程的生命周期内只执行一次(比如扩展配置文件的载入就是发生在这个函数内),当有请求到来和结束时分别执行REQUEST INIT
和REQUEST SHUTDOWN
,他们在每次请求的生命周期内都执行。每个阶段分别对应上面的函数宏。(多进程模式就是把上图大矩形的内容平行复制多个,多线程模式就是把请求生命周期在一个进程内复制多个)
说到这是不是对扩展的载入流程有个初步的了解呢?我们继续看下面这段代码
PHP_FUNCTION(confirm_my_ext_compiled)
{
char *arg = NULL;
size_t arg_len, len;
zend_string *strg;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "s", &arg, &arg_len) == FAILURE) {
return;
}
strg = strpprintf(0, "Congratulations! You have successfully modified ext/%.78s/config.m4. Module %.78s is now compiled into PHP.", "my_ext", arg);
RETURN_STR(strg);
}
这段代码,大家一眼看去可能就会想这大概是实现一个phpconfirm_my_ext_compiled
函数。对,聪明的你们猜的一点都没错。你们是不是对这个函数的输出信息有点熟悉?上面我们执行过一个.php文件,其实那个文件就是对这个函数测试。那我们照葫芦画瓢,也开始写一个。
不知道你们在日常编程中,是否遇到过这种情况,我们经常需要将一个二维数组按照某个字段分组,然后返回新的数组。比如,需要将查出的订单数据按照订单的发起人分组,按照商家分组等等。我们先设计下这个函数,这个函数需要按照某个字段分组,那肯定需要一个参数key
,然后就是待分组的数组input
,最后我们是否需要在分组后的数组中保留原先的字段key
,它显然是一个Bool
类型的, 而且默认为false
。所以函数原型就出来了:
array_groupBy(string $key, array $input, $forget = false):array
我们根据原型然后综合上面测试函数的例子把函数框架先搭起来,看起来是这样的
PHP_FUNCTION(array_groupBy){
zend_string *key;
zval *input, keyZval, forgetZval;
zend_bool forget = 0;
HashTable *ht;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "Sa|b", &key, &input, &forget) == FAILURE){
return;
}
}
我们找到以下代码,并注册我们写的函数
const zend_function_entry my_ext_functions[] = {
PHP_FE(confirm_my_ext_compiled, NULL) /* For testing, remove later. */
PHP_FE(array_groupBy, NULL)
PHP_FE_END /* Must be the last line in my_ext_functions[] */
};
首先,我们函数进入第一步要做什么?毫无疑问,我们要接收实参。扩展自动生成的测试函数,给了我们很好的例子。我们用如下函数来接收实参。
int zend_parse_parameters(int num_args, const char *type_spec, ...); //这个函数是一个可变参数的函数
-
num_args
实参数目 -
type_spec
这是类型说明符,全部的类型说明符我们可以在根目录下的 README.PARAMETER_PARSING_API 文件可以找到,并且都有详细的说明和例子,这里不再一一阐述。 -
…
类型说明符对应的本地变量(可变参数,C的可变参数如何实现的,已超出本文的讲解范围,请自己通过google关键字搜索,相信你一定可以看的懂)。
我们简单的修改下代码,把传入的参数以数组的形式返回,看看到底接受到实参了没有:
PHP_FUNCTION(array_groupBy){
zend_string *key;
zval *input, keyZval, forgetZval;
zend_bool forget = 0;
HashTable *ht;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "Sa|b", &key, &input, &forget) == FAILURE){
return;
}
ht = emalloc(sizeof(HashTable)); // 给HashTable分配一块内存
zend_hash_init(ht, 3, NULL, ZVAL_PTR_DTOR, 0); //初始化HashTable,设置一些基础值什么的
//将传入的key转化成一个zval*类型,因为下面的hash方法参数只支持zval*,尴尬了。
//不过在zend_API.h有相应的宏可以替代下面的方法,我就不写了。
ZVAL_STR(&keyZval, key);
ZVAL_BOOL(&forgetZval, forget);
convert_to_long(&forgetZval);
zend_hash_str_add_new(ht, "key", strlen("key"), &keyZval);
zend_hash_str_add_new(ht, "input", strlen("input"), input);
zend_hash_str_add_new(ht, "forget", strlen("forget"), &forgetZval);
RETURN_ARR(ht);
}
我们重新回到根目录下make
项目,然后进入扩展目录下写一个a.php文件。
$key = 'birthday';
$input = [
[
'name' => 'fangxing',
'birthday' => 1993
],
[
'name' => 'marco',
'birthday' => 1990
]
];
$forget = false;
print_r(array_groupBy($key, $input, $forget));
运行
./sapi/cli/php ext/my_ext/a.php
输出
这是PHP5的接收参数的形式,在php7还有一种宏实现。直接上代码,具体的实现可以去看zend_API.h.
ZEND_PARSE_PARAMETERS_START(2, 3)
Z_PARAM_STR(key)
Z_PARAM_ARRAY(input)
Z_PARAM_OPTIONAL
Z_PARAM_BOOL(forget)
ZEND_PARSE_PARAMETERS_END();
现在把上面接收参数的替换成这个,再make
你会发现效果是一样的。换汤不换药,之前是在解析类型说明符的时候调用对应的函数,现在是每个宏里面调用对应的函数,省了一步解析操作,代码多了N行。
回到我们要实现的函数,要将现有的数组按照key
的值分组,分组后的数组肯定是一个至少三维的数组,如下:
$output = [
'group_value1' => [
//符合条件的值
],
'group_value2' => [
//符合条件的值
]
];
那么外围的数组肯定要分配内存,里面每个group_valueN
对应的数组也要分配内存,是否要分配内存取决于外围数组是否已经有相同的group_valueN
存在。如果forget
为真的话,我们还需要将原数组当中的每一项分离出来,然后再删除key
。话不多说,我们上代码:
PHP_FUNCTION(array_groupBy){
zend_string *key;
zval *input, *val, *key_zval;
zval group_zval, copy;
zend_bool forget = 0;
HashTable *ht;
//接收参数
ZEND_PARSE_PARAMETERS_START(2, 3)
Z_PARAM_STR(key)
Z_PARAM_ARRAY(input)
Z_PARAM_OPTIONAL
Z_PARAM_BOOL(forget)
ZEND_PARSE_PARAMETERS_END();
//给第一维数组分配内存,并初始化
ht = (HashTable *)emalloc(sizeof(HashTable));
zend_hash_init(ht, 0, NULL, ZVAL_PTR_DTOR, 0);
ZEND_HASH_FOREACH_VAL(Z_ARR_P(input), val){
ZVAL_COPY(©, val); ////将val指向的HashTable地址拷贝一份给copy
key_zval = zend_symtable_find(Z_ARR_P(val), key);
convert_to_string(key_zval); //强转所要分组的key对应的val
if(zend_hash_exists(ht, Z_STR_P(key_zval))){
group_zval = *zend_hash_find(ht, Z_STR_P(key_zval));
}else{
//初始化第二维数组,并将地址添加到第一维数组中
array_init(&group_zval);
zend_hash_add_new(ht, Z_STR_P(key_zval), &group_zval);
}
if(forget){
SEPARATE_ARRAY(©); //为了不改变原来的数组,需要分离数组
zend_symtable_del(Z_ARR(copy), key); //在分离后的数组中删除key对应的val
}
add_next_index_zval(&group_zval, ©); //将copy对应的val地址拷贝到第二维数组中
}ZEND_HASH_FOREACH_END();
RETURN_ARR(ht);
}
这还不能满足我们的需求,有些时候需要对分组的key对应的val进行处理,比如我们数据库里取出来的date是2017-07-12,但是我们的需求是把这些数据按月分组,这时,你直接传date作为key,恐怕就无能为力了。那我们能不能让传一个可调用类型(闭包函数/[object, method])进去呢?答案显然是可以的,我们增加一个参数callable
。代码如下:
PHP_FUNCTION(array_groupBy){
zend_string *key;
zval *input, *val, *key_zval;
zval group_zval, copy, retval, copy_key_zval;
zend_bool forget = 0, have_callback = 0;
HashTable *ht;
zend_fcall_info fcall_info = empty_fcall_info;
zend_fcall_info_cache fcall_info_cache = empty_fcall_info_cache;
int ret;
ZEND_PARSE_PARAMETERS_START(2, 4)
Z_PARAM_STR(key)
Z_PARAM_ARRAY(input)
Z_PARAM_OPTIONAL
Z_PARAM_BOOL(forget)
Z_PARAM_FUNC(fcall_info, fcall_info_cache)//接收一个可调用类型(闭包函数 or [o, m])
ZEND_PARSE_PARAMETERS_END();
if(ZEND_NUM_ARGS() > 3){
have_callback = 1;
}
ht = (HashTable *)emalloc(sizeof(HashTable));
zend_hash_init(ht, 0, NULL, ZVAL_PTR_DTOR, 0);
ZEND_HASH_FOREACH_VAL(Z_ARR_P(input), val){
ZVAL_COPY(©, val);
key_zval = zend_symtable_find(Z_ARR_P(val), key);
if(have_callback){
ZVAL_COPY(©_key_zval, key_zval);
fcall_info.retval = &retval; //绑定函数返回值地址
fcall_info.params = &key_zval; //绑定函数参数地址
fcall_info.no_separation = 0;
fcall_info.param_count = 1; //参数个数
ret = zend_call_function(&fcall_info, &fcall_info_cache);
zval_ptr_dtor(©_key_zval);
if(ret != SUCCESS || Z_TYPE(retval) == IS_UNDEF){
zend_array_destroy(ht);
RETURN_NULL();
}
ZVAL_STR(©_key_zval, Z_STR(retval));
}else{
ZVAL_STR(©_key_zval, zend_string_dup(Z_STR_P(key_zval), 0));
}
convert_to_string(©_key_zval);
if(zend_hash_exists(ht, Z_STR(copy_key_zval))){
group_zval = *zend_hash_find(ht, Z_STR(copy_key_zval));
}else{
array_init(&group_zval);
zend_hash_add_new(ht, Z_STR(copy_key_zval), &group_zval);
}
zval_ptr_dtor(©_key_zval); //释放copy_key_zval
if(forget){
SEPARATE_ARRAY(©);
zend_symtable_del(Z_ARR(copy), key);
}
add_next_index_zval(&group_zval, ©);
}ZEND_HASH_FOREACH_END();
RETURN_ARR(ht);
}
到这,我们已经实现了我们要的函数。里面的分离机制理解起来有些困难,我会专门抽一节来和大家探讨探讨。(也许你看其他人写的关于分离/引用的文章很好理解,但实际用起来可不是这样哦,反正我是理解了好久)。还有一些心得和大家分享下,学习扩展其实没有必要刚开始就深入php的内核,我们重点关注这几个文件zend_API.h,zend_API.c,zend_type.h,zend_hash.c
就差不多了,遇到不懂的我们可以去看原函数的实现,看不懂的地方,我们根据它的函数命名去猜想是干什么的(函数命名很重要哦),没有必要去深究(因为深究的话,你会晕掉的,反正我会晕 →_ →)。实在不懂可以Google,如果google不到的话可以去stackoverflow上去提问,不过提问最好用英语,不然国外的大神看不懂,也就没办法回答你了。
函数多了怎么办?零零散散的总不好,下一节将和大家讨论下,如何把函数封装成一个类 :)。
参考书籍:
《Extending and embedding PHP》
备注:如果有错误或者有更好的实现方式,请读者们不吝指出,感激不尽。
本文版权归作者所有,转载请注明出处。