资讯动态

用C/C++扩展你的PHP 为你的php增加功能

发布时间:2026/10/23 10:35:05 来源:尧图企业网站定制
前言PHP 本身是用 C 实现的所谓「扩展 PHP」就是写一个动态库让 Zend 引擎在启动时加载它并把其中的函数注册进 PHP 的函数表。扩展里可以直接调用 C/C 代码、复用已有库也可以把热点逻辑从 PHP 搬到编译型语言里执行。需要先纠正两个常见说法。第一「扩展就是一个.so/.dll随便放进去就行」不准确它必须导出约定的模块入口符号按 Zend 的规则注册模块、函数与生命周期回调否则加载时会报「Unable to load dynamic library」或「Invalid library」。第二「PHP 扩展 API 很稳定」也不对PHP 5、PHP 7、PHP 8 之间参数解析、字符串表示、返回值宏都发生过变化PHP 7 引入的zend_string就是分水岭。写扩展时必须先确定目标 PHP 版本并以该版本的官方文档与php-src头文件为准。本文以 PHP 8.x 为基准给出一个最小扩展的完整文件结构与构建步骤然后讨论用 C 实现时的链接、异常与内存问题最后介绍不写扩展也能调用 C 库的 FFI 方案。文中代码按 API 语义推演不声称在本机编译或加载过。一、扩展在 PHP 里的位置一次 PHP 请求的执行可以粗略分成启动引擎、编译脚本为 opcode、执行 opcode、关闭请求。扩展可以挂在这几个阶段的回调上回调触发时机典型用途MINIT模块初始化进程启动时一次注册常量、初始化全局资源MSHUTDOWN模块关闭进程结束时一次释放持久资源RINIT每个请求开始重置请求级状态RSHUTDOWN每个请求结束清理请求级内存MINFOphpinfo()被调用时输出模块信息扩展编译成共享库后Zend 在加载时查找名为get_module的导出函数拿到zend_module_entry结构再按其中的函数表注册 PHP 可见的函数。这个入口符号的名字是固定的因此不能用 C 的名字修饰否则dlsym找不到。这是 C 写扩展时第一个必须处理的点。二、最小扩展的文件结构一个可加载的扩展通常包含四个文件extname/├── config.m4 # Unix 下的构建配置├── php_extname.h # 模块声明与版本号├── extname.c # 模块实现└── extname.cpp # 可选C 实现本例用 C 调用它2.1 config.m4dnl 适用于 PHP 8.x phpizePHP_ARG_ENABLE([extname],[whether to enable extname support],[AS_HELP_STRING([--enable-extname], [Enable extname support])],[no])if test $PHP_EXTNAME ! no; thenPHP_NEW_EXTENSION(extname, extname.c extname.cpp, $ext_shared)fiPHP_ARG_ENABLE定义--enable-extname开关PHP_NEW_EXTENSION声明扩展名与源文件列表。若源文件里含.cpp构建系统需要用 C 编译器不同 PHP 版本的phpize生成的规则可能不同若链接阶段报 C 符号缺失应在config.m4中显式设置CXX或把 C 部分单独编译成静态库再链接。具体写法以目标版本的ext/目录下现成扩展为参照。2.2 头文件/* 适用于 PHP 8.x */#ifndef PHP_EXTNAME_H#define PHP_EXTNAME_Hextern zend_module_entry extname_module_entry;#define phpext_extname_ptr extname_module_entry#define PHP_EXTNAME_VERSION 0.1.0#endifextname_module_entry是模块描述结构phpext_extname_ptr是给其他扩展引用的便捷宏。2.3 C 实现可选把核心逻辑放在 C 里用extern C导出一个 C 可见的函数避免名字修饰// 适用于 C17#include cstddef#include cstring#include stringextern C std::size_t extname_upper(char* out, std::size_t cap,const char* in, std::size_t len) {std::string s(in, len);for (char c : s) {if (c a c z) {c static_castchar(c - a A);}}const std::size_t need s.size() 1;if (need cap) {return need; // 告诉调用方需要多大}std::memcpy(out, s.c_str(), need);return need;}这里刻意用「调用方提供缓冲区 返回所需大小」的接口避免跨模块传递new出来的内存——C 的new与 PHP 的emalloc是两套分配器谁分配谁释放必须严格对应。2.4 模块实现/* 适用于 PHP 8.x */#ifdef HAVE_CONFIG_H#include config.h#endif#include php.h#include php_ini.h#include ext/standard/info.h#include php_extname.hextern size_t extname_upper(char* out, size_t cap, const char* in, size_t len);PHP_FUNCTION(extname_add){zend_long a;zend_long 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(extname_upper){char* input;size_t input_len;ZEND_PARSE_PARAMETERS_START(1, 1)Z_PARAM_STRING(input, input_len)ZEND_PARSE_PARAMETERS_END();char* out emalloc(input_len 1);extname_upper(out, input_len 1, input, input_len);RETVAL_STRINGL(out, input_len);efree(out);}ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_extname_add, 0, 2, IS_LONG, 0)ZEND_ARG_TYPE_INFO(0, a, IS_LONG, 0)ZEND_ARG_TYPE_INFO(0, b, IS_LONG, 0)ZEND_END_ARG_INFO()ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_extname_upper, 0, 1, IS_STRING, 0)ZEND_ARG_TYPE_INFO(0, input, IS_STRING, 0)ZEND_END_ARG_INFO()static const zend_function_entry extname_functions[] {PHP_FE(extname_add, arginfo_extname_add)PHP_FE(extname_upper, arginfo_extname_upper)PHP_FE_END};PHP_MINIT_FUNCTION(extname){return SUCCESS;}PHP_MSHUTDOWN_FUNCTION(extname){return SUCCESS;}PHP_RINIT_FUNCTION(extname){#if defined(COMPILE_DL_EXTNAME) defined(ZTS)ZEND_TSRMLS_CACHE_UPDATE();#endifreturn SUCCESS;}PHP_MINFO_FUNCTION(extname){php_info_print_table_start();php_info_print_table_header(2, extname support, enabled);php_info_print_table_end();}zend_module_entry extname_module_entry {STANDARD_MODULE_HEADER,extname,extname_functions,PHP_MINIT(extname),PHP_MSHUTDOWN(extname),PHP_RINIT(extname),NULL,PHP_MINFO(extname),PHP_EXTNAME_VERSION,STANDARD_MODULE_PROPERTIES};#ifdef COMPILE_DL_EXTNAME# ifdef ZTSZEND_TSRMLS_CACHE_DEFINE()# endifZEND_GET_MODULE(extname)#endif几个关键点PHP_FUNCTION(name)会展开成一个固定签名的 C 函数函数体里用RETURN_*/RETVAL_*设置返回值。ZEND_PARSE_PARAMETERS_START/END是 PHP 7 起的参数解析宏Z_PARAM_LONG、Z_PARAM_STRING分别取整型与字符串。它们的错误处理会抛出TypeError或ArgumentCountError不需要手写。arginfo描述参数与返回类型供反射和类型检查使用。PHP 8 对内部函数的类型声明比 PHP 7 更严格arginfo 写错会导致调用时被拒绝。emalloc/efree是请求级分配器请求结束时未释放的内存会被统一回收但仍在请求内及时efree是好习惯。RETVAL_STRINGL会复制一份字符串所以out可以随后释放。ZEND_GET_MODULE只在COMPILE_DL_EXTNAME定义时展开phpize构建会自动定义它。三、用 C 写扩展的四个约束3.1 入口符号必须保持 C 链接get_module是加载器按名字查找的符号。C 默认会对函数名做修饰导致导出名不是get_module。解决方式是在 C 编译单元里把模块入口包进extern C// 适用于 C17需要 PHP 8.x 头文件extern C {#include php.h#include php_extname.h}extern C {ZEND_GET_MODULE(extname)}是否需要包裹 PHP 头文件取决于该版本头文件自身是否已经提供extern C保护统一包一层通常更稳妥但若头文件内部已有 C 相关声明可能产生冲突。以实际头文件为准不要盲目照抄。3.2 异常不能穿过 Zend 的 C 栈Zend 引擎是 C 写的没有为 C 异常准备栈展开信息。让异常从PHP_FUNCTION里逃逸属于未定义行为常见结果是进程直接终止。边界处必须try/catch全部异常并转换成 PHP 异常// 适用于 C17#include stdexceptextern C int extname_safe_div(long a, long b, long* out) {try {if (b 0) {throw std::domain_error(division by zero);}*out a / b;return 0;} catch (...) {return -1; // 由 C 包装层转成 PHP 异常}}C 包装层收到 -1 后用zend_throw_exception抛出 PHP 异常再RETURN_THROWS()。不要把 C 异常类型直接暴露给 PHP。3.3 两套内存分配器不能混用分配方式归属释放方式emalloc/efreeZend 请求内存efree请求结束自动回收pemalloc/pefree持久内存pefree跨请求存活new/deleteC 运行时deletemalloc/freeC 运行时free用emalloc分配的对象不会调用 C 析构函数。若一定要在 Zend 内存里放 C 对象必须用 placement new 构造、显式调用析构再efree。反过来把new出来的指针交给 PHP 并由 Zend 回收会调用错误的释放函数属于 UB。3.4 线程安全ZTSZend Thread Safety构建下模块级全局变量需要声明为模块全局并通过ZEND_TSRMLS_CACHE访问普通 C 静态变量在所有线程间共享必须自行同步。更简单的策略是尽量避免扩展级可变全局状态把状态放在请求级结构里。四、构建与加载Unix 下的典型流程cd extnamephpize./configure --enable-extnamemakemake installphpize会根据当前 PHP 版本生成configure与 Makefilemake install把extname.so放到extension_dir。随后在php.ini中启用; 适用于 PHP 8.xextensionextname检查是否加载成功php -m | grep extnamephp -r var_dump(extname_add(2, 3));若报「Unable to load dynamic library」依次检查.so路径与extension_dir是否一致PHP 的 API 版本号php -i中的PHP API与编译时头文件是否匹配是否在 ZTS/非 ZTS 之间混用依赖的共享库是否可被动态链接器找到。Windows 下需要用 PHP SDK 与对应的 Visual Studio 工具集构建流程与 Unix 差别较大按官方文档操作。五、不写扩展的替代方案FFIPHP 7.4 起提供 FFI 扩展可以直接声明 C 函数签名并调用共享库不需要编译 PHP 扩展?php// 适用于 PHP 7.4 及以上需启用 FFI$ffi FFI::cdef(int add(int a, int b);,./libdemo.so);var_dump($ffi-add(2, 3));FFI 适合「调用现成 C 库」而不是「给 PHP 增加新语法/新类型」。它的限制包括需要开启ffi.enable默认策略通常只允许预加载文件具体以官方文档为准、跨平台库名不同、复杂结构体与回调的声明比较繁琐。若目标是分发一个通用扩展仍然应当走 Zend 扩展的路线。常见坑点❌ 用 C 编译模块却让get_module被名字修饰。✅ 用extern C包住模块入口与ZEND_GET_MODULE确保导出符号就是get_module。❌ 让 C 异常从PHP_FUNCTION中逃逸。✅ 在边界catch (...)转换成 PHP 异常并RETURN_THROWS()。❌ 用emalloc分配含析构函数的 C 对象直接efree。✅ 用 placement new 构造并显式调用析构或改用new/delete并保证不交给 Zend 回收。❌ 把new出来的指针通过RETURN_*交给 PHP期望 Zend 释放。✅ Zend 只认识自己的分配器跨边界的内存必须由分配方释放或用复制语义传递。❌ 在 PHP 5 风格的代码里用zend_parse_parameters并假设字符串是char*。✅ PHP 7 起字符串是zend_string按目标版本使用Z_PARAM_STR/Z_PARAM_STRING并查对应版本文档。❌ arginfo 写的参数个数与ZEND_PARSE_PARAMETERS_START不一致。✅ 两者保持一致PHP 8 会依据 arginfo 做类型检查不一致会导致调用被拒绝。❌ 在 MINIT 里分配请求级内存。✅ MINIT 只在进程启动时执行一次应使用持久内存pemalloc或纯静态数据请求级内存在 RINIT 之后分配。❌php -m看不到模块只改php.ini不检查 SAPI。✅ CLI 与 FPM/Apache 可能读取不同的php.ini用php --ini确认实际加载的配置文件。总结环节要点常见错误模块入口导出get_module返回zend_module_entryC 名字修饰导致找不到符号函数注册PHP_FE arginfo参数个数/类型不一致参数解析ZEND_PARSE_PARAMETERS_START沿用 PHP 5 的 API返回值RETURN_LONG/RETVAL_STRINGL等返回栈上或已释放的内存内存emalloc/efree、pemalloc/pefree配对与new/free混用异常边界捕获转成 PHP 异常C 异常穿越 C 栈构建phpizeconfiguremakeAPI 版本或 ZTS 不匹配写 PHP 扩展的门槛不在业务逻辑而在边界C 与 C 的链接边界、两套内存分配器的边界、异常不能跨越的边界、以及 PHP 版本之间的 API 边界。把这四条守住扩展就是一个普通的动态库守不住问题往往以「段错误」或「模块加载失败」的形式出现且很难从 PHP 层定位。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