函数
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_info
和 zend_fcall_info_cache
时,我们将分别使用常用的缩写 FCI
和 FCC
。在使用 ZPP f
参数标志时,或者当您需要从扩展中调用 PHP 函数或方法时,您很可能会遇到这些。
zend_fcall_info 结构
在 PHP 7.1.0 之前, |
从 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 之前, |
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 之前,存在一个 |
FCC 未初始化的唯一情况是函数是 trampoline,即当类的方法不存在但由魔术方法 call()
/callStatic()
处理时。这是因为 trampoline 是由 ZPP 释放的,因为它是一个新分配的 zend_function
结构,其中复制了 op 数组,并且在调用时被释放。要手动检索它,请使用 zend_is_callable_ex()
。
仅存储 FCC 不足以在稍后阶段调用用户函数。如果 FCI 中的可调用 zval 是一个对象(因为它具有 |
要确定两个用户函数是否相等,通常只需比较 |
在大多数情况下,FCC 不需要释放,但例外情况是如果 FCC 可能持有 trampoline,则应使用 |
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)
函数直接调用它。
不应使用 |
在更可能的情况下,您只有一个可调用的 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 开始, |
这些函数的缺点是它们将验证 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_function
和 zend_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);
}
后者的具体参数数量变化。
如果您想在对象存在的情况下调用其方法,请使用 |