Objects in PHP 7

Original author: Julien Pauli
  • Transfer
image

Today, PHP developers are working on a C-level API. And in this post I will mostly talk about the internal development of PHP, although if during the course of the story there is something interesting from the point of view of the user level, I will digress and explain .

Changes to objects compared to PHP 5


To fully understand the topic of objects in PHP, I recommend that you first read the post in more detail about objects and classes in PHP .

So, what has changed in the seventh version compared to the fifth?

  • At the user level, almost nothing has changed. In other words, in PHP 7 the objects remained the same as in PHP 5. No deep changes were made, nothing that you might notice in your daily work. Objects behave exactly the same. Why has nothing been changed? We believe that our object model is mature, it is very actively used, and we do not see the need to introduce confusion in new versions of PHP.
  • But still there were several low-level improvements. The changes are small, however, they require patch extensions. In principle, in PHP 7, internal objects have become much more expressive, clearer and more logical than in PHP 5. The most important innovation is related to the main change in PHP 7: updating zval and managing garbage collection. But in this post we will not consider the latter, because the topic of the post is objects. However, it must be remembered that the very nature of the new zval and the garbage collection mechanism have an impact on the internal management of objects.

Object Structure and Memory Management


First of all, you can say goodbye to zend_object_value, this structure was abandoned in PHP 7.

Let's see an example of a zend_object object definition:

/* 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 */
};

As you can see, there are slight differences from PHP 5.

Firstly, it contains a header zend_refcounted_h, which is part of the new zval and the garbage collection mechanism.

Secondly, now the object contains its own handle, while in PHP 5 it performed this task zend_object_store. And in PHP 7, the object store has much less responsibilities.

Thirdly, a properties_tablestructural hack in the C language is used to replace the zval vector; it is useful when creating custom objects.

Custom Object Management


An important change has affected the management of custom objects that we create for our needs. Now they include zend_object. This is a very important feature of the Zend Engine object model: extensions can declare and manage their own objects, developing the capabilities of the standard implementation of objects in Zend without changing the source code of the engine.

In PHP 5, we simply create inheritance in the form of a C-structure, including a basic definition zend_object:

/* PHP 5 */
typedef struct _my_own_object {
    zend_object        zobj;
    my_custom_type    *my_buffer;
} my_own_object;

Thanks to the inheritance of the C-structure, it is enough for us to create a simple construction:

/* 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;

You may have noticed that when you get a zval in PHP 5, for example $ this in OO methods, you cannot access the object that it points directly to from inside this zval. To do this, you have to go to the object store. Extract the handler from zval (in PHP 5) and use it to ask the repository to return the found object. This object - it can be custom - is returned in the form void*. If you have not customized anything, then it must be presented in the form zend_object*, otherwise - in the form my_own_object*.

In short, in order to get an object from a method, in PHP 5 you need to implement a search procedure. And this does not affect performance too well.

In PHP 7, things are different. The object - whether it is custom or classic zend_object- is stored directly in zval. Whereinobject storage no longer supports the extraction operation . That is, you can no longer read the contents of the object store, just write to it or erase it.

The hosted object is entirely embedded in zval , so if you call zval as a parameter and want to get the memory area of ​​the object that it points to, then you will not need to additionally search. Here's how to get an object in PHP 7:

/* PHP 7 */
zend_object *zobj;
zobj = Z_OBJ_P(this_zval);

Much easier than in PHP 5, isn't it?

Otherwise, work with custom placement of objects is now arranged. It can be seen from the above code that the only way to get a custom object is to manipulate memory: move the pointer in any desired direction within the required memory size. Pure C programming and high performance: you will probably stay in the same physical memory page, and therefore, the kernel will not load a new page.

Declaring a custom object in PHP 7:

/* PHP 7 */
typedef struct _my_own_object {
    my_custom_type *my_buffer;
    zend_object     zobj;
} my_own_object;

Note the permutation of the structure components compared to PHP 5. What is this for? When you read zend_objectfrom zval, then to get my_own_objectyours you have to take the memory in the opposite direction, subtracting the offset zend_objectin the structure. This is done by means OffsetOf()of stddef.h (can be easily emulated if necessary). This is considered using an advanced C-structure, but if you know the language you are using well (and it shouldn’t be any different), then you probably already had to do this.

To get a custom object in PHP 7 you need to do this:

