如何使用 C 语言编写 PHP 扩展

为什么要为 PHP 编写扩展

与外部的库做交互, 比如你有一个C/C++的库, 不妨假设,这个库呢就是实现了一个字符串加密和解密,而你并没有这个库的源码,也就是说,你无法把这个库在PHP中实现, 那么你只有编写一个PHP扩展,来做为一个桥梁,连接起你的PHP和这个库。
效率, 不容置疑,PHP编写的脚本,比同等的C/C++编写的脚本,效率自然要低很多。 当你用常见的手段,再也无法提高你的脚本的效率,而效率要求又是那么的紧迫, 那么我只能劝你, 把你的逻辑,用C/C++改写吧。。

编写扩展

在 PHP 源码文件夹中找到 ext 文件夹,为扩展新建一个文件夹。假设扩展名为 hello,那么可以新建一个 hello 文件夹。 文件夹创建好之后,接着新建三个和扩展相关的文件:config.m4, php_hello.h, hello.c。
config.m4 是一个配置文件。它的内容如下:

1
2
3
4
5
6
7
8
xxx@Vm-template-100G:/usr/include/php5/ext/hello$ nl config.m4
1 PHP_ARG_ENABLE(hello, whether to enable Hello World support,
2 [ --enable-hello Enable Hello World support])
3 if test "$PHP_HELLO" = "yes"; then
4 AC_DEFINE(HAVE_HELLO, 1, [Whether you have Hello World])
5 PHP_NEW_EXTENSION(hello, hello.c, $ext_shared)
6 fi

php_hello.h 是一个头文件,放置的是一些与扩展有关的函数的声明信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xxx@Vm-template-100G:/usr/include/php5/ext/hello$ nl php_hello.h
1 #ifndef PHP_HELLO_H
2 #define PHP_HELLO_H 1
3 #define PHP_HELLO_WORLD_VERSION "1.0"
4 #define PHP_HELLO_WORLD_EXTNAME "hello"
5 PHP_MINIT_FUNCTION(hello);
6 PHP_MSHUTDOWN_FUNCTION(hello);
7 PHP_FUNCTION(hello_world);
8 PHP_FUNCTION(hello_arg);
9 PHP_FUNCTION(hello_add);
10 PHP_FUNCTION(self_concat);
11 extern zend_module_entry hello_module_entry;
12 #define phpext_hello_ptr &hello_module_entry;
13 #endif

hello.c 里面是与扩展有关的函数的定义。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
xxx@Vm-template-100G:/usr/include/php5/ext/hello$ nl hello.c
1 #ifdef HAVE_CONFIG_H
2 #include "config.h"
3 #endif
4 #include "php.h"
5 #include "php_ini.h"
6 #include "php_hello.h"
7 ZEND_BEGIN_ARG_INFO(arg1_hello_arg, 0)
8 ZEND_END_ARG_INFO()
9 ZEND_BEGIN_ARG_INFO(arg_hello_add, 0)
10 ZEND_ARG_INFO(0, arg_hello_num1)
11 ZEND_ARG_INFO(0, arg_hello_num2)
12 ZEND_END_ARG_INFO()
13 ZEND_BEGIN_ARG_INFO(arg_self_concat, 0)
14 ZEND_ARG_INFO(0, arg1_self_concat)
15 ZEND_ARG_INFO(0, arg2_self_concat)
16 ZEND_END_ARG_INFO()
17 static function_entry hello_functions[] = {
18 PHP_FE(hello_world, NULL)
19 PHP_FE(hello_arg, arg1_hello_arg)
20 PHP_FE(hello_add, arg_hello_add)
21 PHP_FE(self_concat, arg_self_concat)
22 {NULL, NULL, NULL}
23 };
24 zend_module_entry hello_module_entry = {
25 #if ZEND_MODULE_API_NO >= 20010901
26 STANDARD_MODULE_HEADER,
27 #endif
28 PHP_HELLO_WORLD_EXTNAME,
29 hello_functions,
30 PHP_MINIT(hello),
31 PHP_MSHUTDOWN(hello),
32 NULL,
33 NULL,
34 NULL,
35 #if ZEND_MODULE_API_NO >= 20010901
36 PHP_HELLO_WORLD_VERSION,
37 #endif
38 STANDARD_MODULE_PROPERTIES
39 };
40 PHP_INI_BEGIN()
41 PHP_INI_ENTRY("hello.greeting", "Hello World", PHP_INI_ALL, NULL)
42 PHP_INI_END()
43 PHP_MINIT_FUNCTION(hello)
44 {
45 REGISTER_INI_ENTRIES();
46 return SUCCESS;
47 }
48 PHP_MSHUTDOWN_FUNCTION(hello)
49 {
50 UNREGISTER_INI_ENTRIES();
51 return SUCCESS;
52 }
53 #ifdef COMPILE_DL_HELLO
54 ZEND_GET_MODULE(hello)
55 #endif
56 PHP_FUNCTION(hello_world)
57 {
58 RETURN_STRING(INI_STR("hello.greeting"), 1);
59 }
60 PHP_FUNCTION(hello_arg)
61 {
62 char* name_val;
63 long name_len;
64 if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &name_val, &name_len) == FAILURE)
65 {
66 return;
67 }
68 php_printf("Hello %s \n", name_val);
69 RETURN_TRUE;
70 }
71 PHP_FUNCTION(hello_add)
72 {
73 long a;
74 long b;
75 long c;
76 if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC , "ll", &a, &b) == FAILURE)
77 {
78 RETURN_FALSE;
79 }
80 c = a + b;
81 php_printf("sum is %ld \n", c);
82 RETURN_LONG(c);
83 }
84 PHP_FUNCTION(self_concat)
85 {
86 char* str = NULL;
87 int str_len;
88 char* ret = NULL;
89 int ret_len;
90 char* tmp = NULL;
91 int n;
92 int arg_num = ZEND_NUM_ARGS();
93 if (zend_parse_parameters(arg_num TSRMLS_CC, "sl", &str, &str_len, &n) == FAILURE)
94 {
95 return;
96 }
97 ret_len = str_len * n;
98 ret = (char*)emalloc(ret_len + 1);
99 tmp = ret;
100 while (n--)
101 {
102 memcpy(tmp, str, str_len);
103 tmp = tmp + str_len;
104 }
105 *tmp = '\0';
106 php_printf("ret = %s", tmp);
107 php_printf(" ");
108 RETURN_STRINGL(ret, ret_len, 0)
109 }

关于 RETURN_STRINGL(ret, ret_len, 0)中最后一个参数为 0 的解释如下:

php 本身是类型安全的脚本语言,对于 RETURN_STRINGL 或是 RETURN_STRING 返回的字符串,php 会在适当的时候 free 掉,所以程序员要保证返回的字符串在堆里,能够free掉,这就是为什么动态分配就没事的原因。而:

1
2
char* ret = "hello world";
RETURN_STRINGL(ret, strlen(ret), 0);

这是直接返回了一个静态字符串,导致 php 在 free 这个字符串的时候出错。
RETURN_STRINGL 和 RETURN_STRING 最后一个参数,如果是 1,表示对第一个参数中的字符串在堆里复制一份返回。这就是为什么最后一个参数等于 1 的时候,程序正常的原因。

编译与安装扩展

编写完 config.m4, php_hello.h, hello.c 三个文件之后,开始对扩展进行编译、安装。依次在终端中执行以下命令:

1
2
3
4
sudo phpize
sudo ./configure
sudo make
sudo make install

根据终端中的提示信息来判断这些命令是否执行成功。

测试扩展

编译完成 PHP 扩展之后,便可以对扩展进行测试了。在测试之前,需要在 php.ini 文件中添加扩展项。然后,就可以开始测试了。
假设添加的扩展名为 hello。其中有一个函数为 sayHello() 。新建 test.php,并在其中调用 sayHello()函数。那么测试方法可以通过下面两种方式:

  1. 运行命令 php -r ‘echo sayHello();’;
  2. 运行命令 php ./test.php;

