首页
/ PHP 内核数据结构剖析:zend_constant 如何承载全局常量

PHP 内核数据结构剖析:zend_constant 如何承载全局常量

2026-09-05 13:41:32作者:魏献源Searcher

本文基于 PHP 源码仓库中的核心数据结构文档 zend_constant.rst,系统讲解 PHP 解释器(Zend Engine)中用于存放“非类常量”的专属结构体 zend_constant。读完本篇,你将掌握该结构体四个成员(valuenamefilenameattributes)各自的职责,理解 zval 中“额外位”如何被复用来保存常量标志位与所属模块编号,并能在 Zend/zend_constants.hZend/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 宏最终落到 zvalu2.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,并对 namefilename、字符串值做深拷贝——再次印证 CONST_PERSISTENT 与跨请求生命周期强绑定。

三、注册路径:zend_register_constant 如何填充 namefilename

文档中 namefilename 的说明,在注册入口 zend_register_constant 里得到了完整印证。该函数定义于 Zend/zend_constants.c,其中有几处与文档描述直接对应的关键逻辑:

  1. 小写化处理命名空间前缀。若常量名包含命名空间分隔符 \,只有斜杠之前的部分会被转小写并内化(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;
}
  1. 只有用户常量才记录 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);
    }
}

这也解释了为何文档特别强调 filenameReflectionConstant::getFileName() 的基础:内置常量(由 C 扩展注册)通常没有“定义文件”,而 define() 定义的用户常量会携带来源文件名。

  1. 重名保护与特殊常量拦截。函数会拦截试图重定义 __COMPILER_HALT_OFFSET__、特殊常量(NULL/TRUE/FALSE 等)或已存在常量的行为,并给出“将在 PHP 9 成为错误”的警告,随后清理资源并返回 NULL

各类型常量的便捷注册函数(如 zend_register_long_constantzend_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)则是扩展开发者注册常量时的常用入口。

四、namefilename 两个字符串成员

name

name 持有一个保存常量名的 zend_string,目的是支持“查找已定义常量”这一操作(哈希表键)。文档明确指出:该字符串在常量本身被释放时一并释放。这一点在 free_zend_constantZend/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.czend_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)
        );
    }
}

这段实现揭示了两点值得注意的事实:

  1. 属性表被直接挂到常量上,并通过 GC_TRY_ADDREF 增加引用计数(属性表是受 GC 管理的对象);
  2. 若属性中包含 #[\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_constantZend/zend_constants.c)完成命名空间小写化、按 PHP_USER_CONSTANT 决定是否记录 filename、插入哈希表并做重名保护。
  • 属性挂接:编译期常量经 zend_constant_add_attributesZend/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_constantZend/zend_constants.c),依据 CONST_PERSISTENT 选择正确的分配器与字符串释放方式,回收 namefilenameattributes
  • 模块卸载clean_module_constantsZend/zend_constants.c)借助 ZEND_CONSTANT_MODULE_NUMBER 精确移除“属于某模块”的常量,这正是第二节“高位存模块编号”设计的实际用途。

七、适用前提与阅读建议

需要说明的是,本文所述字段布局、标志位取值与宏定义均以当前仓库源码为准,对应文件为 Zend/zend_constants.hZend/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 从“一段结构体定义”变成一条可定位、可验证、可迁移到扩展开发的完整知识线。

登录后查看全文
热门项目推荐
相关项目推荐