Press "Enter" to skip to content

Understanding PHP 7 Internals: OBJECT

In the previous few articles, I systematically introduced PHP7's ZVAL, Hashtable, and Reference. Today I'll talk about some of the changes to Object (objects).

PHP5

As is my custom, let me first take you through a recap of zend_object in PHP5 (this content was also covered in earlier articles; if you're familiar with it, you can skip). If you're interested, you can also look at my article from ten years ago, Deep dive into PHP internals: Objects.

In PHP5, the object definition is as follows:

typedef struct _zend_object {
    zend_class_entry *ce;
    HashTable *properties;
    zval **properties_table;
    HashTable *guards;
} zend_object;

Here ce stores the class this object belongs to. Regarding properties_table and properties: properties_table is for declared properties, properties is for dynamic properties — that is, for example:

<?php
class Foo {
    public $a = 'defaul property';
}
$a = New Foo();
$a->b = 'dynamic property';

Since in the definition of Foo we declared public $a, then $a is a known declared property; its visibility, including the location where it's stored in properties_table, is all determined right after the declaration.

Whereas $a->b is a property we added dynamically; it doesn't belong to the already-declared properties, and this is stored in properties.

In fact, you can also tell from the types: properties_table is an array of zval*, while properties is a Hashtable.

guards is mainly used for nested protection when magic methods are invoked, e.g. __isset/__get/__set.

Overall, zend_object (hereafter just "object") in PHP5 is actually a relatively special existence. In PHP5, only resource and object are passed by reference — that is, during assignment and passing, what's passed is the thing itself. It's precisely for this reason that, in addition to using Zval's reference counting, Object and Resource also adopt an independent counting system of their own.

We can also see the difference between object and others like strings from zval:

typedef union _zvalue_value {
    long lval;
    double dval;
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;
    zend_object_value obj;
} zvalue_value;

For strings and arrays, zval directly stores their pointer, but for object it's a zend_object_value struct:

typedef unsigned int zend_object_handle;

typedef struct _zend_object_value {
    zend_object_handle handle;
    const zend_object_handlers *handlers;
} zend_object_value;

Actually obtaining the object requires going through this zend_object_handle — an int index — to look it up in the global object buckets:

ZEND_API void *zend_object_store_get_object_by_handle(zend_object_handle handle TSRMLS_DC)
{
    return EG(objects_store).object_buckets[handle].bucket.obj.object;
}

And EG(objects_store).object_buckets is an array that holds:

typedef struct _zend_object_store_bucket {
    zend_bool destructor_called;
    zend_bool valid;
    zend_uchar apply_count;
    union _store_bucket {
        struct _store_object {
            void *object;
            zend_objects_store_dtor_t dtor;
            zend_objects_free_object_storage_t free_storage;
            zend_objects_store_clone_t clone;
            const zend_object_handlers *handlers;
            zend_uint refcount;
            gc_root_buffer *buffered;
        } obj;
        struct {
            int next;
        } free_list;
    } bucket;
} zend_object_store_bucket;

Among these, zend_object_store_bucket.bucket.obj.object is what stores the real zend_object pointer. Notice this is a void *, because many of our extensions' custom objects can also be stored here.

We also notice zend_object_store_bueckt.bucket.obj.refcount — this is the object's own reference counting that I just mentioned. That is, zval has its own set of reference counting, and object also has its own set of reference counting.

<?php
$o1 = new Stdclass();
//o1.refcount == 1, object.refcount == 1
$o2 = $o1;
//o1.refcount == o2.refcoun == 2; object.refcount = 1;
$o3 = &$o2;
//o3.isref == o2.isref==1
//o3.refcount == o2.refcount == 2
//o1.isref == 0; o1.refcount == 1
//object.refcount == 2

This lets object guarantee a COW mechanism different from an ordinary zval, and can ensure that object can be passed by reference globally.

So we can see that, from a zval to actually retrieving the object, we first need to get zval.value.obj.handle, then take that index and query EG(objects_store) again — which is relatively inefficient.

For another common operation — getting the class of a zval object — we also need to call a function:

#define Z_OBJCE(zval) zend_get_class_entry(&(zval) TSRMLS_CC)

PHP7

By PHP7, as my earlier article Deep dive into the PHP7 kernel: ZVAL described, zval directly stores the pointer to the zend_object:

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];
};

And EG(objects_store) just simply holds pointers like zend_object**:

typedef struct _zend_objects_store {
    zend_object **object_buckets;
    uint32_t top;
    uint32_t size;
    int free_list_head;
} zend_objects_store;

And regarding the COW example above, for IS_OBJECT we distinguish using IS_TYPE_COPYABLE — that is, when COW occurs, if this type hasn't set IS_TYPE_COPYABLE, then no "copy" happens.

