PHP是当前应用非常广泛的一门语言,从国外的Facebook、Twitter到国内的淘宝、腾讯、百度等都能见到它的身影。
当php支撑的流量越来越大需要进行代码性能优化时,执行效率优化最高的方法就是把调用频次频繁的php函数封装为c或c++编写的php扩展。或当系统需要实现一个对性能要求比较高的模块时,用c或c++实现php扩展会是一个很好的选择。
当前PHP的扩展机制是基于Zend API的,Zend API提供了丰富的接口和宏定义,加上一些实用工具,使得PHP扩展开发起来难度并不算特别大。本文介绍PHP扩展开发的基本知识,并通过一个实例展示开发PHP扩展的基本过程。
一、生成扩展代码框架
1. 下载php源码
#wget https://www.php.net/distributions/php-7.0.33.tar.gz
wget https://www.php.net/distributions/php-8.0.1.tar.gz
cd php-8.0.1/build
../configure
make
sudo make install
2. 用ext_skel生成php7扩展模版代码
cd php-8.0.1/ext
#sh ./ext_skel --extname=mars
php ext_skel.php --ext mars
*注:输出如下内容代表生成php7 mars扩展代码成功了
Creating directory mars
Creating basic files: config.m4 config.w32 .gitignore mars.c php_mars.h CREDITS EXPERIMENTAL tests/001.phpt mars.php [done].
To use your new extension, you will have to execute the following steps:
1. $ cd ..
2. $ vi ext/mars/config.m4
3. $ ./buildconf
4. $ ./configure --[with|enable]-mars
5. $ make
6. $ ./sapi/cli/php -f ext/mars/mars.php
7. $ vi ext/mars/mars.c
8. $ make
Repeat steps 3-6 until you are satisfied with ext/mars/config.m4 and
step 6 confirms that your module is compiled into PHP. Then, start writing
code and repeat the last two steps as often as necessary.
此步骤之后会在ext目录下生成一个名为mars的文件夹,其中这三个文件必须注意:
config.m4:Unix环境下的Build System配置文件,后面将会通过它生成配置和安装
php_mars.h:这个文件是扩展模块的头文件。遵循C语言一贯的作风,这个里面可以放置一些自定义的结构体、全局变量等等。
mars.c:这个就是扩展模块的主程序文件了,最终的扩展模块各个函数入口都在这里。当然,你可以将所有程序代码都塞到这里面,也可以遵循模块化思想,将各个功能模块放到不同文件中。
3. PHP Extension及Zend_Module结构分析
以上可以看成是为开发PHP扩展而做的准备工作,下面就要编写核心代码了。上文说过,编写PHP扩展是基于Zend API和一些宏的,所以如果要编写核心代码,我们首先要弄清楚PHP Extension的结构。因为一个PHP Extension在C语言层面实际上就是一个zend_module_entry结构体,这点可以从“php_mars.h”中得到证实。打开“php_mars.h”,会看到里面有怎么一行:
extern zend_module_entry mars_module_entry;
mars_module_entry就是mars扩展的C语言对应元素,而关于其类型zend_module_entry的定义可以在PHP源代码的“Zend/zend_modules.h”文件里找到,下面代码是zend_module_entry的定义:
typedef struct _zend_module_entry zend_module_entry;
struct _zend_module_entry {
unsigned short size;
unsigned int zend_api;
unsigned char zend_debug;
unsigned char zts;
const struct _zend_ini_entry *ini_entry;
const struct _zend_module_dep *deps;
const char *name; # PHP Extension的名字
const struct _zend_function_entry *functions; # 存放我们在此扩展中定义的函数的引用
int (*module_startup_func)(INIT_FUNC_ARGS); # 函数指针,扩展模块加载时被调用
int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS); # 函数指针,扩展模块卸载时时被调用
int (*request_startup_func)(INIT_FUNC_ARGS); # 函数指针,每个请求开始时时被调用
int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS); # 函数指针,每个请求结束时时被调用
void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS); # 函数指针,这个指针指向的函数会在执行phpinfo()时被调用,用于显示自定义模块信息。
const char *version; # 模块的版本
size_t globals_size;
#ifdef ZTS
ts_rsrc_id* globals_id_ptr;
#else
void* globals_ptr;
#endif
void (*globals_ctor)(void *global TSRMLS_DC);
void (*globals_dtor)(void *global TSRMLS_DC);
int (*post_deactivate_func)(void);
int module_started;
unsigned char type;
void *handle;
int module_number;
char *build_id;
};
这个结构体就是PHP Extension的原型,如果不搞清楚,就没法开发PHP Extension了。其中拣关键的、这篇文章会用到的字段注释了一下,其他许多字段并不需要我们手工填写,而是可以使用某些预定义的宏填充。
现在我们看下“mars.c”中自动生成的“mars_module_entry”框架代码:
/* {{{ mars_module_entry
*/
zend_module_entry mars_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
STANDARD_MODULE_HEADER,
#endif
"mars",
mars_functions,
PHP_MINIT(mars),
PHP_MSHUTDOWN(mars),
PHP_RINIT(mars), /* Replace with NULL if there's nothing to do at request start */
PHP_RSHUTDOWN(mars), /* Replace with NULL if there's nothing to do at request end */
PHP_MINFO(mars),
#if ZEND_MODULE_API_NO >= 20010901
"0.1", /* Replace with version number for your extension */
#endif
STANDARD_MODULE_PROPERTIES
};
/* }}} */
首先,宏“STANDARD_MODULE_HEADER”会生成前6个字段,“STANDARD_MODULE_PROPERTIES ”会生成“version”后的字段,所以现在我们还不用操心。而我们关心的几个字段,也都填写好或由宏生成好了,并且在“mars.c”的相应位置也生成了几个函数的框架。这里要注意,几个宏的参数均为“mars”,但这并不表示几个函数的名字全为“mars”,C语言中也不可能存在函数名重载机制。实际上,在开发PHP Extension的过程中,几乎处处都要用到Zend里预定义的各种宏,从全局变量到函数的定义甚至返回值,都不能按照“裸写”的方式来编写C语言,这是因为PHP的运行机制可能会导致命名冲突等问题,而这些宏会将函数等元素变换成一个内部名称,但这些对程序员都是透明的(除非你去阅读那些宏的代码),我们通过各种宏进行编程,而宏则为我们处理很多内部的东西。
写到这里,我们的任务就明了了:第一,如果需要在相应时机处理一些东西,那么需要填充各个拦截函数内容;第二,编写mars的功能函数,并将引用添加到mars_functions中。
二、编写扩展函数
1. 修改ext/mars/config.m4 Build System配置文件
*注:config.m4文件中以“dnl”开头的全是注释
将config.m4文件中下面三行
dnl PHP_ARG_ENABLE(mars, whether to enable mars support,
dnl Make sure that the comment is aligned:
dnl [ --enable-mars Enable mars support])
改为:
PHP_ARG_ENABLE(mars, whether to enable mars support,
dnl Make sure that the comment is aligned:
[ --enable-mars Enable mars support])
*注:如果你的扩展引用了外部组件就使用PHP_ARG_WITH,否则使用PHP_ARG_ENABLE。
2. c++扩展需求
如果没有C++需求的,可以跳过该部分。
# 2.1 修改文件名
-将mars.c改为php_mars.cpp
-将config.m4文件中的mars.c改为php_mars.cpp
-在config.m4文件中PHP_NEW_EXTENSION这一样的前面增加如下三行代码:
PHP_REQUIRE_CXX()
PHP_ADD_LIBRARY(stdc++, "", MARS_SHARED_LIBADD)
PHP_SUBST(MARS_SHARED_LIBADD)
# 2.2 修改php_mars.cpp文件
将php_mars.cpp文件中的php相关头文件用extern "C"包起来,修改如下:
#ifndef __cplusplus
extern "C" {
#endif
#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#ifndef __cplusplus
}
#endif
# 2.3 依赖库
如果你的扩展中有依赖其他的库,需要在config.m4文件中声明被依赖的库对应的include和lib文件的位置。
dnl 声明被依赖库的include文件位置
PHP_ADD_INCLUDE(../libs/include)
dnl 声明被依赖库的lib文件位置
PHP_ADD_LIBRARY_WITH_PATH(ullib, ../libs/lib, EXAMPLE_SHARED_LIBADD)
PHP_ADD_LIBRARY_WITH_PATH(uconv, ../libs/lib, EXAMPLE_SHARED_LIBADD)
3. 添加扩展函数 mars_sum、mars_echo
这种方式添加的扩展函数可以在php里通过函数名直接调用。
# 3.1 mars.h函数声明
// 定义全局函数
PHP_FUNCTION(mars_sum);
PHP_FUNCTION(mars_echo);
# 3.2 php_mars.cpp函数注册和实现
/* 向PHP空间注册函数 */
const zend_function_entry mars_functions[] = { #注;mars_functions已默认传入扩展实体zend_module_entry mars_module_entry参数中
PHP_FE(mars_sum, NULL)
PHP_FE(mars_echo, NULL)
PHP_FE_END /* Must be the last line in mars_functions[] */
};
/* 实现全局函数 mars_sum */
PHP_FUNCTION(mars_sum)
{
double num1,num2,sum;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "dd", &num1, &num2) == FAILURE) {
RETURN_FALSE;
}
sum = num1 + num2;
// RETURN_* 的定义在zend_API.h中
RETURN_DOUBLE(sum);
}
/* 实现全局函数 mars_echo */
PHP_FUNCTION(mars_echo)
{
char *arg = NULL;
int arg_len, len;
char *strg;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &arg, &arg_len) == FAILURE) {
RETURN_FALSE;
}
len = spprintf(&strg, 0, "%.78s", arg);
// PHP_MAJOR_VERSION的定义在php_version.h中
#if PHP_MAJOR_VERSION <= 5
RETURN_STRINGL(strg,len,0);
#else
RETURN_STRINGL(strg,len);
#endif
}
*注:解析参数是通过zend_parse_parameters函数实现的,这个函数的作用是从函数用户的输入栈中读取数据,然后转换成相应的函数参数填入变量以供后面核心功能代码使用。zend_parse_parameters的第一个参数是用户传入参数的个数,可以由宏“ZEND_NUM_ARGS() TSRMLS_CC”生成;第二个参数是一个字符串,其中每个字母代表一个变量类型,我们只有一个字符串型变量,所以第二个参数是“s”;最后各个参数需要一些必要的局部变量指针用于存储数据,下表给出了不同变量类型的字母代表及其所需要的局部变量指针。
b zend_bool
l long
d double
s char*,int
h HashTable*
4. 类函数/静态函数定义
这种方式定义的函数可以在php里通过“类名::函数名”的方式调用。不需要时跳过此步即可。
# 4.1 mars.h静态函数定义
// 定义Mars类的静态成员函数sum
static PHP_METHOD(Mars, sum);
// 定义Mars类的普通成员函数sub
PHP_METHOD(Mars, sub);
# 4.2 php_mars.cpp静态函数实现
# 类定义
#define MARS "Mars"
static zend_class_entry * static_mars_class_entry_ptr;
/* 模块初始化时注册类 */
PHP_MINIT_FUNCTION(mars)
{
zend_class_entry static_mars_class_entry;
INIT_CLASS_ENTRY(static_mars_class_entry, MARS, static_mars_functions);
static_mars_class_entry_ptr = zend_register_internal_class(&static_mars_class_entry TSRMLS_CC);
return SUCCESS;
}
/* 向php空间注册static静态函数 */
static zend_function_entry static_mars_functions[] = {
PHP_ME(Mars, sum, NULL, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
PHP_ME(Mars, sub, NULL, ZEND_ACC_PUBLIC)
{NULL, NULL, NULL} #注意这个数组最后一行必须是{NULL, NULL, NULL} ,请不要删除
};
/* 实现Mars类的静态成员函数sum。sub函数实现方法同sum */
PHP_METHOD(Mars, sum)
{
double num1,num2,sum;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "dd", &num1, &num2) == FAILURE) {
RETURN_FALSE;
}
sum = num1 + num2;
RETURN_DOUBLE(sum);
}
5. 编写phpinfo()回调函数
用户在phpinfo()页面显示扩展信息。
PHP_MINFO_FUNCTION(mars)
{
php_info_print_table_start();
php_info_print_table_header(2, "mars support", "enabled");
php_info_print_table_header(2, "Version", PHP_MARS_VERSION);
php_info_print_table_row(2, "Build Date", __DATE__ " " __TIME__);
php_info_print_table_row(2, "Author", "Yan Jingang");
php_info_print_table_end();
/* Remove comments if you have entries in php.ini
DISPLAY_INI_ENTRIES();
*/
}
三、编译扩展
phpize
./configure --enable-mars --with-config-path=/usr/local/php/bin/php-config
make # make完毕就能看到mars.so文件了,自己拷贝so并编辑php.ini或 make install安装均可
make install
四、测试扩展
1. php命令行检查扩展:
$ php/bin/php -m |grep mars
mars
2. web服务phpinfo()检查扩展:
php/sbin/php-fpm restart
<?php
phpinfo();
3. php代码测试扩展函数
:
<?php
var_dump(get_extension_funcs('mars')); //打印扩展的函数列表
echo mars_sum(1,2);
echo mars_echo("hello world");
echo Mars::sum(1,2);
$obj = new Mars();
echo $obj->sub(2,1);
yan 19.8.30
参考: