注册和使用 PHP 函数

PHP 扩展的主要目标是为用户空间注册新的 PHP 函数。PHP 函数非常复杂,很难完全理解其与 Zend 引擎深度挂钩的机制,但幸运的是,我们不需要这方面的知识,因为 PHP 扩展机制提供了许多方法来抽象出如此复杂的内容。

在扩展中注册和使用新的 PHP 函数是一个简单的步骤。然而,深入了解大局则相当复杂。第一步是阅读 zend_function 章节,这可能会有所帮助。

显然,您需要掌握类型,尤其是 zval 和内存管理。此外,还要了解您的钩子。

zend_function_entry 结构

不要与 zend_function 结构混淆,zend_function_entry 用于在扩展中向引擎注册函数。它如下:

#define INTERNAL_FUNCTION_PARAMETERS zend_execute_data *execute_data, zval *return_value

typedef struct _zend_function_entry {
        const char *fname;
        void (*handler)(INTERNAL_FUNCTION_PARAMETERS);
        const struct _zend_internal_arg_info *arg_info;
        uint32_t num_args;
        uint32_t flags;
} zend_function_entry;

您可以发现这个结构并不复杂。这就是您声明和注册新函数所需的全部内容。让我们一起详细说明:

函数有一个名字:fname。没什么好补充的,你知道它的用途了吧?只是注意到了 const char * 类型。这在引擎中是不适用的。这个 fname 是一个模型,引擎会从中创建一个内部的 zend_string

然后是 handler。这是一个指向将成为该函数主体的 C 代码的函数指针。在这里,我们将使用宏来简化其声明(我们稍后会看到)。在这个函数中,我们将能够解析函数接收的参数,并像任何 PHP 用户空间函数一样生成返回值。请注意,此返回值作为参数传递给我们的处理程序。

参数。arg_info 变量用于声明我们的函数将接受的 API 参数。同样,这部分可能很难深入理解,但我们不需要太深入,我们将再次使用宏来抽象和简化参数声明。您应该知道的是,您不需要在这里声明任何参数才能使函数工作,但强烈建议这样做。我们稍后会回到这一点。参数是 arg_info 的数组,因此其大小作为 num_args 传递。

然后是 flags。我们不会在本章中详细介绍标志。它们是内部使用的,您可以在专门的 zend_function 章节中找到一些详细信息。

注册 PHP 函数

PHP 函数会在加载扩展时注册到引擎中。扩展可以在扩展结构中声明一个函数向量。扩展声明的函数被称为 “内部” 函数,与 “用户” 函数(使用 PHP 用户域声明和使用的函数)相反,它们不会在当前请求结束时取消注册:它们是永久性的。

为了便于阅读,下面是 PHP 扩展结构的缩写:

struct _zend_module_entry {
        unsigned short size;
        unsigned int zend_api;
        unsigned char zend_debug;
        unsigned char zts;
        const struct _zend_ini_entry *ini_entry;
        const struct _zend_module_dep *deps;
        const char *name;
        const struct _zend_function_entry *functions;     /* function declaration vector */
        int (*module_startup_func)(INIT_FUNC_ARGS);
        int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);
    /* ... */
};

您将向 functions 向量传递一个已声明的函数向量。让我们来看一个简单的例子:

/* pib.c */
PHP_FUNCTION(fahrenheit_to_celsius) // 声明函数
{

}

static const zend_function_entry pib_functions[] =
{
    PHP_FE(fahrenheit_to_celsius, NULL) // 使用函数,无参数声明
    PHP_FE_END
};

zend_module_entry pib_module_entry = {
    STANDARD_MODULE_HEADER,
    "pib",
    pib_functions, // 函数数组
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
    "0.1",
    STANDARD_MODULE_PROPERTIES
};

让我们用一个简单的 fahrenheit_too_celsius() 函数来玩玩(这个函数的名字告诉了我们它将执行什么)。

