PHP 内核数据结构剖析:zend_constant 如何承载全局常量
本文基于 PHP 源码仓库中的核心数据结构文档 zend_constant.rst,系统讲解 PHP 解释器(Zend Engine)中用于存放“非类常量”的专属结构体 zend_constant。读完本篇,你将掌握该结构体四个成员(value、name、filename、attributes)各自的职责,理解 zval 中“额外位”如何被复用来保存常量标志位与所属模块编号,并能在 Zend/zend_constants.h 与 Zend/zend_constants.c 中定位到注册、查询与释放常量的真实实现,从而为阅读或扩展 PHP 常量子系统打下可靠的地基。
一、zend_constant 结构定义:常量在解释器中的“户口”
PHP 中的常量(这里特指非类常量,即全局命名空间常量)并不直接以裸 zval 散落在各处,而是被统一收纳进一个专属结构体 zend_constant。它同时保存了常量的值与使用该常量所需的元信息。
原始文档给出的定义如下:
typedef struct _zend_constant {
zval value;
zend_string *name;
zend_string *filename;
HashTable *attributes;
} zend_constant;
在源码中,这一结构定义于 Zend/zend_constants.h:
typedef struct _zend_constant {
zval value;
zend_string *name;
zend_string *filename;
HashTable *attributes;
} zend_constant;
四个成员各司其职:
value:既存放常量的实际取值,又借zval的冗余空间承载常量元数据(详见第二节)。name:常量的名字,用于在哈希表中检索“该常量是否已被定义”。filename:定义该常量所在文件的文件名,为反射接口ReflectionConstant::getFileName()提供依据。attributes:以HashTable(本质上是一个数组)保存施加在该常量上的属性(attributes)详情。
所有全局常量最终都注册进全局哈希表 EG(zend_constants)。该表在启动时由 Zend/zend_constants.c 中的 zend_startup_constants() 初始化:
void zend_startup_constants(void)
{
EG(zend_constants) = (HashTable *) malloc(sizeof(HashTable));
zend_hash_init(EG(zend_constants), 128, NULL, ZEND_CONSTANT_DTOR, 1);
}
注意其中的 ZEND_CONSTANT_DTOR(定义为 free_zend_constant),它决定了表中每个常量条目在被移除或解释器退出时如何被正确释放。
二、value 字段:zval 里的“额外位”与常量标志
常量的值存放在 zval 类型的 value 中。然而 zval 结构本身存在额外空间,对常量而言,这部分空间被用来同时保存两件事:该常量定义于哪个 PHP 模块,以及一组影响该常量用法的标志位(flags)。
这些额外信息被写入 uint32_t 类型的 value.u2.constant_flags 字段。在 Zend/zend_constants.h 中,文档提到的四个核心标志位为:
#define CONST_PERSISTENT (1<<0) /* Persistent */
#define CONST_NO_FILE_CACHE (1<<1) /* Can't be saved in file cache */
#define CONST_DEPRECATED (1<<2) /* Deprecated */
#define CONST_OWNED (1<<3) /* constant should be destroyed together with class */
除文档列出的四个外,源码中还定义了一个用于常量求值递归保护的标志:
#define CONST_RECURSIVE (1<<4) /* Recursion protection for constant evaluation */
标志位与模块编号的存取宏
文档指出:低位保存常量标志位,高位保存注册该常量的 PHP 模块编号;用户定义的常量,其模块编号存储为 PHP_USER_CONSTANT。对应的存取宏定义在 Zend/zend_constants.h:
#define PHP_USER_CONSTANT 0x7fffff /* a constant defined in user space */
#define ZEND_CONSTANT_FLAGS(c) \
(Z_CONSTANT_FLAGS((c)->value) & 0xff)
#define ZEND_CONSTANT_MODULE_NUMBER(c) \
(Z_CONSTANT_FLAGS((c)->value) >> 8)
#define ZEND_CONSTANT_SET_FLAGS(c, _flags, _module_number) do { \
Z_CONSTANT_FLAGS((c)->value) = \
((_flags) & 0xff) | ((_module_number) << 8); \
} while (0)
从源码结构看,Z_CONSTANT_FLAGS 宏最终落到 zval 的 u2.constant_flags 成员上(见 Z_CONSTANT_FLAGS(zval) (zval).u2.constant_flags)。这里值得注意的一个实现细节是:当前版本中 ZEND_CONSTANT_FLAGS 用 & 0xff 取出低 8 位标志、ZEND_CONSTANT_MODULE_NUMBER 用 >> 8 取出高位模块编号——即“低标志 / 高模块”的分段约定在现行宏实现里是按字节边界切分的。理解这一点的意义在于:当你需要在扩展中自行构造常量时,务必通过 ZEND_CONSTANT_SET_FLAGS 一次性写入,避免手动拼接位段出错。
CONST_PERSISTENT 对内存生命周期的决定性影响
CONST_PERSISTENT 决定了常量是用“持久内存”还是“常规内存”分配,进而决定其释放方式。这一点在释放函数 Zend/zend_constants.c 中体现得非常清楚:
void free_zend_constant(zval *zv)
{
zend_constant *c = Z_PTR_P(zv);
if (!(ZEND_CONSTANT_FLAGS(c) & CONST_PERSISTENT)) {
zval_ptr_dtor_nogc(&c->value);
if (c->name) { zend_string_release_ex(c->name, 0); }
if (c->filename) { zend_string_release_ex(c->filename, 0); }
if (c->attributes) { zend_hash_release(c->attributes); }
efree(c);
} else {
zval_internal_ptr_dtor(&c->value);
if (c->name) { zend_string_release_ex(c->name, 1); }
if (c->filename) { zend_string_release_ex(c->filename, 1); }
if (c->attributes) { zend_hash_release(c->attributes); }
free(c);
}
}
可以看到,CONST_PERSISTENT 的取值直接切换了分配器(efree / free)与字符串释放标志(zend_string_release_ex 的第二个参数),这是编写扩展常量时最容易踩坑的地方。
此外,在多线程(ZTS)构建下,常量在请求间需要被复制,Zend/zend_constants.c 中的 copy_zend_constant 会断言其必须带 CONST_PERSISTENT,并对 name、filename、字符串值做深拷贝——再次印证 CONST_PERSISTENT 与跨请求生命周期强绑定。
三、注册路径:zend_register_constant 如何填充 name 与 filename
文档中 name 与 filename 的说明,在注册入口 zend_register_constant 里得到了完整印证。该函数定义于 Zend/zend_constants.c,其中有几处与文档描述直接对应的关键逻辑:
- 小写化处理命名空间前缀。若常量名包含命名空间分隔符
\,只有斜杠之前的部分会被转小写并内化(interned),以保证命名空间检索大小写不敏感而末段保持原样:
const char *slash = strrchr(ZSTR_VAL(c->name), '\\');
if (slash) {
lowercase_name = zend_string_init(ZSTR_VAL(c->name), ZSTR_LEN(c->name), persistent);
zend_str_tolower(ZSTR_VAL(lowercase_name), slash - ZSTR_VAL(c->name));
lowercase_name = zend_new_interned_string(lowercase_name);
name = lowercase_name;
} else {
name = c->name;
}
- 只有用户常量才记录
filename。文档称filename在“非用户态代码定义时为NULL”,源码正是这样实现的——仅当模块编号为PHP_USER_CONSTANT时才取当前执行文件名:
c->filename = NULL;
if (ZEND_CONSTANT_MODULE_NUMBER(c) == PHP_USER_CONSTANT) {
zend_string *filename = zend_get_executed_filename_ex();
if (filename) {
c->filename = zend_string_copy(filename);
}
}
这也解释了为何文档特别强调 filename 是 ReflectionConstant::getFileName() 的基础:内置常量(由 C 扩展注册)通常没有“定义文件”,而 define() 定义的用户常量会携带来源文件名。
- 重名保护与特殊常量拦截。函数会拦截试图重定义
__COMPILER_HALT_OFFSET__、特殊常量(NULL/TRUE/FALSE等)或已存在常量的行为,并给出“将在 PHP 9 成为错误”的警告,随后清理资源并返回NULL。
各类型常量的便捷注册函数(如 zend_register_long_constant、zend_register_stringl_constant 等,见 Zend/zend_constants.c)都只是先 ZVAL_* 设值、再 ZEND_CONSTANT_SET_FLAGS 写入标志与模块编号、随后用 zend_string_init_interned 内化名字,最终统一交给 zend_register_constant 完成哈希表插入。配套的 REGISTER_*_CONSTANT / REGISTER_NS_*_CONSTANT 宏(见 Zend/zend_constants.h)则是扩展开发者注册常量时的常用入口。
四、name 与 filename 两个字符串成员
name
name 持有一个保存常量名的 zend_string,目的是支持“查找已定义常量”这一操作(哈希表键)。文档明确指出:该字符串在常量本身被释放时一并释放。这一点在 free_zend_constant(Zend/zend_constants.c)中得到验证:无论常量是持久还是非持久,都会调用 zend_string_release_ex(c->name, ...) 释放名字字符串。在 ZTS 复制场景下,copy_zend_constant 会用 zend_string_copy 为每个请求维护独立的 name 副本,避免跨请求共享可变字符串。
filename
filename 持有另一个 zend_string,保存定义该常量的文件名;若常量并非定义于用户态代码,则该字段为 NULL。文档强调,正是这一字段支撑了 PHP 反射方法 ReflectionConstant::getFileName()。结合第三节的注册逻辑可以推断:用户态 define() 创建的常量会携带 zend_get_executed_filename_ex() 返回的文件名副本,从而让反射能如实报出“常量来自哪个文件”;而 C 扩展在模块初始化时注册的内置常量一般不带文件名。
五、attributes:仅编译期 const 才拥有的属性表
attributes 是一个 HashTable,保存施加于常量上的属性(attributes)详情。文档给出了一条重要限制:属性只能加在编译期通过 const 声明的常量上(例如 const EXAMPLE = 123;),而不能加在运行时通过 define('EXAMPLE', 123); 声明的常量上。
源码中 attributes 的填充发生在 Zend/zend_constants.c 的 zend_constant_add_attributes:
void zend_constant_add_attributes(zend_constant *c, HashTable *attributes) {
GC_TRY_ADDREF(attributes);
c->attributes = attributes;
zend_attribute *deprecated_attribute = zend_get_attribute_str(
c->attributes, "deprecated", strlen("deprecated")
);
if (deprecated_attribute) {
ZEND_CONSTANT_SET_FLAGS(
c,
ZEND_CONSTANT_FLAGS(c) | CONST_DEPRECATED,
ZEND_CONSTANT_MODULE_NUMBER(c)
);
}
}
这段实现揭示了两点值得注意的事实:
- 属性表被直接挂到常量上,并通过
GC_TRY_ADDREF增加引用计数(属性表是受 GC 管理的对象); - 若属性中包含
#[\Deprecated](这里以字符串名"deprecated"匹配),会自动为常量置上CONST_DEPRECATED标志。也就是说,文档中列出的CONST_DEPRECATED标志位与 PHP 8 的属性系统在这里产生了直接联动——编译期常量上的废弃属性会被“翻译”成结构体里的一枚标志位,供后续执行阶段做废弃告警。
这也从反面印证了文档的说法:由于属性是在编译阶段解析并挂接的,因此只有编译期 const 常量才可能拥有 attributes;运行时 define() 不经过这套编译期属性收集流程,自然无法携带属性。
六、把文档与源码串起来:一条完整的常量生命周期
将前文各节串成一条可复现的证据链,有助于对 zend_constant 建立整体认识(路径均可在当前仓库中直接查看):
- 初始化:
zend_startup_constants()(Zend/zend_constants.c)建立EG(zend_constants)哈希表,并指定析构器ZEND_CONSTANT_DTOR。 - 注册:各类型便捷注册函数设值 →
ZEND_CONSTANT_SET_FLAGS写入标志与模块号(Zend/zend_constants.h)→zend_register_constant(Zend/zend_constants.c)完成命名空间小写化、按PHP_USER_CONSTANT决定是否记录filename、插入哈希表并做重名保护。 - 属性挂接:编译期常量经
zend_constant_add_attributes(Zend/zend_constants.c)挂接attributes,并按#[\Deprecated]置位CONST_DEPRECATED。 - 查询:文档提到的“检索已定义常量”由
zend_get_constant*系列完成(声明见 Zend/zend_constants.h),其中对NULL/TRUE/FALSE等短名还有zend_get_special_const快速路径(Zend/zend_constants.c)。 - 释放:条目离开哈希表时触发
free_zend_constant(Zend/zend_constants.c),依据CONST_PERSISTENT选择正确的分配器与字符串释放方式,回收name、filename与attributes。 - 模块卸载:
clean_module_constants(Zend/zend_constants.c)借助ZEND_CONSTANT_MODULE_NUMBER精确移除“属于某模块”的常量,这正是第二节“高位存模块编号”设计的实际用途。
七、适用前提与阅读建议
需要说明的是,本文所述字段布局、标志位取值与宏定义均以当前仓库源码为准,对应文件为 Zend/zend_constants.h 与 Zend/zend_constants.c,原始说明文档为 docs/source/core/data-structures/zend_constant.rst。由于文档中“低位 16 位存标志、高位 16 位存模块号”的表述与现行宏实现(& 0xff 取低 8 位、>> 8 取高位)存在粒度差异,实际以源码宏定义为准。
建议阅读顺序:先通读本文第二节理解“值 + 标志 + 模块号”的位段设计,再对照第三节注册流程看清 name/filename 如何被填充,最后用第六节的生命周期链条把 zend_constants.h / zend_constants.c 通读一遍。这样既保留了原始文档对四个成员的解释,又补齐了源码级的实现依据,使 zend_constant 从“一段结构体定义”变成一条可定位、可验证、可迁移到扩展开发的完整知识线。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00