4.8. Zend 扩展

PHP 知道两种扩展:

  • PHP 扩展,最常用的
  • Zend 扩展很少见,它允许其他钩子

本章将详细说明Zend扩展和PHP扩展之间的主要区别是什么,何时应使用一个而不是另一个,以及如何构建混合扩展,即扩展同时为 PHP 和 Zend(以及为什么要这样做)。

关于 PHP 和 Zend 扩展之间的区别

只是说,在PHP的源代码中,PHP扩展名为“ PHP modules ”,而 Zend 扩展称为“ Zend extensions ”

因此,在 PHP 的核心,如果阅读到 “extension” 关键字,则应首先考虑 Zend 扩展。而如果阅读 “module” 关键字,则应该考虑 PHP 扩展。

在传统生活中,我们谈论的是“ PHP 扩展 ”“ Zend 扩展 ”

它们不同的点在于它们的加载方式:

  • PHP 扩展(又名 PHP “modules”)作为“ extension = pib.so ”行加载到 INI 文件中
  • Zend扩展作为 “zend_extension = pib.so” 行加载到 INI 文件中

这是我们从 PHP 用户区看到的唯一可见差异。

但是从内部角度来看,这又是一个不同的故事。

什么是 Zend 扩展?

首先,Zend 扩展的编译和加载方式与 PHP 扩展相同。因此,如果你尚未阅读构建 PHP 扩展章节,你应该看一下,因为它对 Zend 扩展同样有效。

注意

如果没有完成,获得有关 PHP 扩展的一些信息,我们将在这里与它们进行比较。Zend 扩展与 PHP 扩展在概念上有很大一部分相同。

这是 Zend 扩展。请注意,你需要为引擎发布两个结构(而不是一个)来加载 Zend扩展:

  1. /* Zend 主扩展结构 */
  2. struct _zend_extension {
  3. char *name; /*
  4. char *version; * 一些信息
  5. char *author; *
  6. char *URL; *
  7. char *copyright; */
  8. startup_func_t startup; /*
  9. shutdown_func_t shutdown; * 特定分支生命点
  10. activate_func_t activate; * ( 钩子 )
  11. deactivate_func_t deactivate; */
  12. message_handler_func_t message_handler; /* 钩子调用zend_extension 注册 */
  13. op_array_handler_func_t op_array_handler; /* 在 Zend 编译之后,Hook被调用 */
  14. statement_handler_func_t statement_handler; /*
  15. fcall_begin_handler_func_t fcall_begin_handler; * 通过 Zend VM 调用钩子作为特定的操作码
  16. fcall_end_handler_func_t fcall_end_handler; */
  17. op_array_ctor_func_t op_array_ctor; /* 钩子调用 OPArray 构造 */
  18. op_array_dtor_func_t op_array_dtor; /* 钩子销毁 OPArray */
  19. int (*api_no_check)(int api_no); /* 检查zend_extension 不兼容
  20. int (*build_id_check)(const char* build_id); */
  21. op_array_persist_calc_func_t op_array_persist_calc; /* 如果zend_extension 扩展了
  22. op_array_persist_func_t op_array_persist; * OPArray 结构并需要声明一些 SHM 数据,调用钩子
  23. */
  24. void *reserved5; /*
  25. void *reserved6; * 用那些指针做你想做的事
  26. void *reserved7; *
  27. void *reserved8; */
  28. DL_HANDLE handle; /* dlopen() returned handle */
  29. int resource_number; /* 用于管理扩展的内部号码 */
  30. };
  31. /* Zend 扩展加载到引擎时使用的结构 */
  32. typedef struct _zend_extension_version_info {
  33. int zend_extension_api_no;
  34. char *build_id;
  35. } zend_extension_version_info;

注意

与往常一样,请阅读源代码。Zend 扩展被管理到 Zend/zend_extension.c (和.h文件)

如你所见,Zend 扩展比 PHP 扩展更复杂,因为它们具有更多的挂钩,而且它们更接近 Zend 引擎及其虚拟机 (整个PHP源代码中最复杂的部分)。

为什么需要 Zend 扩展?

让我们警告你:除非你对 PHP 内部的 Vritual Machine 有了非常充分的了解,并且需要深入了解它,否则不需要 Zend 扩展,PHP 扩展就足够了。

