PHP 基本结构
在本节中,我们将深入探讨最重要的 PHP 内部数据结构,其中包括:
-
Values
-
Strings
-
Parameter parsing API
-
Return Values
在本书的示例扩展中,你可以看到一个使用基本 PHP 结构的例子。
PHP VALUES (ZVAL)
Zval
是 PHP 的关键结构。它按照简化的 C 语言定义,代表任何 PHP 值(如数字、字符串或数组)。
typedef struct _zval_struct {
union {
zend_long lval;
double dval;
zend_refcounted *counted;
zend_string *str;
zend_array *arr;
zend_object *obj;
zend_resource *res;
zend_reference *ref;
//…
} value;
zend_uchar type;
zend_uchar type_flags;
uint16_t extra;
uint32_t reserved;
} zval;
PHP 是一种动态类型语言,同一个 zval
可能包含不同类型的值。zval
的第一个字段是所有可能值类型的联合。它可以保存整数、双精度浮点数或指向某种依赖结构的指针。Zval
还保留了 type
、type_flags
和两个保留字段。无论如何,这些空间都将用于正确的结构对齐,但通过保留这些空间,我们可以存储一些从属数据。
在内存中,zval
表示为两个 64 位字。第一个字保存值,第二个字保存 type
、type_flags
、extra
和保留字段。

Zvals 通常在 PHP 堆栈或其它数据结构中分配。它们几乎从不在堆上分配。
唯一重要的 zval 标志是 IS_TYPE_REFCOUNTED
,它定义了在复制和销毁过程中如何处理 zval。
如果没有设置,zval 就是标量。
复制 zval 时,我们要复制第一个字(及其值)和第二个字的一半(包括 type、type_flags 和额外字段)。我们不需要任何特殊操作来销毁它们。
很少有纯标量 PHP 类型可以完全用这种 zval 结构来表示:
-
IS_UNDEF:未初始化的 PHP 本地变量。(在 PHP 扩展库中通常不会出现这种类型。PHP 解释器会关心初始化、警告和转换为 NULL。)
-
IS_NULL:空常量。(值未使用)
-
IS_FALSE:布尔类型的假常量。(值未使用)
-
IS_TRUE:布尔类型的 True 常量。(不使用该值)
-
IS_LONG:长整数。
-
IS_DOUBLE:长浮点数。
有一个特殊的 C 宏 API 用于检索 zvals 的字段。所有宏都以两种形式定义:普通(用于 zvals)和后缀为 _P
的宏(用于 zvals 的指针),例如 Z_TYPE(zval)
和 Z_TYPE_P(zval_ptr)
。以下是最重要的参数:
-
Z_TYPE_FLAGS(zv):返回类型标志(一组位:IS_TYPE_REFCOUNTED 及其他几个位)。
-
Z_REFCOUNTED(zv):如果设置了 IS_TYPE_REFCOUNTED,则返回 true。
-
Z_TYPE(zv):返回 zval 的类型(IS_NULL、IS_LONG 等)。
-
Z_LVAL(zv):返回 zval 的长整型值(类型必须是 IS_LONG)。
-
Z_DVAL(zv):返回 zval 的双精度数值(类型必须是 IS_DOUBLE)。
另一组宏用于 zval 初始化:
-
ZVAL_UNDEF(zv):初始化未定义的 zval
-
ZVAL_NULL(zv):用 null 常量初始化 zval
-
ZVAL_FALSE(zv):用 false 常量初始化 zval
-
ZVAL_TRUE(zv):用 true 常量初始化 zval
-
ZVAL_BOOL(zv, bval):如果 bval 为 true,则用 true 常量初始化 zval,否则用 false 初始化
-
ZVAL_LONG(zv, lval):初始化一个长整数 zval
-
ZVAL_DOUBLE(zv, dval):初始化一个长浮点数 zval
大多数与 zval 相关的声明都在 Zend/zend_types.h
中完成。
所有非标量值,如字符串、数组、对象、资源和引用,都由特定类型的结构来表示。Zval 保存的只是指向该结构的指针。就面向对象编程而言,所有这些特定结构都有一个共同的抽象父类:zend_refcounted
。它定义了结构的第一个 64 位字的格式。它包含引用计数器、类型、标志和垃圾回收器使用的信息。
具体类型的特定结构建立在这个结构之上,并在第一个字之后添加一些额外的数据。

