4.2. PHP 扩展初探

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

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

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

引擎如何加载扩展

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

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

  1. #define ZEND_GET_MODULE(name)
  2. BEGIN_EXTERN_C()
  3. ZEND_DLEXPORT zend_module_entry *get_module(void) { return &name##_module_entry; }
  4. END_EXTERN_C()

如你所见,该宏使用时声明了一个全局符号:get_module() 函数,一旦加载扩展,将通过引擎调用该函数。

注意

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

什么是 PHP 扩展?

PHP 扩展,不要和 Zend extension 混淆,它是通过使用 zend_module_entry 结构来设置的:

  1. struct _zend_module_entry {
  2. unsigned short size; /*
  3. unsigned int zend_api; * STANDARD_MODULE_HEADER
  4. unsigned char zend_debug; *
  5. unsigned char zts; */
  6. const struct _zend_ini_entry *ini_entry; /* 没用过 */
  7. const struct _zend_module_dep *deps; /* 模块依赖 */
  8. const char *name; /* 模块名称 */
  9. const struct _zend_function_entry *functions; /* 模块发布函数 */
  10. int (*module_startup_func)(INIT_FUNC_ARGS); /*
  11. int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS); *
  12. int (*request_startup_func)(INIT_FUNC_ARGS); * 生命周期函数(钩子)
  13. int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS); *
  14. void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS); */
  15. const char *version; /* 模块版本 */
  16. size_t globals_size; /*
  17. #ifdef ZTS *
  18. ts_rsrc_id* globals_id_ptr; *
  19. #else * Globals management
  20. void* globals_ptr; *
  21. #endif *
  22. void (*globals_ctor)(void *global); *
  23. void (*globals_dtor)(void *global); */
  24. int (*post_deactivate_func)(void); /* 很少使用的生命周期钩子 */
  25. int module_started; /* 是否已启动模块(内部使用) */
  26. unsigned char type; /* 模块类型(内部使用) */
  27. void *handle; /* dlopen() 返回句柄 */
  28. int module_number; /* 模块号 */
  29. const char *build_id; /* 构建编号, STANDARD_MODULE_PROPERTIES_EX 的一部分*/
  30. };

前四个参数已经在构建扩展章节解释过了。它们通常使用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 的发展,脚本和结构移也有一些变化。

你可以分析那些脚本是如何工作的,但是基本的用法是:

  1. > cd /tmp
  2. /tmp> /path/to/php/ext/ext_skel --skel=/path/to/php/ext/skeleton --extname=pib
  3. [ ... generating ... ]
  4. /tmp> tree pib/
  5. pib/
  6. ├── config.m4
  7. ├── config.w32
  8. ├── CREDITS
  9. ├── EXPERIMENTAL
  10. ├── php_pib.h
  11. ├── pib.c
  12. ├── pib.php
  13. └── tests
  14. └── 001.phpt
  15. /tmp>

你可以看见一个非常基本的、最小的结构生成了。 你已经学习过构建扩展章节,扩展的待编译文件一定要声明为 config.m4 。该框架只生成 .c 文件。例如,我们将扩展名为 “pib”,因此得到一个 pib.c 文件,并且我们必须取消 config.m4 中的 –enable-pib 注释,让它能被编译。

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

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

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

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

  1. #ifdef COMPILE_DL_PIB
  2. #ifdef ZTS
  3. ZEND_TSRMLS_CACHE_DEFINE()
  4. #endif
  5. ZEND_GET_MODULE(pib)
  6. #endif

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

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

扩展框架生成器的新时代

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

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

这里是有效的选项:

  1. php ext_skel.php --ext <name> [--experimental] [--author <name>]
  2. [--dir <path>] [--std] [--onlyunix]
  3. [--onlywindows] [--help]
  4. --ext <name> The name of the extension defined as <name>
  5. --experimental Passed if this extension is experimental, this creates
  6. the EXPERIMENTAL file in the root of the extension
  7. --author <name> Your name, this is used if --header is passed and
  8. for the CREDITS file
  9. --dir <path> Path to the directory for where extension should be
  10. created. Defaults to the directory of where this script
  11. lives
  12. --std If passed, the standard header and vim rules footer used
  13. in extensions that is included in the core, will be used
  14. --onlyunix Only generate configure scripts for Unix
  15. --onlywindows Only generate configure scripts for Windows
  16. --help This help

新的框架生成器将生成具有固定三个功能的框架,你可以定义其他函数,并且将具体的主体改成你想要的。

注意

记住新的 ext_skel 不再支持原型文件。

发布 API

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

  1. #ifdef PHP_WIN32
  2. # define PHP_PIB_API __declspec(dllexport)
  3. #elif defined(__GNUC__) && __GNUC__ >= 4
  4. # define PHP_PIB_API __attribute__ ((visibility("default")))
  5. #else
  6. # define PHP_PIB_API
  7. #endif

这些定义了名为 PHP__API的宏(对我们来说是 PHP_PIB_API),并解析为 GCC 自定义属性可见性(“默认”)。

在 C 语言,你可以告诉链接器从最终对象中隐藏每个符号。这是用 PHP 做的,对每个符号,不止是静态符号(根据定义,这些符号均未发布)。

警告

默认 PHP 编译行告诉我们编译器隐藏了每个符号而不导出它们。

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

注意

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

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

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

注意

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