当今,PHP 世界中最广为人知的 Zend 扩展是 OPCache,XDebug,phpdbg 和 Blackfire。但是你知道旁边有数十个 PHP 扩展吗? 这是一个明显的迹象:

  • 对于大部分问题,你都不需要 Zend 扩展
  • Zend 扩展也可以用作 PHP 扩展(稍后会有更多介绍)
  • 一个 PHP 扩展仍然可以做很多事情
  • 通常,Zend 扩展对于两种任务都是必需的:调试器和分析器。

注意

没有框架生成器的 Zend 扩展就像 PHP 扩展。

警告

使用 Zend 扩展,没有生成器,没有帮助。Zend 扩展保留给高级程序员,它们理解起来更复杂,它们具有更深入的引擎行为,通常需要对 PHP 内部机制有深入的了解。

基本上,如果你需要创建调试器,则需要一个 Zend 扩展。对于探查器,你可以将其作为传统的 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_DEBUGZTS。如果存在不匹配的情况,PHP 扩展将拒绝加载,则 Zend 扩展将有机会加载,因为引擎会检查build_id_check()钩子,并将其传递给ZEND_EXTENSION_BUILD_ID。在这里,你再次说是否兼容。再一次,如果你说“否”,引擎将不会加载你的扩展程序并打印警告消息。

请记住,我们在有关构建PHP扩展的章节中详细介绍了 API 与 ABI 的编号方式。

在实际应用中,很少使用强制东西到引擎的能力。

注意

你看到与 PHP 扩展相比,Zend 扩展的复杂程度如何?引擎的限制较少,它假设你知道自己在做什么,无论是最佳还是最差。

警告

Zend 扩展确实应该由经验丰富的高级程序员开发,因为引擎的检查能力较弱。显然,你应该做好自己的工作。

总结一下API兼容性,每一步都在zend_load_extension()

然后是 Zend 扩展冲突的问题。一个可能与另一个不兼容,并且要知道,每个 Zend 扩展都有一个名为message_handler的钩子。如果声明了,则在另一个 Zend 扩展程序被加载时,在每个已加载的扩展程序上触发此钩子。你将传递一个指向其 zend_extension 结构的指针,然后可以检测到它是哪个,如果你认为会与之冲突则中止。这也是实践中很少使用的东西。

Zend 扩展生命周期钩子

如果你还记得 PHP 生命周期(你应该阅读此章节),Zend 扩展以这种方式插入该生命周期:

php_extensions_lifecycle_full.png

我们可以注意到我们的api_no_check()build_id_check()message_handler() 检查钩子仅在 PHP 启动时触发。后面的三个钩子将在前面的部分中进行详细介绍。

然后要记住的重要事情:

  • MINIT() 在 Zend 扩展之前startup()),在 PHP 扩展程序上触发。
  • RINIT() 在 PHP 扩展之前,触发 Zend 扩展(activate())。
  • Zend 扩展请求关闭过程(deactivate())在 PHP 扩展的 RSHUTDOWN()PRSHUTDOWN()之间被调用。
  • MSHUTDOWN() 首先在 PHP 扩展调用,然后在 Zend 扩展 之后(shutdown())。

警告

像每个钩子一样,有一个精确定义的顺序,你必须掌握它并记住它以进行复杂的用例扩展。

实践中,我们可以说的是:

  • Zend 扩展是在 PHP 扩展之后启动的。这使 Zend 扩展可以确保每个 PHP 扩展在启动时都已加载。然后,他们可以替换并挂钩到 PHP 扩展中。例如,如果你需要将session_start()函数处理函数替换为你的函数,在 Zend 扩展程序中这样做会更容易。如果在 PHP 扩展中执行此操作,则必须确保在 session 扩展之后加载该文件,这对于检查和掌握它很棘手。(你仍然可以使用 zend_module_dep 指定依赖项)。但是记住,静态编译扩展总是在动态编译扩展之前启动因此,对于 session 用例来说,这不是问题,因为ext/session是静态加载的。除非某些发行版(FreeBSD 听到我们)改变了……
  • 当请求显示时,与 PHP 扩展相比,会先触发 Zend 扩展。这意味着它们有机会针对即将到来的当前请求修改引擎,以便 PHP 扩展使用修改后的上下文。OPCache 使用了这种技巧,因此可以在任何扩展有机会阻止它之前执行复杂的任务。
  • 对于请求关闭也是如此:Zend 扩展可以假定每个 PHP 扩展都已关闭请求。

练习:我的第一个 Zend 扩展示例

在这里,我们将详细介绍 Zend 扩展程序在某些非常简单的情况下可以使用的钩子以及如何使用它们。

警告

请记住,Zend 扩展设计通常需要你很好地掌握 Zend 引擎

对于此处的示例,我们将设计一个使用这些钩子的 Zend 扩展:

  • fcall_begin_handler :我们将检测 VM 当前正在执行哪些指令,并打印一条消息。钩子捕获了两件事:调用 require/include/eval 或调用任何函数/方法。
  • op_array_handler :我们将检测当前正在编译的 PHP 函数,并输出一条消息。
  • message_handler : 我们将检测到其他加载的 Zend 扩展,并输出一条消息。

这就是我们的骨架,我们必须像 Zend 扩展一样编写,没有像 PHP 扩展那样的骨架生成器。这些文件分别称为 pib.cphp_pib.h,文件的结构与 PHP 扩展相同,只是我们不会在其中声明相同的内容:

  1. #include "php.h"
  2. #include "Zend/zend_extensions.h"
  3. #include "php_pib.h"
  4. #include "Zend/zend_smart_str.h"
  5. /* 请记住,我们必须在 Zend 扩展中声明这样的符号。
  6. * 用于检查它是否根据与 PHP 运行时使用的 API 相同的 API构建的 */
  7. zend_extension_version_info extension_version_info = {
  8. ZEND_EXTENSION_API_NO,
  9. ZEND_EXTENSION_BUILD_ID
  10. };
  11. zend_extension zend_extension_entry = {
  12. "pib-zend-extension",
  13. "1.0",
  14. "PHPInternalsBook Authors",
  15. "http://www.phpinternalsbook.com",
  16. "Our Copyright",
  17. NULL, /* startup() : 模块启动 */
  18. NULL, /* shutdown() : 模块关闭 */
  19. pib_zend_extension_activate, /* activate() : 请求启动 */
  20. pib_zend_extension_deactivate, /* deactivate() : 请求关闭 */
  21. pib_zend_extension_message_handler, /* message_handler() */
  22. pib_zend_extension_op_array_handler, /* compiler op_array_handler() */
  23. NULL, /* VM statement_handler() */
  24. pib_zend_extension_fcall_begin_handler, /* VM fcall_begin_handler() */
  25. NULL, /* VM fcall_end_handler() */
  26. NULL, /* compiler op_array_ctor() */
  27. NULL, /* compiler op_array_dtor() */
  28. STANDARD_ZEND_EXTENSION_PROPERTIES /* 结构结束宏 */
  29. };
  30. static void pib_zend_extension_activate(void) { }
  31. static void pib_zend_extension_deactivate(void) { }
  32. static void pib_zend_extension_message_handler(int code, void *ext) { }
  33. static void pib_zend_extension_op_array_handler(zend_op_array *op_array) { }
  34. static void pib_zend_extension_fcall_begin_handler(zend_execute_data *ex) { }

到目前为止,该扩展可以编译为 Zend 扩展,但是什么也不做。并非完全没有。zend_extension结构的第一行出现在 phpinfo()中:

  1. This program makes use of the Zend Scripting Language Engine:
  2. Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies
  3. with pib-zend-extension v1.0, Our Copyright, by PHPInternalsBook Authors

这是强制性的,引擎的反应如下:对于每个已加载的 Zend 扩展程序,它将在引擎信息中打印第一个zend_extension字段。

目前为止就这样了。现在,让我们填写这些空函数:

  1. static void pib_zend_extension_message_handler(int code, void *ext)
  2. {
  3. php_printf("We just detected that zend_extension '%s' is trying to load\n", ((zend_extension *)ext)->name);
  4. }

就像之前说的,message_handler()是一个特殊的钩子,当加载另一个 Zend 扩展时,Zend 扩展可能会声明以引起注意。但是请注意顺序。你必须先注册我们的 “pib” Zend 扩展名,然后是另一个 Zend 扩展(例如 OPCache),因为message_handler()仅在加载 Zend 扩展名时被调用,显然你需要在声明它之前加载它。

然后,我们将通过op_array_handler钩子开始深入研究引擎:

  1. static void pib_zend_extension_op_array_handler(zend_op_array *op_array)
  2. {
  3. smart_str out = {0};
  4. smart_str_appends(&out, "We just compiled ");
  5. if (op_array->function_name) {
  6. uint32_t i, num_args = op_array->num_args;
  7. if (op_array->fn_flags & ZEND_ACC_CLOSURE) {
  8. smart_str_appends(&out, "a closure ");
  9. } else {
  10. smart_str_appends(&out, "function ");
  11. smart_str_append(&out, op_array->function_name);
  12. }
  13. smart_str_appendc(&out, '(');
  14. /* 内部没有将可变参数arg声明为arg */
  15. if (op_array->fn_flags & ZEND_ACC_VARIADIC) {
  16. num_args++;
  17. }
  18. for (i=0; i<num_args; i++) {
  19. zend_arg_info arg = op_array->arg_info[i];
  20. if (arg.class_name) {
  21. smart_str_append(&out, arg.class_name);
  22. smart_str_appendc(&out, ' ');
  23. }
  24. if (arg.pass_by_reference) {
  25. smart_str_appendc(&out, '&');
  26. }
  27. if (arg.is_variadic) {
  28. smart_str_appends(&out, "...");
  29. }
  30. smart_str_appendc(&out, '$');
  31. smart_str_append(&out, arg.name);
  32. if (i != num_args - 1) {
  33. smart_str_appends(&out, ", ");
  34. }
  35. }
  36. smart_str_appends(&out, ") in file ");
  37. smart_str_append(&out, op_array->filename);
  38. smart_str_appends(&out, " between line ");
  39. smart_str_append_unsigned(&out, op_array->line_start);
  40. smart_str_appends(&out, " and line ");
  41. smart_str_append_unsigned(&out, op_array->line_end);
  42. } else {
  43. smart_str_appends(&out, "the file ");
  44. smart_str_append(&out, op_array->filename);
  45. }
  46. smart_str_0(&out);
  47. php_printf("%s\n", ZSTR_VAL(out.s));
  48. smart_str_free(&out);
  49. }

Note

如果你需要,获取一些有关 Zend 引擎 的信息。

该钩子由编译器的第二遍触发。 当 Zend 编译器启动时,它编译脚本或函数。即将结束时,它将启动第二次编译过程,目标是解决未解析的指针(在编译脚本时无法得知哪个值)。你可以分析下pass_two()函数的源代码

pass_two()源代码中,您可以看到它触发了到目前为止每个已注册Zend扩展的op_array_handler(),并将其作为参数传递给了当前未完全尚未解决的OPArray。这就是我们在函数中作为参数得到的。然后,我们对其进行分析,并尝试提取一些有关它的信息,例如当前正在编译的函数,其参数信息等。。。与Reflection API的功能非常接近,此处的准确性稍差一些,因为OPArray尚未完全解决,我们仍然是此处编译步骤的一部分。我们本可以收集默认参数值f.e(在此不做),但这会为示例增加太多的复杂性,因此我们决定不显示这一部分。

Note

Remember that smart_str are detailed here, zvals here, OPArrays here, etc…

那我们继续 ?:

  1. static void pib_zend_extension_activate(void)
  2. {
  3. CG(compiler_options) |= ZEND_COMPILE_EXTENDED_INFO;
  4. }
  5. static void pib_zend_extension_deactivate(void)
  6. {
  7. CG(compiler_options) &= ~ZEND_COMPILE_EXTENDED_INFO;
  8. }
  9. static void pib_zend_extension_fcall_begin_handler(zend_execute_data *execute_data)
  10. {
  11. if (!execute_data->call) {
  12. /* Fetch the next OPline. We use pointer arithmetic for that */
  13. zend_op n = execute_data->func->op_array.opcodes[(execute_data->opline - execute_data->func->op_array.opcodes) + 1];
  14. if (n.extended_value == ZEND_EVAL) {
  15. php_printf("Beginning of a code eval() in %s:%u", ZSTR_VAL(execute_data->func->op_array.filename), n.lineno);
  16. } else {
  17. /* The file to be include()ed is stored into the operand 1 of the OPLine */
  18. zend_string *file = zval_get_string(EX_CONSTANT(n.op1));
  19. php_printf("Beginning of an include of file '%s'", ZSTR_VAL(file));
  20. zend_string_release(file);
  21. }
  22. } else if (execute_data->call->func->common.fn_flags & ZEND_ACC_STATIC) {
  23. php_printf("Beginning of a new static method call : '%s::%s'",
  24. ZSTR_VAL(Z_CE(execute_data->call->This)->name),
  25. ZSTR_VAL(execute_data->call->func->common.function_name));
  26. } else if (Z_TYPE(execute_data->call->This) == IS_OBJECT) {
  27. php_printf("Beginning of a new method call : %s->%s",
  28. ZSTR_VAL(Z_OBJCE(execute_data->call->This)->name),
  29. ZSTR_VAL(execute_data->call->func->common.function_name));
  30. } else {
  31. php_printf("Beginning of a new function call : %s", ZSTR_VAL(execute_data->call->func->common.function_name));
  32. }
  33. PHPWRITE("\n", 1);
  34. }

在请求启动时,我们告诉编译器将一些扩展信息生成到要创建的OPArray中。其标志是ZEND_COMPILE_EXTENDED_INFO。扩展信息是VM OPCode挂钩,也就是说,编译器将在调用每个函数之前和完成每个函数调用之后生成一个特殊的OPCode。这些是FCALL_BEGINFCALL_ENDOPCode。

这是一个简单的PHP函数OPCodes的示例,其中'foo'字符串作为第一个solo参数:

  1. L9 #1 INIT_FCALL 112 "foo"
  2. L9 #2 SEND_VAL "foo" 1
  3. L9 #3 DO_FCALL
  4. L11 #4 RETURN 1

现在,当我们告诉编译器生成其他OPCode时,它也是如此:

  1. L9 #3 INIT_FCALL 112 "foo"
  2. L9 #4 EXT_FCALL_BEGIN
  3. L9 #5 SEND_VAL "foo" 1
  4. L9 #6 DO_FCALL
  5. L9 #7 EXT_FCALL_END
  6. L11 #8 RETURN 1

就像您看到的那样,关于发送参数和调用函数的OPCode被两个EXT_FCALL_BEGINEXT_FCALL_ENDOPCode包围了,这两个稍后将执行fcall_begin()以及每个声明的Zend扩展的fcall_end()处理程序,例如我们的。

请记住,引擎中的函数调用是真正的函数调用,还是执行新的包含的PHP文件,还是执行新的eval()块。查看反汇编的require()

  1. L9 #3 EXT_FCALL_BEGIN
  2. L9 #4 INCLUDE_OR_EVAL "foo.php"
  3. L9 #5 EXT_FCALL_END
  4. L11 #6 RETURN 1

生成这些“标记” OPCode后,VM运行OPArray OPCode时,它将运行我们声明的fcall_begin()处理程序。这对我们来说是一种检测下一步将要执行的功能/文件/评估的方法。我们只打印这些信息。

Note

要求编译器生成EXT_FCALL语句将大大降低执行器的速度。运行完全相同的代码要慢大约四倍。EXT_FCALL应该仅用于调试器,或至少不用于生产代码,因为Zend VM执行器在激活它们时会慢得多:对于每个fcall / include / eval来说,这是运行更多的代码。

混合扩展

我们所谓的混合扩展是** Zend扩展和PHP扩展。

那怎么可能呢?和什么?

这个问题有几个答案:

-要注册新的PHP函数,PHP扩展胜过Zend扩展,因为它已经知道该怎么做并且已针对首先要达到特定目的。可惜不使用它。 OPCache做到了。

-如果您需要注册整个生命周期中的所有钩子,则显然需要双方

-如果您需要掌握Zend扩展的加载顺序(例如在OPCache之后加载),则需要混合使用

诀窍很简单,请在以下选项之间进行选择:

-您主要是一个PHP扩展。您已注册为PHP扩展,并且在启动时(MINIT()),将自己注册为Zend扩展(从属)。

-您主要是Zend扩展。您已注册为Zend扩展,然后在启动(startup())时,将自己注册为PHP扩展(从属)。

因此,无论您是PHP扩展大师还是Zend扩展奴隶;或相反的味道。

至于要完全理解的技巧,我们在此重复PHP和Zend扩展的整个生命周期。将其图片打印到您的脑海中:

php_extensions_lifecycle_full.png

但是请记住,无论选择哪种模式,都必须注册从属部件并手动触发它,因为引擎显然不会这样做。引擎自动触发主要部分。

Hybrid Zend扩展主站,PHP扩展从站

好吧,那很容易。我们不想将其作为PHP扩展加载,而希望仅作为Zend扩展加载。为了强制,我们不会发布强制性符号get_module,当引擎尝试通过读取INI文件来注册PHP扩展时,该符号会寻找。

因此,我们将只能注册为 zend_extension = pib.so 。注册为 extension = pib.so 将失败,因为引擎将无法找到我们未导出的get_module符号。

但是,在我们的Zend扩展启动钩中,没有什么阻止我们将自己注册为PHP扩展:

  1. #include "php.h"
  2. #include "Zend/zend_extensions.h"
  3. #include "php_pib.h"
  4. #定义PRINT(什么)fprintf(stderr,什么是“ \ n”);
  5. / *声明为静态,因此为私有* /
  6. static zend_module_entry pib_module_entry = {
  7. STANDARD_MODULE_HEADER,
  8. "pib",
  9. NULL, /* Function entries */
  10. PHP_MINIT(pib), /* Module init */
  11. PHP_MSHUTDOWN(pib), /* Module shutdown */
  12. PHP_RINIT(pib), /* Request init */
  13. PHP_RSHUTDOWN(pib), /* Request shutdown */
  14. NULL, /* Module information */
  15. "0.1", /* Replace with version number for your extension */
  16. STANDARD_MODULE_PROPERTIES
  17. };
  18. / *此行应保持注释
  19. ZEND_GET_MODULE(pib)
  20. */
  21. zend_extension_version_info extension_version_info = {
  22. ZEND_EXTENSION_API_NO,
  23. ZEND_EXTENSION_BUILD_ID
  24. };
  25. zend_extension zend_extension_entry = {
  26. "pib-zend-extension",
  27. "1.0",
  28. "PHPInternalsBook Authors",
  29. "http://www.phpinternalsbook.com",
  30. "Our Copyright",
  31. pib_zend_extension_startup,
  32. pib_zend_extension_shutdown,
  33. pib_zend_extension_activate,
  34. pib_zend_extension_deactivate,
  35. NULL,
  36. NULL,
  37. NULL,
  38. NULL,
  39. NULL,
  40. NULL,
  41. NULL,
  42. STANDARD_ZEND_EXTENSION_PROPERTIES
  43. };
  44. static void pib_zend_extension_activate(void)
  45. {
  46. PRINT("Zend extension new request starting up");
  47. }
  48. static void pib_zend_extension_deactivate(void)
  49. {
  50. PRINT("Zend extension current request is shutting down");
  51. }
  52. static int pib_zend_extension_startup(zend_extension *ext)
  53. {
  54. PRINT("Zend extension is starting up");
  55. /* When the Zend extension part will startup(), make it register
  56. a PHP extension by calling ourselves zend_startup_module() */
  57. return zend_startup_module(&pib_module_entry);
  58. }
  59. static void pib_zend_extension_shutdown(zend_extension *ext)
  60. {
  61. PRINT("Zend extension is shutting down");
  62. }
  63. static PHP_MINIT_FUNCTION(pib)
  64. {
  65. PRINT("PHP extension is starting up");
  66. return SUCCESS;
  67. }
  68. static PHP_MSHUTDOWN_FUNCTION(pib)
  69. {
  70. PRINT("PHP extension is shutting down");
  71. return SUCCESS;
  72. }
  73. static PHP_RINIT_FUNCTION(pib)
  74. {
  75. PRINT("PHP extension new request starting up");
  76. return SUCCESS;
  77. }
  78. static PHP_RSHUTDOWN_FUNCTION(pib)
  79. {
  80. PRINT("PHP extension current request is shutting down");
  81. return SUCCESS;
  82. }

我们完了。在激活了这样的Zend扩展的情况下启动PHP将在stderr上显示以下内容:

  1. Zend extension is starting up
  2. PHP extension is starting up
  3. Zend extension new request starting up
  4. PHP extension new request starting up
  5. PHP extension current request is shutting down
  6. Zend extension current request is shutting down
  7. PHP extension is shutting down
  8. Zend extension is shutting down

如您所见,除了前两个钩子之外,每个钩子都按正确的顺序排列。从理论上讲,PHP扩展应该在Zend扩展之前启动,但是当我们注册为Zend扩展时,当引擎运行我们的Zend扩展挂钩时,它对我们的PHP扩展模块启动部分一无所知(MINIT())然而。我们告诉它启动我们的PHP扩展,然后作为Zend扩展startup()挂钩的一部分,通过调用zend_startup_module()。显然,您将必须注意不要创建一个循环循环,也不要使引擎对您在此类钩子中将要执行的操作感到疯狂。

这既简单又合乎逻辑。

从现在开始,我们既是PHP扩展,又是Zend扩展。看那个:

  1. > php -dzend_extension=pib.so -m
  2. [PHP modules]
  3. Core
  4. date
  5. (...)
  6. pib
  7. posix
  8. Reflection
  9. (...)
  10. [Zend Modules]
  11. pib-zend-extension

我们的PHP扩展名有效地称为“ pib”并显示,而我们的Zend扩展名有效地称为“ pib-zend-extension”并显示。我们为两个部分选择了两个不同的名称,我们可以选择相同的名称。

Note

OPCache和Xdebug使用这种混合模型,它们是Zend扩展,但是它们需要发布PHP函数,因此它们也是这样做的PHP扩展。

混合PHP扩展主机,Zend扩展从机

现在让我们走另一条路:我们希望引擎将其注册为PHP扩展,而不是Zend扩展,但仍希望是混合的。

好吧,我们会做相反的事情:我们不会发布zend_extension_version_info符号,这样就不可能将我们作为Zend扩展加载:引擎会拒绝这样做。但显然这次,我们将声明一个get_module符号,以便能够将其作为PHP扩展加载。并且,在我们的MINIT()中,我们将自己注册为Zend扩展

  1. #include "php.h"
  2. #include "Zend/zend_extensions.h"
  3. #include "php_pib.h"
  4. #include "Zend/zend_smart_str.h"
  5. #定义PRINT(什么)fprintf(stderr,什么是“ \ n”);
  6. zend_module_entry pib_module_entry = {
  7. STANDARD_MODULE_HEADER,
  8. "pib",
  9. NULL, /* Function entries */
  10. PHP_MINIT(pib), /* Module init */
  11. PHP_MSHUTDOWN(pib), /* Module shutdown */
  12. PHP_RINIT(pib), /* Request init */
  13. PHP_RSHUTDOWN(pib), /* Request shutdown */
  14. NULL, /* Module information */
  15. "0.1", /* Replace with version number for your extension */
  16. STANDARD_MODULE_PROPERTIES
  17. };
  18. ZEND_GET_MODULE(pib)
  19. / *应保持评论
  20. * zend_extension_version_info extension_version_info = {
  21. * ZEND_EXTENSION_API_NO,
  22. * ZEND_EXTENSION_BUILD_ID
  23. * };
  24. */
  25. static zend_extension zend_extension_entry = {
  26. "pib-zend-extension",
  27. "1.0",
  28. "PHPInternalsBook Authors",
  29. "http://www.phpinternalsbook.com",
  30. "Our Copyright",
  31. pib_zend_extension_startup,
  32. pib_zend_extension_shutdown,
  33. pib_zend_extension_activate,
  34. pib_zend_extension_deactivate,
  35. NULL,
  36. NULL,
  37. NULL,
  38. NULL,
  39. NULL,
  40. NULL,
  41. NULL,
  42. STANDARD_ZEND_EXTENSION_PROPERTIES
  43. };
  44. static void pib_zend_extension_activate(void)
  45. {
  46. PRINT("Zend extension new request starting up");
  47. }
  48. static void pib_zend_extension_deactivate(void)
  49. {
  50. PRINT("Zend extension current request is shutting down");
  51. }
  52. static int pib_zend_extension_startup(zend_extension *ext)
  53. {
  54. PRINT("Zend extension is starting up");
  55. return SUCCESS;
  56. }
  57. static PHP_MINIT_FUNCTION(pib)
  58. {
  59. PRINT("PHP extension is starting up");
  60. /* Register our zend_extension part now */
  61. zend_register_extension(&zend_extension_entry, NULL);
  62. return SUCCESS;
  63. }
  64. static void pib_zend_extension_shutdown(zend_extension *ext)
  65. {
  66. PRINT("Zend extension is shutting down");
  67. }
  68. static PHP_MSHUTDOWN_FUNCTION(pib)
  69. {
  70. PRINT("PHP extension is shutting down");
  71. return SUCCESS;
  72. }
  73. static PHP_RINIT_FUNCTION(pib)
  74. {
  75. PRINT("PHP extension new request starting up");
  76. return SUCCESS;
  77. }
  78. static PHP_RSHUTDOWN_FUNCTION(pib)
  79. {
  80. PRINT("PHP extension current request is shutting down");
  81. return SUCCESS;
  82. }

并最终在pib_zend_extension_shutdown()的结尾处严重崩溃(悲伤!)。触发调试器,很容易知道原因。

在这里,我们被加载为PHP扩展。看钩子。当点击MSHUTDOWN()时,引擎运行我们的MSHUTDOWN(),但是**它随后将卸载我们!它要求在我们的扩展程序上使用dlclose()查看源代码 ,解决方案通常位于该处。

所以发生的事情很容易,只要触发我们的RSHUTDOWN()之后,引擎就会卸载我们的 pib.so ;谈到我们的Zend扩展部分shutdown()时,我们不再是进程地址空间的一部分,因此会严重崩溃整个PHP进程。

解决办法是什么 ?好吧,如果您阅读了源代码,并且阅读了本书的其他章节,则应该知道,如果我们通过envZEND_DONT_UNLOAD_MODULES并将其设置为1,引擎将无法卸载我们。然后我们可以在MSHUTDOWN()中编写这样的env,然后在shutdown()中将其取消写入。 putenv()将完成这项工作。即使很棘手也可以。同样,如果我们之间的另一扩展与之一起玩,那对我们来说也会有味道。

第二种解决方案是通过卸载机制来实现的。如果您将libdl句柄从PHP扩展结构转移到Zend结构,则可以轻松卸载引擎。

补丁代码:

  1. static PHP_MINIT_FUNCTION(pib)
  2. {
  3. Dl_info infos;
  4. PRINT("PHP extension is starting up");
  5. / *注册我们的zend_extension部分,并为其提供我们自己的PHP扩展句柄* /
  6. zend_register_extension(&zend_extension_entry, pib_module_entry.handle);
  7. / *防止引擎卸载我们的PHP扩展* /
  8. pib_module_entry.handle = NULL;
  9. return SUCCESS;
  10. }

如果立即启动,您将获得预期的结果:

  1. PHP extension is starting up
  2. Zend extension is starting up
  3. Zend extension new request starting up
  4. PHP extension new request starting up
  5. PHP extension current request is shutting down
  6. Zend extension current request is shutting down
  7. PHP extension is shutting down
  8. Zend extension is shutting down

Note

Blackfire使用了这种混合模型,但是没有Zend扩展shutdown()钩子,因此不需要有关模块卸载的补丁。

Hybrid 混合

Hybrid混合只是一个模型,您可以在其中允许用户以Zend扩展或PHP扩展的形式加载您。

您所要做的就是记住您的加载方式,例如使用全局变量,以便您可以同时使用这两种模式。

让我们只写差异部分:

  1. static char started = 0;
  2. static int pib_zend_extension_startup(zend_extension *ext)
  3. {
  4. if (!started) {
  5. started = 1;
  6. return zend_startup_module(&pib_module_entry);
  7. }
  8. PRINT("Zend extension is starting up");
  9. return SUCCESS;
  10. }
  11. static PHP_MINIT_FUNCTION(pib)
  12. {
  13. if (!started) {
  14. started = 1;
  15. Dl_info infos;
  16. zend_register_extension(&zend_extension_entry, pib_module_entry.handle);
  17. dladdr(ZEND_MODULE_STARTUP_N(pib), &infos);
  18. dlopen(infos.dli_fname, 0);
  19. }
  20. PRINT("PHP extension is starting up");
  21. return SUCCESS;
  22. }

显然,每个符号都必须是公开的。使用上面的代码,您可以作为PHP扩展(extension = pib.so)或Zend扩展(zend_extension = pib.so)加载;最终用户将有选择权,这就是这种模型的优势,即使我们的作者不了解其用法。