技术分享之PHP5扩展开发入门

这次分享准备的是php5的扩展开发入门。

本文只针对php5,php7可能有不一样的地方。

什么算入门呢,每个人估计有不同的看法,比如像下面这样:

极简入门

  1. 初始化扩展文件

    1
    2
    3
    cd source_dir/ext
    ./ext_skel --extname=t1
    cd t1
  2. 修改config.m4,去掉16-18的注释,即开头的dnl

    1
    2
    3
    PHP_ARG_ENABLE(wzh, whether to enable wzh support,
    dnl Make sure that the comment is aligned:
    [ --enable-wzh Enable wzh support])

m4是一门编程语言,参考Wikipedia)。在linux下我们可以用m4 xxx.m4来解析文件。

注意下面我们所有的路径都尽量指定清楚,以免在系统存在多个php版本的时候出错,并且在调用php命令的时候需要指定上配置文件,否则不会加载自定义扩展。

  1. 编译安装:

    1
    2
    3
    phpize
    ./configure --with-php-config=dir/php-config
    make && make install
  2. 修改php.ini:

    1
    2
    3
    # 安装完成会给出这个路径
    extension_dir = "/usr/local/debug_php/lib/php/extensions/debug-non-zts-20180731/"
    extension = t1.so
  3. 验证扩展是否加载:

    1
    dir/php -c dir/php.ini -m | grep t1
  4. 按上面方式初始化的扩展,默认会有一个接收一个参数的函数,名称格式为confirm_extname_compiled,所以我们的函数是confirm_t1_compiled,调用扩展函数验证:

    1
    2
    3
    4
    dir/php -c dir/php.ini -r "echo confirm_t1_compiled('good');"

    # 输出
    Congratulations! You have successfully modified ext/t1/config.m4. Module good is now compiled into PHP.

到这里就完成了。

啊,有点太简单了吧,那接下来做更进一步的入门。

稍微理解一点儿的入门

从头分析,添加扩展的目的是给php代码提供之前没有的函数,或者更高效的函数。首先需要知道php扩展开发的规范,包括代码的执行流程、组成部分及其作用等。之后进入具体的开发环节,我们需要了解Zend引擎的api来完善扩展代码功能,包括但不限于内存分配、参数校验、返回值、IO等。代码写完需要测试,测试就涉及到调试。调试完成需要发布,需要让大家看到我们的扩展。

下面分别说说每个流程。

  1. php代码执行流程,网上有个广为流传的图,原始出处可能是《php核心技术与最佳实践》。
02-01-01-cgi-lift-cycle
02-01-01-cgi-lift-cycle

根据图以及书上的介绍我们可以了解到:
1). 扩展被载入的时候会调用PHP_MINIT_FUNCTION,这里的MINIT字面意思应该是module init,也就是模块的初始化。通常我们用它来初始化一些常量之类的操作。

2). 之后请求到达test.php文件,这时候会调用所有的模块的RINIT函数,这里面通常会做一些跟请求相关的变量初始化工作,例如上一次错误信息之类的。

例如mysqli扩展的RINIT是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
PHP_RINIT_FUNCTION(mysqli)
{
#if !defined(MYSQLI_USE_MYSQLND) && defined(ZTS) && MYSQL_VERSION_ID >= 40000
if (mysql_thread_init()) {
return FAILURE;
}
#endif
MyG(error_msg) = NULL;
MyG(error_no) = 0;
MyG(report_mode) = 0;

return SUCCESS;
}

3). 之后是执行test.php代码,这个步骤会将php代码编译成Opcodes,供Zend引擎调用。

4). 后面两部分是RSHUTDOWNMSHUTDOWN,先执行RSHUTDOWN释放一些对应RINIT中申请的内存空间等。最后执行MSHUTDOWN,同理也是主要用来释放资源的。

例如mysqli扩展的RSHUTDOWN是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PHP_RSHUTDOWN_FUNCTION(mysqli)
{
/* check persistent connections, move used to free */

#if !defined(MYSQLI_USE_MYSQLND) && defined(ZTS) && MYSQL_VERSION_ID >= 40000
mysql_thread_end();
#endif
if (MyG(error_msg)) {
efree(MyG(error_msg));
}
#if defined(A0) && defined(MYSQLI_USE_MYSQLND)
/* psession is being called when the connection is freed - explicitly or implicitly */
zend_hash_apply(&EG(persistent_list), (apply_func_t) php_mysqli_persistent_helper_once TSRMLS_CC);
#endif
return SUCCESS;
}

因为缺少完善的扩展编写知识,所以这里的流程以及作用可能有偏颇,可选择性参考。

  1. 执行流程大概理清之后,来实现代码,这里我们还是打开第一部分生成的t1.c文件,所有的代码实现都在这里。

1). 包含头文件

1
2
3
4
#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "php_t1.h"

前三个是默认生成扩展时自带的,最后一个是我们扩展的头文件,方便定义一些特定的常量。

2). 实现导出函数
这个步骤是真正的编写暴漏给php代码的函数,如下默认的confirm函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PHP_FUNCTION(confirm_t1_compiled)
{
char *arg = NULL;
int arg_len, len;
char *strg;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &arg, &arg_len) == FAILURE) {
return;
}