#define IS_ARRAY_EX  (IS_ARRAY | ((IS_TYPE_REFCOUNTED | IS_TYPE_COLLECTABLE | IS_TYPE_COPYABLE) << Z_TYPE_FLAGS_SHIFT))
#define IS_OBJECT_EX (IS_OBJECT | ((IS_TYPE_REFCOUNTED | IS_TYPE_COLLECTABLE) << Z_TYPE_FLAGS_SHIFT))

As above, you can see that for ARRAY, IS_TYPE_REFCOUNTED, IS_TYPE_COLLECTABLE, and IS_TYPE_COPYABLE are all defined, but for OBJECT, IS_TYPE_COPYABLE is missing.

In SEPARATE_ZVAL:

#define SEPARATE_ZVAL(zv) do {                          
        zval *_zv = (zv);                               
        if (Z_REFCOUNTED_P(_zv) ||                      
            Z_IMMUTABLE_P(_zv)) {                       
            if (Z_REFCOUNT_P(_zv) > 1) {                
                if (Z_COPYABLE_P(_zv) ||                
                    Z_IMMUTABLE_P(_zv)) {               
                    if (!Z_IMMUTABLE_P(_zv)) {          
                        Z_DELREF_P(_zv);                
                    }                                   
                    zval_copy_ctor_func(_zv);           
                } else if (Z_ISREF_P(_zv)) {            
                    Z_DELREF_P(_zv);                    
                    ZVAL_DUP(_zv, Z_REFVAL_P(_zv));     
                }                                       
            }                                           
        }                                               
    } while (0)

If it's not Z_COPYABLE_P, then no write-time separation happens.

Here some might ask: since we already directly store the zend_object* in zval, why do we still need EG(objects_store)?

There are two main reasons here:

  • 1. We need to ensure that when a PHP request ends, the destructors of all objects are called. Because objects can have circular references, how do we quickly traverse all live objects? EG(objects_store) is a very good choice for that.
  • 2. When developing PHPNG, to guarantee maximum backward compatibility, we still needed to ensure an interface for getting an object's handle, and that handle had to preserve its original semantics.

But in practice, EG(objects_store) really doesn't have much use anymore; we could remove it in the future.

OK, the next thing that came up is another problem. Let's look again at the zend_object definition. Notice the properties_table[1] at the end — that is, we now allocate memory for the object's properties together with the object itself. This is cache-friendly. But it brings a change: the zend_object struct can now be variable-length.

This caused me a problem when writing PHPNG back then. In the PHP5 era, many custom objects were defined like this (taking mysqli as an example):

typedef struct _mysqli_object {
    zend_object         zo;
    void                *ptr;
    HashTable           *prop_handler;
} mysqli_object; /* extends zend_object */

That is, zend_object is at the head of the custom inner struct. Of course one benefit of this is that it makes casting very convenient. But since zend_object is now variable-length, and even worse, you don't know how many new property definitions the user will add after inheriting your class in PHP.

So there was no choice; when writing PHPNG, I made a lot of adjustments like the following (a manual task):

typedef struct _mysqli_object {
    void                *ptr;
    HashTable           *prop_handler;
    zend_object         zo;
} mysqli_object; /* extends zend_object */

That is, moving zend_object from the head to the tail. So in order to be able to get the custom object from zend_object, we need to add a new definition:

static inline mysqli_object *php_mysqli_fetch_object(zend_object *obj) {
    return (mysqli_object *)((char*)(obj) - XtOffsetOf(mysqli_object, zo));
}

You should be able to see similar code like this in many extensions that use custom objects.

This way we sidestep the problem. And when actually allocating custom objects, we also need to adopt the following method:

obj = ecalloc(1, sizeof(mysqli_object) + zend_object_properties_size(class_type));

Here, when everyone writes extensions, if you use a custom class, you must be careful about this.

And regarding the guard in PHP5 from before — we also know that not every class declares magic methods. In PHP5, putting the guard in the object wastes memory in most cases. So in PHP7, we decide whether to allocate it based on whether a class declares magic methods (IS_OBJ_HAS_GUARDS), and the specific allocation location was also moved to the end of properties_table:

if (GC_FLAGS(zobj) & IS_OBJ_HAS_GUARDS) {
        guards = Z_PTR(zobj->properties_table[zobj->ce->default_properties_count]);
....
}

This saves the memory allocation of a pointer in most cases.

Finally, in PHP7, getting an object's class becomes very convenient — just use zval.value.obj->ce directly. Some of the handlers customized by a class can also be accessed very conveniently, and the performance improvement is obvious.

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.