0%

PHP扩展开发 - Part 1

在网上找到几篇基于PHP5的扩展开发的文章,有些古老,与PHP7不完全兼容,自己做了些简单的翻译、修改。

建议开发者先阅读PHP Internals Book这份在线文档,里面对PHP做了比较全面的讲解。由于PHP扩展开发的资料比较少,也鲜有PHP开发者触及到这块,编写扩展时如果遇到问题,最好是先查找PHP源代码是否有相关的使用案例,再通过网上论坛、QQ等方式寻求帮助。

本文是PHP扩展开发系列的第一篇,主要介绍如何开发一个简单的PHP扩展、如何获取ini配置参数、如何配置全局变量

前期准备:

PHP编译选项加入--enable-debug以及--enable-maintainer-zts--enable-debug生成额外的调试符号,并将优化等级设置为-O0,报告内存泄露等错误,使使用gdb进行debug时更为准确;--enable-maintainer-zts启用线程安全,开启ZTS宏定义,配置PHP为TSRM机制,用于编写调试多线程代码

Hello World

  1. 扩展目录结构

你可能会使用PHP源代码自带的脚本ext/ext_skel.php生成扩展骨架,但这里选择极简方式创建一个扩展,最基本的扩展只需要三个文件:配置文件config.m4,头文件php_test.h,源文件test.c

test扩展目录结构

1
2
3
4
5
6
php-7.4.1
└── ext
└── test
├── config.m4
├── php_test.h
└── test.c
  1. 配置文件
1
2
3
4
5
6
7
8
9
10
dnl 用于生成configure及其他文件
dnl `dnl`开头的行为注释内容
dnl 若扩展依赖外部库,使用--with,否则默认使用--enable
PHP_ARG_ENABLE(test, whether to enable test support,
[ --enable-test Enable test support], no)

if test "$PHP_TEST" != "no"; then
AC_DEFINE(HAVE_TEST, 1, [ Have test support ])
PHP_NEW_EXTENSION(test, test.c, $ext_shared)
fi
  1. 头文件

头文件默认格式为php_extname.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef PHP_TEST_H
# define PHP_TEST_H

/* PHP扩展结构zend_module_entry */
extern zend_module_entry test_module_entry;
# define phpext_test_ptr &test_module_entry

/* version扩展版本号 */
# define PHP_TEST_VERSION "0.1.0"

/* 自定义函数声明PHP_FUNCTION(your_function_name) */
PHP_FUNCTION(test);

#if defined(ZTS) && defined(COMPILE_DL_TEST)
ZEND_TSRMLS_CACHE_EXTERN()
#endif

#endif
  1. 源文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "php.h"
#include "php_test.h"

/* 自定义函数注册,最终*test_functions注册到模块声明test_module_entry */
static const zend_function_entry test_functions[] = {
/*
* 注册`test`函数,格式为PHP_FE(func_name, arg_info),arg_info声明参数信息,如是否是引用等,这里先忽略
*/
PHP_FE(test, NULL)
PHP_FE_END
};

/* 模块注册 */
zend_module_entry test_module_entry = {
STANDARD_MODULE_HEADER,
"test", /* 扩展名称 */
test_functions, /* 自定义函数注册 */
NULL, /* PHP_MINIT */
NULL, /* PHP_MSHUTDOWN */
NULL, /* PHP_RINIT */
NULL, /* PHP_RSHUTDOWN */
NULL, /* PHP_MINFO */
PHP_TEST_VERSION, /* version */
STANDARD_MODULE_PROPERTIES
};

/* 函数test */
PHP_FUNCTION(test)
{
RETURN_STRING("Hello World");
}

#ifdef COMPILE_DL_TEST
# ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
# endif
ZEND_GET_MODULE(test)
#endif
  1. 编译
1
2
3
4
phpize
./configure
# 除非config.m4有改动,否则只执行以下命令
make && sudo make install
  1. 验证

php.ini添加extension=test.so,执行脚本获取输出

1
php -r "echo test();" # 输出:Hello World

INI参数

Zend Engine提供了两种方式访问ini参数,以下是第一种较为简单的方式,第二种方式与全局变量搭配使用,后面介绍

我们在PHP的声明周期MINIT阶段注册ini参数,在php_test.h添加MINIT原型声明

1
2
3
4
// test模块初始化
PHP_MINIT_FUNCTION(test);
// test模块关闭清理函数
PHP_MSHUTDOWN_FUNCTION(test);

test.c做以下调整,注册MINIT,init参数声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include "php_ini.h"

zend_module_entry test_module_entry = {
STANDARD_MODULE_HEADER,
"test",
test_functions,
PHP_MINIT(test), /* 注册MINIT */
PHP_MSHUTDOWN(test), /* 注册MSHUTDOWN */
NULL,
NULL,
NULL,
PHP_TEST_VERSION,
STANDARD_MODULE_PROPERTIES
};

// ini参数获取代码块
PHP_INI_BEGIN()
// ini参数test.foo声明
PHP_INI_ENTRY("test.foo", "bar", PHP_INI_ALL, NULL)
PHP_INI_END();
// 以上宏等同于声明zend_ini_entry_def[]变量

PHP_MINIT_FUNCTION(test)
{
REGISTER_INI_ENTRIES();

return SUCCESS;
}

PHP_MSHUTDOWN_FUNCTION(test)
{
UNREGISTER_INI_ENTRIES();

return SUCCESS;
}

