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

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

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

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

极简入门

  1. 初始化扩展文件
cd source_dir/ext
./ext_skel --extname=t1
cd t1
  1. 修改config.m4,去掉16-18的注释,即开头的dnl
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. 编译安装:
phpize
./configure --with-php-config=dir/php-config
make && make install
  1. 修改php.ini:
# 安装完成会给出这个路径
extension_dir = "/usr/local/debug_php/lib/php/extensions/debug-non-zts-20180731/"
extension = t1.so
  1. 验证扩展是否加载:
dir/php -c dir/php.ini -m | grep t1
  1. 按上面方式初始化的扩展,默认会有一个接收一个参数的函数,名称格式为confirm_extname_compiled,所以我们的函数是confirm_t1_compiled,调用扩展函数验证:
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核心技术与最佳实践》。

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

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

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

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是这样的:

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). 包含头文件

#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "php_t1.h"

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

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

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);
}

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

接收参数用这段代码:

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

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

RETURN_STRINGL(strg, len, 0);

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

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

3). 声明Zend函数块

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执行流程很像呢

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里面,有这样一段条件编译:

#ifdef COMPILE_DL_T1
ZEND_GET_MODULE(t1)
#endif

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

#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的写法:

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。后面两个标识符说明如下:

CONST_CS
标识常量大小写敏感

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

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

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

  1. 现在我们可以写出很简单的扩展了,可能第一件事情是放在phpinfo里面炫耀一下,同样,在t1.c里面,可以用这段代码来生成phpinfo展示:
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核心技术与最佳实践》

(完)