使用 PHP_FUNCTION() 宏可以定义一个函数。后者将接收其参数并展开为正确的结构。然后,我们收集函数符号并将其添加到 pib_functions 向量中。这个向量的类型是 zend_function_entry *,也就是我们的 zend_module_entry 符号所保护的类型。在这个向量中,我们使用 PHP_FE 宏添加 PHP 函数。后者需要 PHP 函数名和一个参数向量,我们暂时将其传递为 NULL

php_pib.h 头文件中,我们应该像 C 语言那样声明我们的函数:

/* pib.h */
PHP_FUNCTION(fahrenheit_to_celsius);

正如您所看到的,声明函数非常简单。宏为我们完成了所有艰巨的工作。下面是相同的代码,但宏已经展开,这样你就可以看看它们的工作了:

/* pib.c */
void zif_fahrenheit_to_celsius(zend_execute_data *execute_data, zval *return_value)
{

}

static const zend_function_entry pib_functions[] =
{
    { "fahrenheit_to_celsius", zif_fahrenheit_to_celsius, ((void *)0),
        (uint32_t) (sizeof(((void *)0))/sizeof(struct _zend_internal_arg_info)-1), 0 },
}

请注意 PHP_FUNCTION() 是如何扩展为以 zif_ 开头的 C 符号的。"zif" 代表 Zend 内部函数(Zend Internal Function),添加到函数名称中是为了防止在编译 PHP 及其模块时出现符号名冲突。因此,我们的 fahrenheit_too_celsius() PHP 函数使用了名为 zif_fahrenheit_too_celsius() 的 C 处理程序。几乎所有的 PHP 函数都是如此。如果查找 “zif_var_dump”,就会看到 PHP var_dump() 源代码函数,等等…​

声明函数参数

到目前为止一切顺利,如果编译扩展并将其加载到 PHP 中,就可以通过反射看到函数的存在:

