资源类型:zend_resource

尽管 PHP 确实可以摆脱 “资源” 类型,因为自定义对象存储允许构建任何抽象类型数据的 PHP 表示,但该资源类型仍然存在于当前版本的 PHP 中,您可能需要处理它。

如果您需要创建资源,我们真的希望您不要这样做,而是使用对象及其自定义存储管理。对象是可以嵌入任何类型的 PHP 类型。然而,由于历史原因,PHP 仍然知道那个特殊的类型 “资源”,并且仍然在其核心或某些扩展中使用它。让我们一起看看这个类型。但要注意,它真的很神秘,历史悠久,所以不要对它的设计感到惊讶,尤其是在阅读它的源代码时。

什么是 Resource 类型

你知道的,这很简单。我们在这里讨论这个问题:

$fp = fopen('/proc/cpuinfo', 'r');
var_dump($fp); /* resource(2) of type (stream) */

在内部,资源绑定到 zend_resource 结构类型:

struct _zend_resource {
        zend_refcounted_h gc;
        int               handle;
        int               type;
        void             *ptr;
};

我们找到了传统的 zend_refcounted_h 标头,这意味着资源是可引用计数的。

handle 是一个整数,引擎内部使用它来将资源定位到内部资源表中。它用作此类表的键。

type 用于将相同类型的资源重新组合在一起。这涉及资源被销毁的方式以及如何从句柄中取回它们。

最后,zend_resource 中的 ptr 字段是您的抽象数据。请记住,资源是关于存储无法容纳任何 PHP 可以本机表示的数据类型的抽象数据(但对象可以,就像我们之前所说的那样)。

资源类型和资源销毁

资源必须注册一个析构函数。当用户在 PHP 用户空间中使用资源时,他们通常不会在不再使用时清理它们。例如,看到 fopen() 调用而看不到匹配的 fclose() 调用的情况并不少见。使用 C 语言,这充其量是个坏主意,最多是一场灾难。但使用像 PHP 这样的高级语言,事情就会变得简单。

作为内部开发人员,您必须做好准备,因为用户会创建大量您允许他使用的资源,而不会正确清理它们并释放内存/操作系统资源。因此,您必须注册一个析构函数,该函数将在引擎即将销毁该类型的资源时被调用。

析构函数按类型分组,资源本身也是如此。您不会将析构函数应用于类型为 “数据库” 的资源,而是应用于类型为 “文件” 的资源。

还存在两种资源,这里再次区分了它们的生命周期。

  • 最常用的传统资源不会在多个请求中持续存在,它们的析构函数在请求关闭时被调用。

  • 持久性资源将在多个请求中持续存在,并且仅在 PHP 进程终止时才会被销毁。

您可能对 PHP 生命周期章节 感兴趣,该章节向您展示了 PHP 进程生命周期中发生的不同步骤。此外,Zend 内存管理器章节 可能有助于理解持久内存分配和请求绑定内存分配的概念。

玩转资源

资源相关的 API 可以在 zend/zend_list.c 中找到。你可能会发现其中存在一些不一致的地方,比如将 “资源” 说成了 “列表”。

创建资源

要创建资源,必须先为其注册一个析构函数,并使用 zend_register_list_destructors_ex() 将其与资源类型名称关联。该调用将返回一个整数,表示您注册的资源类型。您必须记住该整数,因为稍后您将需要它从用户那里取回您的资源。

之后,您可以使用 zend_register_resource() 注册新资源。它将返回一个 zend_resource。让我们一起看一个简单的用例示例:

#include <stdio.h>

int res_num;
FILE *fp;
zend_resource *my_res;
zval my_val;

static void my_res_dtor(zend_resource *rsrc)
{
    fclose((FILE *)rsrc->ptr);
}

/* module_number should be your PHP extension number here */
res_num = zend_register_list_destructors_ex(my_res_dtor, NULL, "my_res", module_number);
fp      = fopen("/proc/cpuinfo", "r");
my_res  = zend_register_resource((void *)fp, res_num);