调试扩展

编程离不开调试。用 C 语言编写的 PHP 扩展可以通过 GDB 来调试。
调试之前需要对 config.m4 文件添加以下配置:

1
2
3
4
5
6
7
if test -z "$PHP_DEBUG"; then
AC_ARG_ENABLE(debug,
[ --enable-debug compile with debugging symbols],[
PHP_DEBUG=$enableval
],[ PHP_DEBUG=no
])
fi

然后再重新编译、安装。注意:

1
./configure --enable-debug

通过以下命令来查看符号表。

1
nm hello.so

查看到的符号表是这样子的。

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
xxx@test-db:/data/app/php/lib/php/extensions/no-debug-non-zts-20121212$ nm test.so
0000000000201e20 a _DYNAMIC
0000000000201fe8 a _GLOBAL_OFFSET_TABLE_
w _Jv_RegisterClasses
0000000000201d98 d __CTOR_END__
0000000000201d90 d __CTOR_LIST__
0000000000201da8 d __DTOR_END__
0000000000201da0 d __DTOR_LIST__
0000000000000eb0 r __FRAME_END__
0000000000201db0 d __JCR_END__
0000000000201db0 d __JCR_LIST__
0000000000202108 A __bss_start
w __cxa_finalize@@GLIBC_2.2.5
0000000000000be0 t __do_global_ctors_aux
00000000000009e0 t __do_global_dtors_aux
0000000000202040 d __dso_handle
w __gmon_start__
0000000000202108 A _edata
0000000000202118 A _end
0000000000000c18 T _fini
0000000000000920 T _init
0000000000000d20 r arg_sayHello
00000000000009c0 t call_gmon_start
0000000000202108 b completed.6531
0000000000202110 b dtor_idx.6533
0000000000000a60 t frame_dummy
0000000000000bd0 T get_module
U php_info_print_table_end
U php_info_print_table_header
U php_info_print_table_start
U php_printf
U spprintf
0000000000201dc0 D test_functions
0000000000202060 D test_module_entry
U zend_parse_parameters
0000000000000b30 T zif_confirm_test_compiled
0000000000000ad0 T zif_sayHello
0000000000000ab0 T zm_activate_test
0000000000000ac0 T zm_deactivate_test
0000000000000ba0 T zm_info_test
0000000000000aa0 T zm_shutdown_test
0000000000000a90 T zm_startup_test

通过 GDB 来调试。

1
2
3
gdb php
> break zif_sayHello
> run mytest.php

如果提示

(gdb) break zif_sayHello
Function "zif_sayHello" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y

选择 y,继续调试。

总结

其实,还有一种更简单的创建扩展方法。下面对两种方法进行总结:
第一种创建扩展的方式:

  1. 在 ext 目录下创建 hello 目录;
  2. 在 hello 目录下创建三个文件, config.m4, php_hello.h, hello.c;
  3. 编译、安装扩展

第二种创建扩展的方式:
使用 ext_skel 创建扩展

  1. 运行 ./ext_skel –extname=say_hello;
  2. 进入 say_hello 目录,修改 config.m4 文件;
  3. 编写核心函数。这里可以参考 hello.c 和 php_hello.h。
  4. 编译、安装扩展。命令同方式一。

参考

  1. PHP 扩展编写第一步:PHP 和 Zend介绍
  2. [原创]快速开发一个PHP扩展
  3. php的RETURN_STRINGL 为什么使用静态字符串会出现 Segmentation fault
  4. PHP Extension开发基础
  5. PHP扩展开发之扩展函数
  6. 用 C/C++ 扩展你的 PHP
  7. 从零开始创建一个 PHP 扩展
  8. 扩展PHP [Extending PHP] (一)
  9. 调试PHP扩展