Ссылки¶
Ссылки PHP (в смысле символа &) в основном прозрачны для пользовательского кода, но требуют последовательной
особой обработки в реализации. В этой главе рассматривается, как ссылки представлены внутри, и как внутренний код
должен с ними работать.
Семантика ссылок¶
Прежде чем переходить к внутреннему представлению ссылок PHP, может быть полезно разъяснить некоторые распространённые заблуждения о семантике ссылок в PHP. Рассмотрим этот базовый пример:
$a = 0;
$b =& $a;
$a++;
$b++;
var_dump($a); // int(2)
var_dump($b); // int(2)
Люди часто говорят, что “$b является ссылкой на $a”. Однако это не совсем верно, поскольку ссылки в PHP не
имеют понятия направленности. После $b =& $a и $a, и $b ссылаются на общее значение, и ни одна из
переменных не является привилегированной в каком-либо смысле.
Это становится особенно проблематичным, когда мы рассматриваем взаимодействие ссылок и копий массивов:
$array = [0];
$ref =& $array[0];
$array2 = $array;
$array2[] = 42; // Triggering copy-on-write makes no difference here.
$ref++;
var_dump($array[0]); // int(1)
var_dump($array2[0]); // int(1)
Строка $ref =& $array[0] создаёт ссылку между $ref и $array[0]. Когда массив впоследствии копируется, она
становится ссылкой между $ref, $array[0] и $array2[0], так как ссылка также копируется.
Интуитивно это поведение кажется неправильным. Есть две причины, почему это происходит: первая — упомянутое выше
отсутствие направленности. Это поведение имело бы смысл, если бы мы написали $array[0] =& $ref. В этом случае
было бы ожидаемо, что копия $array2[0] также указывает на $ref. Однако мы на самом деле не можем различить
эти два случая.
Вторая и более важная причина носит более технический характер: $array2 = $array выполняет только увеличение
счётчика ссылок, что означает, что у нас не было бы возможности отказаться от ссылки, даже если бы мы хотели.
Представление¶
Ссылки представлены с помощью zval типа IS_REFERENCE, который указывает на структуру zend_reference:
struct _zend_reference {
zend_refcounted_h gc;
zval val;
zend_property_info_source_list sources;
};
У самих zval нет счётчика ссылок, и они не могут быть общими. Структура zend_reference по сути представляет
собой zval с подсчётом ссылок, который может быть общим. Несколько zval могут указывать на одну и ту же
zend_reference, и любое изменение содержащегося в ней val будет наблюдаемо из всех источников.
Источники типов¶
Обычно PHP не отслеживает, кто или что использует данную ссылку. Единственное знание, которое сохраняется, — это сколько у неё пользователей (через счётчик ссылок), чтобы ссылка могла быть своевременно уничтожена.
Однако из-за введения типизированных свойств в PHP 7.4, нам действительно нужно отслеживать, какие типизированные свойства используют определённую ссылку, чтобы обеспечить соблюдение типов свойств при косвенных изменениях через ссылки:
class Test {
public int $prop = 42;
}
$test = new Test;
$ref =& $test->prop;
$ref = "string"; // TypeError
Член sources структуры zend_reference хранит список указателей на zend_property_info, чтобы отслеживать
типизированные свойства, использующие эту ссылку. Макросы, такие как ZEND_REF_HAS_TYPE_SOURCES(),
ZEND_REF_ADD_TYPE_SOURCE() и ZEND_REF_DEL_TYPE_SOURCE(), используются для управления этим списком источников,
но обычно только коду движка нужно иметь с этим дело.
Инициализация ссылок¶
Как и другие zval, ссылки инициализируются через набор макросов. Самый базовый принимает уже созданный указатель
zend_reference:
zval ref;
ZVAL_REF(ref, zend_reference_ptr);
Чтобы создать ссылку с нуля, можно использовать ZVAL_NEW_REF():
zval ref;
zval initial_val;
ZVAL_STRING(initial_val, "test");
ZVAL_NEW_REF(&ref, &initial_val);
Этот макрос принимает начальное значение для ссылки. Обратите внимание, что оно перемещается в ссылку с помощью
ZVAL_COPY_VALUE, счётчик ссылок не увеличивается. В качестве альтернативы, ZVAL_NEW_EMPTY_REF() оставляет
значение неинициализированным:
zval ref;
ZVAL_NEW_EMPTY_REF(&ref);
ZVAL_STRING(Z_REFVAL(ref), "test");
Здесь мы создаём пустую ссылку, а затем инициализируем значение ссылки Z_REFVAL(ref) напрямую. Наконец,
ZVAL_MAKE_REF() можно использовать для преобразования существующего zval в ссылку:
zval *zv = /* ... */;
ZVAL_MAKE_REF(zv);
Если zv уже был ссылкой, это ничего не делает. Если он ещё не был ссылкой, это превратит zv в ссылку и
установит в качестве её начального значения старое значение zv.
Разыменование и разворачивание¶
Большая часть кода не хочет обрабатывать ссылки каким-либо особым образом и просто хочет “смотреть насквозь” на лежащее в основе значение:
zval *zv = /* ... */;
if (Z_ISREF_P(zv)) {
zv = Z_REFVAL_P(zv);
}
Если значение является ссылкой (Z_ISREF), мы переключаемся на просмотр значения, которое оно содержит. Эта
операция называется “разыменованием” и более компактно записывается как ZVAL_DEREF(zv). Она крайне
распространена и должна применяться практически в любом месте, где могут встретиться ссылочные zval. Например, вот
как может выглядеть типичный цикл по массиву:
zval *val;
ZEND_HASH_FOREACH_VAL(ht, val) {
ZVAL_DEREF(val);
/* Do something with val, now a guaranteed non-reference. */
} ZEND_HASH_FOREACH_END();
Макрос ZVAL_COPY_DEREF(target, source) — это комбинированная форма ZVAL_COPY и ZVAL_DEREF. Он копирует
разыменованное значение source в target.
Разыменование просто перемещает указатель с внешнего zval на внутренний, не изменяя ни тот, ни другой. Также возможно фактически убрать обёртку ссылки, выполнив разворачивание. Проще всего понять эту операцию, посмотрев на её реализацию:
static zend_always_inline void zend_unwrap_reference(zval *op) {
if (Z_REFCOUNT_P(op) == 1) {
ZVAL_UNREF(op);
} else {
Z_DELREF_P(op);
ZVAL_COPY(op, Z_REFVAL_P(op));
}
}
Если счётчик ссылок равен 1, то внутреннее значение перемещается в op, а обёртка ссылки уничтожается. Это то,
что делает ZVAL_UNREF(). Если счётчик ссылок больше единицы, то мы уменьшаем счётчик ссылок обёртки ссылки и
копируем (с увеличением счётчика ссылок) внутреннее значение в op. Это означает, что операция разворачивания не
обязательно уничтожает ссылку (если у неё есть другие пользователи), но удалит одно конкретное использование.
Косвенные zval¶
Помимо ссылок, у PHP также есть более прямой механизм для совместного использования zval. Тип IS_INDIRECT
хранит прямой указатель на другой zval:
zval val1;
ZVAL_LONG(&val1, 42);
zval val2;
ZVAL_INDIRECT(&val2, &val1);
ZEND_ASSERT(Z_INDIRECT(val2) == &val1);
Хотя здесь есть некоторое внешнее сходство со ссылками, этот механизм в целом непригоден для использования, потому что ничто не гарантирует, что zval, на который указывает указатель, не будет освобождён. По этой причине косвенные zval могут использоваться только в контролируемых ситуациях, например, для указания из хеш-таблицы свойств на таблицу слотов свойств. Это возможно, потому что мы знаем, что таблица слотов свойств не переаллоцируется в течение жизни объекта, а хеш-таблица свойств и таблица слотов свойств освобождаются одновременно, так что никаких висячих указателей не остаётся.
Таким образом, косвенные zval могут встречаться только в определённых ситуациях и не могут храниться в универсальных zval, экспонируемых в пользовательский код.