PHP Callables (zend_fcall_info/zend_fcall_info_cache)
A common pattern for custom behaviour is to accept a user callback. As such we need a way to
represent PHP callables in C. For this purpose there exists two structures zend_fcall_info and
zend_fcall_info_cache. zend_fcall_info_cache is a pure representation of a PHP callable, and
should be the structure used to store PHP callables. zend_fcall_info contains all the required
information to perform a call to a PHP callable. We will use the commonly used abbreviation of FCI
and FCC when talking about zend_fcall_info and zend_fcall_info_cache respectively. The most
likely way to encounter those structs is by using the ZPP f argument specifier.
Generic callable API
There is a set of common parameters used within the callable APIs which are:
There are some functions that do not require the use of either the FCI or the FCC structs:
zend_fcall_info_cache
typedef struct _zend_fcall_info_cache {
zend_function *function_handler;
zend_class_entry *calling_scope;
zend_class_entry *called_scope;
zend_object *object; /* Pointer for object representing $this */
zend_object *closure; /* Closure reference, only if the callable *is* the object */
} zend_fcall_info_cache;
function_handler:The actual body of a PHP function that will be used by the VM, can be retrieved from the global function table or a class function table (
zend_class_entry->function_table). May beNULLfor trampolines, see details below.object:If the function is an instance object method, this field represents the value of $this. Should be
NULLfor static methods and global functions.called_scope:The scope in which to call the method, generally it’s
object->ce.calling_scope:The scope in which this call is made, only used by
zend_is_callable_ex()to resolve the actual scope for the deprecated partially supported callables (e.g.[$obj, "parent::method"]). Can be ignored as it is not used when actually calling a function.closure:Storage field for the
Closureobject or the object containing the__invoke()/__call()/__callStatic()method pointed at infunction_handler.
Note
Prior to PHP 8.3.0, the FCC did not contain the closure field, and the relevant FCC API
didn’t exist.
FCCs are the goto structure to handle and store PHP callables as most of the time they don’t need
any reference counting. The one exception is when the FCC represents a trampoline. A trampoline is a
heap-allocated zend_function*, the most common trampolines are calls to a non existing class
method handled by the __call() or __callStatic() magic methods. A trampoline releases the
zend_function* when it is called, as the op array will be consumed. However the f ZPP
argument specifier will free the trampoline and assign NULL to function_handler, making
the FCC uninitialized. The trampoline can be manually fetched by using zend_is_callable() or to
prevent ZPP from freeing it one can use the F argument specifier`. However, in that case careful
consideration need to be done to free the potential trampoline in any failure path, including ZPP
failure.
The only case where an FCC may be uninitialized is if the function is a trampoline, i.e. when the
method of a class does not exist but is handled by the magic methods
__call()/__callStatic(). This is because a trampoline is freed by ZPP as it is a newly
allocated zend_function struct with the op array copied, and is freed when called. To retrieve
it manually use zend_is_callable_ex(). To free a trampoline the void
zend_release_fcall_info_cache(zend_fcall_info_cache *fcc) should be used to release it. This
function can be called even if the FCC is not a trampoline. However, if a reference to the closure
is kept, this must be called prior to freeing the closure, as the trampoline will partially
refer to a zend_function * entry in the closure CE.
Note
There is investigative work being done to simplify the handling of trampoline in the future.
FCC API
The FCC API is defined in Zend/zend_API.h.
zend_fcall_info
typedef struct _zend_fcall_info {
size_t size;
zval function_name;
zval *retval;
zval *params;
zend_object *object;
uint32_t param_count;
uint32_t consumed_args;
/* This hashtable can also contain positional arguments (with integer keys),
* which will be appended to the normal params[]. This makes it easier to
* integrate APIs like call_user_func_array(). The usual restriction that
* there may not be position arguments after named arguments applies. */
HashTable *named_params;
} zend_fcall_info;
size:Mandatory field, which is the size of an FCI structure, thus always:
sizeof(zend_fcall_info)function_name:The actual callable, do not be fooled by the name of this field as this is a leftover when PHP didn’t have objects and class methods. It must be a string zval or an array following the same rules as callables in PHP, namely the first index is a class or instance object, and the second one is the method name. If an initialized FCC representing the function exists, it may be omitted.
object:Only used for callability checks in conjunction with
function_name, seezend_is_callable_ex()for more details. If an initialized FCC representing the function exists, it may be omitted.retval:Mandatory field, which will contain the result of the PHP function.
param_count:Mandatory field, the number of arguments that will be provided to this call to the function.
consumed_args:Specifies positional arguments whose ownership is transfered to the function being called. This is used as an optimization to avoid an unnecessary
Z_ADDREF_P()/zval_ptr_dtor()when the value is not used after the call. Only one argument can be specified at once withzend_fci_consumed_arg(n)(zero-based), and the value must not be a reference. Must be set to0when no parameter applies. New since PHP 8.6.params:contains positional arguments that will be provided to this call to the function. If
param_count = 0, it can beNULL.named_params:A HashTable containing named (or positional) arguments.
FCI API
The FCI API is defined in Zend/zend_API.h.
Deprecated FCI API
There is also a set of FCI API that is now considered deprecated as it is unintuitive and does unnecessary parameter copying.
In general the IS_ARRAY zval should be assigned to the named_params field of the FCI instead of
copying the arguments.