老铁们,大家好!今天咱来聊点儿刺激的——用 C 语言给 PHP 搞点儿“大保健”,啊不,是扩展它的功能,提升它的性能! 别怕,C 语言没那么可怕,咱用最接地气的方式,带你一步步玩转 PHP 扩展开发。
开场白:PHP 为啥需要 C 扩展?
PHP 够强大了吧?为啥还要用 C 扩展?原因很简单:
- 性能瓶颈: PHP 毕竟是解释型语言,执行速度比编译型语言 C 慢。对于计算密集型任务,C 扩展能大幅提升性能。
- 系统级操作: 有些底层系统操作,PHP 搞不定,或者搞起来很麻烦。C 扩展可以轻松搞定。
- 复用现有 C/C++ 代码: 很多成熟的 C/C++ 库,可以直接封装成 PHP 扩展来使用,避免重复造轮子。
- 安全考虑: 一些敏感操作,用 C 扩展实现更安全,避免 PHP 代码直接暴露敏感信息。
第一部分:环境搭建与基本框架
-
安装 PHP 开发环境: 这个不用多说,确保你的 PHP 版本高于 7.0,最好是 8.0+。
-
安装 PHP 开发包: 这是关键!不同系统安装方式不一样,但目的都是为了获得
phpize
和php-config
这两个神器。- Debian/Ubuntu:
sudo apt-get install php-dev
- CentOS/RHEL:
sudo yum install php-devel
- macOS (Homebrew):
brew install php
(确保你的 PHP 版本和 Homebrew 安装的版本一致)
- Debian/Ubuntu:
-
生成扩展框架: 打开终端,进入你想要存放扩展代码的目录,执行:
phpize ./configure --with-php-config=/path/to/php-config make
/path/to/php-config
是你的php-config
文件的路径,可以用which php-config
命令找到。成功执行后,会生成一堆文件,其中
config.m4
是配置文件,php_<your_extension>.c
是扩展的 C 代码文件。注意: 如果
phpize
提示找不到命令,说明你没安装或者没正确配置 PHP 开发包。 -
修改
config.m4
: 打开config.m4
文件,找到下面这行:PHP_ARG_ENABLE(your_extension, whether to enable your_extension support, Make sure that the comment is aligned: [ --enable-your_extension Enable your_extension support])
把
your_extension
替换成你扩展的名字,比如my_awesome_extension
。然后去掉注释符号dnl
。修改后类似这样:PHP_ARG_ENABLE(my_awesome_extension, whether to enable my_awesome_extension support, Make sure that the comment is aligned: [ --enable-my_awesome_extension Enable my_awesome_extension support])
-
编写 C 代码: 打开
php_<your_extension>.c
文件,你会看到一个基本的扩展框架。里面有很多注释,可以先忽略。
第二部分:编写你的第一个 PHP 扩展
咱来写一个简单的扩展,实现一个 my_add
函数,接受两个整数参数,返回它们的和。
-
修改
php_<your_extension>.c
: 在文件末尾,找到PHP_FUNCTION(confirm_your_extension_compiled)
函数,把它改成我们的my_add
函数:PHP_FUNCTION(my_add) { zend_long a, b; ZEND_PARSE_PARAMETERS_START(2, 2) Z_PARAM_LONG(a) Z_PARAM_LONG(b) ZEND_PARSE_PARAMETERS_END(); RETURN_LONG(a + b); }
PHP_FUNCTION(my_add)
:定义一个 PHP 函数,名字是my_add
。zend_long a, b;
:声明两个zend_long
类型的变量,用来存储参数。zend_long
是 PHP 内部使用的整数类型。ZEND_PARSE_PARAMETERS_START(2, 2)
和ZEND_PARSE_PARAMETERS_END()
:用来解析 PHP 函数的参数。Z_PARAM_LONG(a)
和Z_PARAM_LONG(b)
:指定参数类型是long
,并将参数值分别赋值给变量a
和b
。RETURN_LONG(a + b)
:返回a + b
的结果,类型是long
。
-
注册函数: 找到
PHP_FE_END
前面的位置,添加一行代码,将my_add
函数注册到 PHP 中:const zend_function_entry my_awesome_extension_functions[] = { PHP_FE(my_add, NULL) /* For testing, remove later. */ PHP_FE_END /* Must be the last line in my_awesome_extension_functions[] */ };
PHP_FE(my_add, NULL)
:将my_add
函数注册到 PHP 中。第一个参数是函数名,第二个参数是参数信息,这里我们先用NULL
。
-
重新编译安装: 依次执行以下命令:
phpize ./configure --with-php-config=/path/to/php-config make sudo make install
注意: 每次修改 C 代码后,都需要重新编译安装。
-
修改
php.ini
: 找到你的php.ini
文件,添加一行:extension=php_my_awesome_extension.so
把
my_awesome_extension
替换成你的扩展名。 - 重启 Web 服务器: 重启 Apache 或 Nginx,让 PHP 加载新的扩展。
-
测试: 创建一个 PHP 文件,例如
test.php
,内容如下:<?php echo my_add(10, 20); // 输出 30 ?>
在浏览器中访问
test.php
,如果看到30
,恭喜你,你的第一个 PHP 扩展就成功了!
第三部分:深入了解 PHP 扩展开发
-
参数解析:
ZEND_PARSE_PARAMETERS_START
和ZEND_PARSE_PARAMETERS_END
是参数解析的核心。Z_PARAM_XXX
系列宏用于指定参数类型。常用的类型有:类型 宏 说明 long Z_PARAM_LONG
整数 double Z_PARAM_DOUBLE
浮点数 string Z_PARAM_STRING
字符串,需要手动释放内存 string (copy) Z_PARAM_STR
字符串,自动拷贝,不需要手动释放内存 bool Z_PARAM_BOOL
布尔值 array Z_PARAM_ARRAY
数组 object Z_PARAM_OBJECT
对象 resource Z_PARAM_RESOURCE
资源 null Z_PARAM_NULL
NULL 值 is_null Z_PARAM_ZVAL
任意类型,需要使用 Z_ISNULL
宏判断是否为 NULL示例:
PHP_FUNCTION(my_function) { zend_long a; double b; char *str; size_t str_len; ZEND_PARSE_PARAMETERS_START(2, 3) Z_PARAM_LONG(a) Z_PARAM_DOUBLE(b) Z_PARAM_OPTIONAL Z_PARAM_STRING(str, str_len) ZEND_PARSE_PARAMETERS_END(); // ... 使用 a, b, str ... efree(str); // 释放字符串内存 }
Z_PARAM_OPTIONAL
:表示后面的参数是可选的。Z_PARAM_STRING(str, str_len)
:获取字符串参数,str
指向字符串内容,str_len
是字符串长度。 注意: 使用Z_PARAM_STRING
获取的字符串,需要手动使用efree()
函数释放内存。
-
返回值:
RETURN_XXX
系列宏用于返回不同类型的值。常用的类型有:类型 宏 说明 long RETURN_LONG
整数 double RETURN_DOUBLE
浮点数 string RETURN_STRING
字符串,需要手动拷贝字符串 string (copy) RETURN_STR
字符串,自动拷贝,不需要手动释放内存 bool RETURN_TRUE
,RETURN_FALSE
布尔值 array RETURN_ARR
数组,返回的是 zend_array *
指针,需要先创建zend_array
结构体object RETURN_OBJ
对象,返回的是 zend_object *
指针,需要先创建zend_object
结构体resource RETURN_RES
资源,返回的是 zend_resource *
指针,需要先创建zend_resource
结构体null RETURN_NULL
NULL 值 void RETURN_VOID
没有返回值 示例:
PHP_FUNCTION(my_string_function) { char *str = "Hello, world!"; RETURN_STRING(estrdup(str)); // 使用 estrdup 拷贝字符串 } PHP_FUNCTION(my_array_function) { zval return_value; array_init(&return_value); // 初始化数组 add_assoc_string(&return_value, "key1", "value1"); add_index_long(&return_value, 0, 123); RETURN_ZVAL(&return_value, 1, 0); // 返回数组,并释放 return_value 内存 }
estrdup()
:用于拷贝字符串,返回的指针需要手动使用efree()
释放内存。array_init()
:初始化一个zval
类型的变量,作为数组使用。add_assoc_string()
和add_index_long()
:向数组中添加元素。RETURN_ZVAL(&return_value, 1, 0)
:返回zval
类型的变量,第一个参数是zval
的指针,第二个参数表示是否拷贝zval
,第三个参数表示是否释放zval
。
-
资源管理: 在 PHP 扩展中,经常需要操作资源,例如文件句柄、数据库连接等。为了避免内存泄漏,需要正确管理资源。PHP 提供了资源管理机制,使用
zend_resource
结构体来表示资源。示例:
typedef struct { FILE *fp; } my_resource; PHP_FUNCTION(my_open_file) { char *filename; size_t filename_len; zend_resource *res; my_resource *my_res; ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_STRING(filename, filename_len) ZEND_PARSE_PARAMETERS_END(); FILE *fp = fopen(filename, "r"); if (!fp) { RETURN_FALSE; } my_res = emalloc(sizeof(my_resource)); my_res->fp = fp; res = zend_register_resource(my_res, le_my_resource); RETURN_RES(res); efree(filename); } PHP_FUNCTION(my_read_file) { zend_resource *res; my_resource *my_res; char buf[1024]; ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_RESOURCE(res) ZEND_PARSE_PARAMETERS_END(); my_res = (my_resource *)zend_fetch_resource(res, "my_resource", le_my_resource); if (!my_res) { RETURN_FALSE; } size_t bytes_read = fread(buf, 1, sizeof(buf) - 1, my_res->fp); if (bytes_read > 0) { buf[bytes_read] = ''; RETURN_STRINGL(buf, bytes_read); } else { RETURN_FALSE; } } PHP_FUNCTION(my_close_file) { zend_resource *res; my_resource *my_res; ZEND_PARSE_PARAMETERS_START(1, 1) Z_PARAM_RESOURCE(res) ZEND_PARSE_PARAMETERS_END(); my_res = (my_resource *)zend_fetch_resource(res, "my_resource", le_my_resource); if (!my_res) { RETURN_FALSE; } fclose(my_res->fp); efree(my_res); zend_list_delete(Z_RES_HANDLE_P(res)); RETURN_TRUE; } static void my_resource_dtor(zend_resource *rsrc) { my_resource *my_res = (my_resource *)rsrc->ptr; fclose(my_res->fp); efree(my_res); } PHP_MINIT_FUNCTION(my_awesome_extension) { le_my_resource = zend_register_list_destructors_ex(my_resource_dtor, NULL, "my_resource", module_number); return SUCCESS; }
my_resource
结构体:定义了资源的数据结构,这里包含一个文件指针fp
。zend_register_resource()
:注册资源,将my_resource
结构体和资源类型le_my_resource
关联起来。zend_fetch_resource()
:获取资源,从zend_resource
结构体中获取my_resource
结构体的指针。zend_list_delete()
:删除资源,释放资源占用的内存。my_resource_dtor()
:资源析构函数,在资源被销毁时调用,用于释放资源。zend_register_list_destructors_ex()
:注册资源析构函数,在PHP_MINIT_FUNCTION
中调用。
-
异常处理: 在 C 代码中,如果发生错误,可以使用
zend_throw_exception
函数抛出 PHP 异常。示例:
PHP_FUNCTION(my_divide) { zend_long a, b; ZEND_PARSE_PARAMETERS_START(2, 2) Z_PARAM_LONG(a) Z_PARAM_LONG(b) ZEND_PARSE_PARAMETERS_END(); if (b == 0) { zend_throw_exception_ex(zend_ce_exception, 0, "Division by zero"); RETURN_FALSE; } RETURN_LONG(a / b); }
zend_throw_exception_ex()
:抛出异常,第一个参数是异常类,第二个参数是异常代码,第三个参数是异常信息。
第四部分:性能优化技巧
- 避免内存拷贝: 尽量避免在 C 代码和 PHP 代码之间拷贝大量数据。可以使用指针传递数据,或者使用共享内存。
- 使用缓存: 对于重复计算的结果,可以使用缓存来避免重复计算。可以使用 PHP 的
apcu
扩展或者自己实现缓存。 - 减少函数调用: 函数调用会带来额外的开销,尽量减少函数调用次数。可以将一些常用的代码内联到函数中。
- 使用位运算: 位运算比算术运算更快,可以使用位运算来优化代码。
- 使用编译器优化: 在编译扩展时,可以使用编译器优化选项,例如
-O3
,来提高性能。
第五部分:总结与展望
PHP 扩展开发是一项非常有用的技能,可以让你突破 PHP 的限制,实现更强大的功能,提升 PHP 的性能。虽然 C 语言学习曲线比较陡峭,但是只要掌握了基本概念和技巧,就可以轻松开发出高效稳定的 PHP 扩展。
未来的 PHP 扩展开发,将会更加注重性能优化、安全性和易用性。希望大家能够积极参与到 PHP 扩展开发的行列中来,为 PHP 生态系统贡献力量!
一些实用表格总结:
任务 | 常用命令/函数 | 说明 |
---|---|---|
生成扩展框架 | phpize , ./configure --with-php-config=/path/to/php-config , make |
初始化扩展开发环境 |
参数解析 | ZEND_PARSE_PARAMETERS_START , ZEND_PARSE_PARAMETERS_END , Z_PARAM_LONG , Z_PARAM_STRING , Z_PARAM_ARRAY , Z_PARAM_OBJECT |
解析 PHP 函数传递的参数 |
返回值 | RETURN_LONG , RETURN_STRING , RETURN_ARR , RETURN_OBJ , RETURN_NULL , RETURN_ZVAL |
返回值给 PHP |
内存管理 | emalloc , efree , estrdup |
分配和释放内存,字符串拷贝 |
资源管理 | zend_register_resource , zend_fetch_resource , zend_list_delete , zend_register_list_destructors_ex |
管理资源,例如文件句柄、数据库连接 |
异常处理 | zend_throw_exception_ex |
抛出 PHP 异常 |
编译安装 | make , sudo make install |
编译和安装扩展 |
配置文件 | config.m4 , php.ini |
扩展配置文件和 PHP 配置文件 |
扩展生命周期函数 | PHP_MINIT_FUNCTION , PHP_MSHUTDOWN_FUNCTION , PHP_RINIT_FUNCTION , PHP_RSHUTDOWN_FUNCTION , PHP_MINFO_FUNCTION |
扩展模块初始化、关闭,请求初始化、关闭,模块信息 |
希望这次的讲座能帮到你!祝你开发顺利,做出牛逼的 PHP 扩展! 拜了个拜!