函数

PHP 函数的主体用 zend_function 结构表示。但是,很少处理它们,因为它们仅供 VM 使用。一般来说,需要处理的是 PHP callable,它们由一对 zend_fcall_info/zend_fcall_info_cache 结构表示。

PHP 回调

处理 C 中的 PHP 函数需要了解以下两个结构 zend_fcall_info/zend_fcall_info_cache。第一个结构必然包含调用函数的信息,例如参数和返回值,但也可能包含实际的可调用函数。后者仅包含可调用函数。在讨论 zend_fcall_infozend_fcall_info_cache 时,我们将分别使用常用的缩写 FCIFCC。在使用 ZPP f 参数标志时,或者当您需要从扩展中调用 PHP 函数或方法时,您很可能会遇到这些。

zend_fcall_info 结构

在 PHP 7.1.0 之前,zend_fcall_info 的实现有很大不同。

从 PHP 8.0.0 开始,zend_fcall_info 具有以下结构:

struct _zend_fcall_info {
    size_t size;
    zval function_name;
    zval *retval;
    zval *params;
    zend_object *object;
    uint32_t param_count;
    /* 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;

让我们详细介绍一下各个 FCI 字段:

size

必填字段,即 FCI 结构的大小,因此始终为:sizeof(zend_fcall_info)

function_name

必填字段,即实际的可调用对象,不要被此字段的名称所迷惑,因为这是 PHP 没有对象和类方法时留下的。它必须是字符串 zval 或数组,遵循与 PHP 中的可调用对象相同的规则,即第一个索引是类或实例对象,第二个索引是方法名称。当且仅当提供了初始化的 FCC 时,它也可以是未定义的。

retval

必填字段,将包含 PHP 函数的结果。

param_count

必填字段,将提供给此函数调用的参数数量。

params

包含将提供给此函数调用的位置参数。如果 param_count = 0,则可以为 NULL

object

要调用存储在 function_name 中的方法名称的对象,如果没有涉及对象,则为 NULL

named_params

包含命名(或位置)参数的 HashTable。

在 PHP 8.0.0 之前,named_params 字段不存在。但是,zend_bool no_separation; 字段存在,它指定数组参数是否应分隔。

zend_fcall_info_cache 结构

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

让我们详细介绍一下 FCC 的各个字段:

function_handler

VM 将使用的 PHP 函数的实际主体,可从全局函数表或类函数表 (zend_class_entry->function_table) 中检索。

object

如果该函数是对象方法,则此字段为相关对象。

called_scope

调用该方法的范围,通常是 object->ce

calling_scope

进行此调用的范围,仅由 VM 使用。

在 PHP 7.3.0 之前,存在一个 initialized 字段。现在,当 function_handler 设置为非空指针时,FCC 被视为已初始化。

FCC 未初始化的唯一情况是函数是 trampoline,即当类的方法不存在但由魔术方法 call()/callStatic() 处理时。这是因为 trampoline 是由 ZPP 释放的,因为它是一个新分配的 zend_function 结构,其中复制了 op 数组,并且在调用时被释放。要手动检索它,请使用 zend_is_callable_ex()

仅存储 FCC 不足以在稍后阶段调用用户函数。如果 FCI 中的可调用 zval 是一个对象(因为它具有 __invoke 方法、是 Closure 或 trampoline),则还必须存储对 zend_object 的引用,增加引用计数,并根据需要释放。此外,如果可调用函数是 trampoline,则必须复制 function_handler 以在调用之间持久化(请参阅 SPL 如何实现自动加载函数的存储)。

要确定两个用户函数是否相等,通常只需比较 function_handlerobjectcalled_scopecalling_scope 和指向闭包的 zend_object 的指针即可。除非用户函数是 trampoline,这是因为 function_handler 会为每次调用重新分配,在这种情况下,需要使用 zend_string_equals() 比较 function_handler->common.function_name 字段,而不是直接比较函数处理程序的指针。

在大多数情况下,FCC 不需要释放,但例外情况是如果 FCC 可能持有 trampoline,则应使用 void zend_release_fcall_info_cache(zend_fcall_info_cache *fcc) 来释放它。此外,如果保留了对闭包的引用,则必须在释放闭包之前调用此方法,因为 trampoline 将部分引用闭包 CE 中的 zend_function * 条目。

Zend Engine 可调用 API

API 位于 Zend_API.h 头文件的各个位置。我们将描述处理 PHP 中的可调用函数所需的各种 API。

首先,要检查 FCI 是否已初始化,请使用 ZEND_FCI_INITIALIZED(fci) 宏。

如果您已正确初始化并设置了可调用函数的 FCI/FCC 对,则可以使用 zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_cache) 函数直接调用它。

不应使用 zend_fcall_info_arg*()zend_fcall_info_call() API。zval *args 参数不会直接设置 FCI 的 params 字段。相反,它期望它是一个包含位置参数的 PHP 数组 (IS_ARRAY zval),这些参数将重新分配到新的 C 数组中。由于 named_params 字段接受位置参数,因此通常最好将此参数的 HashTable 指针简单地分配给此字段。此外,由于用户空间调用的参数是预先确定的并且堆栈分配,因此最好直接分配 paramsparam_count 字段。

在更可能的情况下,您只有一个可调用的 zval,您可以根据用例选择几个不同的选项。

对于一次性调用,call_user_function(function_table, object, function_name, retval_ptr, param_count, params) 和 call_user_function_named(function_table, object, function_name, retval_ptr, param_count, params, named_params) 宏函数可以解决问题。

从 PHP 7.1.0 开始,function_table 参数不再使用,并且应该始终为 NULL

这些函数的缺点是它们将验证 zval 是否确实可调用,并在每次调用时创建 FCI/FCC 对。如果您知道需要多次调用这些函数,最好使用 zend_result zend_fcall_info_init(zval *callable, uint32_t check_flags, zend_fcall_info *fci, zend_fcall_info_cache *fcc, zend_string callable_name, char error) 函数自己创建 FCI/FCC 对。如果此函数返回 FAILURE,则 zval 不是合适的可调用函数。check_flags 被转发到 zend_is_callable_ex(),通常您不想传递任何修改标志,但 IS_CALLABLE_SUPPRESS_DEPRECATIONS 在某些情况下可能会有用。

如果您只有 FCC(或 zend_functionzend_object 的组合),则可以使用以下函数:

/* Call the provided zend_function with the given params.
 * If retval_ptr is NULL, the return value is discarded.
 * If object is NULL, this must be a free function or static call.
 * called_scope must be provided for instance and static method calls. */
ZEND_API void zend_call_known_function(
            zend_function *fn, zend_object *object, zend_class_entry *called_scope, zval *retval_ptr,
            uint32_t param_count, zval *params, HashTable *named_params);

/* Call the provided zend_function instance method on an object. */
static zend_always_inline void zend_call_known_instance_method(
            zend_function *fn, zend_object *object, zval *retval_ptr,
            uint32_t param_count, zval *params)
{
        zend_call_known_function(fn, object, object->ce, retval_ptr, param_count, params, NULL);
}

后者的具体参数数量变化。

如果您想在对象存在的情况下调用其方法,请使用 zend_call_method_if_exists() 函数。