PHP Internals Book [русский перевод]

Базовая структура

Структура данных zval (сокращение от "Zend value") используется для представления любых значений PHP. Это одна из самых важных структур во всем PHP и вы будете много работать с ней. Этот раздел описаывет базовые концепции лежащие в основе zval-ов и способы их использования.

Типы и значения

Кроме всего прочего, каждый zval хранит некоторое значение и тип этого значения. Это необходимо потому что PHP — это язык с динамической типизацией и поэтому тип переменных известен только во время выполнения программы (run-time), а не во время компиляции (compile-time). Кроме того, тип переменной может быть изменен в течение жизни zval, то есть zval ранее хранимый как целое число (integer) позднее может содержать строку (string).

Тип переменной хранится как целочисленная метка (type tag, unsigned char). Метка может принимать одно из 8 значений, которое соответствует 8 типам данных доступных в PHP. Эти значения должны присваиваться с использованием констант вида IS_TYPE. Например, IS_NULL соответствует типу данных null, а IS_STRING — строке.

Фактическое значение переменной хранится в типе данных union ("объединение", в дальнейшем я буду использовать термины union или юнион — примечание переводчика), который определен следующим образом:

typedef union _zvalue_value {
    long lval;
    double dval;
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;
    zend_object_value obj;
} zvalue_value;

Небольшое пояснение для тех кто не знаком с концепцией union-ов. Union определяет несколько членов-данных различных типов, но в каждый момент времени может использоваться только одно значение из определенных в юнионе. Например, если члену данных value.lval было присвоено значение, то для доступа к данным вы можете использовать только value.lval, доступ к другим членам данных недопустим и может приводить к непредсказуемому поведению программы. Причина этого в том, что юнионы хранят данные всех своих членов в одной области памяти и интерпретируют значение по разному исходя из имени, к которому вы обращаетесь. Размер памяти, выделяемой для юниона, соответствует размеру самого большого его члена-данных.

При работе с zval-ами используется специальная метка (type tag), которая позволяет определить какой тип данных хранится в юнионе в данный момент. Прежде чем обратиться к API давайте посмотрим какие типы данных поддерживаются в PHP и как они хранятся.

Простейший тип данных — IS_NULL: он не должен хранить какое-либо значение, так как это просто null.

Для хранения чисел PHP представляет 2 типа: IS_LONG и IS_DOUBLE, которые используют члены long lval и double dval соответственно. Первый используется для хранения целых чисел, второй — для чисел с плавающей точкой.

Есть несеколько вещей, которые вам следует знать о типе данных long. Во-первых, это signed integer, то есть он может содержать положительные и отрицательные значения, но этот тип данных не подходит для побитовых операций. Во-вторых, long имеет разные размеры на разных платформах: на 32-битных системах он имеет размер 32 бита или 4 байта, но на 64-битных системах он может иметь размер как 4, так и 8 байт. В Unix-системах он обычно имеет размер в 8 байт, в то время как в 64-битных версиях Windows использует только 4 байта.

