小猪教你开发php扩展

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

 

参考:

php扩展开发

PHP扩展开发教程总结

PHP扩展开发快速入门

欢迎关注下方“非著名资深码农“公众号进行交流~

发表评论

邮箱地址不会被公开。