/* 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));

Here the use introduces some confusion offsetof(): the last component of the object your_custom_structshould bezend_object . Obviously, if you declare types after this, then due to the peculiarities of arranging your placement zend_objectin PHP 7, you will subsequently have difficulty accessing these types.

Remember that PHP 7 zend_objectnow uses a structural hack. This means that the allocated memory will be different from sizeof(zend_object). Accommodation 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));

Since your class knows everything about declared attributes, it determines the size of memory that you should allocate for components.

Object Creation


Consider a real example. Suppose we have a custom object:

/* PHP 7 */
typedef struct _my_own_object {
    void        *my_custom_buffer;
    zend_object zobj; /* MUST be the last element */
} my_own_object;

This might look like its handler 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;
}

Unlike PHP 5, we must not forget about the amount of allocated memory: remember the structural hack that replaces the properties zend_object. In addition, object storage is no longer used here. In PHP 5, an object creation handler had to register it in the repository, and then pass some function pointers for future destruction and release of the object. In PHP 7, this no longer needs to be done, the function create_object()works much more clearly.

To use this custom handler, create_object()you need to declare it in your extension. This way you will declare each handler:

/* 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;
}

As you can see, in MINIT we announce free_obj()and dtor_obj(). In PHP 5, when registering an object in storage, they both need to be declared in zend_objects_store_put(), but in PHP 7, this is no longer necessary . Now zend_object_std_init()he will write the object to the repository himself, there is no need to do this manually, so do not forget about this call.

So, we registered our handlers free_obj()and dtor_obj(), as well as the offset component, used to calculate the location of our custom object in memory. The engine needs this information, because now it is he who is engaged in the release of objects, not you . In PHP 5, you had to do this manually, usually with free(). And since now the engine does it, then to free the entire pointer it needs to get not only typeszend_object, but also the value offsetfor your custom structure. An example can be seen here .

Destruction of an object


I want to remind you that the destructor is called when the object is destroyed at the user level in PHP, just like it is called __destruct(). So in case of critical errors, the destructor may not be called at all, and in PHP 7 this situation has not changed. If you carefully studied the post that was mentioned at the beginning, or this presentation, then, most likely, remember that the destructor should not leave the object in an unstable state, since once the destroyed object must be available in certain situations. Therefore, in PHP, handlers for destroying and freeing an object are separate from each other. The release handler is called when the engine is completely sure that the object is not used anywhere else. The destructor is called when the object's refcount reaches 0, but since some custom code can be executed (__destruct()), then the current object cannot be reused anywhere as a link, which means that it must remain in an unstable state. Therefore, be very careful if you free memory using a destructor. Typically, a destructor stops using resources, but does not free them. The release handler is already doing this.

So, to summarize the work of the destructor:
  • It is either not called at all, or called once (most often), but never more than once. In case of a critical error, the destructor is not called.
  • The destructor does not free resources, because in some rare cases an object can be reused by the engine.
  • If you do not call from your custom destructor zend_objects_destroy_object(), then the custom __destruct()one will not be initiated.

/* 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() на пользовательском уровне */
}

Release Object Storage


The function of freeing storage is initiated by the engine when it is absolutely sure that the object is not used anywhere else. Before the destruction of the object, the engine calls the handler free_obj(). Have you placed any resources in your custom handler create_object()? The time has come to free them:

/* 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); /* Вызов обработчика освобождения, который освободит свойства объекта */
}

And that’s it. You no longer need to do the release yourself, as in PHP 5. Previously, the handler ended with something like free(object); Now the handler create_object()allocates space for your custom object structure, but when you pass a value to the engine in MINIT offset, it gets the opportunity to independently release it. For example, like here .

Of course, in many cases, the handler free_obj()is called immediately after the handler dtor_obj(). The exception is situations when the user destructor passes $ this to someone, or in the case of a custom extension object that is poorly designed. If you are interested in the complete sequence of code when releasing an object with the engine, read about zend_object_store_del().

Conclusion


We looked at how object handling has changed in PHP 7. At the user level, everything remained virtually unchanged, only the object model was optimized: it began to work faster and has a bit more features. But there are no significant innovations.

But “under the hood” there is much more change. They are also not too large and will not require many hours of study, but still it will take some effort. Both object models have become incompatible at a low level, so you have to rewrite the part of the source code of your extensions that relates to objects. In this post I tried to explain the difference. If you switch to PHP 7 in development, you will notice that it has become clearer and more structured compared to PHP 5. The new version has been freed from the heavy ten-year heritage. Many things in PHP 7 have been improved and redone to eliminate the need for code patches in some cases.

Also popular now: