PHP内核详解· 类型篇(一)· 类型的基本概念

121 阅读11分钟

一、类型的基本概念

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;
  1. 这一段定义中,首先通过 typedef int64_t zend_long; 为原生 64 位整数 int64_t 起了一个别名 zend_long。紧接着定义了一个联合类型 _zend_value,并通过 typedef 为它起了别名 zend_value。在后续代码中,大部分场景使用的都是 zend_value 这个别名,而不是底层的 _zend_value,这是 Zend 源码中比较常见的命名习惯。

  2. 从内存布局的角度看,zend_value 是一个 占用 8 字节的联合体

在 64 位环境中,联合体的大小由其中“占用空间最大”的成员决定。由于第一个成员是 zend_long(底层为 int64_t),本身就是 64 位整数,因此整个联合体大小固定为 8 字节。

  1. 在 64 位构建下,这个联合体中的各个成员要么是 64 位整数,要么是 64 位指针,它们在同一片 8 字节存储区域上“复用”空间,不会产生额外的填充或浪费。这种设计既保证了空间紧凑,又为上层类型系统提供了足够的表达能力。

  2. 结构体末尾的 ww 成员是一个由两个 uint32_t 组成的小结构体,用于在需要按 32 位视角访问同一片内存时提供兼容支持,例如在 32 位构建或某些需要拆分高低 32 位的场景中更灵活地操作底层比特位。在纯 64 位的常规使用场景中,这个成员通常不会直接参与业务逻辑。

  3. 可以看到,zend_value 中联合的,基本都是 PHP 运行时最常见、最基础的几类“承载体”:标量值、字符串、数组、对象、资源、引用等。这些成员并不是“同时存在”,而是根据实际的变量类型,用其中的一种解释方式来看待这 8 个字节的内容。

从抽象角度理解,zend_value 更像是一个“物理载体”:它负责存放具体值或指针;而“这个值到底应该被解释成什么类型”,则交由更高一层的 zval 结构中的类型信息来决定。

  1. 在源码全局范围内,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;
};
  1. 从内存布局上看,一个 zval 一共占用 16 字节:
  • 前 8 字节是 zend_value value,用来存放具体的值(标量、指针等);
  • 之后的 4 字节是联合体 u1,核心成员是 type_info,用来记录“这 8 字节目前被当成什么类型使用”;
  • 最后 4 字节是联合体 u2,记录与上下文相关的额外信息,例如哈希表链表位置、行号、foreach 状态等。

u1 中的 type_info 与内部结构 v 共享同一块 4 字节空间:

  • 通过 type_info 可以整体读写类型信息;
  • 通过 v.typev.type_flags 和 v.u.extra 可以按字段拆开来访问。
  1. 由于 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
  1. 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.vu1.type_info 共享同一个 union 存储区,对 type_info 的写入会同时改写 u1.v 中的多个字段。通过 zval_get_type()Z_TYPE() 的定义可以看出,type 这个字段只提供读取入口,没有单独的写入接口;而 type_flagstype_info 则是可以被直接更新的。

zend_types.h 中,底层类型被分成若干分组,其中一组就是最常用的普通数据类型(Regular data types)。对应的常量定义可以整理如下:

常量名说明底层类型常用更新宏
IS_UNDEF0未定义ZVAL_UNDEF()
IS_NULL1NULLZVAL_NULL()
ZVAL_BOOL()
IS_FALSE2ZVAL_FALSE()
ZVAL_BOOL()
IS_TRUE3ZVAL_TRUE()
IS_LONG4整数zend_longZVAL_LONG()
IS_DOUBLE5小数doubleZVAL_DOUBLE()
IS_STRING6字符串zend_string
IS_ARRAY7数组zend_array / HashTable
IS_OBJECT8对象zend_object
IS_RESOURCE9资源zend_resource
IS_REFERENCE10引用zend_referenceZVAL_NEW_REF()
IS_CONSTANT_AST11常量表达式zend_ast_refZVAL_AST()

PHP 提供了一系列函数与宏来读取或判断 zval 的类型信息,例如通过 Z_TYPE(zv) 取得类型枚举,通过 Z_TYPE_P(zv_ptr) 访问指针形式的 zval 类型等。更多细节可以放在后续的“通用宏程序”部分集中展开,这里先给出整体图景,方便在阅读后面的具体类型时建立映射。

五、小结

本篇围绕 PHP 类型系统的底层基础展开,重点在于从源代码视角理解“变量”在 Zend 引擎中的真实样貌。文中介绍了最核心的数据结构 zend_value,并进一步说明了 PHP 中变量 zval 的类型体系以及其与源码宏的对应关系。通过这些基础结构,可以逐步看清各类 PHP 数据在内核中的存储方式与运行机制,为后续深入探讨字符串、数组、对象、引用等更复杂类型打下必要的理解基础。

如果你对 PHP 的类型设计有不同的理解,或者希望我在后续文章中讲解具体的分配策略,欢迎留言讨论~


本文项目地址:github.com/xuewolf/php…