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 还保留了 typetype_flags 和两个保留字段。无论如何,这些空间都将用于正确的结构对齐,但通过保留这些空间,我们可以存储一些从属数据。

在内存中,zval 表示为两个 64 位字。第一个字保存值,第二个字保存 typetype_flagsextra 和保留字段。

image 2023 11 14 10 57 00 512

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 位字的格式。它包含引用计数器、类型、标志和垃圾回收器使用的信息。

具体类型的特定结构建立在这个结构之上,并在第一个字之后添加一些额外的数据。

image 2023 11 14 11 15 55 026

以下引用计数类型是可能的:

  • 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 值。

image 2023 11 14 11 35 42 943

字符串在没有设置 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_STARTZEND_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():该宏用于终止参数获取代码块。

STARTEND 宏之间的块内有一些 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 变量 destsize_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"