添加新功能

到此为止,你应该已经了解了 PHP 扩展的基本结构和构建过程。现在,我们将学习如何实现新的基本 PHP 扩展功能。从本节开始,我将用红色写入新代码,并将现有代码保留为黑色。

函数

函数是添加新功能的最简单基元。要实现一个新函数,我们首先要编写函数代码本身。这是一段以 PHP_FUNCTION() 宏和函数名开头的普通 C 代码。这段代码需要一个双精度数值参数,并返回一个按因子 2 缩放的参数。稍后我将介绍参数解析 API 和大多数用于操作数值的宏。

PHP_FUNCTION(test_scale)
{
    double x;

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

    RETURN_DOUBLE(x * 2);
}

我们还必须定义参数描述块。该代码块从 ZEND_BEGIN_ARG_INFO() 宏(或其变体)开始,以 ZEND_END_ARG_INFO() 宏结束。

ZEND_BEGIN_ARG_INFO() 的第一个参数是 arg_info 结构的名称。该名称应在 PHP_FE() 宏中重复使用。

第二个参数被忽略。(在 PHP 5 中,它意味着通过引用传递其余参数。) 每个参数都由 ZEND_ARG_INFO() 宏定义,该宏接收 "通过引用传递" 值和参数名称。

ZEND_BEGIN_ARG_INFO(arginfo_test_scale, 0)
    ZEND_ARG_INFO(0, x)
ZEND_END_ARG_INFO()

还可以使用 ARG_INFO 宏的扩展变量来指定额外的参数和返回类型提示、空值、通过引用返回、所需参数数、参数数可变的函数等。

最后,我们必须将新函数添加到扩展函数列表中:

static const zend_function_entry test_functions[] = {
    PHP_FE(test_test1, arginfo_test_test1)
    PHP_FE(test_test2, arginfo_test_test2)
    //--begin-- 新增
    PHP_FE(test_scale, arginfo_test_scale)
    //--end-- 新增
    PHP_FE_END
};

在 PHP 的 Zend 引擎中,ZEND_BEGIN_ARG_INFO() 和 ZEND_PARSE_PARAMETERS_START() 都是用于处理函数参数的宏,但它们的用途和作用范围不同。下面详细解释它们的区别和用途。

  • ZEND_BEGIN_ARG_INFO()

    ZEND_BEGIN_ARG_INFO() 是一个用于描述函数参数信息的宏,通常用于定义 PHP 函数的参数信息,帮助引擎在函数调用时进行正确的参数解析和类型检查。

    用途

    描述参数信息:用于描述一个 PHP 函数的参数信息,包括参数的类型、是否可选、是否传递引用等。

    生成参数信息表

    生成一个用于参数解析的表格,供 Zend 引擎在运行时使用。

    示例

    ZEND_BEGIN_ARG_INFO(arginfo_my_function, 0)
        ZEND_ARG_INFO(0, param1)
        ZEND_ARG_INFO(0, param2)
    ZEND_END_ARG_INFO()

    在这个例子中:

    • arginfo_my_function 是参数信息表的名称。

    • 0 表示函数不是对象方法。

    • ZEND_ARG_INFO(0, param1) 表示第一个参数 param1 不是通过引用传递。

    • ZEND_ARG_INFO(0, param2) 表示第二个参数 param2 也不是通过引用传递。

    ZEND_BEGIN_ARG_INFO() 通常与 zend_function_entry 结构体一起使用,以注册扩展中的函数。

  • ZEND_PARSE_PARAMETERS_START()

    ZEND_PARSE_PARAMETERS_START() 是一个用于解析函数参数的宏,通常在函数实现中用于提取和验证传递给函数的参数。

    用途

    参数解析

    用于从 zend_parse_parameters 中解析函数参数。

    参数验证

    在解析过程中进行参数类型和数量的验证。

    示例

    PHP_FUNCTION(my_function)
    {
        zend_string *param1;
        zend_long param2;
    
        ZEND_PARSE_PARAMETERS_START(2, 2)
            Z_PARAM_STR(param1)
            Z_PARAM_LONG(param2)
        ZEND_PARSE_PARAMETERS_END();
    
        // 函数体实现
    }

    在这个例子中:

    • ZEND_PARSE_PARAMETERS_START(2, 2) 表示函数期望接收 2 个参数,最少和最多都是 2 个。

    • Z_PARAM_STR(param1) 表示第一个参数应该是一个字符串,并将其解析到 param1 变量中。

    • Z_PARAM_LONG(param2) 表示第二个参数应该是一个长整型,并将其解析到 param2 变量中。

    • ZEND_PARSE_PARAMETERS_END() 用于结束参数解析过程。

    用法

    ZEND_PARSE_PARAMETERS_START() 通常在 PHP 函数的实现中使用,以确保传递给函数的参数是有效的,并将它们解析到相应的 C 变量中。