len = spprintf(&strg, 0, "Congratulations! You have successfully modified ext/%.78s/config.m4. Module %.78s is now compiled into PHP.", "t1",
arg);
RETURN_STRINGL(strg, len, 0);
}

这段代码接收一个参数,返回一个字符串。从这段代码中我们能分析出来如何接收参数以及如何返回值。

接收参数用这段代码:

1
2
3
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &arg, &arg_len) == FAILURE) {
return;
}

ZEND_NUM_ARGS()这个宏返回的是参数的个数,"s", &arg, &arg_len,这部分的s代表我们接收一个字符串参数(这些标识符,跟变量的类型首字母基本一致),赋值给arg,字符串长度赋值给arg_len。也即一个字符串参数需要两个变量来接收,分别对应字符串的值和长度。如果接收字符串和浮点数两个参数,可以这样写"sd", &arg, &arg_len, &dl

返回值可以通过这个宏来完成:

1
RETURN_STRINGL(strg, len, 0);

每个类型都有一个类似的宏,例如RETURN_LONG,可以在源码的Zend/zend_API.h目录下查看。

还有就是这里返回的值是Zval,而不是单一的c变量,因为Zval才是php里面的变量。

3). 声明Zend函数块

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

Zend函数块,顾名思义就是需要Zend加载的函数列表,在这里声明的函数,必须要实现。

Zend内部会通过一个循环来遍历加载函数,看代码的注释,最后的PHP_FE_END是固定的,因为Zend会根据这个来判断是否加载完成。

4). 声明Zend模块,在t1.c里面会看到这段代码,是不是跟上面的php执行流程很像呢

1
2
3
4
5
6
7
8
9
10
11
12
zend_module_entry t1_module_entry = {
STANDARD_MODULE_HEADER,
"t1",
t1_functions,
PHP_MINIT(t1),
PHP_MSHUTDOWN(t1),
PHP_RINIT(t1), /* Replace with NULL if there's nothing to do at request start */
PHP_RSHUTDOWN(t1), /* Replace with NULL if there's nothing to do at request end */
PHP_MINFO(t1),
PHP_T1_VERSION,
STANDARD_MODULE_PROPERTIES
};

5). 实现get_moudle函数

t1.c里面,有这样一段条件编译:

1
2
3
#ifdef COMPILE_DL_T1
ZEND_GET_MODULE(t1)
#endif

查看一下这个宏ZEND_GET_MODULE的源代码,在Zend/zend_API.c中,如下:

1
2
3
4
#define ZEND_GET_MODULE(name) \
BEGIN_EXTERN_C()\
ZEND_DLEXPORT zend_module_entry *get_module(void) { return &name##_module_entry; }\
END_EXTERN_C()

可以看出这段宏代码直接引用的第四步的Zend模块,而模块会引用之前定义的函数,以及执行php代码流程,到这里我们发现是这个宏让Zend引擎和扩展文件关联在一起了。

顺着这个步骤理清了php代码的执行流程,以及具体的代码实现流程,并且包含了接收参数,返回值的说明。

6). 最后可能还需要给扩展添加一个预定义的常量,根据前面执行流程讲解的,可以放在MINIT内,例如source_dir/ext/json/json.c的写法:

1
2
3
REGISTER_LONG_CONSTANT("JSON_HEX_TAG",  PHP_JSON_HEX_TAG,  CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("JSON_HEX_AMP", PHP_JSON_HEX_AMP, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("JSON_HEX_APOS", PHP_JSON_HEX_APOS, CONST_CS | CONST_PERSISTENT);

REGISTER_LONG_CONSTANT这个宏接收三个参数,第一个是常量名,第二个是常量值,可以放在扩展专用的头文件内,例如json.h。后面两个标识符说明如下:

1
2
3
4
5
CONST_CS
标识常量大小写敏感

CONST_PERSISTENT
标识常量是持久化的,也就是该常量不会被释放,多个请求共用

通常把这两个标识符都设置在常量上,即符合预期,也不会有其他影响。

7). 调试代码可以参考我之前写的这篇文章

  1. 现在我们可以写出很简单的扩展了,可能第一件事情是放在phpinfo里面炫耀一下,同样,在t1.c里面,可以用这段代码来生成phpinfo展示:
1
2
3
4
5
6
7
8
9
10
11
PHP_MINFO_FUNCTION(t1)
{
php_info_print_table_start();
php_info_print_table_header(2, "t1 support", "enabled"); // 添加表头
php_info_print_table_row(2, "author", "wzh"); // 添加一行
php_info_print_table_end();

/* Remove comments if you have entries in php.ini
DISPLAY_INI_ENTRIES();
*/
}

这段代码在phpinfo的输出会看到这样的展示:

t1 support enabled
author wzh

最后

到此为止,我们能基本理清php的执行流程,扩展的编写流程,比较常用的语法写法,剩下的内容就是熟悉Zend API了。

参考文献

  • 《php核心技术与最佳实践》

(完)