> ~/php/bin/php -dextension=pib.so --re pib
Extension [ <persistent> extension #37 pib version 0.1 ] {

  - Functions {
    Function [ <internal:pib> function fahrenheit_to_celsius ] {
    }
}

但缺少参数。如果我们要发布一个 fahrenheit_too_celsius($fahrenheit) 函数签名,我们需要一个强制参数。

你必须知道的是,参数声明与函数内部工作无关。这意味着,如果我们现在就编写函数体,这个函数就可以正常工作。即使没有声明参数。

声明参数不是必须的,但强烈建议。参数被反射 API 用于获取函数的相关信息。引擎也会使用参数,尤其是当我们讨论通过引用传递参数或返回引用的函数时。

要声明参数,我们需要熟悉 zend_internal_arg_info 结构:

typedef struct _zend_internal_arg_info {
        const char *name;
        const char *class_name;
        zend_uchar type_hint;
        zend_uchar pass_by_reference;
        zend_bool allow_null;
        zend_bool is_variadic;
} zend_internal_arg_info;

无需详细说明每个字段,但对参数的理解要比这一单独结构复杂得多。幸运的是,我们再次为您提供了一些宏,为您抽象出这项艰巨的工作:

ZEND_BEGIN_ARG_INFO_EX(arginfo_fahrenheit_to_celsius, 0, 0, 1)
    ZEND_ARG_INFO(0, fahrenheit)
ZEND_END_ARG_INFO()

上面的代码详细介绍了如何创建参数,但当我们扩展宏时,会感到有些困难:

static const zend_internal_arg_info arginfo_fahrenheit_to_celsius[] = { \
            { (const char*)(zend_uintptr_t)(1), ((void *)0), 0, 0, 0, 0 },
            { "fahrenheit", ((void *)0), 0, 0, 0, 0 },
    };

我们可以看到,宏创建了一个 zend_internal_arg_info 结构。如果你阅读了这些宏的 API,就会明白一切:

/* API only */
#define ZEND_BEGIN_ARG_INFO_EX(name, _unused, return_reference, required_num_args)
#define ZEND_ARG_INFO(pass_by_ref, name)
#define ZEND_ARG_OBJ_INFO(pass_by_ref, name, classname, allow_null)
#define ZEND_ARG_ARRAY_INFO(pass_by_ref, name, allow_null)
#define ZEND_ARG_CALLABLE_INFO(pass_by_ref, name, allow_null)
#define ZEND_ARG_TYPE_INFO(pass_by_ref, name, type_hint, allow_null)
#define ZEND_ARG_VARIADIC_INFO(pass_by_ref, name)

这组宏允许你处理每一种使用情况。

  • 通过 ZEND_BEGIN_ARG_INFO_EX(),您可以声明您的函数需要接受多少个参数。它还允许声明一个 &return_by_ref() 函数。

  • 然后,每个参数都需要一个 ZEND_ARG_***_INFO()。通过它,您可以判断参数是否为 &$passed_by_ref,以及是否需要类型提示。

如果不知道如何命名参数向量符号,可以使用 “arginfo_[函数名]” 模式。

回到我们的 fahrenheit_to_celsius() 函数,我们声明了一个简单的按值返回函数(非常经典的用例),只有一个名为 fahrenheit 的参数,不通过引用传递(这里也是非常传统的)。

这就创建了 arginfo_fahrenheit_to_celsius 符号,其类型为 zend_internal_arg_info[](一个向量或数组,两者相同),现在我们必须将其用于函数声明中,为其附加一些参数:

PHP_FE(fahrenheit_to_celsius, arginfo_fahrenheit_to_celsius)

这样就完成了,现在反射看到了参数,引擎也知道了在引用不匹配的情况下该怎么做。好极了!

还有其他宏。例如,ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX()。你可以在 Zend/zend_api.h 中的源代码中找到所有这些宏。

C 语言中的 PHP 函数结构和 API

这是一个 PHP 函数,使用 PHP 语言(用户态)进行声明:

function fahrenheit_to_celsius($fahrenheit)
{
    return 5/9 * ($fahrenheit - 32);
}

这是一个简单的函数,便于您理解。下面是用 C 语言编程时的样子:

PHP_FUNCTION(fahrenheit_to_celsius)
{
    /* code to go here */
}

宏扩展,最终生成如下函数:

void zif_fahrenheit_to_celsius(zend_execute_data *execute_data, zval *return_value)
{
    /* code to go here */
}

休息一下,想想其中的主要区别。

首先很奇怪的是,在 C 语言中,函数不会返回任何东西。这是一个 void 声明的函数,在 C 语言中不能返回任何东西。但我们注意到,我们收到了一个名为 return_value 的参数,其类型为 zval *,这似乎很不错。在用 C 编写 PHP 函数时,你会得到指向 zval 的指针作为返回值,并且你需要使用它。这里有更多关于 zval 的资源。

用 C 扩展语言编写 PHP 函数时,返回值是作为参数接收的,而不会从 C 函数体中返回任何东西。

好的,第一点已经解释清楚了。第二点你可能已经猜到了:PHP 函数参数在哪里?$fahreinheit 在哪里?这个问题很难解释清楚,事实上很难解释清楚。

但我们在这里不需要看细节。让我们来解释一下关键概念:

  • 参数已被引擎推入一个堆栈。它们都堆叠在内存的某个地方,彼此相邻。

  • 如果你的函数被调用,这意味着没有阻塞错误,因此你可以浏览参数栈并读取运行时传递的参数。不仅是你声明的参数,还有调用你的函数时传递给它的参数。引擎会为你处理好一切。

  • 要读取参数,你需要一个函数或宏,还需要知道有多少参数被推入堆栈,以便知道何时结束读取。

  • 一切都取决于作为参数接收到的 zend_execute_data *execute_data。但我们现在无法详述。

解析参数:zend_parse_parameters()

要读取参数,请使用 zend_parse_parameters() API(称为 “zpp”)。

在 C 扩展程序中编写 PHP 函数时,由于使用了 zend_parse_parameters() 函数和它的朋友们,你可以接收到 PHP 函数参数。

zend_parse_parameters() 是一个为你在 Zend 引擎堆栈中读取参数的函数。你可以告诉它要读取多少个参数,以及你希望它为你提供哪种类型的服务。根据 PHP 类型转换规则,如果需要且可能的话,该函数会将参数转换成你所要求的类型。如果您需要一个整数,而给定的是一个浮点数,并且如果没有严格的类型提示规则阻止,那么引擎就会将浮点数转换为整数,并将其提供给您。

让我们看看这个函数:

PHP_FUNCTION(fahrenheit_to_celsius)
{
    double f;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "d", &f) == FAILURE) {
        return;
    }

    /* continue */
}

