自定义扩展

前边我们介绍了 PHP 扩展的实现原理,本节我们来看下如何编写一个自己的扩展。

假定我们要经常获取文件的总行数,命令行下可以直接通过 “wc -l filepath” 获取,但是我们希望可以做成 PHP 内置函数来使用,例如:

<?php
$line = wcl('test.txt');

初始化

首先需要生成扩展的基本框架。PHP 官方提供了一个构造器 ext_skel 以帮助我们生成扩展所必要的文件和基本框架,该文件位于源码目录下的 ext 目录:

$ ll ext_skel
-rwxr-xr-x 1 vagrant vagrant 8.5K 4月  112017 ext_skel
$ ./ext_skel -help
./ext_skel --extname=module [--proto=file] [--stubs=file] [--xml[=file]] [--skel=dir] [--full-xml] [--no-help]

ext_skel 有几个参数,其中,--extname 参数用来指定要创建的扩展名称,是一个全为小写字母的标识符,仅包含字母和下划线,需要在 ext 目录下保持唯一;--proto 用来指定函数原型(这一步可以省略)。例如,我们创建一个名为 wcl 的 PHP 扩展,首先定义函数原型文件:

$ touch wcl.def
$ cat wcl.def
int wcl(string filename)

接下来生成扩展框架:

$ ./ext_skel --extname=wcl --proto=wcl.def
Creating directory wcl
Creating  basic  files:  config.m4  config.w32  .gitignore  wcl.c  php_wcl.h  CREDITS
    EXPERIMENTAL tests/001.phpt wcl.php [done].

To use your new extension, you will have to execute the following steps:

1.  $ cd ..
2.  $ vi ext/wcl/config.m4
3.  $ ./buildconf
4.  $ ./configure --[with|enable]-wcl
5.  $ make
6.  $ ./sapi/cli/php -f ext/wcl/wcl.php
7.  $ vi ext/wcl/wcl.c
8.  $ make

Repeat steps 3-6 until you are satisfied with ext/wcl/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.

$ ll wcl
-rw-r--r--1 vagrant vagrant 2.0K 1月  25 15:20 config.m4
-rw-r--r--1 vagrant vagrant  334 1月  25 15:20 config.w32
-rw-r--r--1 vagrant vagrant    3 1月  25 15:20 CREDITS
-rw-r--r--1 vagrant vagrant    0 1月  25 15:20 EXPERIMENTAL
-rw-r--r--1 vagrant vagrant 2.3K 1月  25 15:20 php_wcl.h
drwxr-xr-x 2 vagrant vagrant   21 1月  25 15:20 tests
-rw-r--r--1 vagrant vagrant 5.3K 1月  25 15:20 wcl.c
-rw-r--r--1 vagrant vagrant  493 1月  25 15:20 wcl.php

命令执行完,可以看到在当前目录下已经有了一个名为 wcl 的目录,目录中有很多文件。

  • config.m4autoconf 语法规则的编译配置文件,它可以指定扩展支持的 configure 选项以及扩展需要的额外的库,包含哪些源文件等。

  • config.w32Windows 平台下的编译配置文件,它的作用同 config.m4,但是它是使用 JavaScript 编写的。

  • CREDITS:用纯文本格式列出了扩展的贡献者和维护者。文件的第一行应保存扩展的名称,第二行是用逗号分隔的贡献者名单。

  • EXPERIMENTAL:实验功能说明文件。

  • php_wcl.h:当将扩展作为静态模块构建并放入 PHP 二进制包时,构建系统要求用 php_ 加扩展的名称命名的头文件包含一个对扩展模块结构的指针定义。就像其他头文件,此文件经常包含附加的宏、原型和全局变量。

  • tests:测试脚本目录。

  • wcl.c:扩展的主要源文件,通常,此文件名就是扩展的文件名。此文件包含模块结构定义、ini 配置项、扩展提供的函数和其他扩展所需的内容。

  • wcl.php:测试脚本,可以输出扩展支持的函数列表以及当前扩展是否已经被编译到 PHP。

编译配置

框架初始化之后,我们来看下如何修改编译配置文件。

这里补充一个小细节:读者可能注意到我们在编译安装 PHP 的时候,会指定一些编译配置选项,有些是 -with,有些是 -enable,这里有什么区别呢?一般来说,enable 表示某个内置功能是否开启,而 with 表示是否需要添加某个功能,通常需要指定依赖的外部库。

