Базовая структура¶
zval (сокращение от “Zend value”) представляет произвольное значение PHP. Как таковой, это, вероятно, самая важная структура во всём PHP, и вам предстоит много с ней работать. В этом разделе описаны базовые концепции zval и их использование.
Типы и значения¶
Помимо прочего, каждый zval хранит некое значение и тип этого значения. Это необходимо, потому что PHP — динамически типизированный язык, и как следствие типы переменных известны только во время выполнения, а не во время компиляции. Более того, тип может меняться в течение жизни zval, так что если zval ранее хранил целое число, позже он может содержать строку.
Тип хранится как целочисленный тег, который может принимать одно из нескольких значений. Некоторые значения
соответствуют восьми типам, доступным в PHP, другие используются только для внутренних целей движка. На эти
значения ссылаются с помощью констант вида IS_TYPE. Например, IS_NULL соответствует типу null, а
IS_STRING соответствует строковому типу.
Само значение хранится в объединении (union), которое определено так:
typedef union _zend_value {
zend_long lval; // For IS_LONG
double dval; // For IS_DOUBLE
zend_refcounted *counted;
zend_string *str; // For IS_STRING
zend_array *arr; // For IS_ARRAY
zend_object *obj; // For IS_OBJECT
zend_resource *res; // For IS_RESOURCE
zend_reference *ref; // For IS_REFERENCE
zend_ast_ref *ast; // For IS_CONSTANT_AST (special)
zval *zv; // For IS_INDIRECT (special)
void *ptr;
zend_class_entry *ce;
zend_function *func;
struct {
uint32_t w1;
uint32_t w2;
} ww;
} zend_value;
Для тех, кто не знаком с концепцией union: union определяет несколько членов разных типов, но в любой момент может
использоваться только один из них. Например, если был установлен член value.lval, то и искать значение нужно
через value.lval, а не через какой-либо другой член (иначе это нарушило бы гарантии “строгого алиасинга” и
привело бы к неопределённому поведению). Причина в том, что union хранит все свои члены в одном и том же месте
памяти и просто по-разному интерпретирует расположенное там значение в зависимости от того, к какому члену вы
обращаетесь. Размер union равен размеру его самого крупного члена.
При работе с zval тег типа используется, чтобы определить, какой из членов union используется в данный момент. Прежде чем взглянуть на API, используемые для этого, давайте пройдёмся по различным типам, которые поддерживает PHP, и по тому, как они хранятся:
Самый простой тип — IS_NULL: ему не нужно хранить какое-либо значение, потому что существует только одно
значение null.
Булевы значения используют тип либо IS_TRUE, либо IS_FALSE, и им также не нужно хранить значение. Внутри PHP
представляет true и false как отдельные типы из соображений эффективности, даже если с точки зрения пользователя они
считаются значениями. Также существует тип _IS_BOOL, однако он никогда не используется как тип zval. Он
используется внутри для обозначения приведений к булевому типу и подобных целей.
Для хранения чисел PHP предоставляет типы IS_LONG и IS_DOUBLE, которые используют соответственно члены
zend_long lval и double dval. Первый используется для хранения целых чисел, тогда как второй хранит числа с
плавающей точкой.
Есть несколько моментов, о которых следует знать относительно типа zend_long: во-первых, это знаковый целочисленный
тип, то есть он может хранить как положительные, так и отрицательные целые числа, но обычно не очень хорошо подходит
для побитовых операций. Во-вторых, zend_long не то же самое, что long, поскольку он абстрагируется от
различий между платформами. zend_long всегда занимает 4 байта на 32-битных платформах и 8 байт на 64-битных
платформах, даже если тип long может иметь другой размер.
По этой причине важно использовать макросы, написанные специально для работы с zend_long, такие как
SIZEOF_ZEND_LONG или ZEND_LONG_MAX. Больше подобных макросов можно найти в
Zend/zend_long.h.
Тип double, используемый для хранения чисел с плавающей точкой, — это 8-байтное значение, соответствующее
спецификации IEEE-754. Детали этого формата здесь не рассматриваются, но вам стоит хотя бы знать о том, что у этого
типа ограниченная точность, и он часто не хранит именно то значение, которое вам нужно.
Оставшиеся четыре типа будут здесь лишь упомянуты, а подробнее рассмотрены в собственных главах:
Строки (IS_STRING) хранятся в структуре zend_string, которая объединяет длину строки и её содержимое в
одном выделении памяти. Больше информации о структуре zend_string и её собственном API вы найдёте в главе про
строки.
Массивы используют тег типа IS_ARRAY и хранятся в члене zend_array *arr. О том, как работает структура
HashTable, будет рассказано в главе Хештаблицы.
Объекты (IS_OBJECT) используют член zend_object *obj. Система классов и объектов PHP будет описана в главе
объекты.
Ресурсы (IS_RESOURCE) используют член zend_resource *res. Ресурсы рассматриваются в главе
Ресурсы.
Подытоживая, вот таблица со всеми доступными “обычными” тегами типов и соответствующим местом хранения их значений:
Тег типа |
Место хранения |
|---|---|
|
нет |
|
нет |
|
|
|
|
|
|
|
|
|
|
|
|
Специальные типы¶
Существует ряд дополнительных типов, у которых нет прямого соответствия в пользовательском пространстве, и которые
используются только внутри движка. Из них IS_UNDEF и IS_REFERENCE — единственные типы, с которыми вы будете
регулярно сталкиваться.
Тип IS_UNDEF используется для обозначения неинициализированного zval. Этот тег типа имеет значение нуль, так что
обнуление zval с помощью memset приведёт к zval типа UNDEF. Точный смысл IS_UNDEF зависит от контекста,
например, он может означать неинициализированное/неустановленное свойство объекта или неиспользуемую корзину
хештаблицы.
Тип IS_REFERENCE в сочетании с членом zend_reference *ref используется для представления ссылки PHP. Хотя с
точки зрения пользовательского пространства ссылки не являются отдельным типом, внутри они представлены как обёртка
вокруг другого zval, которая может быть разделена между несколькими местами.
Член zend_refcounted *counted обращается к общему заголовку для всех типов с подсчётом ссылок, включая строки,
массивы, объекты, ресурсы и ссылки. Как это работает, рассматривается в главе управление памятью.
Тип IS_CONSTANT_AST и член zend_ast_ref *ast используются для хранения невычисленных абстрактных
синтаксических деревьев (AST) константных выражений. Это может встречаться только в определённых местах, например, в
значениях свойств по умолчанию. AST будут рассмотрены в главе компилятор.
Тип IS_INDIRECT и член zval *zv используются для хранения прямого указателя на другой zval. Это используется
в основном для таблиц символов и таблиц динамических свойств, чтобы указывать на реальное значение, хранящееся
где-то ещё.
Тип IS_PTR вместе с полем void *ptr используются для хранения произвольного указателя. В C любой указатель
можно преобразовать в void * и обратно. Это используется для хранения указателей в местах, которые обычно
принимают только zval, например, в значениях хештаблицы.
Члены zend_class_entry *ce и zend_function *func просто задают более точный тип, но в остальном служат той же
цели, что и ptr.
Структура zval¶
Теперь давайте посмотрим, как на самом деле выглядит структура zval:
struct _zval_struct {
zend_value value;
union {
uint32_t type_info;
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type,
zend_uchar type_flags,
union {
uint16_t extra;
} u)
} v;
} u1;
union {
uint32_t next; /* hash collision chain */
uint32_t cache_slot; /* cache slot (for RECV_INIT) */
uint32_t opline_num; /* opline number (for FAST_CALL) */
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 */
uint32_t constant_flags; /* constant flags */
uint32_t extra; /* not further specified */
} u2;
};
Эта структура выглядит немного сложнее, чем она есть на самом деле. По сути, она хранит 8-байтовое value и
однобайтовый тег type, о которых мы уже говорили выше.
Теоретически это оставляло бы нам размер zval в 9 байт. Однако для обеспечения эффективного доступа необходимо выровнять размер структуры по границе 8 байт, так что итоговый размер становится 16 байт. Поскольку дополнительное место будет использовано в любом случае, PHP находит применение этому “потерянному” пространству:
Тег type является частью более крупной структуры type_info, которая дополнительно хранит type_flags. На
момент PHP 7.4 существует только два флага типа: IS_TYPE_REFCOUNTED указывает, что значение подсчитывает ссылки,
а IS_TYPE_COLLECTABLE указывает, что оно участвует в циклической сборке мусора. Мы обсудим оба этих флага в
будущем.
Член u2 — это 32-битное пространство для хранения произвольных данных, которое используется для разных целей в
зависимости от контекста. Хештаблицы используют его для хранения цепочки разрешения коллизий, но, как указывают
приведённые выше комментарии, есть и много других применений. Стоит отметить, что стандартные макросы zval никогда
не изменяют и не копируют поле u2.
Поле u1.v.u.extra, являющееся частью типа, очень редко используется для хранения дополнительной информации.
Однако использование этого поля возможно лишь в очень специфичных обстоятельствах, поскольку PHP обычно
предполагает, что оно равно нулю.
Макросы доступа¶
Зная структуру zval, вы теперь можете писать код, использующий её:
zval *zv_ptr = /* ... get zval from somewhere */;
if (zv_ptr->u1.v.type == IS_LONG) {
php_printf("Zval is a long with value " ZEND_LONG_FMT "\n", zv_ptr->value.lval);
} else /* ... handle other types */
Хотя приведённый выше код работает, это не идиоматичный способ его написания. Он напрямую обращается к членам zval, вместо того чтобы использовать для этого специальный набор макросов доступа:
zval *zv_ptr = /* ... */;
if (Z_TYPE_P(zv_ptr) == IS_LONG) {
php_printf("Zval is a long with value " ZEND_LONG_FMT "\n", Z_LVAL_P(zv_ptr));
} else /* ... */
Приведённый выше код использует макрос Z_TYPE_P() для получения тега типа и Z_LVAL_P() для получения
значения long (целого числа). Все макросы доступа имеют варианты с суффиксом _P (от “pointer”, указатель) или
вовсе без суффикса. Какой из них использовать, зависит от того, работаете вы с zval или с zval*:
zval zv;
zval *zv_ptr;
Z_TYPE(zv); // Same as Z_TYPE_P(&zv).
Z_TYPE_P(zv_ptr); // Same as Z_TYPE(*zv_ptr).
Аналогично Z_LVAL существуют также макросы для получения значений всех остальных типов. Чтобы продемонстрировать
их использование, мы создадим простую функцию для вывода содержимого zval:
PHP_FUNCTION(dump)
{
zval *zv_ptr;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "z", &zv_ptr) == FAILURE) {
return;
}
try_again:
switch (Z_TYPE_P(zv_ptr)) {
case IS_NULL:
php_printf("NULL: null\n");
break;
case IS_TRUE:
php_printf("BOOL: true\n");
break;
case IS_FALSE:
php_printf("BOOL: false\n");
break;
case IS_LONG:
php_printf("LONG: %ld\n", Z_LVAL_P(zv_ptr));
break;
case IS_DOUBLE:
php_printf("DOUBLE: %g\n", Z_DVAL_P(zv_ptr));
break;
case IS_STRING:
php_printf("STRING: value=\"");
PHPWRITE(Z_STRVAL_P(zv_ptr), Z_STRLEN_P(zv_ptr));
php_printf("\", length=%zd\n", Z_STRLEN_P(zv_ptr));
break;
case IS_RESOURCE:
php_printf("RESOURCE: id=%d\n", Z_RES_HANDLE_P(zv_ptr));
break;
case IS_ARRAY:
php_printf("ARRAY: hashtable=%p\n", Z_ARRVAL_P(zv_ptr));
break;
case IS_OBJECT:
php_printf("OBJECT: object=%p\n", Z_OBJ_P(zv_ptr));
break;
case IS_REFERENCE:
// For references, remove the reference wrapper and try again.
// Yes, you are allowed to use goto for this purpose!
php_printf("REFERENCE: ");
zv_ptr = Z_REFVAL_P(zv_ptr);
goto try_again;
EMPTY_SWITCH_DEFAULT_CASE() // Assert that all types are handled.
}
}
Попробуем это в деле:
dump(null); // NULL: null
dump(true); // BOOL: true
dump(false); // BOOL: false
dump(42); // LONG: 42
dump(4.2); // DOUBLE: 4.2
dump("foo"); // STRING: value="foo", length=3
dump(fopen(__FILE__, "r")); // RESOURCE: id=???
dump(array(1, 2, 3)); // ARRAY: hashtable=0x???
dump(new stdClass); // OBJECT: object=0x???
В следующей таблице перечислены наиболее часто используемые макросы доступа, хотя их существует заметно больше.
Макрос |
Возвращаемый тип |
Требуемый тип zval |
Описание |
|---|---|---|---|
|
|
Тип zval. Одна из констант |
|
|
|
|
Целочисленное значение. |
|
|
|
Значение с плавающей точкой. |
|
|
|
Указатель на полную структуру |
|
|
|
Содержимое строки структуры |
|
|
|
Длина строки структуры |
|
|
|
Указатель на структуру |
|
|
|
Синоним |
|
|
|
Указатель на структуру |
|
|
|
Class entry объекта. |
|
|
|
Указатель на структуру |
|
|
|
Указатель на структуру |
|
|
|
Указатель на zval, который оборачивает ссылка. |
Когда вы хотите получить доступ к содержимому zval, вам следует всегда использовать эти макросы, а не обращаться к его членам напрямую. Это сохраняет уровень абстракции и в некоторой степени защищает вас от изменений в реализации.