我们希望在 f 变量上得到一个 double。然后我们调用 zend_parse_parameters()

第一个参数是运行时给定的参数数。ZEND_NUM_ARGS() 是一个告诉我们的宏,我们用它来告诉 zpp() 要读取多少个参数。

然后,我们传递一个 const char *,即 “d” 字符串。在这里,你应该为接收的每个参数写一个字母,除了一些特殊情况,这里就不讲了。简单的 “d” 表示 “如果需要,我希望将接收到的第一个参数转换为浮点数(double)”。

然后,在该字符串后传递满足第二个参数所需的尽可能多的 C 实数参数。一个 “d” 表示 “一个 double”,然后你现在传递一个 double 的地址,引擎就会填充它的值。

您总是传递一个指针,指向您希望填充的数据。

您可以在 PHP 源代码中的 README.PARAMETER_PARSING_API 文件中找到关于 zpp() 字符串格式的最新帮助。请仔细阅读,因为这一步可能会把事情弄糟并导致崩溃。务必检查参数,根据所提供的格式字符串传递与预期相同数量和类型的参数变量。要符合逻辑。

请注意参数解析的正常程序。函数 zend_parse_parameters() 成功时应返回 SUCCESS,失败时应返回 FAILURE。失败可能意味着你没有使用 ZEND_NUM_ARGS() 值,而是手动提供了一个值(坏主意),或者你在参数解析时做错了什么。如果是这种情况,那么就应该返回,中止当前函数(C 语言函数应该返回 void,所以只需 return 即可)。

到目前为止一切顺利,我们收到了一个 double。现在让我们执行数学运算并返回结果:

static double php_fahrenheit_to_celsius(double f)
{
    return ((double)5/9) * (double)(f - 32);
}

PHP_FUNCTION(fahrenheit_to_celsius)
{
    double f;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "d", &f) == FAILURE) {
        return;
    }

    RETURN_DOUBLE(php_fahrenheit_to_celsius(f));
}

对您来说,返回值应该很容易,因为您知道 zvals 是如何工作的。您必须填写 return_value

为此,需要使用 RETURN_*() 宏和 RETVAL_() 宏。这两种宏都只是设置 return_value zval 的类型和值,但 RETURN_() 宏会在其后使用 C return 从当前函数返回。

另外,API 还提供了一组宏来处理和解析参数。如果使用 python 风格的指定符,会更易读。

您需要使用以下宏来开始和结束函数参数解析:

ZEND_PARSE_PARAMETERS_START(min_argument_count, max_argument_count) /* takes two parameters */
/* here we will go with argument lists */
ZEND_PARSE_PARAMETERS_END();

可用的参数宏如下:

Z_PARAM_ARRAY()                /* old "a" */
Z_PARAM_ARRAY_OR_OBJECT()      /* old "A" */
Z_PARAM_BOOL()                 /* old "b" */
Z_PARAM_CLASS()                /* old "C" */
Z_PARAM_DOUBLE()               /* old "d" */
Z_PARAM_FUNC()                 /* old "f" */
Z_PARAM_ARRAY_HT()             /* old "h" */
Z_PARAM_ARRAY_OR_OBJECT_HT()   /* old "H" */
Z_PARAM_LONG()                 /* old "l" */
Z_PARAM_STRICT_LONG()          /* old "L" */
Z_PARAM_OBJECT()               /* old "o" */
Z_PARAM_OBJECT_OF_CLASS()      /* old "O" */
Z_PARAM_PATH()                 /* old "p" */
Z_PARAM_PATH_STR()             /* old "P" */
Z_PARAM_RESOURCE()             /* old "r" */
Z_PARAM_STRING()               /* old "s" */
Z_PARAM_STR()                  /* old "S" */
Z_PARAM_ZVAL()                 /* old "z" */
Z_PARAM_VARIADIC()             /* old "+" and "*" */