以下引用计数类型是可能的:
-
IS_STRING:PHP 字符串。
-
IS_ARRAY:PHP 数组。
-
IS_REFERENCE:PHP 引用。
-
IS_OBJECT:PHP 对象。
-
IS_RESOURCE:PHP 资源。
展望未来,PHP 字符串和数组可能是非引用计数的(或不可变的),其行为类似于标量值。
下面的 API 宏就是为使用引用计数的 zvals 而设计的(也有后缀为 _P
的变体):
-
Z_COUNTED(zv):返回指向依赖的 zend_refcounted 结构的指针。
-
Z_REFCOUNT(zv):返回依赖的 zend_refcounted 结构的引用计数器。
-
Z_SET_REFCOUNT(zv, rc):设置依赖的 zend_refcounted 结构的引用计数器。
-
Z_ADDREF(zv):增加依赖的 zend_refcounted 结构的引用计数器。
-
Z_DELREF(zv):减少依赖的 zend_refcounted 结构的引用计数器。
以下通用宏既可用于引用计数 Zvals,也可用于非引用计数 Zvals:
-
Z_TRY_ADDREF(zv):检查 IS_TYPE_REFCOUNTED 标志,如果已设置,则递增依赖的 zend_refcounted 结构的引用计数器。
-
Z_TRY_DELREF(zv):检查 IS_TYPE_REFCOUNTED 标志,并递减从属的 zend_refcounted 结构的引用计数器(如果已设置)。
-
zval_ptr_dtor(zv):释放 zval 值:检查 IS_TYPE_REFCOUNTED 标志;递减依赖的 zend_refcounted 结构的引用计数器(如果已设置);如果引用计数器变为零,调用 zval 类型销毁函数。
-
ZVAL_COPY_VALUE(dst, src):从 src 复制 zval(值、类型和 type_flags)到 dst。
-
ZVAL_COPY(dst, src):如果设置了 IS_TYPE_REFCOUNTED 标志,则从 src 复制 zval(值、类型和 type_flags)到 dst,并递增依赖的 zend_refcounted 结构的引用计数器。
PHP STRINGS
字符串由从属的 zend_string
结构表示。它的第一个字重复 zend_refcounted
结构所定义的字。zend_string
还保留了哈希函数的预计算值、字符串长度和实际嵌入的字符。哈希值不必预先计算。它的初始化值为零,根据需要懒散地计算,然后重复使用。PHP 字符串复制不需要复制实际字符。多个 zval 结构可以指向同一个 zend_string
和相应的 referencecounter
值。