ZVAL_RES(&my_val, my_res);

在上面的代码中,我们使用 libcfopen() 打开一个文件,并将返回的指针存储到资源中。在此之前,我们注册了一个析构函数,当调用该析构函数时,它将对指针使用 libcfclose()。然后,我们向引擎注册资源,并将资源传递到可以返回到用户空间的 zval 容器中。

Zvals 章节可以在 这里 找到。

必须记住的是资源类型。在这里,我们注册一个类型为 “my_res” 的资源。这是类型名称。引擎实际上并不关心类型名称,而是类型标识符,即 zend_register_list_destructors_ex() 返回的整数。你应该把它记在某个地方,就像我们在 res_num 变量中所做的那样。

正在取回资源

现在我们注册了一个资源并将其放入 zval 中作为示例,我们应该学习如何从用户空间中取回该资源。请记住,资源存储在 zval 中。资源中存储着资源类型编号(在类型字段中)。因此,要从用户那里取回我们的资源,我们必须从 zval 中提取 zend_resource,并调用 zend_fetch_resource() 来取回我们的 FILE * 指针:

/* ... later on ... */

zval *user_zval = /* fetch zval from userland, assume type IS_RESOURCE */

ZEND_ASSERT(Z_TYPE_P(user_zval) == IS_RESOURCE); /* just a check to be sure */

fp = (FILE *)zend_fetch_resource(Z_RESVAL_P(user_zval), "my_res", res_num);

就像我们所说的:从用户那里获取一个 zval(类型为 IS_RESOURCE),并通过调用 zend_fetch_resource() 从中获取资源指针。

该函数将检查资源的类型是否是您作为第三个参数传递的类型(此处为 res_num)。如果是,它会提取您需要的 void * 资源指针,然后我们就完成了。如果不是,它会抛出一个警告,如 “提供的资源不是有效的 {type name} 资源”。例如,如果您期望资源类型为 “my_res”,并且您得到的 zval 具有类型为 “gzip” 的资源,例如 gzopen() PHP 函数返回的资源,则可能会发生这种情况。

资源类型只是引擎将不同类型的资源(类型为 “文件”、“gzip” 甚至 “mysql 连接”)混合到同一个资源表中的一种方式。资源类型有名称,以便可以在错误消息或调试语句中使用(如 var_dump($my_resource)),并且它们还表示为内部使用的整数,用于从中取回资源指针,并使用资源类型注册析构函数。

如您所见,如果我们使用对象,它们本身就代表类型,就不必执行从标识符中获取资源并验证其类型的步骤。对象是自描述类型。但资源对于当前 PHP 版本来说仍然是有效的数据类型。

资源的引用计数

与许多其他类型一样,zend_resource 是引用计数的。我们可以看到它的 zend_refcounted_h 标头。如果您需要它(一般来说您不需要它),下面是使用引用计数的 API:

  • zend_list_delete(zend_resource *res):减少引用计数,如果降至零,则销毁资源。

  • zend_list_free(zend_resource *res): 检查引用计数是否为零,如果为真,则销毁资源。

  • zend_list_close(zend_resource *res):无论条件如何,都会调用资源析构函数。

持久资源

持久资源不会在请求结束时被销毁。其经典用例是持久数据库连接。这些连接会从一个请求回收到另一个请求(会带来所有麻烦)。

传统上,您不应该使用持久资源,因为一个请求与另一个请求不同。在这样做之前,重复使用同一资源确实应该深思熟虑。

要注册持久资源,请使用持久析构函数而不是经典析构函数。这是在调用 zend_register_list_destructors_ex() 时完成的,该 API 如下:

zend_register_list_destructors_ex(rsrc_dtor_func_t destructor, rsrc_dtor_func_t persistent_destructor,
                                  const char *type_name, int module_number);