区别总结

  1. 用途不同:

    • ZEND_BEGIN_ARG_INFO():用于描述函数的参数信息,主要在函数注册时使用。

    • ZEND_PARSE_PARAMETERS_START():用于解析和验证函数的实际参数,主要在函数实现时使用。

  2. 作用范围:

    • ZEND_BEGIN_ARG_INFO():生成参数信息表,供 Zend 引擎在运行时进行参数解析和类型检查。

    • ZEND_PARSE_PARAMETERS_START():在函数实现中解析传递给函数的参数,并进行类型和数量验证。

  3. 使用时机:

    • ZEND_BEGIN_ARG_INFO():在定义函数的参数信息时使用,通常在扩展的注册阶段。

    • ZEND_PARSE_PARAMETERS_START():在实际处理函数调用时使用,通常在函数实现阶段。

示例总结

ZEND_BEGIN_ARG_INFO()
ZEND_BEGIN_ARG_INFO(arginfo_my_function, 0)
    ZEND_ARG_INFO(0, param1)  // 参数1:字符串
    ZEND_ARG_INFO(0, param2)  // 参数2:长整型
ZEND_END_ARG_INFO()

const zend_function_entry my_functions[] = {
    PHP_FE(my_function, arginfo_my_function)
    PHP_FE_END
};
ZEND_PARSE_PARAMETERS_START()
PHP_FUNCTION(my_function)
{
    zend_string *param1;
    zend_long param2;

    ZEND_PARSE_PARAMETERS_START(2, 2)
        Z_PARAM_STR(param1)
        Z_PARAM_LONG(param2)
    ZEND_PARSE_PARAMETERS_END();

    // 函数体实现
}

扩展重建和安装后,新功能应该开始工作。

$ php -r 'var_dump(test_scale(5));'
float(10)

要在命名空间中声明函数,可以使用 ZEND_NS_FUNCTION(ns, name) 代替 PHP_FUNCTION(name),也可以使用 ZEND_NS_FE(ns, name, arg_info) 代替 PHP_FE(name, arg_info)

还可以添加一些函数标志(例如,添加 ZEND_ACC_DEPRECATED 标志的废弃函数),使用 ZEND_FENTRY() 代替 PHP_FE()。主扩展 API 参见 Zend/zend_API.h

扩展回调

只有 PHP 扩展函数可以纯声明方式实现。所有其他扩展功能都可以在 PHP 启动时调用特殊的 API 函数来实现。为此,扩展应实现 MINIT() 回调。这也是一个普通的 C 语言函数,以 PHP_MINIT_FUNCTION() 宏和扩展名称作为参数。该函数应返回 SUCCESS,以便将 PHP 扩展连接到内核,并启用其所有功能和其它特性。我们的 MINIT 函数只是初始化线程本地存储缓存。以前,这段代码是从 RINIT 中调用的,但如果在 MINIT 中执行某些操作,并直接或间接访问模块全局变量或通用全局变量,最好将这段代码移到 MINIT 的开头。

PHP_MINIT_FUNCTION(test)
{
#if defined(ZTS) && defined(COMPILE_DL_TEST)
    ZEND_TSRMLS_CACHE_UPDATE();
#endif

    return SUCCESS;
}

MINIT 回调地址应添加到模块入口结构中。如果 RINIT 回调仅用于线程本地存储,且现在为空,也可以将其删除。

zend_module_entry test_module_entry = {
    STANDARD_MODULE_HEADER,
    "test",                /* Extension name */
    test_functions,        /* zend_function_entry */
    PHP_MINIT(test),       /* PHP_MINIT - Module initialization --新增 */
    NULL,                  /* PHP_MSHUTDOWN - Module shutdown */
    NULL,                  /* PHP_RINIT - Request initialization --新增 */
    NULL,                  /* PHP_RSHUTDOWN - Request shutdown */
    PHP_MINFO(test),       /* PHP_MINFO - Module info */
    PHP_TEST_VERSION,      /* Version */
    STANDARD_MODULE_PROPERTIES
};

MINIT 类似,当需要在 PHP 终止前释放一些资源时,也可以执行其他回调,如 MSHUTDOWN

常量

MINIT 回调适用于添加各种扩展实体,如新的内部常量。这需要使用 REGISTER_LONG_CONSTANT() 宏,其中第一个参数是常量名称,第二个参数是常量值,第三个参数是常量标志:

  • CONST_CS 指区分大小写的常量名称。

  • CONST_PERSISTENT 指持久常量。(所有内部扩展常量都应是持久常量)。

PHP_MINIT_FUNCTION(test)
{
#if defined(ZTS) && defined(COMPILE_DL_TEST)
    ZEND_TSRMLS_CACHE_UPDATE();
#endif

    // --- start 新增
    REGISTER_LONG_CONSTANT("TEST_SCALE_FACTOR", 2, CONST_CS | CONST_PERSISTENT);
    // --- end
    return SUCCESS;
}

扩展重建和重新安装后,您可以访问新的常量。

$ php -r 'var_dump(TEST_SCALE_FACTOR);'
int(2)

当然,还有其他各种 API 宏来声明不同值类型的常量:

  • REGISTER_NULL_CONSTANT(name, flags):值为 NULL 的常量。

  • REGISTER_BOOL_CONSTANT(name, bval, flags):FALSE 或 TRUE。

  • REGISTER_LONG_CONSTANT(name, lval, flags):任意长数字。

  • REGISTER_DOUBLE_CONSTANT(name, dval, flags):一个双数值。

  • REGISTER_STRING_CONSTANT(name, str, flags):以零结尾的字符串。

  • REGISTER_STRINGL_CONSTANT(name,str,len,flags):字符串(带长度)。

也可以使用类似的 REGISER_NS_…​() 宏来声明某些命名空间中的常量。有关完整的 PHP 常量 API,请参见 Zend/zend_constants.h

模块全局变量

目前,我们的新 scale() 函数是纯功能性的。它不依赖于任何内部状态,并且始终只根据输入参数返回一个值。然而,现实世界中有些函数会访问或更新全局状态。在 C 语言中,它通常存储在全局变量中,但多线程软件必须使用一些技巧来区分不同线程的状态。

PHP 通常是在单线程环境中运行的,但也可以配置为多线程。无论如何,为了使全局变量能够在不同的配置中移植,PHP 建议将全局变量声明为一个特别声明的 "module globals" 结构。

应在 php_test.h 文件末尾添加以下代码:

ZEND_BEGIN_MODULE_GLOBALS(test)
    zend_long scale;
ZEND_END_MODULE_GLOBALS(test)

ZEND_EXTERN_MODULE_GLOBALS(test)

#define TEST_G(v) ZEND_MODULE_GLOBALS_ACCESSOR(test, v)

模块全局变量是在 ZEND_BEGIN_MODULE_GLOBALSZEND_END_MODULE_GLOBALS 宏之间声明的。这些只是普通的 C 声明。实际上,C 预处理器会将它们转换为 zend_tests_globlas C 结构定义。ZEND_EXTERN_MODULE_GLOBLAS() 定义了访问该结构的外部 C 语言名称,而 TEST_G() 宏则提供了访问模块全局变量的方法。因此,我们将使用 TEST_G(scale) 来代替全局变量 scale

我们应该在 test.c 文件中声明真正的模块全局变量:

ZEND_DECLARE_MODULE_GLOBALS(test)

我们还将定义一个 GINIT 回调来初始化该结构。该回调会在 MINIT 之前调用,因此我们必须将线程本地存储缓存的初始化代码移至此处:

static PHP_GINIT_FUNCTION(test)
{
#if defined(COMPILE_DL_BCMATH) && defined(ZTS)
    ZEND_TSRMLS_CACHE_UPDATE();
#endif
    test_globals->scale= 1;
}

我们还应在扩展入口结构中添加有关模块全局变量及其初始化回调的信息:

zend_module_entry test_module_entry = {
    STANDARD_MODULE_HEADER,
    "test", /* Extension name */
    test_functions, /* zend_function_entry */
    PHP_MINIT(test), /* PHP_MINIT - Module initialization */
    NULL, /* PHP_MSHUTDOWN - Module shutdown */
    NULL, /* PHP_RINIT - Request initialization */
    NULL, /* PHP_RSHUTDOWN - Request shutdown */
    PHP_MINFO(test), /* PHP_MINFO - Module info */
    PHP_TEST_VERSION, /* Version /
    // ----start 新增
    PHP_MODULE_GLOBALS(test), /* Module globals */
    PHP_GINIT(test), /* PHP_GINIT – Globals initialization */
    NULL, /* PHP_GSHUTDOWN – Globals shutdown */
    NULL,
    STANDARD_MODULE_PROPERTIES_EX
    // ----end
};

现在我们可以更新 scale 函数,使用模块全局变量:

PHP_FUNCTION(test_scale)
{
    double x;

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

    //新增 TEST_G(scale)
    RETURN_DOUBLE(x * TEST_G(scale));
}

重新编译和重新安装扩展后,它就能正常工作了:

$ php -r 'var_dump(test_scale(5));'
float(5)

配置指令

我们还能做什么?我们可以通过 php.ini 中的配置指令来定义 scale 因子的值。这可以通过 test.c. 中的两段附加代码来实现。

第一段定义了配置指令、其名称、默认值、类型和存储位置:

PHP_INI_BEGIN()
    STD_PHP_INI_ENTRY("test.scale", "1", PHP_INI_ALL, OnUpdateLong, scale, zend_test_globals, test_globals)
PHP_INI_END()

STD_PHP_INI_ENTRY() 声明了一个名为 test.scale 的配置指令,默认值为 "1"。PHP_INI_ALL 表示可以随时修改该配置指令(在 php.ini、每个目录的配置文件和脚本执行过程中的 ini_set() 函数中)。而 PHP_INI_SYSTEM 则只允许在 PHP 启动时(php.ini 中)修改。PHP_INI_PERDIR 只允许在 php.ini 和每个目录的配置文件中修改。

OnUpdateLong 是一个常用的回调,用于设置指令的整数值。(scale "是模块的全局变量名称。zend_test_globals 是保存模块全局变量的结构名(C 类型名)。test_globals 是全局变量,用于保存非线程安全构建的模块全局变量。

完整的 PHP.ini API 定义在 Zend/zend_ini.hmain/php_ini.h

第二部分调用了一个 API 函数,用于注册前一个代码块中声明的指令:

PHP_MINIT_FUNCTION(test)
{
    //---start 新增
    REGISTER_INI_ENTRIES();
    //---end

    return SUCCESS;
}

PHP 配置指令可以通过 -d 命令行参数设置:

$ php -d test.scale=4 -r 'var_dump(test_scale(5));'
float(20)

常见的 php 全局

除了我们自己的全局变量外,我们可能还需要从常见的 PHP 全局变量中获取一些值,这些全局变量被封装在类似的模块全局结构中,并可以通过类似的宏来访问:

  • CG(name):编译器全局变量。

  • EG(name):执行器全局变量。

所有声明可在 Zend/zend_globals.h 中查看。