wcl 扩展不依赖外部组件,所以这里选择 enable 方式。修改 config.m4,去掉 PHP_ARG_ENABLE--enable-wcl 两行前面的 dnl(在 autoconf 语法中,dnl 表示注释)。修改后如下:

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

    if test "$PHP_WCL" ! = "no"; then
    PHP_NEW_EXTENSION(wcl,  wcl.c,  $ext_shared, ,  -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1)
fi

宏说明如下。

  • PHP_ARG_ENABLE 宏:第一个参数表示扩展名称;第二个参数在执行 ./configure 处理到该扩展时,显示该参数的内容;第三个参数是执行 ./configure -help 的输出信息。

  • PHP_NEW_EXTENSION 宏:声明了扩展的名称、源文件列表(多个文件的时候在文件名称后边加空格,如果需要换行还需加上反斜杠 “\”)、此扩展是动态库还是静态库,扩展是否只能在 CLICGI 模式下运行等。

更多的宏定义参见源码目录下的 acinclude.m4 文件。

在 14.2.1 节中,我们提到使用动态链接库方式的扩展还需要实现 get_module 方法,这里 exk_skel 帮我们做了这个工作:

#ifdef COMPILE_DL_WCL
#ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
#endif
ZEND_GET_MODULE(wcl)
#endif

功能实现

我们在生成扩展框架的时候已经指定了函数原型,ext_skel 会自动生成该函数的基本定义,并通过宏 PHP_FE 把函数注册到 zend_function_entry。如果不指定原型函数,那么这两步需要我们手动完成。另外,每个扩展都会注册一个名为 confirm_ 扩展名称 _compiled 的函数用来输出当前扩展是否已经被编译到 PHP:

/* {{{ proto int wcl(string filepath)
    */
PHP_FUNCTION(wcl)
{
    char *filepath = NULL;
    int argc = ZEND_NUM_ARGS();
    size_t filepath_len;

    if (zend_parse_parameters(argc, "s", &filepath, &filepath_len) == FAILURE) {
        return;
    }
    php_error(E_WARNING, "wcl: not yet implemented");
}
/* }}} */
/* {{{ wcl_functions[]
  *
  * Every user visible function must have an entry in wcl_functions[].
  */
const zend_function_entry wcl_functions[] = {
    PHP_FE(confirm_wcl_compiled, NULL)           /* For testing, remove later. */
    PHP_FE(wcl,    NULL)
    PHP_FE_END     /* Must be the last line in wcl_functions[] */
};
/* }}} */

扩展中函数的定义由 PHP_FUNCTION 宏来完成,实际展开如下:

void zif_wcl(zend_execute_data *execute_data, zval *return_value)

zend_parse_parameters 用来对函数参数做校验并获取函数的参数。第一个参数为传递给函数的参数个数,通常 ZEND_NUM_ARGS 来获取;第二个参数指定函数的参数类型,其后是要解析的参数。

接下来我们修改 PHP_FUNCTION(wcl) 实现功能即可,打开 wcl.c,修改这个函数为以下代码:

PHP_FUNCTION(wcl)
{
    char *filepath = NULL;
    int argc = ZEND_NUM_ARGS();
    size_t filepath_len;
    char ch;
    FILE *fp;
    Zend_long lcount = 0;

    if (zend_parse_parameters(argc, "s", &filepath, &filepath_len) == FAILURE)
    {
        return;
    }

    /* php_error(E_WARNING, "wcl: not yet implemented"); */

    if ((fp = fopen(filepath, "r")) == NULL) {
        RETURN_FALSE;
    }

    while ((ch = fgetc(fp)) ! = EOF) {
        if (ch == '\n') {
            lcount++;
        }
    }
    fclose(fp);
    RETURN_LONG(lcount);
}

这个函数在无法打开文件的时候返回 false,正常情况下遍历文件内容判断换行符,如果当前字符为换行符,则计数加 1,最终返回文件总行数。

这里的 RETURN_FALSERETURN_LONG 是用于从函数中返回值的宏,分别表示返回 falselong 类型的返回值,

类似的还有 RETVAL_NULLRETVAL_BOOLRETVAL_TRUERETVAL_DOUBLERETVAL_STRINGRETVAL_STRINGLRETVAL_RESOURCE,见名思义,这里就不再逐一解释了。

注册配置项

如果不考虑各种边界问题,这个扩展的基本功能就完成了,后边只需要安装并启用这个扩展即可,但是这里我们稍微扩展下:注册一个 ini 配置项来控制是否计算空行(暂时只考虑首字符为换行符的情况)。

配置解析相关内容请参考第 8 章,这里不再赘述。

php_wcl.h 中声明扩展内的全局变量:

ZEND_BEGIN_MODULE_GLOBALS(wcl)
    Zend_long filter_blank;
