当前位置:首页 > 焦点

php扩展开发

写一个PHP扩展,展开本质上就是展开在C语言层面给PHP内核打补丁。你写的展开不是独立程序,而是展开一段会被Zend引擎加载、注册、展开调用的展开动态库。听起来门槛高,展开实际动手后会发现,展开核心套路就那么几板斧。展开

我第一次编译扩展时踩的展开坑很典型:改了config.m4却忘了跑phpize,直接make报了一堆找不到头文件的展开错。后来才搞明白,展开PHP扩展的展开构建流程是phpize生成configure脚本,./configure检测环境,展开make编译链接,展开最后make install把.so丢进扩展目录。少一步都不行。

扩展的骨架长什么样

一个最简扩展至少需要两个文件:config.m4负责告诉构建系统怎么编译,myext.c里写实际的C代码。PHP 7之后内核API改动很大,zval结构、参数解析宏、内存管理都跟PHP 5时代不同。如果你在网上搜到2014年的教程,大概率跑不通。

核心入口是一个叫zend_module_entry的结构体,它声明了模块名、版本、启动/关闭回调、函数列表等。PHP启动时扫描所有已注册扩展,找到这个结构体,调用你的MINIT(模块初始化)函数。你写的每个PHP函数,都要通过zend_function_entry数组注册进去,否则PHP层根本看不见。

zend_function_entry myext_functions[] = {  PHP_FE(myext_hello, NULL) PHP_FE_END };

就这么几行,把C函数myext_hello暴露成了PHP里可以调用的myext_hello()。

参数解析不是闹着玩的

PHP 7提供了ZEND_PARSE_PARAMETERS_START宏来解析参数,比老版本的zend_parse_parameters更直观。比如你要接收一个字符串和一个可选整数:

ZEND_PARSE_PARAMETERS_START(1, 2) Z_PARAM_STRING(str, str_len) Z_PARAM_OPTIONAL Z_PARAM_LONG(num) ZEND_PARSE_PARAMETERS_END();

类型不匹配时,引擎自动抛TypeError,不用你手动检查。这点比写纯C舒服不少。但要注意,Z_PARAM_STRING拿到的指针不保证以\0结尾,长度得单独用str_len。我见过有人在扩展里直接printf("%s", str),结果字符串后面跟了一串乱码——因为PHP的字符串是二进制安全的,不靠\0判断结束。

内存管理:别用malloc

这是新手最容易翻车的地方。PHP请求周期结束时,引擎会一次性释放所有通过emalloc分配的内存。你要是用malloc,就得自己free,漏一个就是内存泄漏。更麻烦的是,如果用malloc分配了zval相关结构,请求结束后引擎可能已经回收了关联资源,你的指针就成了野指针。

规则很简单:请求生命周期内的内存,一律用emalloc/efree。需要跨请求持久的,才考虑pemalloc配合PERSISTENT标志。我写过一个统计请求耗时的扩展,最初用malloc存全局计数器,跑了一天服务器就OOM了。换成pemalloc之后稳定运行了半年。

调试手段有限,但够用

扩展开发没有var_dump那么方便的东西。常用的手段是php_printf直接输出到标准输出,或者用zend_printf。更正式的做法是写日志到error_log。GDB也能用,但得用php -d extension=myext.so的方式加载调试符号,稍微折腾一点。

我习惯在MINIT里加一行php_printf("myext loaded\n"),确认扩展是否被正确加载。这招笨但有效,比翻phpinfo()快多了。

什么时候值得写扩展

不是所有性能问题都该用扩展解决。PHP的FFI(外部函数接口)已经能直接调用C库,Swoole、RoadRunner这类方案也覆盖了大量场景。真正需要写扩展的情况,通常是你要hook Zend引擎的行为——比如自定义opcode处理器、修改编译流程、或者实现PHP语法层面的新特性。纯计算密集型的任务,先试试FFI,不行再考虑扩展。

写扩展最大的收获不是性能提升,而是你开始理解PHP内部怎么运转的。变量怎么存储、函数怎么调用、内存怎么回收——这些知识反过来会让你写出更靠谱的PHP代码。至少我现在看到zval这个词,不会再觉得它只是个抽象概念了。

分享到: