4.2. PHP 扩展初探
深入了解 PHP 扩展和扩展框架
在这里,我们会详细介绍 PHP 扩展是什么样的,以及如何使用一些工具生成框架。这允许我们使用框架代码并且修改它,而不是从头开始手动创建每个需要的部分。
我们也将详细介绍你可以/应该如何组织你的扩展文件,引擎如何加载它们,还有基本上了解有关 PHP 扩展的所有信息。
引擎如何加载扩展
你若记得关于构建 PHP 扩展的章节,便知道如何编译/构建并安装它。
你可以构建静态编译扩展,那、这些是 PHP 的核心并且融入其中。它们不是表示为 .so 的文件,而是表示链接最终的 PHP 可执行(ELF)的 .o 对象。因此,此类扩展不可以禁用,它们是 PHP 可执行主体代码的一部分:无论你如何说和做, 它们都在这里面。某些扩展要求静态构建,即,ext/core,ext/standard,ext/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.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 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_
宏。我们也看到在 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_
的宏(对我们来说是 PHP_PIB_API
),并解析为 GCC 自定义属性可见性(“默认”)。
在 C 语言,你可以告诉链接器从最终对象中隐藏每个符号。这是用 PHP 做的,对每个符号,不止是静态符号(根据定义,这些符号均未发布)。
警告
默认 PHP 编译行告诉我们编译器隐藏了每个符号而不导出它们。
然后,你想要你的扩展发布给其他扩展或其他部分最终 ELF 文件使用的话,你应该“不隐藏”符号。
注意
记住,你可以在 Unix 下使用
nm
阅读 ELF 已发布和隐藏符号。
我们无法深入解释这些概念,也许下面的链接可以帮助你?
- gcc.gnu.org/wiki/Visibility
- www.iecc.com/linker/linker10.html
- www.akkadia.org/drepper/dsohowto.p…
- www.faqs.org/docs/Linux-HOWTO/Progr…
- developer.apple.com/library/conten…
基本上,如果你想要你的 C 符号对其他扩展公开有效,你应该使用特殊的 PHP_PIB_API
宏声明。传统的用例是发布类符号(zend_class_entry*
类型),以便其他扩展可以挂载你的已发布类,并替换它们的一些句柄。
注意
请注意,这仅在传统 PHP 有效。如果你使用 Linux 发行版的 PHP,这些补丁是在加载时为了解析符号,而不是懒惰符号,因此不起作用。