На сегодняшний день разработчики PHP ведут работу над API уровня С. И в этом посте я буду по большей части рассказывать о внутренней разработке PHP, хотя если по ходу повествования встретится что-то интересное с точки зрения пользовательского уровня, то я буду делать отступление и объяснять.
Изменения в объектах по сравнению с PHP 5
Чтобы полностью вникнуть в тему объектов в PHP, рекомендую сначала ознакомиться с постом Подробно об объектах и классах в PHP.
Итак, что же изменилось в седьмой версии по сравнению с пятой?
- На пользовательском уровне почти ничего не изменилось. Иными словами, в PHP 7 объекты остались такими же, как и в PHP 5. Не было сделано каких-то глубоких изменений, ничего такого, что вы могли бы заметить в своей повседневной работе. Объекты ведут себя точно так же. Почему ничего не было изменено? Мы считаем, что наша объектная модель является зрелой, она очень активно используется, и мы не видим нужды вносить смуту в новых версиях PHP.
- Но всё же было несколько низкоуровневых улучшений. Изменения небольшие, тем не менее они требуют патчей расширений. В принципе, в PHP 7 внутренние объекты стали гораздо выразительнее, яснее и логичнее, чем в PHP 5. Самое главное нововведение связано с основным изменением в PHP 7: обновлением zval и управлением сборкой мусора. Но в этом посте мы не будем рассматривать последний, потому что тема поста — объекты. Однако нужно помнить, что сама природа новых zval и механизма сборки мусора оказывает влияние на внутреннее управление объектами.
Структура объекта и управление памятью
В первую очередь можете попрощаться с
zend_object_value
, от этой структуры в PHP 7 отказались.Давайте посмотрим пример определения объекта zend_object:
/* in PHP 5 */
typedef struct _zend_object {
zend_class_entry *ce;
HashTable *properties;
zval **properties_table;
HashTable *guards;
} zend_object;
/* in PHP 7 */
struct _zend_object {
zend_refcounted_h gc;
uint32_t handle;
zend_class_entry *ce;
const zend_object_handlers *handlers;
HashTable *properties;
zval properties_table[1]; /* C struct hack */
};
Как видите, есть небольшие отличия от PHP 5.
Во-первых, здесь содержится заголовок
zend_refcounted_h
, являющийся частью нового zval и механизма сборки мусора. Во-вторых, теперь объект содержит свой
handle
, в то время как в PHP 5 эту задачу выполнял zend_object_store
. А в PHP 7 у объектного хранилища (object store) уже гораздо меньше обязанностей. В-третьих, для замещения
properties_table
вектора zval используется структурный хак на языке С, он пригодится при создании кастомных объектов.Управление кастомными объектами
Важное изменение коснулось управления кастомными объектами, которые мы создаём для своих нужд. Теперь они включают в себя
zend_object
. Это очень важная особенность объектной модели Zend Engine: расширения могут объявлять свои собственные объекты и управлять ими, развивая возможности стандартной реализации объектов в Zend без изменения исходного кода движка.В PHP 5 мы просто создаём наследование в виде С-структуры, включающей базовое определение
zend_object
:/* PHP 5 */
typedef struct _my_own_object {
zend_object zobj;
my_custom_type *my_buffer;
} my_own_object;
Благодаря наследованию С-структуры нам достаточно создать простую конструкцию:
/* PHP 5 */
my_own_object *my_obj;
zend_object *zobj;
my_obj = (my_own_object *)zend_objects_store_get_object(this_zval);
zobj = (zend_object *)my_obj;
Вы могли заметить, что при получении zval в PHP 5, например $this в методах ОО, вы не можете изнутри этого zval получить доступ к объекту, на который он прямо указывает. Для этого вам придётся обратиться к объектному хранилищу. Извлечь обработчик из zval (в PHP 5) и с его помощью попросить хранилище вернуть найденный объект. Этот объект — он может быть кастомным — возвращается в виде
void*
. Если вы ничего не кастомизировали, то его нужно представить в виде zend_object*
, в противном случае — в виде my_own_object*
. Короче, чтобы получить от метода объект, в PHP 5 вам нужно осуществить процедуру поиска. А это не слишком хорошо сказывается на производительности.
В PHP 7 всё устроено иначе. Объект — неважно, кастомный или классический
zend_object
, — хранится прямо в zval. При этом объектное хранилище больше не поддерживает операцию извлечения. То есть вы не можете больше считывать содержимое объектного хранилища, только записывать в него или стирать.Размещённый объект целиком встроен в zval, так что если вы вызываете zval в качестве параметра и хотите получить область памяти объекта, на которую он указывает, то вам не нужно будет дополнительно осуществлять поиск. Вот как в PHP 7 можно получить объект:
/* PHP 7 */
zend_object *zobj;
zobj = Z_OBJ_P(this_zval);
Гораздо проще, чем в PHP 5, не так ли?
Иначе теперь устроена работа с кастомным размещением объектов. Из приведённого выше кода видно, что единственный способ получения кастомного объекта заключается в манипуляции с памятью: переместить указатель в любом нужном направлении в рамках необходимого объёма памяти. Чистое программирование на С и высокая производительность: вы наверняка останетесь в той же физической странице памяти, и, следовательно, ядро не будет подгружать новую страницу.
Объявление кастомного объекта в PHP 7:
/* PHP 7 */
typedef struct _my_own_object {
my_custom_type *my_buffer;
zend_object zobj;
} my_own_object;
Обратите внимание на перестановку компонентов структуры по сравнению с PHP 5. Для чего это сделано? Когда вы считываете
zend_object
из zval, то для получения своего my_own_object
вам придётся брать память в обратном направлении, вычитая смещение (offset) zend_object
’а в структуре. Делается это с помощью OffsetOf()
из stddef.h (при необходимости можно легко эмулировать). Это считается использованием продвинутой С-структуры, но если вы хорошо знаете используемый вами язык (а по-другому и быть не должно), то вам наверняка уже приходилось выполнять подобное.Для получения кастомного объекта в PHP 7 нужно сделать так:
/* PHP 7 */
zend_object *zobj;
my_own_object *my_obj;
zobj = Z_OBJ_P(this_zval);
my_obj = (my_own_object *)((char *)zobj - XoffsetOf(struct my_own_object, zobj));
Здесь некоторую путаницу вносит использование
offsetof()
: последним компонентом объекта your_custom_struct
должен быть zend_object
. Очевидно, что если вы после этого объявите типы, то из-за особенностей организации размещения zend_object
в PHP 7 впоследствии у вас будут затруднения с доступом к этим типам. Не забывайте, что в PHP 7
zend_object
теперь использует структурный хак. Это означает, что выделенная память будет отличаться от sizeof(zend_object)
. Размещение zend_object
:/* PHP 5 */
zend_object *zobj;
zobj = ecalloc(1, sizeof(zend_object));
/* PHP 7 */
zend_object *zobj;
zobj = ecalloc(1, sizeof(zend_object) + zend_object_properties_size(ce));
Поскольку ваш класс знает всё об объявленных атрибутах, то он определяет размер памяти, которую вы должны выделить для компонентов.
Создание объекта
Рассмотрим реальный пример. Пусть у нас есть кастомный объект:
/* PHP 7 */
typedef struct _my_own_object {
void *my_custom_buffer;
zend_object zobj; /* MUST be the last element */
} my_own_object;
Так может выглядеть его обработчик
create_object()
:/* PHP 7 */
static zend_object *my_create_object(zend_class_entry *ce)
{
my_own_object *my_obj;
my_obj = ecalloc(1, sizeof(my_obj) + zend_object_properties_size(ce));
my_obj->my_custom_buffer = emalloc(512); /* Допустим, наш кастомный буфер уместится в 512 байт */
zend_object_std_init(&my_obj->zobj, ce); /* Не забудьте также и про zend_object! */
object_properties_init(&my_obj->zobj, ce);
my_obj->zobj.handlers = &my_class_handlers; /* У меня используется кастомный обработчик, о нём речь пойдёт дальше */
return &my_obj->zobj;
}
В отличие от PHP 5, здесь нельзя забывать об объёме выделяемой памяти: помните о структурном хаке, замещающем свойства
zend_object
. Кроме того, здесь больше не используется объектное хранилище. В PHP 5 обработчик создания объекта должен был зарегистрировать его в хранилище, а затем передать некоторые указатели функций для будущего уничтожения и освобождения объекта. В PHP 7 этого больше не нужно делать, функция create_object()
работает гораздо яснее.Для использования этого кастомного обработчика
create_object()
нужно объявить его в вашем расширении. Таким образом вы будете объявлять каждый обработчик:/* PHP 7 */
zend_class_entry *my_ce;
zend_object_handlers my_ce_handlers;
PHP_MINIT_FUNCTION(my_extension)
{
zend_class_entry ce;
INIT_CLASS_ENTRY(ce, "MyCustomClass", NULL);
my_ce = zend_register_internal_class(&ce);
my_ce->create_object = my_create_object; /* Обработчик создания объекта */
memcpy(&my_ce_handlers, zend_get_std_object_handlers(), sizeof(my_ce_handlers));
my_ce_handlers.free_obj = my_free_object; /* Обработчик free */
my_ce_handlers.dtor_obj = my_destroy_object; /* Обработчик dtor */
/* Также доступен my_ce_handlers.clone_obj, хотя здесь мы его не будем использовать */
my_ce_handlers.offset = XtOffsetOf(my_own_object, zobj); /* Здесь мы объявляем смещение для движка */
return SUCCESS;
}
Как видите, в MINIT мы объявляем
free_obj()
и dtor_obj()
. В PHP 5 при регистрации объекта в хранилище их оба нужно объявлять в zend_objects_store_put()
, но в PHP 7 в этом больше нет необходимости. Теперь zend_object_std_init()
сам запишет объект в хранилище, не нужно это делать вручную, так что не забудьте про этот вызов.Итак, мы зарегистрировали наши обработчики
free_obj()
и dtor_obj()
, а также компонент offset, используемый при вычислении расположения нашего кастомного объекта в памяти. Эта информация нужна движку, потому что теперь именно он занимается освобождением объектов, а не вы. В PHP 5 это приходилось делать вручную, обычно с помощью free()
. А раз теперь это делает движок, то для освобождения всего указателя ему нужно получить не только типы zend_object
, но и значение offset
для вашей кастомной структуры. Пример можно посмотреть здесь.Уничтожение объекта
Хочу напомнить, что деструктор вызывается при уничтожении объекта на пользовательском уровне PHP, точно так же, как вызывается
__destruct()
. Так что в случае критических ошибок деструктор может вообще не вызваться, и в PHP 7 эта ситуация не изменилась. Если вы внимательно изучили пост, о котором говорилось в начале, или эту презентацию, то, скорее всего, помните, что деструктор не должен оставлять объект в нестабильном состоянии, поскольку однажды уничтоженный объект должен быть доступен в определённых ситуациях. Поэтому в PHP обработчики уничтожения и освобождения объекта отделены друг от друга. Обработчик освобождения вызывается, когда движок полностью уверен в том, что объект больше нигде не применяется. Деструктор вызывается, когда refcount объекта достигает 0, но поскольку может быть выполнен какой-то пользовательский код (__destruct()
), то текущий объект нигде не может быть переиспользован в качестве ссылки, а значит, он должен оставаться в нестабильном состоянии. Поэтому будьте очень внимательны, если освобождаете память с помощью деструктора. Обычно деструктор прекращает использование ресурсов, но не освобождает их. Этим уже занимается обработчик освобождения.Итак, подведём итоги относительно работы деструктора:
- Он либо не вызывается совсем, либо вызывается один раз (чаще всего), но никогда больше одного раза. В случае критической ошибки деструктор не вызывается.
- Деструктор не освобождает ресурсы, поскольку в некоторых редких случаях объект может быть переиспользован движком.
- Если из своего кастомного деструктора вы не вызываете
zend_objects_destroy_object()
, то пользовательский__destruct()
не будет инициирован.
/* PHP 7 */
static void my_destroy_object(zend_object *object)
{
my_own_object *my_obj;
my_obj = (my_own_object *)((char *)object - XoffsetOf(my_own_object, zobj));
/* Теперь мы можем что-нибудь сделать с my_obj->my_custom_buffer, например отправить его в сокет, или сбросить в файл, или ещё что-то. Но здесь он не освобождается. */
zend_objects_destroy_object(object); /* Вызов __destruct() на пользовательском уровне */
}
Освобождение объектного хранилища
Функция освобождения хранилища инициируется движком, когда он абсолютно уверен, что объект больше нигде не используется. Перед самым уничтожением объекта движок вызывает обработчик
free_obj()
. Вы разместили какие-то ресурсы в своём кастомном обработчике create_object()
? Пришла пора их освобождать:/* PHP 7 */
static void my_free_object(zend_object *object)
{
my_own_object *my_obj;
my_obj = (my_own_object *)((char *)object - XoffsetOf(my_own_object, zobj));
efree(my_obj->my_custom_buffer); /* Освобождение кастомных ресурсов */
zend_object_std_dtor(object); /* Вызов обработчика освобождения, который освободит свойства объекта */
}
И всё. Вам больше не нужно заниматься освобождением самостоятельно, как в PHP 5. Раньше обработчик заканчивался чем-то вроде
free(object)
; теперь в обработчике create_object()
выделяется место для вашей кастомной объектной структуры, но, когда в MINIT вы передаёте движку значение offset
, тот получает возможность самостоятельно провести освобождение. Например, как здесь.Конечно, во многих случаях обработчик
free_obj()
вызывается сразу же после обработчика dtor_obj()
. Исключение составляют ситуации, когда пользовательский деструктор передаёт кому-то $this либо в случае с объектом кастомного расширения, которое плохо спроектировано. Если вас интересует полная последовательность кода при освобождении объекта движком, почитайте о zend_object_store_del()
. Заключение
Мы рассмотрели, как в PHP 7 изменилась работа с объектами. На пользовательском уровне всё осталось практически без изменений, только объектная модель была оптимизирована: стала работать быстрее и имеет чуть больше возможностей. Но никаких значимых нововведений нет.
А вот «под капотом» перемен гораздо больше. Они тоже не слишком большие и не потребуют от вас многих часов изучения, но всё же понадобится приложить определённые усилия. Обе объектные модели стали несовместимы на низком уровне, поэтому вам придётся переписать ту часть исходного кода своих расширений, которая относится к объектам. В этом посте я попытался объяснить разницу. Если в разработке вы перейдёте на PHP 7, то заметите, что он стал яснее и структурированнее по сравнению с PHP 5. Новая версия освободилась от тяжёлого десятилетнего наследия. Многие вещи в PHP 7 улучшены и переделаны, чтобы избавиться от необходимости патчей кода в ряде случаев.