PHP7 扩展开发(四) 变量类型、转换、写时复制

编辑于 2016-10-01

* 移动设备下, 可左滑手指以查看较宽代码

介绍 PHP 的变量在内核中的实现和要注意的点. 其实不论是否接触扩展开发, 都有必要了解这节内容.

变量的底层实现

PHP7 在内核中是通过 zval 这个结构体来存储变量的,它比 PHP5 来得复杂,但节省了内存, 它的定义在 Zend/zend.h 文件里:

//zval 结构
struct _zval_struct {
    zend_value        value;            /* value */
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    type,         /* active type */
                zend_uchar    type_flags,
                zend_uchar    const_flags,
                zend_uchar    reserved)     /* call info for EX(This) */
        } v;
        uint32_t type_info;
    } u1;
    union {
        uint32_t     next;                 /* hash collision chain */
        uint32_t     cache_slot;           /* literal cache slot */
        uint32_t     lineno;               /* line number (for ast nodes) */
        uint32_t     num_args;             /* arguments number for EX(This) */
        uint32_t     fe_pos;               /* foreach position */
        uint32_t     fe_iter_idx;          /* foreach iterator index */
        uint32_t     access_flags;         /* class constant access flags */
        uint32_t     property_guard;       /* single property guard */
    } u2;
};

//在 Zend/zend_types.h 里定义的:
typedef unsigned char zend_uchar;

保存变量值的 value 则是 _zend_value 类型, 它是一个 union , 同样定义在了 Zend/zend.h 文件里:

typedef union _zend_value {
    zend_long         lval;             /* long value */
    double            dval;             /* double value */
    zend_refcounted  *counted;
    zend_string      *str;
    zend_array       *arr;
    zend_object      *obj;
    zend_resource    *res;
    zend_reference   *ref;
    zend_ast_ref     *ast;
    zval             *zv;
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww;
} zend_value;

counted与引用计数 (refcount) 有关,通过一个例子看看它的用途:

$a = 1;
$b = $a;
$b += 5;

第二行 $b 和 $a 同时指向一个数"1", 内核把他们指向同一个 zval 结构来节省内存. 因为 $a 和 $b 同时指向了同一个 zval, 第三句改变了 $b 的值, 这时 $a 也会被修改, 如何避免呢?

很简单, 在 $a 和 $b 对应的 zval 里有 refcount, 每次被引用后+1 (第一行 $a 对应的 zval 的 refcount 为1, 第二行被 $b 引用, 为 2). 第三行, 内核首先查看 refcount 属性, 如果它大于1则为把它从原 zval 结构中复制出一份新的专属与 $b 的 zval 来,并改变其值, 从而不影响 $a. 这就是所谓的 写时复制(Copy-On-Write).

图中 Zval 的由2个 64bits 组成,如果变量类型是 long、bealoon 这些长度不超过64bit 的, 则直接存储到 value 中, 就没有引用了. 当变量类型是array、objec、string等超过64bit的, value 存储的就是一个指针, 指向真实的存储结构地址. 对于简单的变量类型来说, Zval 的存储变得非常简单和高效.

在以上实现的基础上, PHP 语言得以实现了8种数据类型:

常量名称 说明
IS_NULL 第一次使用的变量如果没有初始化过, 则会自动的被赋予这个常量, 当然我们也可以在 PHP 语言中通过 null 这个常量来给予变量 null 类型的值. 这个类型的值只有一个, 就是 NULL,它与 0 与 false 是不同的.
IS_FALSE/ IS_TRUE 布尔类型的变量有两个值, true 或者 false. 在 PHP 语言中, while/if 等语句会自动的把表达式的值转成这个类型的.
IS_LONG PHP 语言中的整型, 在内核中是通过所在操作系统的 signed long 数据类型来表示的. 具有 平台相关性. 在最常见的32位操作系统中, 它可以存储从-2147483648 到 +2147483647 范围内的任一整数. 有一点需要注意的是, 如果 PHP 语言中的整型变量超出最大值或者最小值, 它并不会直接溢出, 而是会被内核转换成 IS_DOUBLE 类型的值然后再参与计算, 这可能造成精度损失甚至错误计算. 再者, 因为使用了 signed long 来作为载体, 所以这也就解释了为什么 PHP 语言中的整型数据都是带符号的了.
IS_DOUBLE PHP 中的浮点数据是通过C语言中的 signed double 型变量来存储的, 这最终取决与所在操作系统的浮点型实现. 计算机是无法精准的表示浮点数的, 而是采用了科学计数法来保存某个精度的浮点数. 因此此类型的计算和比较都不是绝对准确的.
IS_STRING 字符串, 与 C 不同的是, PHP 内核还同时在 zval 结构里保存着这个字符串的实际长度, 这个设计使 PHP 可以在字符串中嵌入‘\0’字符, 而不会在取出时被'\0'截断. 因此可以安全的存储任何数据(此特征被称作 二进制安全). 内核会为字符串申请它长度+1的内存, 最后一个字节存储的是‘\0’字符, 所以在不需要二进制安全操作的时候, 我们可以像通常C语言的方式那样来使用它(不推荐).
IS_ARRAY 数组是一个非常特殊的数据类型, 它唯一的功能就是聚集别的变量. 在C语言中, 一个数组只能承载一种类型的数据, 而 PHP 语言中的数组则灵活的多, 它可以承载任意类型的数据, 这一切都是 哈希表 (HashTable)的功劳, 每个 HashTable 中的元素都有两部分组成: 索引与值, 每个元素的值都是一个独立的 zval (确切的说应该是指向某个 zval 的指针).
IS_OBJECT 和数组一样, 对象也是用来存储复合数据的. 但是与数组不同的是, 对象还需要保存以下信息: 方法/访问权限/类常量以及其它的处理逻辑.
IS_RESOURCE 有一些数据的内容可能无法直接呈现给 PHP 用户的, 比如与某台 MySQL 服务器的链接, 或者直接呈现出来也没有什么意义. 但用户还需要这类数据, 因此 PHP 中提供了一种名为 Resource (资源) 的数据类型. 本质是一个整数, 用于存储资源句柄等.

zval 结构体里的type成员的值便是以上某个 IS_* 常量之一. 内核通过检测变量的这个成员值来知道他是什么类型的数据并做相应的后续处理.

变量类型判断

PHP 内核以后可能会修改变量的实现方式, 所以检测 type 的方法可能在以后就不能用了. 为了解决这个兼容问题, zend 头文件中定义了大量的宏, 供我们检测/操作变量使用. 这些宏不但让我们的程序更易读, 还具有更好的兼容性. 这里我们用Z_TYPE_P()宏:

void describe_zval(zval *foo)
{
    if ( Z_TYPE_P(foo) == IS_NULL ) {
        php_printf("这个变量的数据类型是: NULL");
    } else {
        php_printf("这个变量的数据类型不是 NULL, 这种数据类型对应的数字是: %d", foo->type);
    }
}

其中 php_printf() 是内核对 printf() 的一层封装, 我们可以像使用 printf() 那样使用它.

以 _P 一个 P 结尾的宏的参数大多是 *zval 型变量. 此外获取变量类型的宏还有两个, 分别是 Z_TYPE 和 Z_TYPE_PP, 前者的参数是 zval 型, 而后者的参数则是 **zval. 这三个宏的定义在 Zend/zend_operators.h 里:

#define Z_TYPE(zval)        (zval).type
#define Z_TYPE_P(zval_p)    Z_TYPE(*zval_p)
#define Z_TYPE_PP(zval_pp)  Z_TYPE(**zval_pp)

变量类型的转换

内核中提供了好多函数专门来帮我们实现类型转换的功能, 你需要的只是调用一个函数.

//将任意类型的zval转换成字符串
void change_zval_to_string(zval *value) {
    //不同于下面, 这是个宏
    convert_to_string(value);
}
 
//其它基本的类型转换函数
ZEND_API void convert_to_long(zval *op);
ZEND_API void convert_to_double(zval *op);
ZEND_API void convert_to_null(zval *op);
ZEND_API void convert_to_boolean(zval *op);
ZEND_API void convert_to_array(zval *op);
ZEND_API void convert_to_object(zval *op);

没有 convert_to_resource, 因为资源在用户层面上没有意义.

创建 Zval

参考以下代码:

zval temp;
ZVAL_LONG(&temp, 1234);

字符串

PHP7 新增的字符串 zend_string,增加了性能和便捷性

zend_string *in;
// 注意这里「S」为大写
if (zend_parse_parameters(ZEND_NUM_ARGS(), "S", &in) == FAILURE) {
    RETURN_FALSE;
}
// 获取指针
ZSTR_VAL(in);
// 获取长度
ZSTR_LEN(in);
// 创建 zend_string
zend_string_init(str, len, 0);