要添加一个参数作为可选参数,我们使用以下宏:

Z_PARAM_OPTIONAL              /* old "|" */

下面是我们使用基于宏的参数解析方式的示例:

PHP_FUNCTION(fahrenheit_to_celsius)
{
    double f;

    ZEND_PARSE_PARAMETERS_START(1, 1)
        Z_PARAM_DOUBLE(f);
    ZEND_PARSE_PARAMETERS_END();

    RETURN_DOUBLE(php_fahrenheit_to_celsius(f));
}

添加测试

如果你已经阅读过有关测试的章节(参见使用 .phpt 文件进行测试),现在就应该编写一个简单的测试:

--TEST--
Test fahrenheit_to_celsius
--SKIPIF--
<?php if (!extension_loaded("pib")) print "skip"; ?>
--FILE--
<?php
printf("%.2f", fahrenheit_to_celsius(70));
?>
--EXPECTF--
21.11

然后启动 make test

使用常量

让我们举个高级例子。让我们添加一个相反转换的函数:celsius_to_fahrenheit($celsius)

ZEND_BEGIN_ARG_INFO_EX(arginfo_celsius_to_fahrenheit, 0, 0, 1)
    ZEND_ARG_INFO(0, celsius)
ZEND_END_ARG_INFO();

// 摄氏度 到 华氏度转换
static double php_celsius_to_fahrenheit(double c)
{
    return (((double)9/5) * c) + 32 ;
}

PHP_FUNCTION(celsius_to_fahrenheit)
{
    double c;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "d", &c) == FAILURE) {
        return;
    }

    RETURN_DOUBLE(php_celsius_to_fahrenheit(c));
}

static const zend_function_entry pib_functions[] =
{
    PHP_FE(fahrenheit_to_celsius, arginfo_fahrenheit_to_celsius) /* Done above */
    PHP_FE(celsius_to_fahrenheit,arginfo_celsius_to_fahrenheit) /* just added */
    PHP_FE_END
};

现在是一个更复杂的用例,我们先用 PHP 演示,然后再将其作为 C 语言扩展来实现:

const TEMP_CONVERTER_TO_CELSIUS     = 1;
const TEMP_CONVERTER_TO_FAHREINHEIT = 2;

function temperature_converter($temp, $type = TEMP_CONVERTER_TO_CELSIUS)
{
    switch ($type) {
        case TEMP_CONVERTER_TO_CELSIUS:
            return sprintf("%.2f degrees fahrenheit gives %.2f degrees celsius", $temp,
                            fahrenheit_to_celsius($temp));
        case TEMP_CONVERTER_TO_FAHREINHEIT:
            return sprintf("%.2f degrees celsius gives %.2f degrees fahrenheit, $temp,
                            celsius_to_fahrenheit($temp));
        default:
            trigger_error("Invalid mode provided, accepted values are 1 or 2", E_USER_WARNING);
        break;
    }
}

这个例子有助于我们介绍常量(constants)。

常量在扩展中很容易管理,就像在用户界面中一样。常量是持久的,这通常意味着它们的值应该在不同的请求中持久存在。如果你了解 PHP 的生命周期,就应该知道 MINIT() 是向引擎注册常量的正确阶段。

下面是一个常量,内部是一个 zend_constant 结构:

typedef struct _zend_constant {
    zval value;
    zend_string *name;
    int flags;
    int module_number;
} zend_constant;

