一、类型的基本概念
PHP 的类型系统看似简单,却承担着语言运行期最核心的职责:存储数据、描述数据、并支撑底层引擎完成各种运算。本篇作为“类型篇”的开篇,先对几个关键背景作一个整体说明,为后续章节的深入展开铺好地基。
PHP 属于弱类型语言,书写代码时并不需要显式声明变量类型。然而“写代码时不用声明类型”,绝不等于“内部不区分类型”。在 Zend 引擎的世界里,所有数据都必须被准确识别、分类、存储并处理。引擎内部定义了 12 种常用数据类型,包括整型、浮点型、字符串、数组、对象、资源、常量 AST、引用等。它们是整个 PHP 类型系统的基础,本篇将从底层实现的角度展开介绍。
PHP 的变量系统并不是“随写随变”,而是由引擎在运行期不断维护、转换和管理的复杂结构。理解这些机制,有助于看清 PHP 在运行时到底做了什么,也能解释许多表面上“不符合直觉”的行为。
本篇使用的源码版本为 PHP 8.2.5,调试环境为 64 位 Windows 系统 + Microsoft Visual Studio 2013。为了保持叙述清晰,本篇只讨论 64 位系统中的实现细节,不涉及 32 位版本,也不进行差异化比较。
文中大多数关键定义来自 /Zend/zend_types.h。这是 PHP 类型系统的核心文件,定义了 zval、zend_value、类型宏、辅助函数等重要结构,是阅读 Zend 源码时最应该熟悉的部分。
在正式进入类型系统之前,有必要先从整个机制的根节点——zend_value 数据结构开始介绍,它是 PHP 内部存储所有类型数据的基础结构。
二、zend_value 数据结构
在 PHP 内核中,zend_value 是最核心的底层数据结构之一。几乎所有与变量、常量、数组、对象等相关的类型信息,最终都会在这一层落地。因此,从 zend_value 开始理解 PHP 的类型系统,是进入类型篇的自然起点。
typedef int64_t zend_long; // 长整型,原生 64 位整数
typedef union _zend_value { // 占 8 Bytes,64 位内存
zend_long lval; // 长整型,原生 64 位整数
double dval; // 双精度浮点数
zend_refcounted *counted; // 带引用计数头部的结构体指针
zend_string *str; // 字符串指针
zend_array *arr; // 数组指针
zend_object *obj; // 对象指针
zend_resource *res; // 资源指针
zend_reference *ref; // 引用包装指针
zend_ast_ref *ast; // 抽象语法树节点引用指针
zval *zv; // zval 指针
void *ptr; // 通用指针
zend_class_entry *ce; // 类结构体指针
zend_function *func; // 函数结构体指针
struct { // 兼容 32 位系统的拆分视图
uint32_t w1;
uint32_t w2;
} ww;
} zend_value;
-
这一段定义中,首先通过
typedef int64_t zend_long;为原生 64 位整数int64_t起了一个别名zend_long。紧接着定义了一个联合类型_zend_value,并通过typedef为它起了别名zend_value。在后续代码中,大部分场景使用的都是zend_value这个别名,而不是底层的_zend_value,这是 Zend 源码中比较常见的命名习惯。 -
从内存布局的角度看,
zend_value是一个 占用 8 字节的联合体:
在 64 位环境中,联合体的大小由其中“占用空间最大”的成员决定。由于第一个成员是
zend_long(底层为int64_t),本身就是 64 位整数,因此整个联合体大小固定为 8 字节。
-
在 64 位构建下,这个联合体中的各个成员要么是 64 位整数,要么是 64 位指针,它们在同一片 8 字节存储区域上“复用”空间,不会产生额外的填充或浪费。这种设计既保证了空间紧凑,又为上层类型系统提供了足够的表达能力。
-
结构体末尾的
ww成员是一个由两个uint32_t组成的小结构体,用于在需要按 32 位视角访问同一片内存时提供兼容支持,例如在 32 位构建或某些需要拆分高低 32 位的场景中更灵活地操作底层比特位。在纯 64 位的常规使用场景中,这个成员通常不会直接参与业务逻辑。 -
可以看到,
zend_value中联合的,基本都是 PHP 运行时最常见、最基础的几类“承载体”:标量值、字符串、数组、对象、资源、引用等。这些成员并不是“同时存在”,而是根据实际的变量类型,用其中的一种解释方式来看待这 8 个字节的内容。
从抽象角度理解,
zend_value更像是一个“物理载体”:它负责存放具体值或指针;而“这个值到底应该被解释成什么类型”,则交由更高一层的zval结构中的类型信息来决定。
- 在源码全局范围内,
zend_value本身的直接出现次数并不多(仅在Zend/zend_execute.c中有显式引用),这是因为它几乎总是作为zval的一部分被创建和使用。zval是 PHP 变量在内核层面的统一表示形式,内部正是通过一个zend_value字段承载具体的数据内容,因此在后续内容中,可以看到zval的出现频率远高于单独的zend_value。
围绕 zend_value 的这组设计,构成了 PHP 类型系统在运行时的“最低一层承重结构”。后续关于 zval、引用计数、字符串、数组、对象等各类具体类型的实现,都将在这一层之上逐步展开。
三、zval 结构体
zval 是 PHP 变量在内核中的直接载体,可以理解为“脚本世界里的一个变量,在 C 里的长相”。它的类型定义如下:
typedef struct _zval_struct zval; // zval 是 _zval_struct 结构体的别名
struct _zval_struct { // 占用 8 + 4 + 4 = 16 Bytes,128 位
zend_value value; // 占用 8 Bytes,64 位,存放具体的值
union { // 联合类型,占用 4 Bytes,32 位
uint32_t type_info; // 类型信息,32 位整数
struct { // 用 struct 打包成和 uint32_t 相同的大小
ZEND_ENDIAN_LOHI_3( // 兼容大小端序(高位在前或低位在前),总共 4 Bytes
zend_uchar type, // 无符号字符,1 Byte,基础类型
zend_uchar type_flags, // 无符号字符,1 Byte,类型标记
union { // 为兼容旧版本保留
uint16_t extra; // 无符号 16 位整数,2 Bytes
} u)
} v;
} u1;
union { // 全是 32 位整数,不同成员表示不同用途
uint32_t next; // 哈希表中:指向同一哈希值的下一个元素
uint32_t cache_slot; // opcache 中少量使用
uint32_t opline_num; // 操作码序号
uint32_t lineno; // PHP 脚本行号,解析和编译阶段使用
uint32_t num_args; // 函数调用时的参数数量
uint32_t fe_pos; // foreach 遍历位置
uint32_t fe_iter_idx; // foreach 迭代器序号
uint32_t property_guard;// 对象属性保护标记
uint32_t constant_flags;// 常量标记
uint32_t extra; // 额外数据,少量使用
} u2;
};
- 从内存布局上看,一个 zval 一共占用 16 字节:
- 前 8 字节是
zend_value value,用来存放具体的值(标量、指针等); - 之后的 4 字节是联合体
u1,核心成员是type_info,用来记录“这 8 字节目前被当成什么类型使用”; - 最后 4 字节是联合体
u2,记录与上下文相关的额外信息,例如哈希表链表位置、行号、foreach 状态等。
u1 中的 type_info 与内部结构 v 共享同一块 4 字节空间:
- 通过
type_info可以整体读写类型信息; - 通过
v.type、v.type_flags和v.u.extra可以按字段拆开来访问。
- 由于
type_info是 32 位整数,在不同平台上可能存在大端或小端两种字节序。宏ZEND_ENDIAN_LOHI_3()用来在编译期根据平台字节序调整这三个字段的排列顺序,使它们在内存中的布局始终与type_info一致:
#ifdef WORDS_BIGENDIAN
# define ZEND_ENDIAN_LOHI_3(lo, mi, hi) hi; mi; lo;
#else
# define ZEND_ENDIAN_LOHI_3(lo, mi, hi) lo; mi; hi; // 64 位 Windows 使用这一分支
#endif
u2同样是一个联合体,但所有成员都是uint32_t,只是名字不同,用于在不同场景下复用同一块 4 字节空间。这一设计使得 zval 在保持 16 字节定长的前提下,能够承载解析、编译、执行等多个阶段需要的附加信息,而不会增加额外的内存开销。
结合前一节可以看出:
zend_value负责存放“值本身”;u1.type_info描述“这个值现在属于哪一种 PHP 类型”;u2记录与运行环境相关的辅助信息。
PHP 代码中的每一个变量,在内核层面都对应着一个这样的 zval 实例。整个类型篇后续的分析,都会围绕 zval 展开。
四、变量(zval)的类型信息
zval 结构体可以用来存放多种不同类型的数据,对应到 PHP 代码层面的“变量”时,就形成了那种可以随时改变类型的弱类型体验。例如:
<?php
$var = true; // IS_TRUE 类型
$var = false; // IS_FALSE 类型
$var = null; // IS_NULL 类型
$var = 5; // IS_LONG 类型
$var = 1.5; // IS_DOUBLE 类型
$var = "str"; // IS_STRING 类型
$var = [1,2,3]; // IS_ARRAY 类型
$var = new stdclass(); // IS_OBJECT 类型
$var = fopen("a.txt", 'r'); // IS_RESOURCE 类型
$var = &$var; // IS_REFERENCE 类型
static $a = new stdclass(); // IS_CONSTANT_AST 类型
在以上示例中,代码本身没有显式写出“类型”,但底层的 zval 会在不同阶段分别承载布尔、空值、整数、小数、字符串、数组、对象、资源、引用和常量表达式等不同形式的数据。也正因为 zval 能够在运行期灵活切换,这类语言通常被称为“弱类型语言”;与之相对,C、Java 等需要在声明阶段就写死类型的语言,一般被称为“强类型语言”。
PHP 内核并不允许直接修改 zval.u1.v.type 成员。想要更新变量类型,需要整体更新 zval.u1.type_info。原因在于:u1.v 与 u1.type_info 共享同一个 union 存储区,对 type_info 的写入会同时改写 u1.v 中的多个字段。通过 zval_get_type() 和 Z_TYPE() 的定义可以看出,type 这个字段只提供读取入口,没有单独的写入接口;而 type_flags 和 type_info 则是可以被直接更新的。
在 zend_types.h 中,底层类型被分成若干分组,其中一组就是最常用的普通数据类型(Regular data types)。对应的常量定义可以整理如下:
| 常量名 | 值 | 说明 | 底层类型 | 常用更新宏 |
| IS_UNDEF | 0 | 未定义 | ZVAL_UNDEF() | |
| IS_NULL | 1 | NULL | ZVAL_NULL() ZVAL_BOOL() | |
| IS_FALSE | 2 | 假 | ZVAL_FALSE() ZVAL_BOOL() | |
| IS_TRUE | 3 | 真 | ZVAL_TRUE() | |
| IS_LONG | 4 | 整数 | zend_long | ZVAL_LONG() |
| IS_DOUBLE | 5 | 小数 | double | ZVAL_DOUBLE() |
| IS_STRING | 6 | 字符串 | zend_string | |
| IS_ARRAY | 7 | 数组 | zend_array / HashTable | |
| IS_OBJECT | 8 | 对象 | zend_object | |
| IS_RESOURCE | 9 | 资源 | zend_resource | |
| IS_REFERENCE | 10 | 引用 | zend_reference | ZVAL_NEW_REF() |
| IS_CONSTANT_AST | 11 | 常量表达式 | zend_ast_ref | ZVAL_AST() |
PHP 提供了一系列函数与宏来读取或判断 zval 的类型信息,例如通过 Z_TYPE(zv) 取得类型枚举,通过 Z_TYPE_P(zv_ptr) 访问指针形式的 zval 类型等。更多细节可以放在后续的“通用宏程序”部分集中展开,这里先给出整体图景,方便在阅读后面的具体类型时建立映射。
五、小结
本篇围绕 PHP 类型系统的底层基础展开,重点在于从源代码视角理解“变量”在 Zend 引擎中的真实样貌。文中介绍了最核心的数据结构 zend_value,并进一步说明了 PHP 中变量 zval 的类型体系以及其与源码宏的对应关系。通过这些基础结构,可以逐步看清各类 PHP 数据在内核中的存储方式与运行机制,为后续深入探讨字符串、数组、对象、引用等更复杂类型打下必要的理解基础。
如果你对 PHP 的类型设计有不同的理解,或者希望我在后续文章中讲解具体的分配策略,欢迎留言讨论~
本文项目地址:github.com/xuewolf/php…