字符串在没有设置 IS_TYPE_REFCOUNTED
标记的情况下可能是不可变的(或内部化的),在这种情况下,它们的行为类似于标量。PHP 引擎根本不需要执行任何引用计数。此类字符串只用于读取,并且只能在请求处理结束时销毁。它们绝不会在请求过程中被销毁。
同样的 zend_string 表示法不仅用于 PHP 值,也用于 PHP 引擎中的所有其它字符数据,如函数名、类名和方法名。
zend_string
的不同字段可以通过以下 API 宏(也有后缀为 _P
的变体,用于指向 zvals)来访问:
-
Z_STR(zv):返回指向相应 zend_string 结构的指针。
-
Z_STRVAL(zv):返回指向相应 C 字符串(
char*
) 的指针。 -
Z_STRLEN(zv):返回相应字符串的长度。
-
Z_STRHASH(zv):返回对应字符串的哈希值。
zend_string.hash_value
用作缓存,以消除可重复的哈希函数计算。
PHP 字符串值可以通过以下宏构建:
-
ZVAL_STRING(zv, cstr):分配 zend_string 结构,用给定的 C 零终止字符串初始化它,并初始化 PHP 字符串 zval。
-
ZVAL_STRINGL(zv, cstr, len):分配 zend_string 结构,使用给定的 C 字符串和长度对其进行初始化,并初始化 PHP 字符串 zval。
-
ZVAL_EMPTY_STRING(zv):初始化空 PHP 字符串 zval。
-
ZVAL_STR(zv, zstr):使用给定的 zend_string 初始化 PHP 字符串 zval。
-
ZVAL_STR_COPY(zv, zstr):使用给定的 zend_string 初始化 PHP 字符串zval。如果需要,zend_string 的引用计数器会增加。
当然,也可以不使用 zval,直接使用 zend_string
结构。以下是最重要、最有用的 API 宏和函数:
-
ZSTR_VAL(zstr):返回对应 C 字符串(
char*
)的指针。 -
ZSTR_LEN(zstr):返回字符串的长度。
-
ZSTR_IS_INTERNED(zstr):检查字符串是否为内部字符串(或不可变字符串)。
-
ZSTR_HASH(zstr):使用 hash_value 作为缓存返回字符串的 hash_value。
-
ZSTR_H(zstr):返回 hash_value 字段的值(可能为 0,表示尚未计算)。
-
zend_string_hash_func(zstr):计算并返回字符串的哈希值。
-
zend_hash_func(cstr, len):计算并返回给定 C 字符串(
char*
)和指定长度的哈希值。 -
zstr_empty_alloc():返回一个空的
zend_string
。这个宏实际上并不分配任何东西,而是返回一个指向单个内部zend_string
结构的指针。 -
zend_string_alloc(len, persistent):为指定长度的
zend_string
结构分配内存。参数persistent
表示创建的字符串是否会重新进入请求边界。(通常不应该,因此persistent
应该为零) -
zend_string_safe_alloc(len, number, addition, persistent):类似于
zend_string_alloc()
,但最终字符串大小的计算方法是(len * number + addition
),并检查是否可能溢出:为zend_string
结构分配内存(类似于zend_alloc
),并使用给定的 C 字符串(char*
)和长度对其进行初始化。 -
zend_string_copy(zstr):创建一个给定
zend_string
的副本,并返回一个指向相同字符串的指针,如果需要,还可以增加引用计数器。 -
zend_string_release(zstr):释放指向给定
zend_string
的指针,并检查给定字符串是否被引用计数(非内部引用),递减引用计数器,并释放内存(如果内存为零)。 -
zend_string_equals(zstr1, zstr2):检查两个
zend_string
结构是否相等。 -
zend_string_equals_literal(zstr, cstr):检查两个
zend_string
结构是否相等:检查zend_string
与给定的 C 语言字符串文字是否相等。 -
zend_string_equals_literal_ci(zstr, cstr):不区分大小写的
zend_string_equals_literal()
变体。
Zend/zend_string.h 中定义了完整的 zend_string API。
参数解析 api
参数解析 API 是一种在 PHP 内部函数中获取实际 PHP 参数值的方法。我们已经在 test 扩展中使用了该 API 的某些元素。这些是 ZEND_PARSE_PARAMETERS_START
和 ZEND_PARSE_PARAMETERS_END
之间的区块。让我们更详细地回顾一下这个 API。
首先,有两种不同的参数解析 API:一种是我们已经使用过的(PHP 7 中引入的快速参数解析 API),另一种是与 PHP 5 兼容的旧 API。旧的 API 运行速度较慢,但使用它所需的机器代码较少。如果函数体较小,但需要快速运行,则应使用快速参数解析 API,因为旧 API 的开销可能大于函数本身的语义部分。另一方面,如果函数的运行速度很慢,那么关心参数解析开销和增加代码量就没有意义了。
快速参数解析 api
这个新的应用程序接口是使用 C 预处理器宏实现的,这些宏被转换成几乎最优的 C 代码,用于将实际参数的值提取到 C 变量中,特别是针对这个函数。
-
ZEND_PARSE_PARAMETERS_NONE():该宏用于不期望有任何参数的函数。如果传递了一些参数,函数将发出警告
expects exactly 0 parameters, %d given
,并返回 NULL。 -
ZEND_PARSE_PARAMETERS_START(min_num_args, max_num_args):该宏用于打开一个参数获取代码块。第一个参数是最小参数数,应为 0 或更多。第二个参数是最大参数数。如果传递的参数数超过了定义的参数边界,函数将发出参数数无效的警告并返回 NULL。
-
ZEND_PARSE_PARAMETERS_END():该宏用于终止参数获取代码块。
在 START
和 END
宏之间的块内有一些 ZPP
宏。每个参数都有一个宏,但参数数量可变的函数除外,因为在这些函数中,最后一个参数可能会有多个值。应使用 Z_PARAM_OPTIONAL
宏将必需参数与可选参数分开。
以下是最常用的参数解析宏:
-
Z_PARAM_BOOL(dest):接收布尔参数,并将实际参数的值存储到一个 zend_bool 类型的 C 变量中。在这里和下面,"接收" 意味着检查实际参数的类型,并在可能的情况下将其转换为所需类型,或者产生相应的类型不兼容警告并返回 NULL。
-
Z_PARAM_LONG(dest):接收整数参数,并将实际参数的值存储到一个 zend_long 类型的 C 变量中。
-
Z_PARAM_DOUBLE(dest):接收浮点数参数,并将实际参数值存储到 double 类型的 C 变量中。
-
Z_PARAM_STR(dest):接收一个字符串参数,并将实际参数的值存储到一个
zend_string*
类型的 C 语言变量中。 -
Z_PARAM_STRING(dest, dest_len):接收一个字符串参数,并将指向 C 字符串的指针和传递的字符串长度存储在给定的
char*
类型的 C 变量dest
和size_t
类型的 C 变量dest_len
中。 -
Z_PARAM_ARRAY_HT(dest):接收一个 PHP 数组参数,并将实际参数的值存储到一个 HashTable 类型的 C 变量中。(下一章将介绍 PHP 数组和 HashTables。)
-
Z_PARAM_ARRAY(dest):接收一个 PHP 数组参数,并将实际参数的值存储到一个
zval*
类型的 C 变量中。 -
Z_PARAM_OBJECT(dest):接收一个 PHP 对象参数,并将实际参数的值存储到一个
zval*
类型的 C 语言变量中。 -
Z_PARAM_RESOURCE(dest):接收一个 PHP 资源参数,并将实际参数的值存储到一个
zval*
类型的 C 变量中。 -
Z_PARAM_ZVAL(dest):接收传入的任何 PHP zval,不做任何转换,并将其值存储到
zval*
类型的 C 变量中。 -
Z_PARAM_ZVAL_DEREF(dest):接收任何 PHP zval 并去引用,然后将引用值存储到
zval*
类型的 C 变量中。该宏对于接收通过引用传递的参数非常有用。(我们将在下一章谈到它们。) -
Z_PARAM_VARIADIC(spec, dest, dest_num):以 zvals 数组的形式接收其余参数。该宏必须是参数传递块中的最后一个。spec 参数可以是
*
(0 个或多个参数)或+
(一个或多个参数)。参数数组的地址存储在类型为zval*
的 C 变量dest
中,参数个数存储在类型为int
的变量dest_num
中。
上述大多数宏都有后缀为 _EX
和 _EX2
的扩展变量。这些宏的附加参数允许对无效性检查、去引用和分离进行控制。
旧参数解析 api
旧的参数解析 API 是以类似 C scanf()
的函数形式实现的,带有格式字符串和可变数量的参数,通过地址传递。
zend_parse_parameters(int num_args, const char *type_spec, …);
该函数检查 type_spec
字符串中的每个字母,并相应地接收参数并将其存储到下面的变量中。例如,我们可以在 test_scale()
函数中使用以下代码。
PHP_FUNCTION(test_scale)
{
double x;
if (zend_parse_parameters_throw(ZEND_NUM_ARGS(), "d", &x) == FAILURE) {
return;
}
RETURN_DOUBLE(x * TEST_G(scale));
}
type_spec
中的字母 d
假定接收双参数并将其存储在双类型的 C 变量中。让我们回顾一下大多数 type_spec
字母及其与快速参数解析 API 的关联。
-
'|' - Z_PARAM_OPTIONAL
-
'a' - Z_PARAM_ARRAY(dest)
-
'b' - Z_PARAM_BOOL(dest)
-
'd' - Z_PARAM_DOBLE(dest)
-
'h' - Z_PARAM_ARRAY_HT(dest)
-
'l' - Z_PARAM_LONG(dest)
-
'o' - Z_PARAM_OBJECT(dest)
-
'O' - Z_PARAM_OBJECT_OF_CLASS(dest, ce)
-
'r' - Z_PARAM_RESOURCE(dest)
-
's' - Z_PARAM_STRING(dest, dest_len)
-
'S' - Z_PARAM_STR(dest)
-
'z' - Z_PARAM_ZVAL(dest)
-
'*' - Z_PARAM_VARIADIC('*', dest, dest_num)
-
'+' - Z_PARAM_VARIADIC('+', dest, dest_num)
每个类型说明符后面都可以跟一个修饰符:
-
'/' - 如有必要,分隔 zval。这在函数通过引用接收值并要对其进行修改时非常有用。否则,如果数值被多处引用(引用计数器大于 1),那么所有数值都将被一次性错误修改。
-
'!' - 检查实际参数是否为空,并将相应指针设置为 NULL。对于 'b'、'l' 和 'd',必须在相应的
bool*
、zend_long*
或double*
参数之后传递一个额外的zend_bool*
类型参数。
返回值
每个 PHP 内部函数都有一个 zval*
类型的 return_value
参数。我们可以使用上述的 ZVAL_...()
宏,或者使用特殊的 RETVAL_...()
系列类似宏来写入它:
#define RETVAL_NULL() ZVAL_NULL(return_value)
#define RETVAL_BOOL(b) ZVAL_BOOL(return_value, b)
#define RETVAL_FALSE ZVAL_FALSE(return_value)
#define RETVAL_TRUE ZVAL_TRUE(return_value)
#define RETVAL_LONG(l) ZVAL_LONG(return_value, l)
#define RETVAL_DOUBLE(d) ZVAL_DOUBLE(return_value, d)
#define RETVAL_STR(s) ZVAL_STR(return_value, s)
#define RETVAL_STR_COPY(s) ZVAL_STR_COPY(return_value, s)
#define RETVAL_STRING(s) ZVAL_STRING(return_value, s)
#define RETVAL_STRINGL(s, l) ZVAL_STRINGL(return_value, s, l)
#define RETVAL_EMPTY_STRING() ZVAL_EMPTY_STRING(return_value)
也可以将值写入 return_value
,然后使用 RETURN_...()
系列类似宏执行实际返回操作:
#define RETURN_NULL() {RETVAL_NULL(); return;}
#define RETURN_BOOL(b) {RETVAL_BOOL(b) return;}
#define RETURN_FALSE {RETVAL_FALSE; return;}
#define RETURN_TRUE {RETVAL_TRUE; return;
#define RETURN_LONG(l) {RETVAL_LONG(l); return}
#define RETURN_DOUBLE(d) {RETVAL_DOUBLE(d); return;}
#define RETURN_STR(s) {RETVAL_STR(s); return;}
#define RETURN_STR_COPY(s) {RETVAL_STR_COPY(s); return;}
#define RETURN_STRING(s) {RETVAL_STRING(s); return;}
#define RETURN_STRINGL(s, l) {RETVAL_STRINGL(s, l); return;}
#define RETURN_EMPTY_STRING() {RETVAL_EMPTY_STRING(); return;}
在我们的示例扩展中使用基本的 php 内部函数
让我们扩展 test_scale()
示例,允许将比例因子作为可选的第二个参数传递,并根据第一个参数的类型使其执行不同的操作。当第一个参数是数字时,执行乘法运算,但结果类型与第一个参数的类型保持一致。如果第一个参数是字符串,则应重复几次。
PHP_FUNCTION(test_scale)
{
zval *x;
zend_long factor = TEST_G(scale); // default value
ZEND_PARSE_PARAMETERS_START(1, 2)
Z_PARAM_ZVAL(x)
Z_PARAM_OPTIONAL
Z_PARAM_LONG(factor)
ZEND_PARSE_PARAMETERS_END();
if (Z_TYPE_P(x) == IS_LONG) {
RETURN_LONG(Z_LVAL_P(x) * factor);
} else if (Z_TYPE_P(x) == IS_DOUBLE) {
RETURN_DOUBLE(Z_DVAL_P(x) * factor);
} else if (Z_TYPE_P(x) == IS_STRING) {
zend_string *ret = zend_string_safe_alloc(Z_STRLEN_P(x), factor, 0, 0);
char *p = ZSTR_VAL(ret);
while (factor-- > 0) {
memcpy(p, Z_STRVAL_P(x), Z_STRLEN_P(x));
p += Z_STRLEN_P(x);
}
*p = '\000';
RETURN_STR(ret);
} else {
php_error_docref(NULL, E_WARNING, "unexpected argument type");
return;
}
}
ZEND_BEGIN_ARG_INFO(arginfo_test_scale, 0)
ZEND_ARG_INFO(0, x)
ZEND_ARG_INFO(0, factor)
ZEND_END_ARG_INFO()
至此,你应该了解了 PHP 的所有内部知识,并理解了函数实现的细节。
现在是测试我们的新实现的时候了。
$ php -r 'var_dump(test_scale(2));'
int(2)
$ php -r 'var_dump(test_scale(2,3));'
int(6)
$ php -r 'var_dump(test_scale(2.0, 3));'
float(6)
$ php -r 'var_dump(test_scale("2", 3));'
string(3) "222"