Тип Resource: zend_resource

Хотя PHP вполне мог бы избавиться от типа “resource”, поскольку пользовательское хранилище объектов позволяет строить PHP-представление любого абстрактного вида данных, этот тип resource всё ещё существует в текущей версии PHP, и вам может понадобиться с ним работать.

Если вам нужно создавать ресурсы, мы действительно хотели бы убедить вас этого не делать, а вместо этого использовать объекты и их пользовательское управление хранилищем. Объекты — это тип PHP, который может вместить что угодно любого типа. Однако по историческим причинам PHP всё ещё знает об этом особом типе “Resource” и до сих пор использует его в своём ядре или в некоторых расширениях. Давайте разберёмся с этим типом вместе. Однако будьте осторожны, он действительно непонятен и страдает от долгой истории прошлого, так что не удивляйтесь его устройству, особенно при чтении исходного кода, посвящённого ему

Что такое тип “Resource”?

Достаточно просто, вы уже о нём знаете. Мы говорим вот о чём:

$fp = fopen('/proc/cpuinfo', 'r');
var_dump($fp); /* resource(2) of type (stream) */

Внутренне resource привязан к структуре типа zend_resource:

struct _zend_resource {
        zend_refcounted_h gc;
        int               handle;
        int               type;
        void             *ptr;
};

Мы находим здесь традиционный заголовок zend_refcounted_h, что означает, что ресурсы подлежат подсчёту ссылок.

handle — это целое число, используемое внутренне движком для поиска ресурса во внутренней таблице ресурсов. Оно используется как ключ для такой таблицы.

type используется для группировки ресурсов одного типа вместе. Это связано со способом, которым ресурсы уничтожаются, и как они извлекаются обратно по своему handle.

Наконец, поле ptr в zend_resource — это ваши абстрактные данные. Напомним, что ресурсы предназначены для хранения абстрактных данных, которые не могут вписаться ни в один тип данных, который PHP может представить нативно (но объекты могли бы, как мы говорили ранее).

Типы ресурсов и уничтожение ресурсов

Ресурсы должны регистрировать деструктор. Когда пользователи используют ресурсы в пользовательском коде PHP, они обычно не заботятся о том, чтобы очистить их, когда они больше не используются. Например, нередко можно встретить вызов fopen() без соответствующего вызова fclose(). При использовании языка C это было бы, в лучшем случае, плохой идеей, в худшем — катастрофой. Но при использовании высокоуровневого языка, такого как PHP, это упрощает жизнь.

Вы, как внутренний разработчик, должны быть готовы к тому, что пользователь создаст множество ресурсов, которые вы позволите ему использовать, не очищая их должным образом и не освобождая память/ресурсы ОС. Поэтому вы должны зарегистрировать деструктор, который будет вызываться каждый раз, когда движок собирается уничтожить ресурс этого типа.

Деструкторы группируются по типам, как и сами ресурсы. Вы не будете применять деструктор для ресурса типа ‘database’ к ресурсу типа ‘file’.

Также существуют два вида ресурсов, здесь снова различающихся по времени их жизни.

  • Классические ресурсы, наиболее часто используемые, не сохраняются между несколькими запросами, их деструктор вызывается при завершении запроса.

  • Постоянные ресурсы сохраняются между несколькими запросами и уничтожаются только при завершении процесса PHP.

Note

Вас может заинтересовать глава о жизненном цикле PHP, которая показывает различные этапы, происходящие в течение жизни процесса PHP. Также глава о Менеджере памяти Zend может помочь в понимании концепций постоянного и привязанного к запросу выделения памяти.

Работа с ресурсами

API, связанный с ресурсами, можно найти в zend/zend_list.c. Вы можете найти в нём некоторые несоответствия, например, упоминания “списков” вместо “ресурсов”.

Создание ресурсов

Чтобы создать ресурс, сначала нужно зарегистрировать для него деструктор и связать его с именем типа ресурса, используя zend_register_list_destructors_ex(). Этот вызов вернёт целое число, представляющее тип ресурса, который вы регистрируете. Вы должны запомнить это число, потому что оно понадобится вам позже, чтобы получить обратно ваш ресурс от пользователя.

После этого вы можете зарегистрировать новый ресурс, используя zend_register_resource(). Эта функция вернёт вам zend_resource. Давайте вместе рассмотрим простой пример использования:

#include <stdio.h>

int res_num;
FILE *fp;
zend_resource *my_res;
zval my_val;

static void my_res_dtor(zend_resource *rsrc)
{
    fclose((FILE *)rsrc->ptr);
}

