我以前对于 C 语言的印象是有很强的确定性,而 PHP 在执行的时候会被翻译为 C 语言执行,所以一直很好奇 PHP 怎么调用底层函数。
换句话说就是已知函数名字的情况下如何调用 C 语言中对应名字的函数?
解决这个问题前,首先根据过往的经验做出假设,然后再去验证。
之前在写《用 C 语言实现面向对象》的时候,就意识到使用 void 指针实现很多功能,包括指向任意的函数。接着在写《PHP 数组底层实现》的时候,了解了 HashTable 的实现,即在 C 语言层面通过字符串 key 找到任意类型值。
现在把两者结合起来,是否就能解决以上问题了?比如说把函数名作为 HashTable 的 key,函数指针作为 HashTable 的 value,这样就可以通过函数名获取函数指针来调用函数了。
接下来通过查看 PHP 的源码来看这个假设与真实情况有多少差距。
总体分为三个步骤:
- 从 PHP 层进入 C 语言层
- 找到字符串函数名与函数的关系
- 函数的调用
注:这篇博客的源码对应的版本是 PHP 7.4.4 。
从 PHP 层进入 C 语言层
首先要找到 C 语言层调用函数的地方。怎么找?
经常使用 PHP 的同学看到前面的问题描述很容易联想到 PHP 中的一个传入函数名及其参数就可以调用函数的函数 call_user_func()
。可以从这里入手。
怎么找到 call_user_func()
在 PHP 源码中的位置?这就要根据 PHP 源码的规律来找了。
当然也可以直接全代码搜索,只是比较慢。
PHP 源码里面在定义一个 PHP 函数的时候会用 PHP_FUNCTION(函数名)
,所以只要找到 PHP_FUNCTION(call_user_func)
就可以了。
另外 call_user_func()
不像 array_column()
这种函数有特定前缀 array_
,所以属于比较基础的函数,而 PHP 的基础函数会放在两个地方:
- 内置函数,放在
Zend/zend_buildin_functions.c
; - 标准库函数,放在
ext/standard/
。
举个例子:ext/standard/array.c
里有array_column()
之类的函数。
在这两个地方搜索就能找到 PHP_FUNCTION(call_user_func)
,如下:
ext/standard/basic_functions.c
PHP_FUNCTION(call_user_func)
{
// ...
if (zend_call_function(&fci, &fci_cache) == SUCCESS && Z_TYPE(retval) != IS_UNDEF) {
// ...
}
}
现在我们已经从 PHP 层面进入到 C 语言层面,接下去就是在 C 语言代码里面探索了。
找到字符串函数名与函数的关系
从上文展示位于 ext/standard/basic_functions.c
的 call_user_func()
函数定义可以找到关键点 zend_call_function()
,现在要找到这个函数。
这种以 zend_
开头的函数都在 Zend/
文件夹底下,所以我们要换个目录了。
在 Zend/
文件夹里面随便搜索 zend_call_function
,从搜索结果里面随便挑一个跳转,然后通过 IDE 的功能(ctrl + 鼠标左键)跳转到它定义的地方就可以了。
如果 IDE 能直接跳转就不用在
Zend/
文件夹搜索了,这里是因为 VS Code 没法直接跳转。
注:以下代码中的 // ...
都表示我省略了一部分代码,但我会尽量保持代码结构。
第一遍看代码的时候不需要掌握所有细节,只需要了解整体概念或者前后关系,否则会陷入细节无法自拔。
Zend/zend_execute_API.c
int zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_cache) /* {{{ */
{
// ...
if (!fci_cache || !fci_cache->function_handler) {
// ...
if (!zend_is_callable_ex(&fci->function_name, fci->object, IS_CALLABLE_CHECK_SILENT, NULL, fci_cache, &error)) {
// ...
}
// ...
}
func = fci_cache->function_handler;
// ...
call = zend_vm_stack_push_call_frame(call_info,
func, fci->param_count, object_or_called_scope);
// ...
if (func->type == ZEND_USER_FUNCTION) {
// ...
} else if (func->type == ZEND_INTERNAL_FUNCTION) {
// ...
func->internal_function.handler(call, fci->retval);
// ...
} else {
// ...
}
// ...
return SUCCESS;
}
/* }}} */
这里的关键点在于和函数名以及函数调用相关的词。关键词有:
- function name
- call
- return value
上面的代码片段中,我把几个有可能的点抽出来了。从这几个点出发,往前追溯参数来源或者查看后面使用它的地方就行了。
如果被这个函数里面大量的
EG(...)
吸引而想知道其内部结构的话,就离结果非常近了。如果没有被其吸引,那也没关系,继续看。
优先深入看哪个呢?根据以前看数组源码的经验, “查找” 这个行为更容易获得信息