让我来教你 PHP 函数调用

我以前对于 C 语言的印象是有很强的确定性,而 PHP 在执行的时候会被翻译为 C 语言执行,所以一直很好奇 PHP 怎么调用底层函数。

换句话说就是已知函数名字的情况下如何调用 C 语言中对应名字的函数?

解决这个问题前,首先根据过往的经验做出假设,然后再去验证。

之前在写《用 C 语言实现面向对象》的时候,就意识到使用 void 指针实现很多功能,包括指向任意的函数。接着在写《PHP 数组底层实现》的时候,了解了 HashTable 的实现,即在 C 语言层面通过字符串 key 找到任意类型值。

现在把两者结合起来,是否就能解决以上问题了?比如说把函数名作为 HashTable 的 key,函数指针作为 HashTable 的 value,这样就可以通过函数名获取函数指针来调用函数了。

接下来通过查看 PHP 的源码来看这个假设与真实情况有多少差距。

总体分为三个步骤:

  1. 从 PHP 层进入 C 语言层
  2. 找到字符串函数名与函数的关系
  3. 函数的调用

注:这篇博客的源码对应的版本是 PHP 7.4.4 。

https://github.com/php/php-src/tree/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(...) 吸引而想知道其内部结构的话,就离结果非常近了。如果没有被其吸引,那也没关系,继续看。

优先深入看哪个呢?根据以前看数组源码的经验, “查找” 这个行为更容易获得信息

 

 

更多更全干货 请点击这里

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值