ZEND_END_MODULE_GLOBALS(wcl)

wcl.c 中添加配置项:

ZEND_DECLARE_MODULE_GLOBALS(wcl)

PHP_INI_BEGIN()
    STD_PHP_INI_ENTRY("wcl.filter_blank", "0", PHP_INI_ALL, OnUpdateBool, filter_
        blank, zend_wcl_globals, wcl_globals)
PHP_INI_END()

以上代码表示为当前扩展注册了一个配置项 “wcl.filter_blank”,其默认值为 0。

接下来,我们修改前边的 PHP_FUNCTION(wcl),在函数中获取这个配置项:

PHP_FUNCTION(wcl)
{
    char *filepath = NULL;
    int argc = ZEND_NUM_ARGS();
    size_t filepath_len;
    char ch, pre = '\n';
    FILE *fp;
    zend_long lcount = 0;

    if (zend_parse_parameters(argc, "s", &filepath, &filepath_len) == FAILURE)
    {
        return;
    }

    /* php_error(E_WARNING, "wcl: not yet implemented"); */

    if ((fp = fopen(filepath, "r")) == NULL) {
        RETURN_FALSE;
    }

    while ((ch = fgetc(fp)) ! = EOF) {
        if (ch == '\n') {
            if (WCL_G(filter_blank) && pre == ch) {
                continue;
            }
            lcount++;
        }
        pre = ch;
    }
    fclose(fp);

    RETURN_LONG(lcount);
}

扩展内可以通过注册到 WCL_G 的配置项来获取配置。到这里还差一步就可以真正使用它了。前面我们学习了 PHP 有五大阶段,每个扩展都会通过实现这五个阶段的钩子函数来完成相关工作,具体如下:

/* {{{ wcl_module_entry
  */
zend_module_entry wcl_module_entry = {
    STANDARD_MODULE_HEADER,
    "wcl",
    wcl_functions,
    PHP_MINIT(wcl),
    PHP_MSHUTDOWN(wcl),
    PHP_RINIT(wcl), /* Replace with NULL if there's nothing to do at request start */
    PHP_RSHUTDOWN(wcl), /* Replace with NULL if there's nothing to do at request end */
    PHP_MINFO(wcl),
    PHP_WCL_VERSION,
    STANDARD_MODULE_PROPERTIES
};
/* }}} */

其中,配置项的注册是在模块初始化阶段 PHP_MINIT 这一步完成的,在 wcl.c 文件找到 PHP_MINIT_FUNCTION 函数,可以看到里边已经有如下注释(我们只需要打开这个注释即可,wcl 扩展在启动的时候会自动注册当前扩展的配置项):

/* {{{ PHP_MINIT_FUNCTION
  */
PHP_MINIT_FUNCTION(wcl)
{
    /* If you have INI entries, uncomment these lines
    REGISTER_INI_ENTRIES();
    */
    return SUCCESS;
}
/* }}} */

相应地,在模块关闭阶段 PHP_MSHUTDOWN 中,也需要注销配置项,在 PHP_MSHUTDOWN_FUNCTION 函数中打开对应的注释即可。

编译、安装

完成了扩展功能的实现,并注册了相应的配置项后,只需要编译并安装就可以在 PHP 程序中使用了:

$ pwd
/home/vagrant/php-7.1.0/output/ext/wcl
$ /home/vagrant/php-7.1.0/output/bin/phpize
Configuring for:
PHP Api Version:         20160303
Zend Module Api No:      20160303
Zend Extension Api No:   320160303
$ ./configure --with-php-config=/home/vagrant/php-7.1.0/output/bin/php-config
// 编译过程略
$ make && make install
Installing  shared  extensions:        /home/vagrant/php-7.1.0/output/lib/php/
    extensions/no-debug-non-zts-20160303/
$ ll /home/vagrant/php-7.1.0/output/lib/php/extensions/no-debug-non-zts-20160303/
-rwxr-xr-x 1 root root  33K 1月  27 05:49 wcl.so

可以看到,扩展对应的目录下已经有对应的 wcl.so 文件了,之后的步骤想必读者已经很熟悉了,只需要打开 php.ini 文件启用扩展并配置对应的配置项即可。配置完成以后,执行以下命令:

$ php -f ext/wcl/wcl.php
Functions available in the test extension:
confirm_wcl_compiled
wcl

Congratulations! You have successfully modified ext/wcl/config.m4. Module wcl is
now compiled into PHP.

可以看到,wcl 已经成功编译到 PHP,并且该扩展提供了两个函数。现在我们可以在 PHP 代码中直接使用 wcl 函数了。