了解 PHP 扩展和扩展骨架

这里我们详细介绍了 PHP 扩展是什么样子的,以及如何使用一些工具生成框架。这样我们就可以使用框架代码并对其进行破解,而不是从头开始手动创建每个需要的部分。

我们还将详细介绍您可以/应该如何组织扩展文件、引擎如何加载它们,以及您需要了解的有关 PHP 扩展的所有内容。

引擎如何加载扩展

您还记得 有关构建 PHP 扩展的章节,因此您知道如何编译/构建并安装它。

您可以构建 静态编译 的扩展,这些扩展是 PHP 核心的一部分,并融入其中。它们不是以 .so 文件的形式表示,而是以链接到最终 PHP 可执行文件 (ELF) 的 .o 对象的形式表示。因此,此类扩展无法禁用,它们是 PHP 可执行体代码的一部分:无论您说什么、做什么,它们都在这里。某些扩展需要静态构建,例如 ext/coreext/standardext/splext/mysqlnd(非详尽列表)。

您可以通过查看编译 PHP 时生成的 main/internal_functions.c 来找到静态编译扩展的列表。此步骤在有关构建 PHP 的章节中有详细介绍。

然后,您还可以构建动态加载的扩展。这些是著名的 extension.so 文件,它们在单个编译过程结束时诞生。动态加载的扩展具有运行时可插入和拔出的优势,并且不需要重新编译所有 PHP 即可启用或禁用。缺点是 PHP 进程启动时间较长,因为它必须加载 .so 文件。但这只是几毫秒的问题,您实际上不会因此而受到影响。

动态加载扩展的另一个缺点是扩展加载顺序。某些扩展可能需要先加载其他扩展。虽然这不是一个好的做法,但我们会看到 PHP 扩展系统允许您声明依赖项以掌握这种顺序,但依赖项通常是一个坏主意,应该避免。

最后一件事:PHP 静态编译的扩展在动态编译的扩展之前启动。这意味着它们的 MINIT()extensions.so 文件的 MINIT() 之前被调用。

当 PHP 启动时,它会快速解析其不同的 INI 文件。如果存在,则稍后的那些可以使用 “extension=some_ext.so” 行引用声明要加载的扩展。然后,PHP 收集从 INI 配置解析的每个扩展,并尝试按照添加到 INI 文件的顺序加载它们,直到某些扩展声明了一些依赖项(然后将在之前加载)。

如果您使用操作系统包管理器,您可能已经注意到,打包程序通常使用标题编号来命名其扩展文件,例如 00_ext.ini、01_ext.ini 等……这是为了掌握扩展的加载顺序。一些不常见的扩展需要以特定的顺序运行。我们想提醒您,依赖其他扩展先于您的扩展加载是一个坏主意。

要加载扩展,需要使用 libdl 及其 dlopen()/dlsym() 函数。

所查找的符号是 get_module() 符号,这意味着您的扩展必须将其导出才能加载。通常情况如此,就好像您使用了骨架脚本(我们稍后会预见到),然后使用 ZEND_GET_MODULE(your_ext) 宏生成代码,如下所示:

#define ZEND_GET_MODULE(name) \
    BEGIN_EXTERN_C()\
    ZEND_DLEXPORT zend_module_entry *get_module(void) { return &name##_module_entry; }\
    END_EXTERN_C()

如您所见,该宏在使用它时声明了一个全局符号:get_module() 函数,一旦引擎想要加载您的扩展,它就会被调用。

PHP 用于加载扩展的源代码位于 ext/standard/dl.c

什么是 PHP 扩展?

PHP 扩展(不要与 Zend 扩展混淆)是通过使用 zend_module_entry 结构来设置的:

struct _zend_module_entry {
    unsigned short size;                                /*
    unsigned int zend_api;                               * STANDARD_MODULE_HEADER
    unsigned char zend_debug;                            *
    unsigned char zts;                                   */

    const struct _zend_ini_entry *ini_entry;            /* Unused */
    const struct _zend_module_dep *deps;                /* Module dependencies */
    const char *name;                                   /* Module name */
    const struct _zend_function_entry *functions;       /* Module published functions */

    int (*module_startup_func)(INIT_FUNC_ARGS);         /*
    int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);     *
    int (*request_startup_func)(INIT_FUNC_ARGS);         * Lifetime functions (hooks)
    int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS);    *
    void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS);       */

    const char *version;                                /* Module version */

    size_t globals_size;                                /*
#ifdef ZTS                                               *
    ts_rsrc_id* globals_id_ptr;                          *
#else                                                    * Globals management
    void* globals_ptr;                                   *
#endif                                                   *
    void (*globals_ctor)(void *global);                  *
    void (*globals_dtor)(void *global);                  */

    int (*post_deactivate_func)(void);                   /* Rarely used lifetime hook */
    int module_started;                                  /* Has module been started (internal usage) */
    unsigned char type;                                  /* Module type (internal usage) */
    void *handle;                                        /* dlopen() returned handle */
    int module_number;                                   /* module number among others */
    const char *build_id;                                /* build id, part of STANDARD_MODULE_PROPERTIES_EX */
};

前四个参数已在构建扩展章节中解释过。它们通常使用 STANDARD_MODULE_HEADER 宏填充。

ini_entry 向量实际上未使用。您可以使用特殊宏注册 INI 条目。

然后您可以声明依赖项,这意味着您的扩展可能需要在它之前加载另一个扩展,或者可以声明与其他扩展的冲突。这是使用 deps 字段完成的。实际上,这很少使用,而且更普遍的是,在 PHP 扩展之间创建依赖项是一种不好的做法。

之后您声明一个名称。不用说,这个名称是您的扩展的名称(可以与其自己的 .so 文件的名称不同)。注意在大多数操作中名称区分大小写,我们建议您使用简短、小写且没有空格的名称(以使事情变得更容易一些)。

然后是函数字段。它是指向扩展想要注册到引擎中的一些 PHP 函数的指针。我们在专门的章节中讨论了这一点。

接下来是 5 个生命周期钩子。请参阅它们的专门章节。

您的扩展可以使用版本字段发布版本号,以 char * 的形式。此字段仅作为扩展信息的一部分读取,即通过 phpinfo() 或反射 API 作为 ReflectionExtension::getVersion()

接下来我们会看到很多关于全局变量的字段。全局变量管理有一个专门的章节。

最后,结束字段通常是 STANDARD_MODULE_PROPERTIES 宏的一部分,您不必手动处理它们。引擎将为您提供一个 module_number 用于其内部管理,扩展类型将设置为 MODULE_PERSISTENT。它可能是 MODULE_TEMPORARY,就好像您的扩展是使用 PHP 的用户空间 dl() 函数加载的一样,但这种用例非常罕见,不适用于每个 SAPI,临时扩展通常会导致引擎出现许多问题。

用脚本生成扩展骨架

现在我们将了解如何生成扩展骨架,以便您可以用一些最少的内容和结构开始新的扩展,而不必从头开始手动创建。

骨架生成器脚本位于 php-src/ext/ext_skel 中,它用作模板的结构存储在 php-src/ext/skeleton

随着 PHP 版本的进步,脚本和结构也会略有变化。

您可以分析这些脚本来了解它们的工作原理,但基本用法是:

> cd /tmp
/tmp> /path/to/php/ext/ext_skel --skel=/path/to/php/ext/skeleton --extname=pib
[ ... generating ... ]
/tmp> tree pib/
pib/
├── config.m4
├── config.w32
├── CREDITS
├── EXPERIMENTAL
├── php_pib.h
├── pib.c
├── pib.php
└── tests
    └── 001.phpt
/tmp>

您可以看到生成的结构非常基本,而且是最小的。您已经在构建扩展章节中了解到,扩展的待编译文件必须声明到 config.m4 中。骨架仅生成了 <your-ext-name>.c 文件。例如,我们将扩展命名为 “pib”,因此我们得到了一个 pib.c 文件,并且我们必须取消注释 config.m4 中的 –enable-pib 行才能对其进行编译。

每个 C 文件都带有一个头文件(通常)。这里的结构是 php<your-ext-name>.h_ ,因此对我们来说是 php_pib.h。不要更改该名称,构建系统需要头文件采用这样的命名约定。

您可以看到还生成了一个最小的测试结构。

让我们打开 pib.c。在那里,所有内容都被注释掉了,所以我们在这里不需要写太多行。

基本上,我们可以看到引擎加载我们的扩展所需的模块符号发布在这里:

#ifdef COMPILE_DL_PIB
#ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
#endif
ZEND_GET_MODULE(pib)
#endif

如果您将 –enable-<my-ext-name> 标志传递给配置脚本,则会定义 COMPILE_DL_<YOUR-EXT-NAME> 宏。我们还看到,在 ZTS 模式下,TSRM 本地存储指针被定义为 ZEND_TSRMLS_CACHE_DEFINE() 宏的一部分。

之后,没有什么可说的了,因为所有内容都已被注释掉,您应该很清楚。

扩展骨架生成器的新时代

由于此提交和扩展骨架生成器采用了新样式:

它现在将在没有 Cygwin 和其他无用东西的 Windows 上运行。它不再包含生成 XML 文档的方法(PHP 文档实用程序已经在 phpdoc/doc-base 下的 svn 中获得了用于此目的的工具),并且它不再支持函数存根(stubs)。

以下是可用选项:

php ext_skel.php --ext <name> [--experimental] [--author <name>]
                 [--dir <path>] [--std] [--onlyunix]
                 [--onlywindows] [--help]

  --ext <name>              The name of the extension defined as <name>
  --experimental    Passed if this extension is experimental, this creates
                        the EXPERIMENTAL file in the root of the extension
  --author <name>       Your name, this is used if --header is passed and
                        for the CREDITS file
  --dir <path>              Path to the directory for where extension should be
                        created. Defaults to the directory of where this script
                    lives
  --std                     If passed, the standard header and vim rules footer used
                    in extensions that is included in the core, will be used
  --onlyunix                Only generate configure scripts for Unix
  --onlywindows             Only generate configure scripts for Windows
  --help                This help

新的扩展骨架生成器将生成具有三个固定功能的骨架,您可以根据需要定义任何其他功能并更改具体主体。

请记住,新的 ext_skel 不再支持 proto 文件。

发布 API

如果我们打开头文件,我们可以看到这些行:

#ifdef PHP_WIN32
#   define PHP_PIB_API __declspec(dllexport)
#elif defined(__GNUC__) && __GNUC__ >= 4
#   define PHP_PIB_API __attribute__ ((visibility("default")))
#else
#   define PHP_PIB_API
#endif

这些行定义了一个名为 PHP_<EXT-NAME>_API(对于我们来说为 PHP_PIB_API)的宏,它解析为 GCC 自定义属性可见性(“默认”)。

在 C 中,您可以告诉链接器隐藏最终对象中的每个符号。这就是在 PHP 中所做的,对于每个符号,而不仅仅是静态符号(根据定义,静态符号未发布)。

默认的 PHP 编译行告诉编译器隐藏每个符号并且不导出它们。

然后,您应该 “unhide” 您希望扩展发布的符号,以便在其他扩展或最终 ELF 文件的其他部分中使用。

请记住,您可以在 Unix 下使用 nm 读取 ELF 的已发布和隐藏符号。

我们无法在这里深入解释这些概念,也许这样的链接可以帮助您?

因此,基本上,如果您希望自己的 C 符号可供其他扩展公开使用,则应使用特殊的 PHP_PIB_API 宏进行声明。传统的用例是发布类符号(zend_class_entry* 类型),以便其他扩展可以挂接到您自己发布的类中并替换它们的一些处理程序。

请注意,这只适用于传统 PHP。如果您使用 Linux 发行版的 PHP,这些 PHP 已修补为在加载时解析符号,而不是延迟解析,因此该技巧不起作用。