3.7 KiB
Refcounting Tips
One of the trickiest parts of the C extension for PHP is getting the refcounting right. These are some notes about the basics of what you should know, especially if you're not super familiar with PHP's C API.
These notes cover the same general material as the Memory Management chapter of the PHP internal's book, but calls out some points that were not immediately clear to me.
Zvals
In the PHP C API, the zval
type is roughly analogous to a variable in PHP, eg:
// Think of $a as a "zval".
$a = [];
The equivalent PHP C code would be:
zval a;
ZVAL_NEW_ARR(&a); // Allocates and assigns a new array.
PHP is reference counted, so each variable -- and thus each zval -- will have a reference on whatever it points to (unless its holding a data type that isn't refcounted at all, like numbers). Since the zval owns a reference, it must be explicitly destroyed in order to release this reference.
zval a;
ZVAL_NEW_ARR(&a);
// The destructor for a zval, this must be called or the ref will be leaked.
zval_ptr_dtor(&a);
Whenever you see a zval
, you can assume it owns a ref (or is storing a
non-refcounted type). If you see a zval*
, which is also quite common, then
this is pointing to something that owns a ref, but it does not own a ref
itself.
The ZVAL_*
family of
macros
initializes a zval
from a specific value type. A few examples:
ZVAL_NULL(&zv)
: initializes the value tonull
ZVAL_LONG(&zv, 5)
: initializes azend_long
(integer) valueZVAL_ARR(&zv, arr)
: initializes azend_array*
value (refcounted)ZVAL_OBJ(&zv, obj)
: initializes azend_object*
value (refcounted)
Note that all of our custom objects (messages, repeated fields, descriptors,
etc) are zend_object*
.
The variants that initialize from a refcounted type do not increase the refcount. This makes them suitable for initializing from a newly-created object:
zval zv;
ZVAL_OBJ(&zv, CreateObject());
Once in a while, we want to initialize a zval
while also increasing the
reference count. For this we can use ZVAL_OBJ_COPY()
:
zend_object *some_global;
void GetGlobal(zval *zv) {
// We want to create a new ref to an existing object.
ZVAL_OBJ_COPY(zv, some_global);
}
Transferring references
A zval
's ref must be released at some point. While zval_ptr_dtor()
is the
simplest way of releasing a ref, it is not the most common (at least in our code
base). More often, we are returning the zval
back to PHP from C.
zval zv;
InitializeOurZval(&zv);
// Returns the value of zv to the caller and donates our ref.
RETURN_COPY_VALUE(&zv);
The RETURN_COPY_VALUE()
macro (standard in PHP 8.x, and polyfilled in earlier
versions) is the most common way we return a value back to PHP, because it
donates our zval
's refcount to the caller, and thus saves us from needing to
destroy our zval
explicitly. This is ideal when we have a full zval
to
return.
Once in a while we have a zval*
to return instead. For example when we parse
parameters to our function and ask for a zval
, PHP will give us pointers to
the existing zval
structures instead of creating new ones.
zval *val;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "z", &val) == FAILURE) {
return;
}
// Returns a copy of this zval, adding a ref in the process.
RETURN_COPY(val);
When we use RETURN_COPY
, the refcount is increased; this is perfect for
returning a zval*
when we do not own a ref on it.