По этой причине вы не должны полагаться на конкретное значение типа long. Минимальное и максимальное значения, которые могут быть сохранены в типе данных long доступны в константах LONG_MIN и LONG_MAX и размер этого типа может быть определен с использованием макро SIZEOF_LONG (в отличии от sizeof(long) этот макро может быть использован и в #if директивах).

Тип данных double предназначен для хранения чисел с плавающей точкой и, обычно, следуя спецификации IEEE-754, он имеет размер в 8 байт. Детали этого формата не будут обсуждаться здесь, но вам как минимум следует знать, что этот тип имеет ограниченную точность и часто хранит не точно то значение, на которое вы рассчитываете.

Булевы переменные используют флаг IS_BOOL и хранятся в поле long val как значения 0 (false) и 1 (true). Так как этот тип использут только 2 значения, то, теоретически, достаточно было использовать тип меньшего размера (например zend_bool), но так как zvalue_value — это юнион и под него и так выделен объем памяти соответствующий самому большому члену данных, то применение более компактной переменной для булевых значений не приведет к экономии памяти. Поэтому lval повторно использован в этом случае.

Строки (IS_STRING) хранятся в структуре struct {char *val; int len; } str;, то есть строка хранится как указатель на строку char * и целочисленная длина строки int. Строки в PHP должны явно хранить свою длину для того чтобы иметь возможность содержать NUL байты (\0) и быть бинарно безопасными (binary safe). Но несмотря на это, строки используемые в PHP все равно заканчиваются нулевым байтом (NUL-terminated), чтобы обеспечить совместимость с библиотечными функциями, которые не принимают аргумент с длиной строки, а ожидают найти нулевой байт в конце строки. Конечно, в таких случаях строки больше не могут быть бинарно безопасными и будут обрезаны до первого вхождения нулевого байта. Например, много функций связанных с файловой системой и большинство строковых функций из libc ведут себя подобным образом.

Длина строки измеряется в байтах (не числом Unicode-символов) и не должно включать нулевой байт, то есть длина строки foo равна 3, несмотря на то, что для её хранения используется 4 байта. Если вы оперделяете длину строки с использованием sizeof вам нужно вычитать единицу: strlen("foo") == sizeof("foo") - 1.

Очень важно понимать: длина строки хранится в типе int, а не в long или каком-то другом похожем типе. Это исторический артефакт, который ограничивает длину строки 2147483647 байтами (2 гигабайта). Строки большего размера будут причиной переполнения (что сделает их длину отрицательной).

Оставшиеся три типа будут упомянуты лишь поверхностно и рассмотрены более детально в слудующих главах.

Массивы используют метку IS_ARRAY и хранятся в члене-данных HashTable *ht. Как работает структура данных HashTable рассмотрено в соответствующей главе.

Объекты (IS_OBJECT) исползуют член-данных zend_object_value obj, который состоит из "object handle" (целочисленный ID, используемый для поиска реальных данных) и набора "object handlers", которые определяют поведение объекта. Система классов и объектов в PHP будет описана в главе "Классы и объекты".

Ресурсы (IS_RESOURCE) похожи на объекты, так как они также хранят уникальный ID, используемый для поиска значения. Этот ID хранится в члене long lval. Ресурсы будут описаны в соответствующей главе, которая пока не написана.

Подведем промежуточный итог, ниже представлена таблица с перечислением всех доступных меток типов и соответствующее им хранилище значений:

Type tag Storage location
IS_NULL none
IS_BOOL long lval
IS_LONG long lval
IS_DOUBLE double dval
IS_STRING struct { char *val; int len; } str
IS_ARRAY HashTable *ht
IS_OBJECT zend_object_value obj
IS_RESOURCE long lval

Макросы доступа

Давайте теперь посмотрим как выглядит структура данных zval:

typedef struct _zval_struct {
    zvalue_value value;
    zend_uint refcount__gc;
    zend_uchar type;
    zend_uchar is_ref__gc;
} zval;

Как уже упоминалось, zval содержит члены для хренения значения и его типа. Значение хранится в юнионе zvalue_value, который описан выше. Тип хранится в zend_uchar type. Кроме того эта структура содержит 2 дополнительных свойства, имена которых заканчиваются на __gc, которые используются механизмом сборки мусора. Подробнее эти свойства рассмотрены в следующем разделе.

Зная структуру zval вы можете написать код вида:

zval *zv_ptr = /* ... get zval from somewhere */;

if (zv_ptr->type == IS_LONG) {
    php_printf("Zval is a long with value %ld\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 %ld\n", Z_LVAL_P(zv_ptr));
} else /* ... */

Код выше использует макро Z_TYPE_P для получения метки типа и Z_LVAL_P для получения целочисленного значения. Все макросы доступа имеют варианты с суффиксами _P, _PP и без суффикса. Который из них использовать зависит от того с чем вы работаете: zval, zval* или zval**:

zval zv;
zval *zv_ptr;
zval **zv_ptr_ptr;
zval ***zv_ptr_ptr_ptr;

Z_TYPE(zv);                 // = zv.type
Z_TYPE_P(zv_ptr);           // = zv_ptr->type
Z_TYPE_PP(zv_ptr_ptr);      // = (*zv_ptr_ptr)->type
Z_TYPE_PP(*zv_ptr_ptr_ptr); // = (**zv_ptr_ptr_ptr)->type

Число букв P в суффиксе должно быть равно числу * в типе. Это правило работает только до zval**, то есть нет специального макро для работы с zval*** так как на практике такие указатели встречаются редко.

Аналогично Z_LVAL существуют макросы для извлечения значений всех других типов. Для демонстрации их использования давайте создадим простую функцию выводящую значение zval-а:

PHP_FUNCTION(dump)
{
    zval *zv_ptr;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &zv_ptr) == FAILURE) {
        return;
    }

    switch (Z_TYPE_P(zv_ptr)) {
        case IS_NULL:
            php_printf("NULL: null\n");
            break;
        case IS_BOOL:
            if (Z_BVAL_P(zv_ptr)) {
                php_printf("BOOL: true\n");
            } else {
                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=%d\n", Z_STRLEN_P(zv_ptr));
            break;
        case IS_RESOURCE:
            php_printf("RESOURCE: id=%ld\n", Z_RESVAL_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: ???\n");
            break;
    }
}

const zend_function_entry funcs[] = {
    PHP_FE(dump, NULL)
    PHP_FE_END
};

Давайте испытаем её:

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: ???

Макросы для доступа к данным очевидны: Z_BVAL для доступа к булевым переменным, Z_LVAL для целых чисел, Z_DVAL для чисел с плавающей точкой. Для строк Z_STRVAL возвращает актуальный указатель нс строку char*, а Z_STRLEN возвращает длину строки. ID ресурса может быть получен через Z_RESVAL, HashTable* для массива доступна через Z_ARRVAL. Как осуществляется доступ к значениям обектов мы пока не рассматриваем.

Когда вам нужно получить доступ к данным zval-а вы должны использовать приведенные макросы, вместо прямого обращения к их членам. Это создает уровень абстракции и позволяет явно указать ваши намерения. Например, если вы напрямую обратитесь к члену данных lval вы можете извлечь булево значение, целочисленное значение или ID ресурса. Использование вместо этого Z_BVAL, Z_LVAL и Z_RESVAL делает ваши намерения недвусмысленными. Использование макросов также дает защиту от изменения внутреннего представления zval в будущих версиях PHP.

Установка значения

Макросы представленные выше используются для чтения данных, но часть из них может быть использована также и для записи. Для примера рассмотрим функцию возвращающую “hello world!”:

PHP_FUNCTION(hello_world) {
    Z_TYPE_P(return_value) = IS_STRING;
    Z_STRVAL_P(return_value) = estrdup("hello world!");
    Z_STRLEN_P(return_value) = strlen("hello world!");
};

/* ... */
    PHP_FE(hello_world, NULL)
/* ... */

Исполнение в консоли команды php -r "echo hello_world();" должно напечатать в терминале hello world!.

В этом примере мы устанавливаем значение переменной return_value, которая имеет тип zval* и определена в макро PHP_FUNCTION. Детальнее мы рассмотрим эту переменную в следующей главе, сейчас нам достаточно знать, что значение этой переменной будет значением, возвращаемым функцией. По умолчанию эта переменная инициализируется типом IS_NULL.

Установка значения zval-ов с помощью макросов очень просто, но есть вещи, о которых нужно помнить. Во-первых, вы должны помнить, что метка типа определяет тип zval-а. Недостаточно просто установить значение (через Z_STRVAL и Z_STRLEN в этом примере), вам также нужно установить метку типа (type tag).

Кроме того, вы должны понимать, что в большинстве случаев zval "владеет" своим значением и zval имеет более долгую жизнь чем область видимиости, в которой вы устанавливаете его значение. Иногда это не так, когда вы работаете с временным zval-ами, но в большинстве случаев это так.

В контексте приведенного выше примера это значит, что переменная return_value будет существовать и после выхода за пределы тела нашей функции (что очевидно, так как иначе никто не сможет использовать значение возвращенное функцией), поэтому нельзя использовать временные значения в функциях. Например, нельзя просто написать Z_STRVAL_P(return_value) = "hello world!", так как строковый литерал "hello world!" перестанет существовать после выхода из тела функции (что истинно для всех значений размещенных в стэке в C).

По этой причине мы должны скопировать строку используя estrdup(). Вызов этой функции создаст отдельную копию строки в куче. Так как zval “владеет” своим значением, он сам освободит память выделенную под эту копию когда zval будет уничтожен. Это также применимо к любым другим "комплексным" значениям zval-ов. Например, если вы устанавливаете HashTable* для массива, то zval станет его владельцем и освободит когда zval будет уничтожен. При использовании примитивных типов данных, таких как числа, вам не нужно об этом заботиться, так как они и так всегда копируются.

Последнее на что нужно обратить внимание: не все макросы доступа возвращают член-данных. Например, макро Z_BVAL определен так:

#define Z_BVAL(zval) ((zend_bool)(zval).value.lval)

Так как этот макро содержит присвоение типа (type casting) вы не сможете присвоить значение Z_BVAL_P(return_value) = 1. Это единственное исключение, кроме нескольких макросов связанных с объектами. Остальные макросы могут быть использованы для присвоения значений.

На практике вам не нужно беспокоиться об этом ограничении. Так как присвоение значения это частая задача, PHP предоставляет другой набор макросов для этих целей. Они позволяют вам одновременно задавать метку типа и значение. Предыдущий пример можно переписать так:

PHP_FUNCTION(hello_world) {
    ZVAL_STRINGL(return_value, estrdup("hello world!"), strlen("hello world!"), 0);
}

Очень часто строка должна быть скопирована при присваивании zval-у и последний парметр (типа boolean) в макро ZVAL_STRINGL поможет вам в этом. Если вы передадите 0 — строка будет использована как есть, но если вы передадите 1, то она будет скоприована с помощью estrndup(). Наш пример может быть переписан так:

PHP_FUNCTION(hello_world) {
    ZVAL_STRINGL(return_value, "hello world!", strlen("hello world!"), 1);
}

Более того, нам не нужно вручную рассчитывать длину строки, вместо этого мы можем использовать макро ZVAL_STRING (без L в конце):

PHP_FUNCTION(hello_world) {
    ZVAL_STRING(return_value, "hello world!", 1);
}

Если вам известна длина строки (например она была передана вам кем-то), то вам всегда следует использовать её через макро ZVAL_STRINGL для обеспечения бинарной безопасности. Если вы не знаете длину строки (или вы уверены, что она не содержит нулевые байты, например при использовании литералов), то вы можете использовать ZVAL_STRING.

Кроме ZVAL_STRING(L) есть еще несколько макросов для установки значений, они перечислены в следующем примере:

ZVAL_NULL(return_value);

ZVAL_BOOL(return_value, 0);
ZVAL_BOOL(return_value, 1);
/* or better */
ZVAL_FALSE(return_value);
ZVAL_TRUE(return_value);

ZVAL_LONG(return_value, 42);
ZVAL_DOUBLE(return_value, 4.2);
ZVAL_RESOURCE(return_value, resource_id);

ZVAL_EMPTY_STRING(return_value);
/* = ZVAL_STRING(return_value, "", 1); */

ZVAL_STRING(return_value, "string", 1);
/* = ZVAL_STRING(return_value, estrdup("string"), 0); */

ZVAL_STRINGL(return_value, "nul\0string", 10, 1);
/* = ZVAL_STRINGL(return_value, estrndup("nul\0string", 10), 10, 0); */

Помните, что все эти макросы устанавливают значение, но не уничтожают любое другое значние, которое уже могло содержаться в zval-е. Для переменной return_value это не имеет значения, так как она инициализируется значением IS_NULL (для которого нет значения, которое должно быть освобождено), но в других случаях вы должны уничтожить старое значение прежде чем устанавливать новое.