这是一个非常简单的结构(如果深入研究引擎是如何管理常量的,这可能会成为一场噩梦)。您只需声明一个 name、一个 value、一些 flags(并不多),module_number 会自动设置为您的扩展编号(无需处理)。

要注册常量,这里也没有任何困难,一堆宏就能帮你搞定:

#define TEMP_CONVERTER_TO_FAHRENHEIT 2
#define TEMP_CONVERTER_TO_CELSIUS 1

PHP_MINIT_FUNCTION(pib)
{
    REGISTER_LONG_CONSTANT("TEMP_CONVERTER_TO_CELSIUS", TEMP_CONVERTER_TO_CELSIUS, CONST_CS|CONST_PERSISTENT);
    REGISTER_LONG_CONSTANT("TEMP_CONVERTER_TO_FAHRENHEIT", TEMP_CONVERTER_TO_FAHRENHEIT, CONST_CS|CONST_PERSISTENT);

    return SUCCESS;
}

给 PHP 常量赋予 C 宏的值是一种很好的做法。我们就是这么做的。

根据你的常量类型,你将使用 REGISTER_LONG_CONSTANT()REGISTER_DOUBLE_CONSTANT(),等等…​…​API 和宏位于 Zend/zend_constants.h

标志是 CONST_CS(区分大小写的常量,我们想要的)和 CONST_PERSISTENT(跨请求的持久常量,我们也想要)之间的混合 OR 运算。

现在我们 temperature_converter($temp, $type = TEMP_CONVERTER_TO_CELSIUS) 函数:

ZEND_BEGIN_ARG_INFO_EX(arginfo_temperature_converter, 0, 0, 1)
    ZEND_ARG_INFO(0, temperature)
    ZEND_ARG_INFO(0, mode)
ZEND_END_ARG_INFO();

我们得到了两个强制参数中的一个。这就是我们声明的参数。它的默认值并不是参数声明所能解决的问题,这一点稍后再谈。

然后我们将新函数添加到函数注册向量:

static const zend_function_entry pib_functions[] =
{
    PHP_FE(fahrenheit_to_celsius,arginfo_fahrenheit_to_celsius) /* seen above */
    PHP_FE(celsius_to_fahrenheit,arginfo_celsius_to_fahrenheit) /* seen above */
    PHP_FE(temperature_converter, arginfo_temperature_converter) /* our new function */
}

然后是函数主体:

PHP_FUNCTION(temperature_converter)
{
    double t;
    zend_long mode = TEMP_CONVERTER_TO_CELSIUS;
    zend_string *result;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "d|l", &t, &mode) == FAILURE) {
        return;
    }

    switch (mode)
    {
        case TEMP_CONVERTER_TO_CELSIUS:
            result = strpprintf(0, "%.2f degrees fahrenheit gives %.2f degrees celsius", t, php_fahrenheit_to_celsius(t));
            RETURN_STR(result);
        case TEMP_CONVERTER_TO_FAHRENHEIT:
            result = strpprintf(0, "%.2f degrees celsius gives %.2f degrees fahrenheit", t, php_celsius_to_fahrenheit(t));
            RETURN_STR(result);
        default:
            php_error(E_WARNING, "Invalid mode provided, accepted values are 1 or 2");
    }
}

请务必仔细阅读 README.PARAMETER_PARSING_API。这不是一个很难的 API,你必须熟悉它。

我们使用 “d|l” 作为 zend_parse_parameters() 的参数。一个双参数和一个可选参数(管道 “|”)。注意,如果运行时没有提供可选参数(ZEND_NUM_ARGS() 会提醒我们),zpp() 将不会触及 &mode 变量。这就是我们为该变量提供 TEMP_CONVERTER_TO_CELSIUS 默认值的原因。

然后,我们使用 strpprintf() 创建一个 zend_string,并使用 RETURN_STR() 将其返回到 return_value zval 中。

strpprintf() 及其姊妹函数将在 打印函数 一章中介绍。

使用 Hashtables(PHP 数组)

