PHP扩展本质是Zend引擎的“注册公民”,核心为zend_module_entry结构体,通过dlopen加载、get_module获取,调度生命周期,zend_function_entry映射函数调用,zval承载变量语义,所有操作须遵循Zend API协议。

PHP扩展开发不是“写个函数再编译”,而是用C语言与Zend引擎建立契约式通信——核心在于理解zend_module_entry如何调度生命周期、zend_function_entry如何映射调用、以及zval如何承载PHP变量语义。ZEND_API不是工具集,是PHP运行时的协议层。
扩展的本质:不是插件,是Zend引擎的“注册公民”
一个PHP扩展在内核眼中,就是一个结构体zend_module_entry及其配套函数集合。它不靠配置加载,而是由Zend通过dlopen()载入后,主动调用get_module()(由ZEND_GET_MODULE宏生成)来获取该结构体指针。这个结构体里:
- 字段
name决定extension=myext中的名称,且必须全小写、无下划线(否则php -m不识别) -
functions指向函数表,每一项用PHP_FE(func_name, arg_info)声明,arg_info若为NULL则不校验参数,但8.6+强烈建议用ZEND_ARG_INFO显式定义类型和可选性 - 四个钩子函数(
module_startup_func、request_startup_func等)不是可选回调,而是Zend调度策略的一部分:模块级初始化只执行一次,请求级资源必须在RINIT/RSHUTDOWN中配对申请/释放,否则引发内存泄漏或线程冲突
函数实现的关键约束:别把PHP当C用
在PHP_FUNCTION(myfunc)里,不能直接return值,必须用RETURN_*宏;不能用printf调试,应调用php_printf()或zend_error(E_WARNING, "...");接收参数必须走zend_parse_parameters(),例如:
正确写法:zend_long num; char *str; size_t str_len; if (zend_parse_parameters(ZEND_NUM_ARGS(), "ls", &num, &str, &str_len) == FAILURE) { RETURN_FALSE; }
立即学习“PHP免费学习笔记(深入)”;
漏掉ZEND_NUM_ARGS()或类型码错配(如把"l"写成"i"),会导致段错误而非PHP警告——因为底层没做防御性拷贝,而是直接映射到zval内存布局。
内存与类型安全:zval不是万能容器
PHP 8.6中zval已彻底移除is_ref和refcount字段,改为统一通过GC_REFCOUNT宏访问引用计数,且所有zval默认为IS_INDIRECT或IS_UNDEF以外的类型。这意味着:
- 手动构造zval返回时,不能直接赋值
zv.value.lval = 123,而要调用ZVAL_LONG(&zv, 123)等专用宏,确保类型标记(u1.v.type)和值存储对齐 - 操作数组要用
zend_hash_str_add而非add_assoc_string,后者仅适用于当前作用域内的临时zval,跨生命周期传递需SEPARATE_ZVAL_NOREF确保写时复制生效 - 字符串必须用
zend_string *(非char *),创建时走zend_string_init(str, len, 0),释放交由GC,不可free()
工程化落地:从骨架到上线的硬性步骤
用ext_skel生成初始代码只是起点,真实项目需严格完成以下环节:
- 修改
config.m4:启用PHP_ARG_ENABLE支持--enable-myext开关;若依赖外部库(如libcurl),必须用PHP_CHECK_LIBRARY探测头文件与链接符号,避免编译通过但运行时报undefined symbol - 函数参数校验必须覆盖全部分支:
zend_parse_parameters失败后立即RETURN_FALSE,不可继续执行;空指针、负长度、超大数值等边界情况需在C层拦截,不能留给PHP层throw new TypeError - 安装后验证不止
php -m | grep myext:要跑make test(基于PHP自己的PHPUnit适配器),并用valgrind --tool=memcheck php -r "myext_func();"确认无内存越界或泄漏 - 生产环境加载前,检查
phpinfo()中扩展的module_id是否唯一,重复ID会导致Zend拒绝加载第二个同名模块



















