转换和操作

转换

在许多情况下,您期望收到特定类型的 zval。在这种情况下,您可以严格检查所需的类型:

if (Z_TYPE_P(val) != IS_STRING) {
    zend_type_error("Expected string");
    return;
}

或者,您可以执行转换为所需的类型。有两种方法可以执行转换:第一种是使用 convert_to_* 函数之一实际更改 zval 的类型:

convert_to_string(val);
// Z_TYPE_P(val) == IS_STRING is guaranteed here.

对于具有有意义的类型转换的所有其他类型,都存在类似的函数:

void convert_to_null(zval *op);
void convert_to_boolean(zval *op);
void convert_to_long(zval *op);
void convert_to_double(zval *op);
void convert_to_string(zval *op);
void convert_to_array(zval *op);
void convert_to_object(zval *op);

此外, convert_scalar_to_number() 函数可用于将 zval 转换为整数或浮点数,但需要注意的是数组仍保留为数组:

convert_scalar_to_number(val);
switch (Z_TYPE_P(val)) {
    case IS_LONG:
        php_printf("Long: " ZEND_LONG_FMT "\n", Z_LVAL_P(val));
        break;
    case IS_DOUBLE:
        php_printf("Long: %H\n", Z_DVAL_P(val));
        break;
    case IS_ARRAY:
        php_printf("Array\n");
        break;
    ZEND_EMPTY_SWITCH_DEFAULT_CASE()
}

由于 convert_to_* 就地修改 zval,因此需要注意维护写时复制语义。一个常见的错误是编写如下代码:

zval *val;
ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(array), val) {
    convert_to_string(val);
    // Use val as string.
}

在这里,我们想要迭代一个数组并将所有元素视为字符串。然而,由于 convert_to_string() 就地操作,这意味着数组实际上被修改了。因此,只有当您唯一拥有该数组时,此代码才合法。否则,需要先进行分离:

zval *val;
SEPARATE_ARRAY(array);
ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(array), val) {
    convert_to_string(val);
    // Use val as string.
}

第二组强制转换 API 通过返回转换后的值而不是更改 zval 本身的类型来避免此问题。在可以使用它的情况下,这通常更方便、更高效、更安全(相对于写时复制)。当转换为布尔值、整数和浮点数时,我们只需接收 bool、zend_long 或 double 结果即可:

bool b = zend_is_true(val);
zend_long l = zval_get_long(val);
double d = zval_get_double(val);

对于字符串,我们收到 zend_string * 结果,之后必须将其释放。如果该值已经是一个字符串,这将简单地增加引用计数。如果它不是一个字符串,它将返回一个现有的内部字符串,或者分配一个新的字符串:

zend_string *str = zval_get_string(val);
// Do something with str.
zend_string_release(str);

对于这种临时使用,我们不保留对 str 的长期引用,存在一个额外的优化 API:

zend_string *tmp_str;
zend_string *str = zval_get_tmp_string(val, &tmp_str);
// Do something with str.
zend_tmp_string_release(tmp_str);

此 API 的工作方式与 zval_get_string() 相同,但避免了值已经是字符串的常见情况的引用计数递增和递减。

特别是当涉及到字符串的转换时,还有一个额外的问题需要考虑: __toString() 方法可能会抛出异常(实际上,转换为 int 和 float 也可能会抛出异常,但这个问题通常被忽略)。这可以通过在字符串转换后检查 EG(exception) 来处理:

zend_string *str = zval_get_string(val);
if (EG(exception)) {
    // zend_string_release(str) is safe, but not necessary here.
    return;
}
zend_string_release(str);

然而,处理这种情况的更惯用和有效的方法是使用这些函数的 try 变体,这将指示它们的返回值中是否引发了异常:

if (!try_convert_to_string(val)) {
    // Exception thrown.
    return;
}

zend_string *str = zval_try_get_string(val);
if (!str) {
    // Exception thrown.
    return;
}
zend_string_release(str);

zend_string *tmp_str;
zend_string *str = zend_try_get_tmp_string(val, &tmp_str);
if (!str) {
    // Exception thrown.
    return;
}
zend_tmp_string_release(tmp_str);

操作

像 $op1 + $op2 这样的用户态操作是通过内部相应的函数(例如 add_function() )实现的,这些函数接受结果输出参数,后跟输入操作数:

zval *op1 = /* ... */, *op2 = /* ... */;
zval result;
if (add_function(&result, op1, op2) == FAILURE) {
    // Exception thrown.
    return;
}
// Do something with result.
zval_ptr_dtor(&result);

应该注意的是,这些函数在实践中很少使用,因为大多数代码都使用特定类型的 zval,而不是对完全任意的值进行操作。完整的函数集是:

zend_result add_function(zval *result, zval *op1, zval *op2);                 /* $result = $op1 + $op2 */
zend_result sub_function(zval *result, zval *op1, zval *op2);                 /* $result = $op1 - $op2 */
zend_result mul_function(zval *result, zval *op1, zval *op2);                 /* $result = $op1 * $op2 */
zend_result pow_function(zval *result, zval *op1, zval *op2);                 /* $result = $op1 ** $op2 */
zend_result div_function(zval *result, zval *op1, zval *op2);                 /* $result = $op1 / $op2 */
zend_result mod_function(zval *result, zval *op1, zval *op2);                 /* $result = $op1 % $op2 */
zend_result bitwise_or_function(zval *result, zval *op1, zval *op2);          /* $result = $op1 | $op2 */
zend_result bitwise_and_function(zval *result, zval *op1, zval *op2);         /* $result = $op1 & $op2 */
zend_result bitwise_xor_function(zval *result, zval *op1, zval *op2);         /* $result = $op1 ^ $op2 */
zend_result boolean_xor_function(zval *result, zval *op1, zval *op2);         /* $result = $op1 xor $op2 */
zend_result shift_left_function(zval *result, zval *op1, zval *op2);          /* $result = $op1 << $op2 */
zend_result shift_right_function(zval *result, zval *op1, zval *op2);         /* $result = $op1 >> $op2 */
zend_result concat_function(zval *result, zval *op1, zval *op2);              /* $result = $op1 . $op2 */

zend_result bitwise_not_function(zval *result, zval *op1);                    /* $result = ~$op1 */
zend_result boolean_not_function(zval *result, zval *op1);                    /* $result = !$op1 */

zend_result increment_function(zval *op);                                     /* ++$op */
zend_result decrement_function(zval *op);                                     /* --$op */

zend_result compare_function(zval *result, zval *op1, zval *op2);             /* $result = $op1 <=> $op2 */
zend_result is_equal_function(zval *result, zval *op1, zval *op2);            /* $result = $op1 == $op2 */
zend_result is_not_equal_function(zval *result, zval *op1, zval *op2);        /* $result = $op1 != $op2 */
zend_result is_identical_function(zval *result, zval *op1, zval *op2);        /* $result = $op1 === $op2 */
zend_result is_not_identical_function(zval *result, zval *op1, zval *op2);    /* $result = $op1 !== $op2 */
zend_result is_smaller_function(zval *result, zval *op1, zval *op2);          /* $result = $op1 < $op2 */
zend_result is_smaller_or_equal_function(zval *result, zval *op1, zval *op2); /* $result = $op1 <= $op2 */
/* $op1 > $op2 is same as $op2 < $op1 */
/* $op1 >= $op2 is same as $op2 <= $op1 */

为了进行比较,还有两个变体返回比较结果,而不是将其放在 zval 中:

bool zend_is_identical(zval *op1, zval *op2);
int zend_compare(zval *op1, zval *op2);

zend_compare() 返回一个三向比较结果,类似于 PHP 中的 <=> 运算符,根据 op1 是否小于、等于或大于零小于、等于或大于 op2 。

最后,还有许多带有 fast_ 前缀的变体。这些是优化的实现,将参数限制为某些类型,或实现的内联部分和/或使用内联汇编来实现它:

/* op1 must have type IS_LONG, implementation uses inline assembly. */
static zend_always_inline void fast_long_increment_function(zval *op1);
static zend_always_inline void fast_long_decrement_function(zval *op1);
/* op1 and op2 must have type IS_LONG, implementation uses inline assembly. */
static zend_always_inline void fast_long_add_function(zval *result, zval *op1, zval *op2);
static zend_always_inline void fast_long_sub_function(zval *result, zval *op1, zval *op2);
/* op1, op2 may have any type, but IS_LONG and IS_DOUBLE addition is inlined. */
static zend_always_inline zend_result fast_add_function(zval *result, zval *op1, zval *op2);
/* op1, op2 may have any type, but IS_LONG, IS_DOUBLE and IS_STRING equality is inlined. */
static zend_always_inline bool fast_equal_check_function(zval *op1, zval *op2);
/* op1 must have type IS_LONG, op2 can have any type. */
static zend_always_inline bool fast_equal_check_long(zval *op1, zval *op2);
/* op1 must have type IS_DOUBLE, op2 can have any type. */
static zend_always_inline bool fast_equal_check_string(zval *op1, zval *op2);
/* op1, op2 may have any type, but part of the implementation is inlined. */
static zend_always_inline bool fast_is_identical_function(zval *op1, zval *op2);
static zend_always_inline bool fast_is_not_identical_function(zval *op1, zval *op2);