4.2. PHP 扩展初探

未匹配的标注

深入了解 PHP 扩展和扩展框架

在这里,我们会详细介绍 PHP 扩展是什么样的,以及如何使用一些工具生成框架。这允许我们使用框架代码并且修改它,而不是从头开始手动创建每个需要的部分。

我们也将详细介绍你可以/应该如何组织你的扩展文件,引擎如何加载它们,还有基本上了解有关 PHP 扩展的所有信息。

引擎如何加载扩展

你若记得关于构建 PHP 扩展的章节,便知道如何编译/构建并安装它。

你可以构建静态编译扩展,那、这些是 PHP 的核心并且融入其中。它们不是表示为 .so 的文件,而是表示链接最终的 PHP 可执行(ELF)的 .o 对象。因此,此类扩展不可以禁用,它们是 PHP 可执行主体代码的一部分:无论你如何说和做, 它们都在这里面。某些扩展要求静态构建,即,ext/coreext/standardext/spl 和 ext/mysqlnd (非详尽列表)。

你可以通过在 main/internal_functions.c 查找静态编译扩展的列表,该文件是在编译 PHP 时生成的。此步骤在构建 PHP 章节中详细介绍。

然后,你也可以构建动态加载扩展。那些是著名的 extension.so 文件是在单个编译过程的最后产生的。动态加载扩展具有在运行时可插拔的优点,并且不需要重新编译所有 PHP 即可禁用或启用。缺点是当它必须加载 .so 文件时,PHP 进程启动时间更长。但是这只是几毫秒,你不会感到困扰。

动态加载扩展的另一个缺点是扩展加载顺序。某些扩展可能需要先加载其他扩展。尽管这不是一个好的习惯,我们也可以看到 PHP 扩展系统允许你声明依赖来执行这样的顺序,但是依赖通常是一个坏主意,应该避免。

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

当 PHP 启动,它很快去解析其不同的 INI 文件。如果有的话,在之后可使用 “extension=some_ext.so” 声明要加载的扩展。然后PHP 收集从 INI 配置解析出的每个扩展,并且尝试以同样添加在 INI 文件的顺序加载它们,直到某些扩展声明了某些依赖(依赖将在它之前加载)。

注意

如果你使用操作系统软件包管理器,你可能注意到,软件包通常使用标题编号(即 00_ext.ini01_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 extension 混淆,它是通过使用 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;            /* 没用过 */
    const struct _zend_module_dep *deps;                /* 模块依赖 */
    const char *name;                                   /* 模块名称 */
    const struct _zend_function_entry *functions;       /* 模块发布函数 */

    int (*module_startup_func)(INIT_FUNC_ARGS);         /*
    int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);     *
    int (*request_startup_func)(INIT_FUNC_ARGS);         * 生命周期函数(钩子)
    int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS);    *
    void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS);       */

    const char *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);                   /* 很少使用的生命周期钩子 */
    int module_started;                                  /* 是否已启动模块(内部使用) */
    unsigned char type;                                  /* 模块类型(内部使用) */
    void *handle;                                        /* dlopen() 返回句柄 */
    int module_number;                                   /* 模块号 */
    const char *build_id;                                /* 构建编号, STANDARD_MODULE_PROPERTIES_EX 的一部分*/
};

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

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

然后你可以声明依赖关系,这意味着你的扩展可能需要先加载另一个扩展,或者声明与另一个扩展的冲突。使用 deps 字段可以完成。事实上,这是非常常见的用法,更普遍的做法是,通过 PHP 扩展创建依赖,这是个坏习惯。

之后,你声明一个 name。不用说,这是你的扩展名(可以不同于它的 .so 文件)。在大多数操作下,注意大小写敏感,我们建议你使用缩写,小写,无空格(使操作更容易)。

然后是 functions 字段。它是扩展想要注册到引擎的某些 PHP 函数的指针。我们将在专门章节讨论。

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

你的扩展可以使用 version 字段将版本号发布为 char *。该字段作为扩展信息的一部分读取,即通过 phpinfo() 或者像 ReflectionExtension::getVersion()的反射 API读取。

接下来,我们将看到很多关于全局变量的字段。全局管理有专门章节介绍。

最后,结尾字段通常是STANDARD_MODULE_PROPERTIES宏的一部分,不用你手动去操作它们。引擎会为你提供一个module_number进行内部管理,并且扩展类型将会设置到 MODULE_PERSISTENT。就像你的扩展使用 PHP 的用户区 dl() 函数加载一样,它可以是 MODULE_TEMPORARY,但是该用例很少见的,不适用每个 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 。该框架只生成 .c 文件。例如,我们将扩展名为 “pib”,因此得到一个 pib.c 文件,并且我们必须取消 config.m4 中的 –enable-pib 注释,让它能被编译。

每个 C 文件通常都会附带头文件。这里的结构是 php_.h,所以对我们来说就是 php_pib.h。不要更改它的名字,构建系统希望头文件有这样的命名约定。

你可以看见一个最小的测试结构生成了。

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

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

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

如果你通过了配置脚本的 –enable- 标志,则定义了 COMPILE_DL_<YOUR-EXT-NAME> 宏。我们也看到在 ZTS 模式的情况下,TSRM 本地存储指针定义为 ZEND_TSRMLS_CACHE_DEFINE() 宏的一部分。

之后,没有什么好说的,因为所有的内容都注释了,对你来说应该很清楚。

扩展框架生成器的新时代

自从此提交以来,扩展框架生成器有了新的风格:

它现在可以运行在 Windows 而不需要 Cygwin 和其他没意义的东西。它不再包含生成 XML 文档的方法(PHP 文档程序已经在 phpdoc/doc-base 下的 svn 获得用于该文档的工具),并且它不再支持函数桩。

这里是有效的选项:

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 不再支持原型文件。

发布 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 编译行告诉我们编译器隐藏了每个符号而不导出它们。

然后,你想要你的扩展发布给其他扩展或其他部分最终 ELF 文件使用的话,你应该“不隐藏”符号。

注意

记住,你可以在 Unix 下使用 nm 阅读 ELF 已发布和隐藏符号。

我们无法深入解释这些概念,也许下面的链接可以帮助你?

基本上,如果你想要你的 C 符号对其他扩展公开有效,你应该使用特殊的 PHP_PIB_API 宏声明。传统的用例是发布类符号(zend_class_entry* 类型),以便其他扩展可以挂载你的已发布类,并替换它们的一些句柄。

注意

请注意,这仅在传统 PHP 有效。如果你使用 Linux 发行版的 PHP,这些补丁是在加载时为了解析符号,而不是懒惰符号,因此不起作用。

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
上一篇 下一篇
Summer
贡献者:1
讨论数量: 0
发起讨论 只看当前版本


暂无话题~