/* module_number should be your PHP extension number here */
res_num = zend_register_list_destructors_ex(my_res_dtor, NULL, "my_res", module_number);
fp      = fopen("/proc/cpuinfo", "r");
my_res  = zend_register_resource((void *)fp, res_num);

ZVAL_RES(&my_val, my_res);

Что мы делаем в коде выше — мы открываем файл с помощью libc-функции fopen() и сохраняем возвращённый указатель в ресурс. Перед этим мы зарегистрировали деструктор, который при вызове использует libc-функцию fclose() на указателе. Затем мы регистрируем ресурс в движке и помещаем ресурс в контейнер zval, который может быть возвращён в пользовательский код.

Note

Главу о zval можно найти здесь.

Что нужно запомнить — это тип ресурса. Здесь мы регистрируем ресурс типа “my_res”. Это имя типа. Движку на самом деле не важно имя типа, важен идентификатор типа — целое число, возвращаемое функцией zend_register_list_destructors_ex(). Вы должны где-то его запомнить, как мы делаем в переменной res_num.

Получение ресурсов обратно

Теперь, когда мы зарегистрировали ресурс и поместили его в zval для примера, нам нужно научиться получать этот ресурс обратно из пользовательского кода. Помните, ресурс хранится внутри zval. Внутри ресурса хранится номер типа ресурса (в поле type). Таким образом, чтобы получить обратно наш ресурс от пользователя, мы должны извлечь zend_resource из zval и вызвать zend_fetch_resource(), чтобы получить обратно наш указатель FILE *:

/* ... later on ... */

zval *user_zval = /* fetch zval from userland, assume type IS_RESOURCE */

ZEND_ASSERT(Z_TYPE_P(user_zval) == IS_RESOURCE); /* just a check to be sure */

fp = (FILE *)zend_fetch_resource(Z_RESVAL_P(user_zval), "my_res", res_num);

Как мы говорили: получаем zval от пользователя (типа IS_RESOURCE) и извлекаем из него указатель на ресурс, вызывая zend_fetch_resource().

Эта функция проверит, соответствует ли тип ресурса тому типу, который вы передаёте в качестве третьего параметра (здесь res_num). Если да, она извлекает обратно указатель void * на ресурс, который вам нужен, и мы закончили. Если нет, то она выбрасывает предупреждение вида “supplied resource is not a valid {type name} resource”. Это может произойти, например, если вы ожидаете ресурс типа “my_res”, а вам передали zval с ресурсом типа “gzip”, например, тот, что возвращается функцией PHP gzopen().

Типы ресурсов — это просто способ для движка смешивать разные виды ресурсов (типа “file”, “gzip” или даже “mysql connection”) в одной таблице ресурсов. У типов ресурсов есть имена, чтобы их можно было использовать в сообщениях об ошибках или в отладочных выводах (например, в var_dump($my_resource)), а также они представлены как целое число, используемое внутренне для получения обратно указателя на ресурс и для регистрации деструктора для типа ресурса.

Note

Как видите, если бы мы использовали объекты, они представляют типы сами по себе, и не было бы необходимости в этом шаге получения ресурса обратно по его идентификатору с проверкой его типа. Объекты — самоописывающиеся типы. Но ресурсы всё ещё являются допустимым типом данных в текущей версии PHP.

Подсчёт ссылок ресурсов

Как и многие другие типы, zend_resource подлежит подсчёту ссылок. Мы можем увидеть его заголовок zend_refcounted_h. Вот API для работы со подсчётом ссылок, если он вам понадобится (в среднем он вам не должен понадобиться):

  • zend_list_delete(zend_resource *res) уменьшает счётчик ссылок и уничтожает ресурс, если он падает до нуля

  • zend_list_free(zend_resource *res) проверяет, равен ли счётчик ссылок нулю, и уничтожает ресурс, если это так.

  • zend_list_close(zend_resource *res) вызывает деструктор ресурса независимо от условий

Постоянные ресурсы

Постоянные ресурсы не уничтожаются при завершении запроса. Классический пример использования для этого — постоянные соединения с базой данных. Это соединения, которые повторно используются от запроса к запросу (со всеми проблемами, которые это принесёт).

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

Чтобы зарегистрировать постоянный ресурс, используйте постоянный деструктор вместо классического. Это делается при вызове zend_register_list_destructors_ex(), API которой выглядит так:

zend_register_list_destructors_ex(rsrc_dtor_func_t destructor, rsrc_dtor_func_t persistent_destructor,
                                  const char *type_name, int module_number);