PHP_FUNCTION(test)
{
RETURN_STRING(INI_STR("test.foo"));
}

ini参数声明放置在PHP_INI_BEGINPHP_INI_END,参数声明使用宏PHP_INI_ENTRY,具体格式为PHP_INI_ENTRY(param_name, default_val, access_mode_modifier, validator)

access_mode_modifier决定ini参数可在什么地方被修改,主要有

  • PHP_INI_ALL: 值可通过php.ini、.htaccess、ini_set修改
  • PHP_INI_SYSTEM: 只可通过php.ini修改
  • PHP_INI_PERDIR: 只可通过.htaccess修改

validator校验这里先忽略

ini参数值获取方式如下

当前值local value 默认值master value 返回值类型
INI_STR INI_ORIG_STR char *
INI_INT INI_ORIG_STR zend_long
INI_FLT INI_ORIG_STR double
INI_BOOL INI_ORIG_STR zend_bool

配置PHP,在php.ini添加test.foo=helloworld,执行脚本获取输出

1
php -r "echo test();" # 输出:helloworld

全局变量

上面部分的ini参数获取代码有几个问题,第一,每次读取ini参数值时,都需要系统扫描ini设置表,并转换成相应的类型;第二,没有校验用户输入,这样用户使用ini_set可以设置任意数据

我们可以配置ini参数到全局变量,避免每次扫描ini设置表,在这之前,先看看如何使用全局变量编写一个计数器

头文件php_test.h添加全局变量声明,并定义一个宏来访问我们的全局变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#ifdef ZTS
# include "TSRM.h"
#endif

/* 全局变量声明 */
ZEND_BEGIN_MODULE_GLOBALS(test)
long counter;
ZEND_END_MODULE_GLOBALS(test)
/*
* 上面代码等同
* typedef struct _zend_pib_globals {
* long counter;
* } zend_test_globals;
*/

/* 宏定义 TEST_G - 全局变量访问 */
#ifdef ZTS
# define TEST_G(v) TSRMG(test_globals_id, zend_test_globals *, v)
#else
# define TEST_G(v) (test_globals.v)
#endif

/* 请求初始化 */
PHP_MINIT_FUNCTION(test);

源文件初始化全局变量,MINIT只会在进程或线程创建时执行一次,而一个进程可以服务多个请求,我们需要计数器在每个请求开始时清零,需要RINIT函数清零counter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* 初始化zend_test_globals */
ZEND_DECLARE_MODULE_GLOBALS(test)
/* 等同于 zend_test_globals test_globals; */

zend_module_entry test_module_entry = {
STANDARD_MODULE_HEADER,
"test",
test_functions,
PHP_MINIT(test),
PHP_MSHUTDOWN(test),
PHP_RINIT(test), /* 注册请求初始化函数 */
NULL,
NULL,
PHP_TEST_VERSION,
STANDARD_MODULE_PROPERTIES
};

/* 全局变量初始化,仅在进程/线程创建时初始化 */
static void php_test_init_globals(zend_test_globals *test_globals) {}

// 请求初始化
PHP_RINIT_FUNCTION(test)
{
/* 因一个进程可服务多个请求,我们需要每个请求都有独立的counter */
TEST_G(counter) = 0;

return SUCCESS;
}

// 模块初始化
PHP_MINIT_FUNCTION(test)
{
/* 全局变量表注册 */
ZEND_INIT_MODULE_GLOBALS(test, php_test_init_globals, NULL);

REGISTER_INI_ENTRIES();

return SUCCESS;
}

执行PHP脚本获取输出验证

1
php -r "test(); echo test();" # 输出:2

INI注册全局变量

有了上面的认识,在这里,将ini参数test.direction注册到全局变量,并添加validator校验

头文件添加参数声明

1
2
3
4
ZEND_BEGIN_MODULE_GLOBALS(test)
long counter;
zend_bool direction;
ZEND_END_MODULE_GLOBALS(test)

源文件修改为以下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PHP_INI_BEGIN()
PHP_INI_ENTRY("test.foo", "bar", PHP_INI_ALL, NULL)
// STD_PHP_INI_ENTRY(name, default_value, modifiable, on_modify, property_name, struct_type, struct_ptr)
STD_PHP_INI_ENTRY("test.direction", "1", PHP_INI_ALL, OnUpdateBool, direction, zend_test_globals, test_globals)
PHP_INI_END();

static void php_test_init_globals(zend_test_globals *test_globals) {
/* direction初始化,0|1 */
test_globals->direction = 1;
}

PHP_FUNCTION(test)
{
if (TEST_G(direction)) {
TEST_G(counter)++;
} else {
TEST_G(counter)--;
}

RETURN_LONG(TEST_G(counter));
}

STD_PHP_INI_ENTRY可以使用更多参数,这里我们将参数写入全局变量test_globals。关联第四个参数on_modify校验输入值,如果成功,写入到全局变量,默认为onUpdateLongGEZero,其他的宏还有OnUpdateLongOnUpdateBoolOnUpdateRealOnUpdateStringOnUpdateStringUnempty

配置PHP,在php.ini添加test.direction=0,执行脚本获取输出

1
php -r "echo test();" # 输出:-1

完整代码

php_ext_tutorial

参考文档

How to make a PHP extension
Upgrading PHP extensions from PHP5 to NG
Extension Writing Part I: Introduction to PHP and Zend (已失效)
Extension Writing Part I: Introduction to PHP and Zend
PHP Internals Book
References about Maintaining and Extending PHP