现在让我们来玩玩 PHP 数组和设计:

function multiple_fahrenheit_to_celsius(array $temperatures)
{
    foreach ($temperatures as $temp) {
        $return[] = fahreinheit_to_celsius($temp);
    }

    return $return;
}

因此,考虑 C 实现,我们需要 zend_parse_parameters() 并只请求一个数组,对其进行迭代,进行数学运算并将结果作为数组添加到 return_value 中:

ZEND_BEGIN_ARG_INFO_EX(arginfo_multiple_fahrenheit_to_celsius, 0, 0, 1)
    ZEND_ARG_ARRAY_INFO(0, temperatures, 0)
ZEND_END_ARG_INFO();

static const zend_function_entry pib_functions[] =
{
        /* ... */
    PHP_FE(multiple_fahrenheit_to_celsius, arginfo_multiple_fahrenheit_to_celsius)
    PHP_FE_END
};

PHP_FUNCTION(multiple_fahrenheit_to_celsius)
{
    HashTable *temperatures;
    zval *data;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "h", &temperatures) == FAILURE) {
        return;
    }
    if (zend_hash_num_elements(temperatures) == 0) {
        return;
    }

    array_init_size(return_value, zend_hash_num_elements(temperatures));

    ZEND_HASH_FOREACH_VAL(temperatures, data)
        zval dup;
        ZVAL_COPY_VALUE(&dup, data);
        convert_to_double(&dup);
    add_next_index_double(return_value, php_fahrenheit_to_celsius(Z_DVAL(dup)));
    ZEND_HASH_FOREACH_END();
}

您需要了解 Hashtables 的工作原理,以及必读的 zval 章节。

在这里,C 部分会更快,因为您不会在 C 代码的循环中调用 PHP 函数,而是调用静态(可能由编译器内联)C 函数,该函数的速度要快几个数量级,并且运行时所需的低级 CPU 指令要少得多。这并不是因为这个小演示函数需要如此多的代码性能,而只是为了记住我们有时使用 C 语言而不是 PHP 的一个原因。

管理引用

现在让我们开始玩 PHP 引用。您从 zval 章节中了解到,引用是引擎中使用的一种特殊技巧。提醒一下,引用(我们指的是 &$php_reference)是存储在 zval 容器中的堆分配的 zval。哈哈。

因此,只要您记住引用是什么以及它们的设计用途,将它们处理到 PHP 函数中并不难。

如果您的函数接受参数作为引用,则必须在参数签名中声明该参数,并从 zend_parse_parameter() 调用中传递引用。让我们像往常一样先看一个 PHP 示例:

function fahrenheit_to_celsius_by_ref(&$fahreinheit)
{
    $fahreinheit = 9/5 * $fahrenheit + 32;
}

所以现在在 C 中,首先我们必须改变我们的 arg_info

ZEND_BEGIN_ARG_INFO_EX(arginfo_fahrenheit_to_celsius, 0, 0, 1)
    ZEND_ARG_INFO(1, fahrenheit)
ZEND_END_ARG_INFO();

1,传入的 ZEND_ARG_INFO() 宏告诉引擎该参数必须通过引用传递。

然后,当我们收到参数时,我们使用 “z” 参数类型,告诉我们想要将其作为 zval * 传递。正如我们向引擎暗示它应该传递给我们一个引用一样,我们将获得对该 zval 的引用,也就是说它将是 IS_REFERENCE 类型。我们只需要取消引用它(即获取存储在 zval 中的 zval),并按原样修改它,因为引用的预期行为是您必须修改引用所携带的值:

PHP_FUNCTION(fahrenheit_to_celsius)
{
    double result;
    zval *param;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "z", &param) == FAILURE) {
        return;
    }

    ZVAL_DEREF(param);
    convert_to_double(param);

    ZVAL_DOUBLE(param, php_fahrenheit_to_celsius(Z_DVAL_P(param)));
}

return_value 默认的值为 NULL,如果我们不去修改它,函数就会返回 PHP 的 NULL