Zend 扩展
PHP 有两种扩展:
-
PHP 扩展,最常用
-
Zend 扩展,不太常用,允许其他钩子
本章将详细介绍 Zend 扩展和 PHP 扩展之间的主要区别,何时应该使用其中一种而不是另一种,以及如何构建混合扩展,即同时是 PHP 和 Zend 的扩展(以及为什么这样做)。
PHP 与 Zend 扩展之间的区别
简单说一下。在 PHP 的源代码中,PHP 扩展被命名为 “PHP 模块”,而 Zend 扩展则被称为 “Zend 扩展”。
因此,如果您读到 “extension” 关键字,您应该首先想到 Zend 扩展。如果您读到 “module” 关键字,您可能会想到 PHP 扩展。
在传统生活中,我们谈论的是 “PHP 扩展” 与 “Zend 扩展”。
它们的区别在于加载方式:
-
PHP 扩展(又名 PHP “模块”)在 INI 文件中加载为 “extension=pib.so” 行
-
Zend 扩展在 INI 文件中加载为 “zend_extension=pib.so” 行
这是我们从 PHP 用户空间看到的唯一可见区别。
但从内部角度来看,情况就不同了。
什么是 Zend 扩展?
首先,Zend 扩展的编译和加载方式与 PHP 扩展相同。因此,如果您尚未阅读构建 PHP 扩展章节,您应该查看一下,因为它也适用于 Zend 扩展。
如果还没有完成,请获取一些有关 PHP 扩展 的信息,因为我们将在这里与它们进行比较。Zend 扩展与 PHP 扩展共享很大一部分概念。 |
下面是一个 Zend 扩展。请注意,您需要发布两个结构,而不是一个,以便引擎加载您的 Zend 扩展:
/* Main Zend extension structure */
struct _zend_extension {
char *name; /*
char *version; * Some infos
char *author; *
char *URL; *
char *copyright; */
startup_func_t startup; /*
shutdown_func_t shutdown; * Specific branching lifetime points
activate_func_t activate; * ( Hooks )
deactivate_func_t deactivate; */
message_handler_func_t message_handler; /* Hook called on zend_extension registration */
op_array_handler_func_t op_array_handler; /* Hook called just after Zend compilation */
statement_handler_func_t statement_handler; /*
fcall_begin_handler_func_t fcall_begin_handler; * Hooks called through the Zend VM as specific OPCodes
fcall_end_handler_func_t fcall_end_handler; */
op_array_ctor_func_t op_array_ctor; /* Hook called on OPArray construction */
op_array_dtor_func_t op_array_dtor; /* Hook called on OPArray destruction */
int (*api_no_check)(int api_no); /* Checks against zend_extension incompatibilities
int (*build_id_check)(const char* build_id); */
op_array_persist_calc_func_t op_array_persist_calc; /* Hooks called if the zend_extension extended the
op_array_persist_func_t op_array_persist; * OPArray structure and has some SHM data to declare
*/
void *reserved5; /*
void *reserved6; * Do what you want with those free pointers
void *reserved7; *
void *reserved8; */
DL_HANDLE handle; /* dlopen() returned handle */
int resource_number; /* internal number used to manage that extension */
};
/* Structure used when the Zend extension gets loaded into the engine */
typedef struct _zend_extension_version_info {
int zend_extension_api_no;
char *build_id;
} zend_extension_version_info;
与往常一样,阅读源代码。Zend 扩展被管理到 Zend/zend_extension.c(和 |
正如您所注意到的,Zend 扩展比 PHP 扩展更复杂,因为它们有更多的钩子,并且这些钩子更接近 Zend 引擎及其虚拟机(整个 PHP 源代码中最复杂的部分)。
为什么需要 Zend 扩展?
我们提醒你:除非你对 PHP 内部的虚拟机有非常深入的了解,除非你需要深入研究它,否则你不需要 Zend 扩展,而 PHP 扩展就足够了。
目前,PHP 世界中最常见的 Zend 扩展包括 opcache
、Xdebug
、phpdbg
和 Blackfire
。但除此之外,你还知道几十个 PHP 扩展,不是吗?这充分说明了:
-
对于很大一部分问题,你不需要 Zend 扩展
-
Zend 扩展也可以用作 PHP 扩展(稍后会详细介绍)
-
PHP 扩展仍然可以做很多事情。
-
通常,Zend 扩展需要用于两种任务:调试器和分析器。
与 PHP 扩展类似,Zend 扩展没有 骨架生成器。 |
使用 Zend 扩展,没有生成器,没有帮助。Zend 扩展仅供高级程序员使用,它们更难理解,具有更深层的引擎行为,通常需要对 PHP 的内部机制有深入的了解。 |
基本上,如果您需要创建调试器(debugger),您将需要一个 Zend 扩展。对于分析器(profiler),您可以将其制作为传统的 PHP 扩展,这些扩展可以正常工作,具体取决于您的需求。
此外,如果您需要掌握扩展加载顺序,Zend 扩展将有所帮助(我们将看到这一点)。
最后,如果您的目标 “只是” 向 PHP 添加一些新概念(函数、类、常量等),您将使用 PHP 扩展,但如果您需要更改 PHP 的当前行为,Zend 扩展可能会更好。
我们无法在此给出规则,但我们可以解释所有这些东西的工作原理,以便您自己了解 Zend 扩展相对于 PHP 扩展所带来的功能。
此外,您可以创建一个 混合(hybrid) 扩展,它既是 Zend 扩展又是 PHP 扩展(这很棘手,但完全有效,允许您同时在两个 “世界” 中编程)。
API 版本和冲突管理
您知道,PHP 扩展在加载之前会检查几条规则,以了解它们是否与您尝试加载它们的 PHP 版本兼容。这已在 有关构建 PHP 扩展的章节 中详细说明。
对于 Zend 扩展,适用相同的规则,但略有不同:它将使用您发布的 zend_extension_version_info
结构来了解要做什么。
您声明的 zend_extension_version_info
结构仅包含引擎在开始加载 Zend 扩展时将使用的两个信息:
-
ZEND_EXTENSION_API_NO
-
ZEND_EXTENSION_BUILD_ID
加载 Zend 扩展时会检查 ZEND_EXTENSION_API_NO
。但不同之处在于,如果此数字与您的 Zend 扩展不匹配,您仍有机会加载。引擎将调用您的 api_no_check()
钩子(如果您声明了),并将当前 PHP 运行时 ZEND_EXTENSION_API_NO
传递给它。在这里,您必须告知您是否支持该 API 编号,只需将该信息返回给引擎即可。如果不支持,引擎将不会加载您的扩展并打印一条警告消息。
这同样适用于其他 ABI 设置,例如 ZEND_DEBUG
或 ZTS
。如果存在不匹配,PHP 扩展将拒绝加载,而 Zend 扩展有机会加载,因为引擎会检查 build_id_check()
钩子并向其传递 ZEND_EXTENSION_BUILD_ID
。在这里,您再次说明是否兼容。同样,如果您选择 “否”,引擎将不会加载您的扩展并打印一条警告消息。
请记住,我们在 有关构建 PHP 扩展的章节 中详细介绍了 API 和 ABI 的编号方式。
这些强制执行引擎操作的能力在实践中很少使用。
你看 Zend 扩展与 PHP 扩展相比有多复杂?引擎限制较少,而且它假设你知道自己在做什么,不管是好是坏。 |
Zend 扩展确实应该由经验丰富的高级程序员来开发,因为该引擎的检查能力较弱。它显然假设您精通自己所做的事情。 |
总结一下 API 兼容性,每个步骤都在 zend_load_extension()
中详细说明。
接下来是 Zend 扩展冲突的问题。一个扩展可能与另一个扩展不兼容,为了解决这个问题,每个 Zend 扩展都有一个名为 message_handler
的钩子。如果声明,则当另一个 Zend 扩展被加载时,每个已加载的扩展都会触发此钩子。您将获得一个指向其 zend_extension
结构的指针,然后您可以检测它是哪一个,如果您认为会与它发生冲突,则中止。这在实践中也很少使用。
Zend 扩展的生命周期钩子
如果您还记得 PHP 生命周期(您应该阅读专门的章节),那么 Zend 扩展通过以下方式插入该生命周期:

我们可以注意到,我们的 api_no_check()
、build_id_check()
和 message_handler()
检查钩子仅在 PHP 启动时触发。后面三个钩子在前面的部分(上文)中有详细介绍。
然后要记住的重要一点是:
-
MINIT()
在 Zend 扩展(startup()
)之前在 PHP 扩展上触发。 -
在 PHP 扩展程序
RINIT()
之前触发 Zend 扩展程序(activate()
)。 -
Zend 扩展请求关闭过程(
deactivate()
)在 PHP 扩展的RSHUTDOWN()
和PRSHUTDOWN()
之间调用。 -
MSHUTDOWN()
首先在 PHP 扩展上调用,然后在 Zend 扩展上调用(shutdown()
)。
就像每个钩子一样,都有一个精确定义的顺序,您必须掌握它并记住它以进行复杂的用例扩展。 |
实际上,我们可以说的是:
-
Zend 扩展是在 PHP 扩展之后启动的。这样 Zend 扩展就可以确保在启动时每个 PHP 扩展都已加载。然后它们就可以替换并挂接到 PHP 扩展中。例如,如果您需要用自己的函数处理程序替换
session_start()
函数处理程序,那么在 Zend 扩展中执行此操作会更容易。如果您在 PHP 扩展中执行此操作,则必须确保在会话扩展之后加载,这可能很难检查和掌握(您仍然可以使用 zend_module_dep 指定依赖项)。但是,请记住,静态编译的扩展始终在动态编译的扩展之前启动。因此,对于会话用例,这不是问题,因为 ext/session 是作为静态加载的。直到某些发行版(FreeBSD 听我们的)改变了这一点…… -
当请求出现时,Zend 扩展会在 PHP 扩展之前触发。这意味着他们有机会修改有关当前请求的引擎,以便 PHP 扩展使用修改后的上下文。Opcache 使用这样的技巧,以便它可以在任何扩展有机会阻止它之前执行其复杂的任务。
-
请求关闭也是如此:Zend 扩展可以假设每个 PHP 扩展都已关闭请求。
实践:我的第一个 Zend 扩展实例
在这里,我们将在一些非常简单的场景中详细介绍 Zend 扩展可以使用的一些钩子,以及如何使用它们。
请记住,Zend 扩展设计通常要求您深入掌握 Zend 引擎。 |
在我们这里的例子里,我们将设计一个使用这些钩子的 Zend 扩展:
-
fcall_begin_handler
:我们将检测虚拟机当前正在执行的指令,并打印一条消息。钩子捕获两件事:对require/include/eval
的调用或对任何函数/方法的调用。 -
op_array_handler
:我们将检测当前正在编译的 PHP 函数,并打印一条消息。 -
message_handler
:我们将检测已加载的其他 Zend 扩展,并打印一条消息。
这就是我们的骨架,我们必须自己编写,因为对于 Zend 扩展,没有像 PHP 扩展那样的骨架生成器。这些文件称为 pib.c
和 php_pib.h
,文件的结构与 PHP 扩展相同,只是我们不会在其中声明相同的内容:
#include "php.h"
#include "Zend/zend_extensions.h"
#include "php_pib.h"
#include "Zend/zend_smart_str.h"
/* Remember that we must declare such a symbol in a Zend extension. It is used to check
* if it was built against the same API as the one PHP runtime uses */
ZEND_DLEXPORT zend_extension_version_info extension_version_info = {
ZEND_EXTENSION_API_NO,
ZEND_EXTENSION_BUILD_ID
};
ZEND_DLEXPORT zend_extension zend_extension_entry = {
"pib-zend-extension",
"1.0",
"PHPInternalsBook Authors",
"http://www.phpinternalsbook.com",
"Our Copyright",
NULL, /* startup() : module startup */
NULL, /* shutdown() : module shutdown */
pib_zend_extension_activate, /* activate() : request startup */
pib_zend_extension_deactivate, /* deactivate() : request shutdown */
pib_zend_extension_message_handler, /* message_handler() */
pib_zend_extension_op_array_handler, /* compiler op_array_handler() */
NULL, /* VM statement_handler() */
pib_zend_extension_fcall_begin_handler, /* VM fcall_begin_handler() */
NULL, /* VM fcall_end_handler() */
NULL, /* compiler op_array_ctor() */
NULL, /* compiler op_array_dtor() */
STANDARD_ZEND_EXTENSION_PROPERTIES /* Structure-ending macro */
};
static void pib_zend_extension_activate(void) { }
static void pib_zend_extension_deactivate(void) { }
static void pib_zend_extension_message_handler(int code, void *ext) { }
static void pib_zend_extension_op_array_handler(zend_op_array *op_array) { }
static void pib_zend_extension_fcall_begin_handler(zend_execute_data *ex) { }
到目前为止一切顺利,此扩展已编译为 Zend 扩展,但什么也不做。实际上什么也不做。zend_extension
结构中的前几行出现在 phpinfo()
中:
This program makes use of the Zend Scripting Language Engine:
Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies
with pib-zend-extension v1.0, Our Copyright, by PHPInternalsBook Authors
这是强制性的,引擎的反应如下:对于每个加载的 Zend 扩展,它将第一个 zend_extension
字段打印到引擎信息中。
现在就这些。现在让我们填写那些空函数:
static void pib_zend_extension_message_handler(int code, void *ext)
{
php_printf("We just detected that zend_extension '%s' is trying to load\n", ((zend_extension *)ext)->name);
}
如前所述,message_handler()
是一个特殊的钩子,Zend 扩展可以声明它在加载另一个 Zend 扩展时被注意到。但要注意顺序。您必须先注册我们的 “pib” Zend 扩展,然后再注册另一个 Zend 扩展(如 opcache),因为 message_handler()
仅在 Zend 扩展加载时调用,因此您显然需要在声明它之前加载它。先有鸡还是先有蛋。
然后我们将开始深入引擎,使用我们的 op_array_handler
钩子:
static void pib_zend_extension_op_array_handler(zend_op_array *op_array)
{
smart_str out = {0};
smart_str_appends(&out, "We just compiled ");
if (op_array->function_name) {
uint32_t i, num_args = op_array->num_args;
if (op_array->fn_flags & ZEND_ACC_CLOSURE) {
smart_str_appends(&out, "a closure ");
} else {
smart_str_appends(&out, "function ");
smart_str_append(&out, op_array->function_name);
}
smart_str_appendc(&out, '(');
/* The variadic arg is not declared as an arg internally */
if (op_array->fn_flags & ZEND_ACC_VARIADIC) {
num_args++;
}
for (i=0; i<num_args; i++) {
zend_arg_info arg = op_array->arg_info[i];
if (arg.class_name) {
smart_str_append(&out, arg.class_name);
smart_str_appendc(&out, ' ');
}
if (arg.pass_by_reference) {
smart_str_appendc(&out, '&');
}
if (arg.is_variadic) {
smart_str_appends(&out, "...");
}
smart_str_appendc(&out, '$');
smart_str_append(&out, arg.name);
if (i != num_args - 1) {
smart_str_appends(&out, ", ");
}
}
smart_str_appends(&out, ") in file ");
smart_str_append(&out, op_array->filename);
smart_str_appends(&out, " between line ");
smart_str_append_unsigned(&out, op_array->line_start);
smart_str_appends(&out, " and line ");
smart_str_append_unsigned(&out, op_array->line_end);
} else {
smart_str_appends(&out, "the file ");
smart_str_append(&out, op_array->filename);
}
smart_str_0(&out);
php_printf("%s\n", ZSTR_VAL(out.s));
smart_str_free(&out);
}
如果需要,获取一些有关 Zend Engine 的信息。 |
这个钩子是由编译器的 Pass Two 触发的。当 Zend 编译器启动时,它会编译一个脚本或函数。在编译结束前,它会启动第二次编译,目的是解决未解决的指针问题(在编译脚本时无法知道指针的值)。您可以分析 源代码 的 pass_two()
函数。
在 pass_two()
源代码中,你可以看到它触发了迄今为止所有已注册 Zend 扩展的 op_array_handler()
,并将当前尚未完全解析的 OPArray 作为参数传递给它。这就是我们在函数中得到的参数。然后,我们对其进行分析,并尝试获取一些相关信息,如当前正在编译的函数、其参数信息等……这与 Reflection API 所做的非常相似,只是准确性稍差一些,因为 OPArray 还未完全解析,我们仍然是编译步骤的一部分。我们本可以收集默认参数值 f.e(这里没有这样做),但这会给示例增加很多复杂性,所以我们决定不展示这部分内容。
那我们继续吧?:
static void pib_zend_extension_activate(void)
{
CG(compiler_options) |= ZEND_COMPILE_EXTENDED_INFO;
}
static void pib_zend_extension_deactivate(void)
{
CG(compiler_options) &= ~ZEND_COMPILE_EXTENDED_INFO;
}
static void pib_zend_extension_fcall_begin_handler(zend_execute_data *execute_data)
{
if (!execute_data->call) {
/* Fetch the next OPline. We use pointer arithmetic for that */
zend_op n = execute_data->func->op_array.opcodes[(execute_data->opline - execute_data->func->op_array.opcodes) + 1];
if (n.extended_value == ZEND_EVAL) {
php_printf("Beginning of a code eval() in %s:%u", ZSTR_VAL(execute_data->func->op_array.filename), n.lineno);
} else {
/* The file to be include()ed is stored into the operand 1 of the OPLine */
zend_string *file = zval_get_string(EX_CONSTANT(n.op1));
php_printf("Beginning of an include of file '%s'", ZSTR_VAL(file));
zend_string_release(file);
}
} else if (execute_data->call->func->common.fn_flags & ZEND_ACC_STATIC) {
php_printf("Beginning of a new static method call : '%s::%s'",
ZSTR_VAL(Z_CE(execute_data->call->This)->name),
ZSTR_VAL(execute_data->call->func->common.function_name));
} else if (Z_TYPE(execute_data->call->This) == IS_OBJECT) {
php_printf("Beginning of a new method call : %s->%s",
ZSTR_VAL(Z_OBJCE(execute_data->call->This)->name),
ZSTR_VAL(execute_data->call->func->common.function_name));
} else {
php_printf("Beginning of a new function call : %s", ZSTR_VAL(execute_data->call->func->common.function_name));
}
PHPWRITE("\n", 1);
}
在请求启动时,我们告诉编译器在它要创建的 OPArray 中生成一些扩展信息。该标志为 ZEND_COMPILE_EXTENDED_INFO
。扩展信息是 VM OPCode 钩子,即编译器将在每次调用函数之前以及每次函数调用完成后生成一个特殊的 OPCode。这些是 FCALL_BEGIN
和 FCALL_END
OPCodes。
这是一个简单的 PHP 函数调用 OPCodes 示例,其中 “foo” 字符串作为第一个单独参数:
L9 #1 INIT_FCALL 112 "foo"
L9 #2 SEND_VAL "foo" 1
L9 #3 DO_FCALL
L11 #4 RETURN 1
现在我们告诉编译器生成额外的 OPCode,情况也是一样的:
L9 #3 INIT_FCALL 112 "foo"
L9 #4 EXT_FCALL_BEGIN
L9 #5 SEND_VAL "foo" 1
L9 #6 DO_FCALL
L9 #7 EXT_FCALL_END
L11 #8 RETURN 1
如你所见,发送参数和调用函数的 OPCode 被两个 EXT_FCALL_BEGIN
和 EXT_FCALL_END
OPCode 包围,这两个 OPCode 稍后将执行每个声明的 Zend 扩展的 fcall_begin()
和 fcall_end()
处理程序,例如我们的扩展。
请记住,引擎中的函数调用是真正的函数调用,还是执行一个新的包含的 PHP 文件,或是执行一个新的 eval()
块。请看 require()
的反汇编:
L9 #3 EXT_FCALL_BEGIN
L9 #4 INCLUDE_OR_EVAL "foo.php"
L9 #5 EXT_FCALL_END
L11 #6 RETURN 1
一旦生成了这些 “标记” OPCode,当 VM 运行 OPArray OPCode 时,它将运行我们声明的 fcall_begin()
处理程序。这对我们来说是一种检测接下来要执行的函数/文件/eval 的方法。我们只需打印此类信息即可。
要求编译器生成 |
混合扩展
我们所说的混合扩展,是指既是 Zend 扩展又是 PHP 扩展的扩展。
这怎么可能? 又是为了什么?
这个问题有几种答案:
-
要注册新的 PHP 函数,PHP 扩展比 Zend 扩展更好,因为 Zend 扩展已经知道如何注册,并且是专门为此目的而设计的。如果不使用它,那就太可惜了。Opcache 就是这样做的。
-
如果你需要注册整个生命周期中的所有钩子,显然你需要两方面的支持。
-
如果你需要掌握 Zend 扩展的加载顺序,比如在 opcache 之后加载,你就需要混合使用。
技巧很简单,选择以下两者之一:
-
您主要是一个 PHP 扩展。您被注册为 PHP 扩展,当您启动 (
MINIT()
) 时,您将自己注册为 Zend 扩展 (从属)。 -
您主要是一个 Zend 扩展。您被注册为 Zend 扩展,当您启动 (
startup()
) 时,您将自己注册为 PHP 扩展 (从属)。
因此,无论您是 PHP 扩展主控还是 Zend 扩展从属;或者相反。
至于要完全理解这个技巧,我们在此重复 PHP 和 Zend 扩展的完整生命周期。将其图像化并打印到您的大脑中:

但请记住,无论您选择哪种模式,您都必须手动注册从属部分并触发它,因为引擎显然不会这样做。引擎会自动触发主部分。
混合 Zend 扩展主控、PHP 扩展从属
好吧,这很简单。我们不想以 PHP 扩展名的形式加载,而只想以 Zend 扩展名的形式加载。为了强制起见,我们将不发布强制符号 get_module
,当引擎试图通过读取 INI 文件来注册 PHP 扩展时,会查找该符号。
因此,我们只能注册为 zend_extension=pib.so
。以 extension=pib.so
注册将失败,因为引擎会找不到我们未导出的 get_module
符号。
但是,在我们的 Zend 扩展启动钩子中,没有任何东西可以阻止我们将自己注册为 PHP 扩展:
#include "php.h"
#include "Zend/zend_extensions.h"
#include "php_pib.h"
#define PRINT(what) fprintf(stderr, what "\n");
/* Declared as static, thus private */
static zend_module_entry pib_module_entry = {
STANDARD_MODULE_HEADER,
"pib",
NULL, /* Function entries */
PHP_MINIT(pib), /* Module init */
PHP_MSHUTDOWN(pib), /* Module shutdown */
PHP_RINIT(pib), /* Request init */
PHP_RSHUTDOWN(pib), /* Request shutdown */
NULL, /* Module information */
"0.1", /* Replace with version number for your extension */
STANDARD_MODULE_PROPERTIES
};
/* This line should stay commented
ZEND_GET_MODULE(pib)
*/
ZEND_DLEXPORT zend_extension_version_info extension_version_info = {
ZEND_EXTENSION_API_NO,
ZEND_EXTENSION_BUILD_ID
};
ZEND_DLEXPORT zend_extension zend_extension_entry = {
"pib-zend-extension",
"1.0",
"PHPInternalsBook Authors",
"http://www.phpinternalsbook.com",
"Our Copyright",
pib_zend_extension_startup,
pib_zend_extension_shutdown,
pib_zend_extension_activate,
pib_zend_extension_deactivate,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
STANDARD_ZEND_EXTENSION_PROPERTIES
};
static void pib_zend_extension_activate(void)
{
PRINT("Zend extension new request starting up");
}
static void pib_zend_extension_deactivate(void)
{
PRINT("Zend extension current request is shutting down");
}
static int pib_zend_extension_startup(zend_extension *ext)
{
PRINT("Zend extension is starting up");
/* When the Zend extension part will startup(), make it register
a PHP extension by calling ourselves zend_startup_module() */
return zend_startup_module(&pib_module_entry);
}
static void pib_zend_extension_shutdown(zend_extension *ext)
{
PRINT("Zend extension is shutting down");
}
static PHP_MINIT_FUNCTION(pib)
{
PRINT("PHP extension is starting up");
return SUCCESS;
}
static PHP_MSHUTDOWN_FUNCTION(pib)
{
PRINT("PHP extension is shutting down");
return SUCCESS;
}
static PHP_RINIT_FUNCTION(pib)
{
PRINT("PHP extension new request starting up");
return SUCCESS;
}
static PHP_RSHUTDOWN_FUNCTION(pib)
{
PRINT("PHP extension current request is shutting down");
return SUCCESS;
}
我们完成了。启动激活了此类 Zend 扩展的 PHP 将在 stderr 上打印以下内容:
Zend extension is starting up
PHP extension is starting up
Zend extension new request starting up
PHP extension new request starting up
PHP extension current request is shutting down
Zend extension current request is shutting down
PHP extension is shutting down
Zend extension is shutting down
正如你所看到的,除了前两个钩子外,其他钩子都按照正确的顺序执行。理论上,PHP 扩展应该在 Zend 扩展之前启动,但由于我们注册了 Zend 扩展,当引擎运行我们的 Zend 扩展钩子时,它对我们的 PHP 扩展模块启动部分(MINIT()
)一无所知。我们告诉它启动我们的 PHP 扩展,然后作为 Zend 扩展 startup()
钩子的一部分,我们通过调用 zend_startup_module()
来手动触发 PHP 扩展启动钩子。显然,你必须注意不要创建循环,也不要让引擎对你在此类钩子中的具体操作感到抓狂。
这样做既简单又合乎逻辑。
从现在起,我们既是 PHP 扩展,也是 Zend 扩展。看看这个:
> php -dzend_extension=pib.so -m
[PHP modules]
Core
date
(...)
pib
posix
Reflection
(...)
[Zend Modules]
pib-zend-extension
我们的 PHP 扩展名为 “pib” 并显示出来,我们的 Zend 扩展名为 “pib-zend-extension” 并显示出来。我们为两个部分选择了两个不同的名称,其实我们可以选择相同的名称。
Opcache 和 Xdebug 使用的就是这种混合模式,它们是 Zend 扩展,但需要发布 PHP 函数,因此也是 PHP 扩展。 |
混合 PHP 扩展主控、Zend 扩展从属
现在让我们反过来:我们希望引擎将我们注册为 PHP 扩展,而不是 Zend 扩展,但仍然希望是混合的。
好吧,我们将做相反的事情:我们不会发布我们的 zend_extension_version_info
符号,这样就不可能将我们加载为 Zend 扩展:引擎将拒绝这一点。但显然这一次,我们将声明一个 get_module
符号,以便能够作为 PHP 扩展加载。并且,在我们的 MINIT()
中,我们将自己注册为 Zend 扩展。
#include "php.h"
#include "Zend/zend_extensions.h"
#include "php_pib.h"
#include "Zend/zend_smart_str.h"
#define PRINT(what) fprintf(stderr, what "\n");
zend_module_entry pib_module_entry = {
STANDARD_MODULE_HEADER,
"pib",
NULL, /* Function entries */
PHP_MINIT(pib), /* Module init */
PHP_MSHUTDOWN(pib), /* Module shutdown */
PHP_RINIT(pib), /* Request init */
PHP_RSHUTDOWN(pib), /* Request shutdown */
NULL, /* Module information */
"0.1", /* Replace with version number for your extension */
STANDARD_MODULE_PROPERTIES
};
ZEND_GET_MODULE(pib)
/* Should be kept commented
* zend_extension_version_info extension_version_info = {
* ZEND_EXTENSION_API_NO,
* ZEND_EXTENSION_BUILD_ID
* };
*/
static zend_extension zend_extension_entry = {
"pib-zend-extension",
"1.0",
"PHPInternalsBook Authors",
"http://www.phpinternalsbook.com",
"Our Copyright",
pib_zend_extension_startup,
pib_zend_extension_shutdown,
pib_zend_extension_activate,
pib_zend_extension_deactivate,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
STANDARD_ZEND_EXTENSION_PROPERTIES
};
static void pib_zend_extension_activate(void)
{
PRINT("Zend extension new request starting up");
}
static void pib_zend_extension_deactivate(void)
{
PRINT("Zend extension current request is shutting down");
}
static int pib_zend_extension_startup(zend_extension *ext)
{
PRINT("Zend extension is starting up");
return SUCCESS;
}
static PHP_MINIT_FUNCTION(pib)
{
PRINT("PHP extension is starting up");
/* Register our zend_extension part now */
zend_register_extension(&zend_extension_entry, NULL);
return SUCCESS;
}
static void pib_zend_extension_shutdown(zend_extension *ext)
{
PRINT("Zend extension is shutting down");
}
static PHP_MSHUTDOWN_FUNCTION(pib)
{
PRINT("PHP extension is shutting down");
return SUCCESS;
}
static PHP_RINIT_FUNCTION(pib)
{
PRINT("PHP extension new request starting up");
return SUCCESS;
}
static PHP_RSHUTDOWN_FUNCTION(pib)
{
PRINT("PHP extension current request is shutting down");
return SUCCESS;
}
最后,在 pib_zend_extension_shutdown()
处,程序崩溃得非常厉害(真可悲!)。启动调试器,很容易知道原因。
在这里,我们作为 PHP 扩展加载。看看钩子。当触发 MSHUTDOWN()
时,引擎运行我们的 MSHUTDOWN()
,但之后就卸载了我们!它在我们的扩展上调用 dlclose()
,看看源代码,解决方案通常就在那里。
所以发生的事情很简单,在触发我们的 RSHUTDOWN()
之后,引擎卸载了我们的 pib.so
;当调用我们的 Zend 扩展部分 shutdown()
时,我们不再是进程地址空间的一部分,因此我们严重破坏了整个 PHP 进程。
解决方案是什么?如果你读过源代码,并且读过本书的其他章节,你应该知道,如果我们传递一个环境变量 ZEND_DONT_UNLOAD_MODULES
并将其设置为 1
,引擎将不会卸载我们。然后我们可以在 MSHUTDOWN()
中写入这样的环境变量,并在 shutdown()
中取消写入。putenv()
可以完成这项工作。即使这很棘手,这也很好。此外,如果我们之间的另一个扩展也使用它,那对我们来说会很糟糕。
第二个解决方案是由卸载机制的技巧提供的。如果您将 libdl 句柄从 PHP 扩展结构转移到 Zend 结构,那么引擎卸载就没问题了。
代码已修补:
static PHP_MINIT_FUNCTION(pib)
{
Dl_info infos;
PRINT("PHP extension is starting up");
/* Register our zend_extension part, and give it our own PHP extension handle */
zend_register_extension(&zend_extension_entry, pib_module_entry.handle);
/* Prevent the engine from unloading our PHP extension */
pib_module_entry.handle = NULL;
return SUCCESS;
}
如果你现在启动它,你将会得到预期的结果:
PHP extension is starting up
Zend extension is starting up
Zend extension new request starting up
PHP extension new request starting up
PHP extension current request is shutting down
Zend extension current request is shutting down
PHP extension is shutting down
Zend extension is shutting down
Blackfire 使用了这种混合模型,但没有 Zend 扩展 |
Hybrid hybrid
Hybrid hybrid 只是一种模型,允许用户以 Zend 扩展或 PHP 扩展的形式加载。
您需要做的是记住您是如何加载的,例如使用全局加载,这样您就可以处理这两种模式。
让我们只写不同的部分:
static char started = 0;
static int pib_zend_extension_startup(zend_extension *ext)
{
if (!started) {
started = 1;
return zend_startup_module(&pib_module_entry);
}
PRINT("Zend extension is starting up");
return SUCCESS;
}
static PHP_MINIT_FUNCTION(pib)
{
if (!started) {
started = 1;
Dl_info infos;
zend_register_extension(&zend_extension_entry, pib_module_entry.handle);
dladdr(ZEND_MODULE_STARTUP_N(pib), &infos);
dlopen(infos.dli_fname, 0);
}
PRINT("PHP extension is starting up");
return SUCCESS;
}
显然,每个符号都必须是公共的。使用上面的代码,您可以将其加载为 PHP 扩展 (extension=pib.so) 或 Zend 扩展 (zend_extension=pib.so);最终用户可以选择,这就是这种模型的优势,即使我们作者不知道它的用法。