Ссылки

Ссылки 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, экспонируемых в пользовательский код.